Skip to content

Commit af130f5

Browse files
authored
Launch in + SavedStateHandle (#7)
* chore(refactor) * update activity_add.xml * wip * wip * done * done
1 parent 0f725fe commit af130f5

File tree

15 files changed

+369
-164
lines changed

15 files changed

+369
-164
lines changed

.idea/gradle.xml

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

app/src/main/java/com/hoc/flowmvi/App.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import org.koin.android.ext.koin.androidContext
1212
import org.koin.android.ext.koin.androidLogger
1313
import org.koin.core.context.startKoin
1414
import org.koin.core.logger.Level
15+
import kotlin.time.ExperimentalTime
1516

1617
@Suppress("unused")
1718
@FlowPreview
1819
@ExperimentalCoroutinesApi
20+
@ExperimentalTime
1921
class App : Application() {
2022
override fun onCreate() {
2123
super.onCreate()

buildSrc/src/main/kotlin/deps.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ object deps {
3535

3636
const val viewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$version" // viewModelScope
3737
const val runtimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:$version" // lifecycleScope
38+
const val commonJava8 = "androidx.lifecycle:lifecycle-common-java8:$version"
3839
}
3940

4041
object squareup {
@@ -52,7 +53,7 @@ object deps {
5253
}
5354

5455
object koin {
55-
private const val version = "2.2.0-rc-4"
56+
private const val version = "2.2.2"
5657

5758
const val androidXViewModel = "org.koin:koin-androidx-viewmodel:$version"
5859
const val core = "org.koin:koin-core:$version"

core/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ dependencies {
4141
implementation(deps.androidx.swipeRefreshLayout)
4242
implementation(deps.androidx.recyclerView)
4343
implementation(deps.androidx.material)
44+
45+
implementation(deps.lifecycle.commonJava8)
46+
implementation(deps.lifecycle.runtimeKtx)
4447
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.hoc.flowmvi.core
2+
3+
sealed class Either<out L, out R> {
4+
data class Left<L>(val l: L) : Either<L, Nothing>()
5+
data class Right<R>(val r: R) : Either<Nothing, R>()
6+
7+
fun rightOrNull(): R? = when (this) {
8+
is Left -> null
9+
is Right -> r
10+
}
11+
12+
fun leftOrNull(): L? = when (this) {
13+
is Left -> l
14+
is Right -> null
15+
}
16+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.hoc.flowmvi.core
2+
3+
import androidx.lifecycle.DefaultLifecycleObserver
4+
import androidx.lifecycle.Lifecycle
5+
import androidx.lifecycle.LifecycleOwner
6+
import androidx.lifecycle.lifecycleScope
7+
import kotlinx.coroutines.Job
8+
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.flow.launchIn
10+
11+
fun <T> Flow<T>.launchWhenStartedUntilStopped(owner: LifecycleOwner) {
12+
if (owner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
13+
// ignore
14+
return
15+
}
16+
owner.lifecycle.addObserver(LifecycleBoundObserver(this))
17+
}
18+
19+
private class LifecycleBoundObserver(private val flow: Flow<*>) : DefaultLifecycleObserver {
20+
private var job: Job? = null
21+
22+
override fun onStart(owner: LifecycleOwner) {
23+
job = flow.launchIn(owner.lifecycleScope)
24+
}
25+
26+
override fun onStop(owner: LifecycleOwner) {
27+
cancelJob()
28+
}
29+
30+
override fun onDestroy(owner: LifecycleOwner) {
31+
super.onDestroy(owner)
32+
owner.lifecycle.removeObserver(this)
33+
cancelJob()
34+
}
35+
36+
@Suppress("NOTHING_TO_INLINE")
37+
private inline fun cancelJob() {
38+
job?.cancel()
39+
job = null
40+
}
41+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.hoc.flowmvi.core
2+
3+
import kotlinx.coroutines.delay
4+
import kotlin.time.Duration
5+
import kotlin.time.ExperimentalTime
6+
7+
@ExperimentalTime
8+
suspend inline fun <T> retrySuspend(
9+
times: Int,
10+
initialDelay: Duration,
11+
factor: Double,
12+
maxDelay: Duration = Duration.INFINITE,
13+
shouldRetry: (Throwable) -> Boolean = { true },
14+
block: (times: Int) -> T,
15+
): T {
16+
var currentDelay = initialDelay
17+
repeat(times - 1) {
18+
try {
19+
return block(it)
20+
} catch (e: Throwable) {
21+
if (!shouldRetry(e)) {
22+
throw e
23+
}
24+
// you can log an error here and/or make a more finer-grained
25+
// analysis of the cause to see if retry is needed
26+
}
27+
delay(currentDelay)
28+
currentDelay = (currentDelay * factor).coerceAtMost(maxDelay)
29+
}
30+
return block(times - 1) // last attempt
31+
}

data/src/main/java/com/hoc/flowmvi/data/DataModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import org.koin.core.qualifier.named
1717
import org.koin.dsl.module
1818
import retrofit2.Retrofit
1919
import retrofit2.converter.moshi.MoshiConverterFactory
20+
import kotlin.time.ExperimentalTime
2021

2122
private const val BASE_URL = "BASE_URL"
2223

24+
@ExperimentalTime
2325
@ExperimentalCoroutinesApi
2426
@FlowPreview
2527
val dataModule = module {

data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.hoc.flowmvi.data
33
import android.util.Log
44
import com.hoc.flowmvi.core.Mapper
55
import com.hoc.flowmvi.core.dispatchers.CoroutineDispatchers
6+
import com.hoc.flowmvi.core.retrySuspend
67
import com.hoc.flowmvi.data.remote.UserApiService
78
import com.hoc.flowmvi.data.remote.UserBody
89
import com.hoc.flowmvi.data.remote.UserResponse
@@ -18,7 +19,10 @@ import kotlinx.coroutines.flow.flow
1819
import kotlinx.coroutines.flow.onEach
1920
import kotlinx.coroutines.flow.scan
2021
import kotlinx.coroutines.withContext
22+
import kotlin.time.ExperimentalTime
23+
import kotlin.time.milliseconds
2124

25+
@ExperimentalTime
2226
@ExperimentalCoroutinesApi
2327
internal class UserRepositoryImpl constructor(
2428
private val userApiService: UserApiService,
@@ -34,11 +38,18 @@ internal class UserRepositoryImpl constructor(
3438
class Added(val user: User) : Change()
3539
}
3640

37-
private val changesFlow = MutableSharedFlow<Change>()
41+
private val changesFlow = MutableSharedFlow<Change>(extraBufferCapacity = 64)
3842

3943
private suspend fun getUsersFromRemote(): List<User> {
4044
return withContext(dispatchers.io) {
41-
userApiService.getUsers().map(responseToDomain)
45+
retrySuspend(
46+
times = 3,
47+
initialDelay = 500.milliseconds,
48+
factor = 2.0,
49+
) {
50+
Log.d("###", "[USER_REPO] Retry times=$it")
51+
userApiService.getUsers().map(responseToDomain)
52+
}
4253
}
4354
}
4455

feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,25 @@ import androidx.transition.AutoTransition
1212
import androidx.transition.TransitionManager
1313
import com.hoc.flowmvi.core.clicks
1414
import com.hoc.flowmvi.core.firstChange
15+
import com.hoc.flowmvi.core.launchWhenStartedUntilStopped
1516
import com.hoc.flowmvi.core.navigator.IntentProviders
1617
import com.hoc.flowmvi.core.textChanges
1718
import com.hoc.flowmvi.core.toast
1819
import com.hoc.flowmvi.ui.add.databinding.ActivityAddBinding
1920
import kotlinx.coroutines.ExperimentalCoroutinesApi
2021
import kotlinx.coroutines.FlowPreview
2122
import kotlinx.coroutines.flow.Flow
22-
import kotlinx.coroutines.flow.catch
23-
import kotlinx.coroutines.flow.collect
2423
import kotlinx.coroutines.flow.launchIn
2524
import kotlinx.coroutines.flow.map
2625
import kotlinx.coroutines.flow.merge
2726
import kotlinx.coroutines.flow.onEach
2827
import org.koin.androidx.viewmodel.ext.android.viewModel
28+
import org.koin.androidx.viewmodel.scope.emptyState
2929

3030
@FlowPreview
3131
@ExperimentalCoroutinesApi
3232
class AddActivity : AppCompatActivity() {
33-
private val addVM by viewModel<AddVM>()
33+
private val addVM by viewModel<AddVM>(state = emptyState())
3434
private val addBinding by lazy { ActivityAddBinding.inflate(layoutInflater) }
3535

3636
override fun onCreate(savedInstanceState: Bundle?) {
@@ -51,16 +51,14 @@ class AddActivity : AppCompatActivity() {
5151

5252
private fun bindVM(addVM: AddVM) {
5353
// observe view model
54-
lifecycleScope.launchWhenStarted {
55-
addVM.viewState
56-
.onEach { render(it) }
57-
.collect()
58-
}
59-
lifecycleScope.launchWhenStarted {
60-
addVM.singleEvent
61-
.onEach { handleSingleEvent(it) }
62-
.collect()
63-
}
54+
addVM.viewState
55+
.onEach { render(it) }
56+
.launchWhenStartedUntilStopped(this)
57+
58+
// observe single event
59+
addVM.singleEvent
60+
.onEach { handleSingleEvent(it) }
61+
.launchWhenStartedUntilStopped(this)
6462

6563
// pass view intent to view model
6664
intents()
@@ -124,7 +122,15 @@ class AddActivity : AppCompatActivity() {
124122
addBinding.addButton.isInvisible = viewState.isLoading
125123
}
126124

127-
private fun setupViews() = Unit
125+
private fun setupViews() {
126+
val state = addVM.viewState.value
127+
128+
addBinding.run {
129+
emailEditText.editText!!.setText(state.email)
130+
firstNameEditText.editText!!.setText(state.firstName)
131+
lastNameEditText.editText!!.setText(state.lastName)
132+
}
133+
}
128134

129135
private fun intents(): Flow<ViewIntent> = addBinding.run {
130136
merge(

feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,29 @@ internal enum class ValidationError {
1111
internal data class ViewState(
1212
val errors: Set<ValidationError>,
1313
val isLoading: Boolean,
14+
//
1415
val emailChanged: Boolean,
1516
val firstNameChanged: Boolean,
1617
val lastNameChanged: Boolean,
18+
//
19+
val email: String?,
20+
val firstName: String?,
21+
val lastName: String?
1722
) {
1823
companion object {
19-
fun initial() = ViewState(
24+
fun initial(
25+
email: String?,
26+
firstName: String?,
27+
lastName: String?
28+
) = ViewState(
2029
errors = emptySet(),
2130
isLoading = false,
2231
emailChanged = false,
2332
firstNameChanged = false,
2433
lastNameChanged = false,
34+
email = email,
35+
firstName = firstName,
36+
lastName = lastName,
2537
)
2638
}
2739
}
@@ -72,6 +84,20 @@ internal sealed class PartialStateChange {
7284
}
7385
}
7486
}
87+
88+
sealed class FormValueChange : PartialStateChange() {
89+
override fun reduce(viewState: ViewState): ViewState {
90+
return when (this) {
91+
is EmailChanged -> viewState.copy(email = email)
92+
is FirstNameChanged -> viewState.copy(firstName = firstName)
93+
is LastNameChanged -> viewState.copy(lastName = lastName)
94+
}
95+
}
96+
97+
data class EmailChanged(val email: String?) : FormValueChange()
98+
data class FirstNameChanged(val firstName: String?) : FormValueChange()
99+
data class LastNameChanged(val lastName: String?) : FormValueChange()
100+
}
75101
}
76102

77103
internal sealed class SingleEvent {

feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddModule.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import org.koin.dsl.module
99
@ExperimentalCoroutinesApi
1010
@FlowPreview
1111
val addModule = module {
12-
viewModel { AddVM(addUser = get()) }
12+
viewModel {
13+
AddVM(
14+
addUser = get(),
15+
savedStateHandle = it.get(),
16+
)
17+
}
1318

1419
single<IntentProviders.Add> { AddActivity.IntentProvider() }
1520
}

0 commit comments

Comments
 (0)