Skip to content

Commit b3dfa3c

Browse files
authored
Merge pull request #4 from monstar-lab-oss/feature/flowmvvm
Flow MVVM
2 parents abc6b6d + 73aaf07 commit b3dfa3c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+977
-19
lines changed

.idea/gradle.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ plugins {
22
id 'com.android.application'
33
id 'kotlin-android'
44
id 'kotlin-kapt'
5+
id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.20'
56
}
67

78
android {
89
compileSdkVersion 30
910
buildToolsVersion "30.0.2"
11+
flavorDimensions "default"
1012

1113
defaultConfig {
1214
applicationId "com.monstarlab"
@@ -17,13 +19,44 @@ android {
1719

1820
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1921
}
20-
22+
buildFeatures {
23+
viewBinding true
24+
}
2125
buildTypes {
2226
release {
2327
minifyEnabled false
2428
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
2529
}
2630
}
31+
32+
productFlavors {
33+
dev {
34+
dimension "default"
35+
applicationIdSuffix ".dev"
36+
manifestPlaceholders = [
37+
APP_NAME: "MonstarlabDev"
38+
]
39+
buildConfigField "String", "API_URL", "\"https://jsonplaceholder.typicode.com\""
40+
}
41+
staging {
42+
dimension "default"
43+
applicationIdSuffix ".staging"
44+
//signingConfig signingConfigs.staging
45+
manifestPlaceholders = [
46+
APP_NAME: "MonstarlabStaging"
47+
]
48+
buildConfigField "String", "API_URL", "\"https://jsonplaceholder.typicode.com\""
49+
}
50+
production {
51+
dimension "default"
52+
//signingConfig signingConfigs.production
53+
manifestPlaceholders = [
54+
APP_NAME: "Monstarlab",
55+
]
56+
buildConfigField "String", "API_URL", "\"https://jsonplaceholder.typicode.com\""
57+
}
58+
}
59+
2760
compileOptions {
2861
sourceCompatibility JavaVersion.VERSION_1_8
2962
targetCompatibility JavaVersion.VERSION_1_8
@@ -36,6 +69,12 @@ android {
3669
dependencies {
3770

3871
implementation "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin"
72+
def coroutinesVersion = "1.4.2"
73+
74+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
75+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion"
76+
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
77+
3978
implementation 'androidx.core:core-ktx:1.3.2'
4079
implementation 'androidx.appcompat:appcompat:1.2.0'
4180
implementation 'com.google.android.material:material:1.2.1'
@@ -49,4 +88,18 @@ dependencies {
4988
kapt "com.google.dagger:dagger-compiler:${versions.dagger}"
5089
kapt "com.google.dagger:dagger-android-processor:${versions.dagger}"
5190

91+
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
92+
implementation("com.squareup.retrofit2:retrofit:2.9.0")
93+
implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")
94+
95+
implementation("androidx.navigation:navigation-fragment-ktx:2.3.2")
96+
implementation("androidx.navigation:navigation-ui-ktx:2.3.2")
97+
98+
def lifecycle = "2.2.0"
99+
implementation("androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycle")
100+
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle")
101+
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle")
102+
implementation("androidx.lifecycle:lifecycle-extensions:$lifecycle")
103+
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle")
104+
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle")
52105
}

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
android:allowBackup="true"
77
android:icon="@mipmap/ic_launcher"
88
android:label="@string/app_name"
9+
android:name=".App"
910
android:roundIcon="@mipmap/ic_launcher_round"
1011
android:supportsRtl="true"
1112
android:theme="@style/Theme.Androidtemplate">
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.monstarlab.arch.base
2+
3+
import android.os.Bundle
4+
import androidx.annotation.LayoutRes
5+
import androidx.appcompat.app.AppCompatActivity
6+
import androidx.lifecycle.ViewModel
7+
import androidx.lifecycle.ViewModelProvider
8+
import com.monstarlab.arch.extensions.getViewModel
9+
import com.monstarlab.arch.extensions.lifecycleAwareLazy
10+
import dagger.android.AndroidInjection
11+
import dagger.android.DispatchingAndroidInjector
12+
import dagger.android.HasAndroidInjector
13+
import javax.inject.Inject
14+
15+
abstract class BaseActivity : AppCompatActivity, HasAndroidInjector {
16+
17+
constructor() : super()
18+
constructor(@LayoutRes resId: Int) : super(resId)
19+
20+
@Inject
21+
lateinit var viewModelFactory: ViewModelProvider.Factory
22+
23+
@Inject
24+
lateinit var androidInjector: DispatchingAndroidInjector<Any>
25+
26+
override fun onCreate(savedInstanceState: Bundle?) {
27+
AndroidInjection.inject(this)
28+
super.onCreate(savedInstanceState)
29+
}
30+
31+
protected inline fun <reified VM : ViewModel> getViewModel(): VM =
32+
getViewModel(viewModelFactory)
33+
34+
protected inline fun <reified VM : ViewModel> viewModel(): Lazy<VM> {
35+
return lifecycleAwareLazy(this) { getViewModel<VM>() }
36+
}
37+
38+
override fun androidInjector() = androidInjector
39+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.monstarlab.arch.base
2+
3+
import android.content.Context
4+
import android.os.Bundle
5+
import androidx.annotation.LayoutRes
6+
import androidx.fragment.app.Fragment
7+
import androidx.lifecycle.ViewModel
8+
import androidx.lifecycle.ViewModelProvider
9+
import dagger.android.DispatchingAndroidInjector
10+
import dagger.android.HasAndroidInjector
11+
import dagger.android.support.AndroidSupportInjection
12+
import com.google.android.material.transition.MaterialFadeThrough
13+
import com.monstarlab.arch.extensions.getSharedViewModel
14+
import com.monstarlab.arch.extensions.getViewModel
15+
import com.monstarlab.arch.extensions.lifecycleAwareLazy
16+
import javax.inject.Inject
17+
18+
abstract class BaseFragment : Fragment, HasAndroidInjector {
19+
20+
constructor()
21+
constructor(@LayoutRes resId: Int) : super(resId)
22+
23+
@Inject
24+
lateinit var viewModelFactory: ViewModelProvider.Factory
25+
26+
@Inject
27+
lateinit var androidInjector: DispatchingAndroidInjector<Any>
28+
29+
protected inline fun <reified VM : ViewModel> getViewModel(): VM =
30+
getViewModel(viewModelFactory)
31+
32+
protected inline fun <reified VM : ViewModel> getSharedViewModel(): VM =
33+
getSharedViewModel(viewModelFactory)
34+
35+
protected inline fun <reified VM : ViewModel> viewModel(): Lazy<VM> = lifecycleAwareLazy(this) {
36+
getViewModel<VM>()
37+
}
38+
39+
protected inline fun <reified VM : ViewModel> sharedViewModel(): Lazy<VM> =
40+
lifecycleAwareLazy(this) {
41+
getSharedViewModel<VM>()
42+
}
43+
44+
override fun onAttach(context: Context) {
45+
AndroidSupportInjection.inject(this)
46+
super.onAttach(context)
47+
}
48+
49+
override fun onCreate(savedInstanceState: Bundle?) {
50+
super.onCreate(savedInstanceState)
51+
52+
enterTransition = MaterialFadeThrough()
53+
exitTransition = MaterialFadeThrough()
54+
}
55+
56+
override fun androidInjector() = androidInjector
57+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.monstarlab.arch.data
2+
3+
interface DataSource<T> {
4+
fun getAll(): List<T>
5+
fun add(item: T)
6+
fun addAll(items: List<T>)
7+
fun clear()
8+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.monstarlab.arch.data
2+
3+
import java.util.concurrent.TimeUnit
4+
5+
abstract class Repository constructor(
6+
private val expirationInSeconds: Long = 30
7+
) {
8+
9+
private var lastFetch = 0L
10+
11+
protected val shouldFetch: Boolean
12+
get() {
13+
return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - lastFetch) >= expirationInSeconds
14+
}
15+
16+
protected fun fetched() {
17+
lastFetch = System.currentTimeMillis()
18+
}
19+
20+
21+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.monstarlab.arch.data
2+
3+
import android.content.SharedPreferences
4+
import kotlinx.serialization.decodeFromString
5+
import kotlinx.serialization.encodeToString
6+
import kotlinx.serialization.json.Json
7+
import java.lang.Exception
8+
9+
abstract class SharedPreferenceDataStore<T> constructor(
10+
private val sharedPreferences: SharedPreferences
11+
): DataSource<T> {
12+
13+
private val key = this.javaClass.simpleName
14+
15+
override fun getAll(): List<T> {
16+
return try {
17+
val json = sharedPreferences.getString(key, "") ?: ""
18+
val entries = Json.decodeFromString<List<T>>(json)
19+
entries
20+
} catch (e: Exception) {
21+
emptyList()
22+
}
23+
}
24+
25+
override fun add(item: T) {
26+
val list = getAll().toMutableList()
27+
list.add(item)
28+
addAll(list)
29+
}
30+
31+
override fun addAll(items: List<T>) {
32+
try {
33+
val json = Json.encodeToString(items)
34+
sharedPreferences.edit().putString(key, json).apply()
35+
} catch (e: Exception) {
36+
37+
}
38+
}
39+
40+
override fun clear() {
41+
sharedPreferences.edit().remove(key).apply()
42+
}
43+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.monstarlab.arch.extensions
2+
3+
import android.view.LayoutInflater
4+
import androidx.appcompat.app.AppCompatActivity
5+
import androidx.lifecycle.Lifecycle
6+
import androidx.lifecycle.LifecycleObserver
7+
import androidx.lifecycle.OnLifecycleEvent
8+
import androidx.viewbinding.ViewBinding
9+
import kotlin.properties.ReadOnlyProperty
10+
import kotlin.reflect.KProperty
11+
12+
class ActivityViewBindingDelegate<T : ViewBinding>(
13+
private val activity: AppCompatActivity,
14+
private val viewBinder: (LayoutInflater) -> T,
15+
private val beforeSetContent: () -> Unit = {}
16+
) : ReadOnlyProperty<AppCompatActivity, T>, LifecycleObserver {
17+
18+
private var activityBinding: T? = null
19+
20+
init {
21+
activity.lifecycle.addObserver(this)
22+
}
23+
24+
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
25+
fun createBinding() {
26+
initialize()
27+
beforeSetContent()
28+
activity.setContentView(activityBinding?.root)
29+
activity.lifecycle.removeObserver(this)
30+
}
31+
32+
private fun initialize() {
33+
if (activityBinding == null) {
34+
activityBinding = viewBinder(activity.layoutInflater)
35+
}
36+
}
37+
38+
override fun getValue(thisRef: AppCompatActivity, property: KProperty<*>): T {
39+
ensureMainThread()
40+
41+
initialize()
42+
return activityBinding!!
43+
}
44+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.monstarlab.arch.extensions
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.collect
6+
import kotlinx.coroutines.flow.combine
7+
import kotlinx.coroutines.launch
8+
9+
fun <T1, T2> CoroutineScope.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, collectBlock: (suspend (T1, T2) -> Unit)) {
10+
launch {
11+
flow1.combine(flow2) { v1, v2 ->
12+
collectBlock.invoke(v1, v2)
13+
}.collect {
14+
// Empty collect block to trigger ^
15+
}
16+
}
17+
}
18+
19+
fun <T1, T2, T3> CoroutineScope.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, flow3: Flow<T3>, collectBlock: (suspend (T1, T2, T3) -> Unit)) {
20+
launch {
21+
combine(flow1, flow2, flow3) { v1, v2, v3 ->
22+
collectBlock.invoke(v1, v2, v3)
23+
}.collect {
24+
// Empty collect block to trigger ^
25+
}
26+
}
27+
}
28+
29+
fun <T1, T2, T3, T4> CoroutineScope.combineFlows(flow1: Flow<T1>, flow2: Flow<T2>, flow3: Flow<T3>, flow4: Flow<T4>, collectBlock: (suspend (T1, T2, T3, T4) -> Unit)) {
30+
launch {
31+
combine(flow1, flow2, flow3, flow4) { v1, v2, v3, v4 ->
32+
collectBlock.invoke(v1, v2, v3, v4)
33+
}.collect {
34+
// Empty collect block to trigger ^
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)