diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index d8d3993c..9f770f19 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -7,15 +7,6 @@
-
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 61a9130c..fb7f4a8a 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 25daa0e7..dc743493 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,7 +4,7 @@
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 0dd4b354..6d4f6e02 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -3,4 +3,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index d5d35ec4..ef61796f 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/core/src/main/java/com/hoc/flowmvi/core/CollectIn.kt b/core/src/main/java/com/hoc/flowmvi/core/CollectIn.kt
new file mode 100644
index 00000000..da7e2952
--- /dev/null
+++ b/core/src/main/java/com/hoc/flowmvi/core/CollectIn.kt
@@ -0,0 +1,35 @@
+package com.hoc.flowmvi.core
+
+import android.util.Log
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.addRepeatingJob
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+inline fun Flow.collectIn(
+ owner: LifecycleOwner,
+ minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ crossinline action: suspend (value: T) -> Unit,
+): Job = owner.addRepeatingJob(state = minActiveState, coroutineContext = coroutineContext) {
+ Log.d("collectIn", "Start collecting...")
+ collect { action(it) }
+}
+
+@Suppress("unused")
+inline fun Flow.collectIn(
+ fragment: Fragment,
+ minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ crossinline action: suspend (value: T) -> Unit,
+): Job = collectIn(
+ fragment.viewLifecycleOwner,
+ minActiveState = minActiveState,
+ coroutineContext = coroutineContext,
+ action = action,
+)
diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
index 89f89272..6956eaa8 100644
--- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
+++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
@@ -7,12 +7,11 @@ import android.util.Log
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isInvisible
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import com.hoc.flowmvi.core.clicks
+import com.hoc.flowmvi.core.collectIn
import com.hoc.flowmvi.core.firstChange
import com.hoc.flowmvi.core.navigator.IntentProviders
import com.hoc.flowmvi.core.textChanges
@@ -53,15 +52,11 @@ class AddActivity : AppCompatActivity() {
private fun bindVM(addVM: AddVM) {
// observe view model
addVM.viewState
- .onEach { render(it) }
- .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
- .launchIn(lifecycleScope)
+ .collectIn(this) { render(it) }
// observe single event
addVM.singleEvent
- .onEach { handleSingleEvent(it) }
- .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
- .launchIn(lifecycleScope)
+ .collectIn(this) { handleSingleEvent(it) }
// pass view intent to view model
intents()
diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
index d781e5a6..c27a91d9 100644
--- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
+++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
@@ -47,9 +48,9 @@ internal class AddVM(
init {
val initialVS = ViewState.initial(
- email = savedStateHandle.get("email"),
- firstName = savedStateHandle.get("first_name"),
- lastName = savedStateHandle.get("last_name"),
+ email = savedStateHandle.get(EMAIL_KEY),
+ firstName = savedStateHandle.get(FIRST_NAME_KEY),
+ lastName = savedStateHandle.get(LAST_NAME_KEY),
)
Log.d("###", "[ADD_VM] initialVS: $initialVS")
@@ -85,6 +86,7 @@ internal class AddVM(
private fun Flow.toPartialStateChangesFlow(): Flow {
val emailErrors = filterIsInstance()
.map { it.email }
+ .distinctUntilChanged()
.map { validateEmail(it) to it }
.shareIn(
scope = viewModelScope,
@@ -93,6 +95,7 @@ internal class AddVM(
val firstNameErrors = filterIsInstance()
.map { it.firstName }
+ .distinctUntilChanged()
.map { validateFirstName(it) to it }
.shareIn(
scope = viewModelScope,
@@ -101,6 +104,7 @@ internal class AddVM(
val lastNameErrors = filterIsInstance()
.map { it.lastName }
+ .distinctUntilChanged()
.map { validateLastName(it) to it }
.shareIn(
scope = viewModelScope,
@@ -151,15 +155,15 @@ internal class AddVM(
val formValuesChanges = merge(
emailErrors
.map { it.second }
- .onEach { savedStateHandle.set("email", it) }
+ .onEach { savedStateHandle.set(EMAIL_KEY, it) }
.map { PartialStateChange.FormValueChange.EmailChanged(it) },
firstNameErrors
.map { it.second }
- .onEach { savedStateHandle.set("first_name", it) }
+ .onEach { savedStateHandle.set(FIRST_NAME_KEY, it) }
.map { PartialStateChange.FormValueChange.FirstNameChanged(it) },
lastNameErrors
.map { it.second }
- .onEach { savedStateHandle.set("last_name", it) }
+ .onEach { savedStateHandle.set(LAST_NAME_KEY, it) }
.map { PartialStateChange.FormValueChange.LastNameChanged(it) },
)
@@ -178,6 +182,10 @@ internal class AddVM(
}
private companion object {
+ const val EMAIL_KEY = "email"
+ const val FIRST_NAME_KEY = "first_name"
+ const val LAST_NAME_KEY = "last_name"
+
const val MIN_LENGTH_FIRST_NAME = 3
const val MIN_LENGTH_LAST_NAME = 3
diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt
index f2bef2ac..76b32306 100644
--- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt
+++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt
@@ -6,8 +6,6 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
@@ -15,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.hoc.flowmvi.core.SwipeLeftToDeleteCallback
import com.hoc.flowmvi.core.clicks
+import com.hoc.flowmvi.core.collectIn
import com.hoc.flowmvi.core.navigator.Navigator
import com.hoc.flowmvi.core.refreshes
import com.hoc.flowmvi.core.safeOffer
@@ -86,15 +85,11 @@ class MainActivity : AppCompatActivity() {
private fun bindVM(mainVM: MainVM) {
// observe view model
mainVM.viewState
- .onEach { render(it) }
- .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
- .launchIn(lifecycleScope)
+ .collectIn(this) { render(it) }
// observe single event
mainVM.singleEvent
- .onEach { handleSingleEvent(it) }
- .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
- .launchIn(lifecycleScope)
+ .collectIn(this) { handleSingleEvent(it) }
// pass view intent to view model
intents()