Skip to content

Commit 1680bd7

Browse files
Merge branch 'feature/refactoring' into develop
2 parents a9b7e9d + c0f8c99 commit 1680bd7

24 files changed

Lines changed: 483 additions & 304 deletions

File tree

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# MVVM Clean Architecture with RxJava3+Coroutines Flow, Static Code Analysis, Dagger Hilt, Dynamic Features
2+
3+
[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)
4+
[![Kotlin Version](https://img.shields.io/badge/kotlin-1.4.0-blue.svg)](https://kotlinlang.org)
5+
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)
6+
7+
## About
8+
9+
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.
10+
11+
Unit tests are written with JUnit4, JUnit5, MockK, Truth, MockWebServer.
12+
13+
| Flow | RxJava3 | Pagination | Favorites
14+
| ------------------|-------------| -----|--------------|
15+
| <img src="./screenshots/property_flow.png"/> | <img src="./screenshots/property_rxjava3.png"/> | <img src="./screenshots/property_pagination.png"/> |<img src="./screenshots/favorites.png"/> |
16+
17+
18+
## Overview
19+
* Gradle Kotlin DSL is used for setting up gradle files with ```buildSrc``` folder and extensions.
20+
* KtLint, Detekt, and Git Hooks is used for checking, and formatting code and validating code before commits.
21+
* Dagger Hilt, Dynamic Feature Modules with Navigation Components, ViewModel, Retrofit, Room, RxJava, Coroutines libraries adn dependencies are set up.
22+
* ```features``` and ```libraries``` folders are used to include android libraries and dynamic feature modules
23+
* In core module dagger hilt dependencies and ```@EntryPoint``` is created
24+
* Data module uses Retrofit and Room to provide Local and Remote data sources
25+
* Repository provides offline and remote fetch function with mapping and local save, delete and fetch functions
26+
* Pagination with database
27+
28+
## Built With 🛠
29+
30+
Some of the popular libraries and MVVM clean architecture used with offline-first and offline-last with Room database and Retrofit as data source
31+
32+
* [Kotlin](https://kotlinlang.org/) - First class and official programming language for Android development.
33+
34+
* [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - Threads on steroids for Kotlin
35+
* [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.
36+
* [RxJava3](https://github.com/ReactiveX/RxJava) - Newest version of famous reactive programming library for Java, and other languages
37+
* [Android JetPack](https://developer.android.com/jetpack) - Collection of libraries that help you design robust, testable, and maintainable apps.
38+
* [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) - Data objects that notify views when the underlying database changes.
39+
* [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - Stores UI-related data that isn't destroyed on UI changes.
40+
* [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.
41+
* [Navigation Components](https://developer.android.com/guide/navigation/navigation-getting-started) Navigate fragments as never easier before
42+
* [Dynamic Feature Modules](https://developer.android.com/guide/playcore/dynamic-delivery) Dynamic modules for adding or removing based on preference
43+
* [Material Components for Android](https://github.com/material-components/material-components-android) - Modular and customizable Material Design UI components for Android.
44+
* [Dependency Injection](https://developer.android.com/training/dependency-injection) -
45+
* [Hilt-Dagger](https://dagger.dev/hilt/) - Standard library to incorporate Dagger dependency injection into an Android application.
46+
* [Hilt-ViewModel](https://developer.android.com/training/dependency-injection/hilt-jetpack) - DI for injecting `ViewModel`.
47+
* [Retrofit](https://square.github.io/retrofit/) - A type-safe HTTP client for Android and Java.
48+
* [Glide](https://github.com/bumptech/glide) - Image loading library.
49+
* [Lottie](http://airbnb.io/lottie) - animation library
50+
51+
* Architecture
52+
* Clean Architecture
53+
* MVVM + MVI
54+
* Offline first/last with Room an Retrofit
55+
* [Dynamic feature modules](https://developer.android.com/studio/projects/dynamic-delivery)
56+
* Tests
57+
* [Unit Tests](https://en.wikipedia.org/wiki/Unit_testing) ([JUnit](https://junit.org/junit4/))
58+
* [Mockk](https://mockk.io/)
59+
*
60+
* Gradle
61+
* [Gradle Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html)
62+
* Custom tasks
63+
* 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))
64+

features/home/src/main/java/com/smarttoolfactory/home/propertylist/flow/PropertyListViewModelFlow.kt

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import com.smarttoolfactory.domain.model.PropertyItem
1212
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow
1313
import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM
1414
import kotlinx.coroutines.CoroutineScope
15+
import kotlinx.coroutines.flow.Flow
16+
import kotlinx.coroutines.flow.catch
17+
import kotlinx.coroutines.flow.flatMapConcat
1518
import kotlinx.coroutines.flow.launchIn
1619
import kotlinx.coroutines.flow.onEach
1720
import kotlinx.coroutines.flow.onStart
@@ -35,17 +38,17 @@ class PropertyListViewModelFlow @ViewModelInject constructor(
3538

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

38-
init {
39-
updateOrderByKey()
40-
}
41-
42-
fun updateOrderByKey() {
43-
getPropertiesUseCase.getCurrentSortKey()
41+
private fun getOrderByKey(): Flow<String?> {
42+
return getPropertiesUseCase.getCurrentSortKey()
4443
.onEach {
45-
_orderByKey = it
46-
orderKey.value = _orderByKey
44+
println("🍏 AbstractPropertyListVM init orderKey: $it")
45+
_orderByKey = it ?: _orderByKey
46+
orderKey.postValue(_orderByKey)
47+
}
48+
.catch {
49+
orderKey.postValue(_orderByKey)
50+
println("❌ AbstractPropertyListVM init error: $it")
4751
}
48-
.launchIn(coroutineScope)
4952
}
5053

5154
/**
@@ -61,12 +64,18 @@ class PropertyListViewModelFlow @ViewModelInject constructor(
6164
*/
6265
override fun getPropertyList() {
6366

64-
getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey)
67+
getOrderByKey()
68+
.flatMapConcat {
69+
getPropertiesUseCase
70+
.getPropertiesOfflineFirst(_orderByKey)
71+
}
6572
.convertToFlowViewState()
6673
.onStart {
74+
println("🍏 FlowViewModel getPropertyList() START")
6775
_propertyViewState.value = ViewState(status = Status.LOADING)
6876
}
6977
.onEach {
78+
println("🍎 FlowViewModel getPropertyList() RES: $it")
7079
_propertyViewState.value = it
7180
}
7281
.launchIn(coroutineScope)

features/home/src/main/java/com/smarttoolfactory/home/propertylist/paged/PagedPropertyListViewModel.kt

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import com.smarttoolfactory.domain.model.PropertyItem
1212
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCasePaged
1313
import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM
1414
import kotlinx.coroutines.CoroutineScope
15+
import kotlinx.coroutines.flow.Flow
16+
import kotlinx.coroutines.flow.catch
17+
import kotlinx.coroutines.flow.flatMapConcat
1518
import kotlinx.coroutines.flow.launchIn
1619
import kotlinx.coroutines.flow.onEach
1720
import kotlinx.coroutines.flow.onStart
@@ -35,22 +38,26 @@ class PagedPropertyListViewModel @ViewModelInject constructor(
3538

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

38-
init {
39-
updateOrderByKey()
40-
}
41-
42-
fun updateOrderByKey() {
43-
getPropertiesUseCase.getCurrentSortKey()
41+
private fun getOrderByKey(): Flow<String?> {
42+
return getPropertiesUseCase.getCurrentSortKey()
4443
.onEach {
45-
_orderByKey = it
46-
orderKey.value = _orderByKey
44+
println("🍏 AbstractPropertyListVM init orderKey: $it")
45+
_orderByKey = it ?: _orderByKey
46+
orderKey.postValue(_orderByKey)
47+
}
48+
.catch {
49+
orderKey.postValue(_orderByKey)
50+
println("❌ AbstractPropertyListVM init error: $it")
4751
}
48-
.launchIn(coroutineScope)
4952
}
5053

5154
override fun getPropertyList() {
5255

53-
getPropertiesUseCase.getPagedOfflineLast(_orderByKey)
56+
getOrderByKey()
57+
.flatMapConcat {
58+
println("🔥 refreshPropertyList: $it")
59+
getPropertiesUseCase.getPagedOfflineLast(_orderByKey)
60+
}
5461
.convertToFlowViewState()
5562
.onStart {
5663
_propertyViewState.value = ViewState(status = Status.LOADING)
@@ -63,7 +70,11 @@ class PagedPropertyListViewModel @ViewModelInject constructor(
6370

6471
override fun refreshPropertyList(orderBy: String?) {
6572

66-
getPropertiesUseCase.refreshData(orderBy ?: _orderByKey)
73+
getOrderByKey()
74+
.flatMapConcat {
75+
println("🔥 refreshPropertyList: $it")
76+
getPropertiesUseCase.refreshData(orderBy ?: _orderByKey)
77+
}
6778
.convertToFlowViewState()
6879
.onStart {
6980
_propertyViewState.value = ViewState(status = Status.LOADING)

features/home/src/main/java/com/smarttoolfactory/home/propertylist/rxjava/PropertyListViewModelRxJava3.kt

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.smarttoolfactory.domain.model.PropertyItem
1212
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseRxJava3
1313
import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM
1414
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
15+
import io.reactivex.rxjava3.core.Single
1516
import io.reactivex.rxjava3.schedulers.Schedulers
1617

1718
class PropertyListViewModelRxJava3 @ViewModelInject constructor(
@@ -32,28 +33,25 @@ class PropertyListViewModelRxJava3 @ViewModelInject constructor(
3233

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

35-
init {
36-
updateOrderByKey()
37-
}
38-
39-
private fun updateOrderByKey() {
40-
getPropertiesUseCase.getCurrentSortKey()
36+
private fun getOrderByKey(): Single<String?> {
37+
return getPropertiesUseCase.getCurrentSortKey()
4138
.subscribeOn(Schedulers.io())
4239
.observeOn(AndroidSchedulers.mainThread())
43-
.subscribe(
44-
{
45-
_orderByKey = it
46-
47-
orderKey.value = _orderByKey
48-
},
49-
{
50-
println("PropertyListViewModelRxJava3 init error: $it")
51-
}
52-
)
40+
.doOnSuccess {
41+
_orderByKey = it ?: _orderByKey
42+
orderKey.postValue(_orderByKey)
43+
}
44+
.onErrorResumeNext {
45+
Single.just(_orderByKey)
46+
}
5347
}
5448

5549
override fun getPropertyList() {
56-
getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey)
50+
51+
getOrderByKey()
52+
.flatMap {
53+
getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey)
54+
}
5755
.convertFromSingleToObservableViewStateWithLoading()
5856
.observeOn(AndroidSchedulers.mainThread())
5957
.subscribe(

features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelFlowTest.kt

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.smarttoolfactory.home.viewmodel
33
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
44
import com.google.common.truth.Truth
55
import com.smarttoolfactory.core.viewstate.Status
6+
import com.smarttoolfactory.domain.ORDER_BY_NONE
67
import com.smarttoolfactory.domain.model.PropertyItem
78
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow
89
import com.smarttoolfactory.home.propertylist.flow.PropertyListViewModelFlow
@@ -21,6 +22,9 @@ import org.junit.Before
2122
import org.junit.Rule
2223
import org.junit.Test
2324

25+
/**
26+
* ❌ FIXME Either [LiveDataTestObserver] or Flow is bugged with tests, solve the issue
27+
*/
2428
class PropertyListViewModelFlowTest {
2529

2630
// Run tasks synchronously
@@ -73,12 +77,20 @@ class PropertyListViewModelFlowTest {
7377
emit(throw Exception("Network Exception"))
7478
}
7579

80+
every {
81+
useCase.getCurrentSortKey()
82+
} returns flow {
83+
emit((ORDER_BY_NONE))
84+
}
85+
7686
val testObserver = viewModel.propertyListViewState.test()
7787

7888
// WHEN
89+
7990
viewModel.getPropertyList()
8091

8192
// THEN
93+
println("💀 THEN")
8294
testObserver
8395
.assertValue { states ->
8496
(
@@ -93,9 +105,6 @@ class PropertyListViewModelFlowTest {
93105
verify(atMost = 1) { useCase.getPropertiesOfflineFirst() }
94106
}
95107

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

117+
every {
118+
useCase.getCurrentSortKey()
119+
} returns flow<String> {
120+
emit((ORDER_BY_NONE))
121+
}
122+
108123
val testObserver = viewModel.propertyListViewState.test()
109124

110125
// WHEN
111126
viewModel.getPropertyList()
112127

113-
advanceUntilIdle()
114-
115128
// THEN
129+
println("💀 THEN")
116130
val viewStates = testObserver.values()
117131
Truth.assertThat(viewStates.first().status).isEqualTo(Status.LOADING)
118132

@@ -129,7 +143,7 @@ class PropertyListViewModelFlowTest {
129143

130144
// GIVEN
131145
every {
132-
useCase.getPropertiesOfflineLast()
146+
useCase.getPropertiesOfflineLast(ORDER_BY_NONE)
133147
} returns flow<List<PropertyItem>> {
134148
emit(throw Exception("Network Exception"))
135149
}
@@ -138,6 +152,7 @@ class PropertyListViewModelFlowTest {
138152

139153
// WHEN
140154
viewModel.refreshPropertyList()
155+
advanceUntilIdle()
141156

142157
// THEN
143158
testObserver
@@ -152,7 +167,7 @@ class PropertyListViewModelFlowTest {
152167
val finalState = testObserver.values()[1]
153168
Truth.assertThat(finalState.error?.message).isEqualTo("Network Exception")
154169
Truth.assertThat(finalState.error).isInstanceOf(Exception::class.java)
155-
verify(atMost = 1) { useCase.getPropertiesOfflineLast() }
170+
verify(atMost = 1) { useCase.getPropertiesOfflineLast(ORDER_BY_NONE) }
156171
}
157172

158173
/**
@@ -164,7 +179,7 @@ class PropertyListViewModelFlowTest {
164179

165180
// GIVEN
166181
every {
167-
useCase.getPropertiesOfflineLast()
182+
useCase.getPropertiesOfflineLast(ORDER_BY_NONE)
168183
} returns flow {
169184
emit(itemList)
170185
}
@@ -173,14 +188,15 @@ class PropertyListViewModelFlowTest {
173188

174189
// WHEN
175190
viewModel.refreshPropertyList()
191+
advanceUntilIdle()
176192

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

181197
val actual = viewStates.last().data
182198
Truth.assertThat(actual?.size).isEqualTo(itemList.size)
183-
verify(exactly = 1) { useCase.getPropertiesOfflineLast() }
199+
verify(exactly = 1) { useCase.getPropertiesOfflineLast(ORDER_BY_NONE) }
184200
testObserver.dispose()
185201
}
186202

features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelRxJava3Test.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package com.smarttoolfactory.home.viewmodel
33
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
44
import com.google.common.truth.Truth
55
import com.smarttoolfactory.core.viewstate.Status
6+
import com.smarttoolfactory.domain.ORDER_BY_NONE
67
import com.smarttoolfactory.domain.model.PropertyItem
78
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseRxJava3
8-
import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM.Companion.ORDER_BY_NONE
99
import com.smarttoolfactory.home.propertylist.rxjava.PropertyListViewModelRxJava3
1010
import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH
1111
import com.smarttoolfactory.test_utils.rule.RxImmediateSchedulerRule
@@ -72,6 +72,10 @@ class PropertyListViewModelRxJava3Test {
7272
useCase.getPropertiesOfflineFirst(ORDER_BY_NONE)
7373
} returns Single.error(Exception("Network Exception"))
7474

75+
every {
76+
useCase.getCurrentSortKey()
77+
} returns Single.just(ORDER_BY_NONE)
78+
7579
val testObserver = viewModel.propertyListViewState.test()
7680

7781
// WHEN
@@ -97,6 +101,9 @@ class PropertyListViewModelRxJava3Test {
97101

98102
// GIVEN
99103
every { useCase.getPropertiesOfflineFirst(ORDER_BY_NONE) } returns Single.just(itemList)
104+
every {
105+
useCase.getCurrentSortKey()
106+
} returns Single.just(ORDER_BY_NONE)
100107

101108
val testObserver = viewModel.propertyListViewState.test()
102109

@@ -115,7 +122,9 @@ class PropertyListViewModelRxJava3Test {
115122
val finalState = testObserver.values()[1]
116123
val actual = finalState.data
117124
Truth.assertThat(actual?.size).isEqualTo(itemList.size)
125+
118126
verify(exactly = 1) { useCase.getPropertiesOfflineFirst(ORDER_BY_NONE) }
127+
verify(exactly = 1) { useCase.getCurrentSortKey(ORDER_BY_NONE) }
119128
testObserver.dispose()
120129
}
121130

libraries/data/src/main/java/com/smarttoolfactory/data/model/IEntity.kt renamed to libraries/data/src/main/java/com/smarttoolfactory/data/model/Mappables.kt

File renamed without changes.

0 commit comments

Comments
 (0)