Skip to content

Commit

Permalink
Merge branch 'feature/refactoring' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
SmartToolFactory committed Sep 9, 2020
2 parents a9b7e9d + c0f8c99 commit 1680bd7
Show file tree
Hide file tree
Showing 24 changed files with 483 additions and 304 deletions.
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# MVVM Clean Architecture with RxJava3+Coroutines Flow, Static Code Analysis, Dagger Hilt, Dynamic Features

[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)
[![Kotlin Version](https://img.shields.io/badge/kotlin-1.4.0-blue.svg)](https://kotlinlang.org)
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)

## About

Sample project that build with MVVM clean architure and various cool techs including RxJava3 and Coroutines Flow, Dynamic Feature modules as base of BottomNavigationView or ViewPager2, with both OfflineFirst and OfflineLast approaches as database Single Source of Truth and TDD.

Unit tests are written with JUnit4, JUnit5, MockK, Truth, MockWebServer.

| Flow | RxJava3 | Pagination | Favorites
| ------------------|-------------| -----|--------------|
| <img src="./screenshots/property_flow.png"/> | <img src="./screenshots/property_rxjava3.png"/> | <img src="./screenshots/property_pagination.png"/> |<img src="./screenshots/favorites.png"/> |


## Overview
* Gradle Kotlin DSL is used for setting up gradle files with ```buildSrc``` folder and extensions.
* KtLint, Detekt, and Git Hooks is used for checking, and formatting code and validating code before commits.
* Dagger Hilt, Dynamic Feature Modules with Navigation Components, ViewModel, Retrofit, Room, RxJava, Coroutines libraries adn dependencies are set up.
* ```features``` and ```libraries``` folders are used to include android libraries and dynamic feature modules
* In core module dagger hilt dependencies and ```@EntryPoint``` is created
* Data module uses Retrofit and Room to provide Local and Remote data sources
* Repository provides offline and remote fetch function with mapping and local save, delete and fetch functions
* Pagination with database

## Built With 🛠

Some of the popular libraries and MVVM clean architecture used with offline-first and offline-last with Room database and Retrofit as data source

* [Kotlin](https://kotlinlang.org/) - First class and official programming language for Android development.

* [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - Threads on steroids for Kotlin
* [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/) - A cold asynchronous data stream that sequentially emits values and completes normally or with an exception.
* [RxJava3](https://github.com/ReactiveX/RxJava) - Newest version of famous reactive programming library for Java, and other languages
* [Android JetPack](https://developer.android.com/jetpack) - Collection of libraries that help you design robust, testable, and maintainable apps.
* [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) - Data objects that notify views when the underlying database changes.
* [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - Stores UI-related data that isn't destroyed on UI changes.
* [DataBinding](https://developer.android.com/topic/libraries/data-binding) - Generates a binding class for each XML layout file present in that module and allows you to more easily write code that interacts with views.
* [Navigation Components](https://developer.android.com/guide/navigation/navigation-getting-started) Navigate fragments as never easier before
* [Dynamic Feature Modules](https://developer.android.com/guide/playcore/dynamic-delivery) Dynamic modules for adding or removing based on preference
* [Material Components for Android](https://github.com/material-components/material-components-android) - Modular and customizable Material Design UI components for Android.
* [Dependency Injection](https://developer.android.com/training/dependency-injection) -
* [Hilt-Dagger](https://dagger.dev/hilt/) - Standard library to incorporate Dagger dependency injection into an Android application.
* [Hilt-ViewModel](https://developer.android.com/training/dependency-injection/hilt-jetpack) - DI for injecting `ViewModel`.
* [Retrofit](https://square.github.io/retrofit/) - A type-safe HTTP client for Android and Java.
* [Glide](https://github.com/bumptech/glide) - Image loading library.
* [Lottie](http://airbnb.io/lottie) - animation library

* Architecture
* Clean Architecture
* MVVM + MVI
* Offline first/last with Room an Retrofit
* [Dynamic feature modules](https://developer.android.com/studio/projects/dynamic-delivery)
* Tests
* [Unit Tests](https://en.wikipedia.org/wiki/Unit_testing) ([JUnit](https://junit.org/junit4/))
* [Mockk](https://mockk.io/)
*
* Gradle
* [Gradle Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html)
* Custom tasks
* Plugins ([Ktlint](https://github.com/JLLeitschuh/ktlint-gradle), [Detekt](https://github.com/arturbosch/detekt#with-gradle), [SafeArgs](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args))

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import com.smarttoolfactory.domain.model.PropertyItem
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow
import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
Expand All @@ -35,17 +38,17 @@ class PropertyListViewModelFlow @ViewModelInject constructor(

var orderKey = MutableLiveData<String>().apply { value = _orderByKey }

init {
updateOrderByKey()
}

fun updateOrderByKey() {
getPropertiesUseCase.getCurrentSortKey()
private fun getOrderByKey(): Flow<String?> {
return getPropertiesUseCase.getCurrentSortKey()
.onEach {
_orderByKey = it
orderKey.value = _orderByKey
println("🍏 AbstractPropertyListVM init orderKey: $it")
_orderByKey = it ?: _orderByKey
orderKey.postValue(_orderByKey)
}
.catch {
orderKey.postValue(_orderByKey)
println("❌ AbstractPropertyListVM init error: $it")
}
.launchIn(coroutineScope)
}

/**
Expand All @@ -61,12 +64,18 @@ class PropertyListViewModelFlow @ViewModelInject constructor(
*/
override fun getPropertyList() {

getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey)
getOrderByKey()
.flatMapConcat {
getPropertiesUseCase
.getPropertiesOfflineFirst(_orderByKey)
}
.convertToFlowViewState()
.onStart {
println("🍏 FlowViewModel getPropertyList() START")
_propertyViewState.value = ViewState(status = Status.LOADING)
}
.onEach {
println("🍎 FlowViewModel getPropertyList() RES: $it")
_propertyViewState.value = it
}
.launchIn(coroutineScope)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import com.smarttoolfactory.domain.model.PropertyItem
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCasePaged
import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
Expand All @@ -35,22 +38,26 @@ class PagedPropertyListViewModel @ViewModelInject constructor(

var orderKey = MutableLiveData<String>().apply { value = _orderByKey }

init {
updateOrderByKey()
}

fun updateOrderByKey() {
getPropertiesUseCase.getCurrentSortKey()
private fun getOrderByKey(): Flow<String?> {
return getPropertiesUseCase.getCurrentSortKey()
.onEach {
_orderByKey = it
orderKey.value = _orderByKey
println("🍏 AbstractPropertyListVM init orderKey: $it")
_orderByKey = it ?: _orderByKey
orderKey.postValue(_orderByKey)
}
.catch {
orderKey.postValue(_orderByKey)
println("❌ AbstractPropertyListVM init error: $it")
}
.launchIn(coroutineScope)
}

override fun getPropertyList() {

getPropertiesUseCase.getPagedOfflineLast(_orderByKey)
getOrderByKey()
.flatMapConcat {
println("🔥 refreshPropertyList: $it")
getPropertiesUseCase.getPagedOfflineLast(_orderByKey)
}
.convertToFlowViewState()
.onStart {
_propertyViewState.value = ViewState(status = Status.LOADING)
Expand All @@ -63,7 +70,11 @@ class PagedPropertyListViewModel @ViewModelInject constructor(

override fun refreshPropertyList(orderBy: String?) {

getPropertiesUseCase.refreshData(orderBy ?: _orderByKey)
getOrderByKey()
.flatMapConcat {
println("🔥 refreshPropertyList: $it")
getPropertiesUseCase.refreshData(orderBy ?: _orderByKey)
}
.convertToFlowViewState()
.onStart {
_propertyViewState.value = ViewState(status = Status.LOADING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.smarttoolfactory.domain.model.PropertyItem
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseRxJava3
import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers

class PropertyListViewModelRxJava3 @ViewModelInject constructor(
Expand All @@ -32,28 +33,25 @@ class PropertyListViewModelRxJava3 @ViewModelInject constructor(

var orderKey = MutableLiveData<String>().apply { value = _orderByKey }

init {
updateOrderByKey()
}

private fun updateOrderByKey() {
getPropertiesUseCase.getCurrentSortKey()
private fun getOrderByKey(): Single<String?> {
return getPropertiesUseCase.getCurrentSortKey()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_orderByKey = it

orderKey.value = _orderByKey
},
{
println("PropertyListViewModelRxJava3 init error: $it")
}
)
.doOnSuccess {
_orderByKey = it ?: _orderByKey
orderKey.postValue(_orderByKey)
}
.onErrorResumeNext {
Single.just(_orderByKey)
}
}

override fun getPropertyList() {
getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey)

getOrderByKey()
.flatMap {
getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey)
}
.convertFromSingleToObservableViewStateWithLoading()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.smarttoolfactory.home.viewmodel
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.google.common.truth.Truth
import com.smarttoolfactory.core.viewstate.Status
import com.smarttoolfactory.domain.ORDER_BY_NONE
import com.smarttoolfactory.domain.model.PropertyItem
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow
import com.smarttoolfactory.home.propertylist.flow.PropertyListViewModelFlow
Expand All @@ -21,6 +22,9 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test

/**
* ❌ FIXME Either [LiveDataTestObserver] or Flow is bugged with tests, solve the issue
*/
class PropertyListViewModelFlowTest {

// Run tasks synchronously
Expand Down Expand Up @@ -73,12 +77,20 @@ class PropertyListViewModelFlowTest {
emit(throw Exception("Network Exception"))
}

every {
useCase.getCurrentSortKey()
} returns flow {
emit((ORDER_BY_NONE))
}

val testObserver = viewModel.propertyListViewState.test()

// WHEN

viewModel.getPropertyList()

// THEN
println("💀 THEN")
testObserver
.assertValue { states ->
(
Expand All @@ -93,9 +105,6 @@ class PropertyListViewModelFlowTest {
verify(atMost = 1) { useCase.getPropertiesOfflineFirst() }
}

/**
* ❌ FIXME This test is flaky, find out the cause, sometimes null is returned
*/
@Test
fun `given useCase fetched data, should have ViewState SUCCESS and data offlineFirst`() =
testCoroutineRule.runBlockingTest {
Expand All @@ -105,14 +114,19 @@ class PropertyListViewModelFlowTest {
emit(itemList)
}

every {
useCase.getCurrentSortKey()
} returns flow<String> {
emit((ORDER_BY_NONE))
}

val testObserver = viewModel.propertyListViewState.test()

// WHEN
viewModel.getPropertyList()

advanceUntilIdle()

// THEN
println("💀 THEN")
val viewStates = testObserver.values()
Truth.assertThat(viewStates.first().status).isEqualTo(Status.LOADING)

Expand All @@ -129,7 +143,7 @@ class PropertyListViewModelFlowTest {

// GIVEN
every {
useCase.getPropertiesOfflineLast()
useCase.getPropertiesOfflineLast(ORDER_BY_NONE)
} returns flow<List<PropertyItem>> {
emit(throw Exception("Network Exception"))
}
Expand All @@ -138,6 +152,7 @@ class PropertyListViewModelFlowTest {

// WHEN
viewModel.refreshPropertyList()
advanceUntilIdle()

// THEN
testObserver
Expand All @@ -152,7 +167,7 @@ class PropertyListViewModelFlowTest {
val finalState = testObserver.values()[1]
Truth.assertThat(finalState.error?.message).isEqualTo("Network Exception")
Truth.assertThat(finalState.error).isInstanceOf(Exception::class.java)
verify(atMost = 1) { useCase.getPropertiesOfflineLast() }
verify(atMost = 1) { useCase.getPropertiesOfflineLast(ORDER_BY_NONE) }
}

/**
Expand All @@ -164,7 +179,7 @@ class PropertyListViewModelFlowTest {

// GIVEN
every {
useCase.getPropertiesOfflineLast()
useCase.getPropertiesOfflineLast(ORDER_BY_NONE)
} returns flow {
emit(itemList)
}
Expand All @@ -173,14 +188,15 @@ class PropertyListViewModelFlowTest {

// WHEN
viewModel.refreshPropertyList()
advanceUntilIdle()

// THEN
val viewStates = testObserver.values()
Truth.assertThat(viewStates.first().status).isEqualTo(Status.LOADING)

val actual = viewStates.last().data
Truth.assertThat(actual?.size).isEqualTo(itemList.size)
verify(exactly = 1) { useCase.getPropertiesOfflineLast() }
verify(exactly = 1) { useCase.getPropertiesOfflineLast(ORDER_BY_NONE) }
testObserver.dispose()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.smarttoolfactory.home.viewmodel
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.google.common.truth.Truth
import com.smarttoolfactory.core.viewstate.Status
import com.smarttoolfactory.domain.ORDER_BY_NONE
import com.smarttoolfactory.domain.model.PropertyItem
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseRxJava3
import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM.Companion.ORDER_BY_NONE
import com.smarttoolfactory.home.propertylist.rxjava.PropertyListViewModelRxJava3
import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH
import com.smarttoolfactory.test_utils.rule.RxImmediateSchedulerRule
Expand Down Expand Up @@ -72,6 +72,10 @@ class PropertyListViewModelRxJava3Test {
useCase.getPropertiesOfflineFirst(ORDER_BY_NONE)
} returns Single.error(Exception("Network Exception"))

every {
useCase.getCurrentSortKey()
} returns Single.just(ORDER_BY_NONE)

val testObserver = viewModel.propertyListViewState.test()

// WHEN
Expand All @@ -97,6 +101,9 @@ class PropertyListViewModelRxJava3Test {

// GIVEN
every { useCase.getPropertiesOfflineFirst(ORDER_BY_NONE) } returns Single.just(itemList)
every {
useCase.getCurrentSortKey()
} returns Single.just(ORDER_BY_NONE)

val testObserver = viewModel.propertyListViewState.test()

Expand All @@ -115,7 +122,9 @@ class PropertyListViewModelRxJava3Test {
val finalState = testObserver.values()[1]
val actual = finalState.data
Truth.assertThat(actual?.size).isEqualTo(itemList.size)

verify(exactly = 1) { useCase.getPropertiesOfflineFirst(ORDER_BY_NONE) }
verify(exactly = 1) { useCase.getCurrentSortKey(ORDER_BY_NONE) }
testObserver.dispose()
}

Expand Down
Loading

0 comments on commit 1680bd7

Please sign in to comment.