From 3177128e33c074098343517f8a173835b9956ee5 Mon Sep 17 00:00:00 2001 From: Dmitriy Berdnikov Date: Wed, 29 Nov 2023 14:32:28 +0300 Subject: [PATCH] introduce handy way to start Store when lifecycle reached CREATED state --- .../elmslie/android/renderer/ElmRenderer.kt | 114 +----------------- .../android/renderer/ElmRendererDelegate.kt | 105 ++++++++++++++++ .../samples/coroutines/timer/MainActivity.kt | 1 + .../samples/coroutines/timer/MainFragment.kt | 19 ++- 4 files changed, 120 insertions(+), 119 deletions(-) diff --git a/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRenderer.kt b/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRenderer.kt index b555d54a..945c4016 100644 --- a/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRenderer.kt +++ b/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRenderer.kt @@ -1,140 +1,36 @@ package money.vivid.elmslie.android.renderer -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.annotation.MainThread -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle.State.RESUMED import androidx.lifecycle.Lifecycle.State.STARTED -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.coroutineScope import androidx.lifecycle.flowWithLifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.withCreated -import androidx.savedstate.SavedStateRegistryOwner import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import money.vivid.elmslie.android.elmStore import money.vivid.elmslie.core.config.ElmslieConfig import money.vivid.elmslie.core.store.Store -@Suppress("LongParameterList") -@MainThread -fun < - Event : Any, - Effect : Any, - State : Any, - > elmStoreWithRenderer( - lifecycleOwner: LifecycleOwner, - key: String = lifecycleOwner::class.java.canonicalName ?: lifecycleOwner::class.java.simpleName, - elmRenderer: ElmRendererDelegate, - viewModelStoreOwner: () -> ViewModelStoreOwner, - savedStateRegistryOwner: () -> SavedStateRegistryOwner, - defaultArgs: () -> Bundle, - saveState: Bundle.(State) -> Unit = {}, - storeFactory: SavedStateHandle.() -> Store, -): Lazy> { - val lazyStore = elmStore( - storeFactory = storeFactory, - key = key, - viewModelStoreOwner = viewModelStoreOwner, - savedStateRegistryOwner = savedStateRegistryOwner, - saveState = saveState, - defaultArgs = defaultArgs, - ) - with(lifecycleOwner) { - lifecycleScope.launch { - withCreated { - ElmRenderer( - lazyStore.value, - elmRenderer, - lifecycle, - ) - } - } - } - return lazyStore -} - -@Suppress("LongParameterList") -@MainThread -fun < - Event : Any, - Effect : Any, - State : Any, - > Fragment.elmStoreWithRenderer( - key: String = this::class.java.canonicalName ?: this::class.java.simpleName, - elmRenderer: ElmRendererDelegate, - viewModelStoreOwner: () -> ViewModelStoreOwner = { this }, - savedStateRegistryOwner: () -> SavedStateRegistryOwner = { this }, - defaultArgs: () -> Bundle = { arguments ?: bundleOf() }, - saveState: Bundle.(State) -> Unit = {}, - storeFactory: SavedStateHandle.() -> Store, -): Lazy> { - return elmStoreWithRenderer( - lifecycleOwner = this, - key = key, - elmRenderer = elmRenderer, - viewModelStoreOwner = viewModelStoreOwner, - savedStateRegistryOwner = savedStateRegistryOwner, - defaultArgs = defaultArgs, - saveState = saveState, - storeFactory = storeFactory, - ) -} - -@Suppress("LongParameterList") -@MainThread -fun < - Event : Any, - Effect : Any, - State : Any, - > ComponentActivity.elmStoreWithRenderer( - key: String = this::class.java.canonicalName ?: this::class.java.simpleName, - elmRenderer: ElmRendererDelegate, - viewModelStoreOwner: () -> ViewModelStoreOwner = { this }, - savedStateRegistryOwner: () -> SavedStateRegistryOwner = { this }, - defaultArgs: () -> Bundle = { intent?.extras ?: bundleOf() }, - saveState: Bundle.(State) -> Unit = {}, - storeFactory: SavedStateHandle.() -> Store, -): Lazy> { - return elmStoreWithRenderer( - lifecycleOwner = this, - key = key, - elmRenderer = elmRenderer, - viewModelStoreOwner = viewModelStoreOwner, - savedStateRegistryOwner = savedStateRegistryOwner, - defaultArgs = defaultArgs, - saveState = saveState, - storeFactory = storeFactory, - ) -} - internal class ElmRenderer( private val store: Store<*, Effect, State>, private val delegate: ElmRendererDelegate, - private val screenLifecycle: Lifecycle, + private val lifecycle: Lifecycle, ) { private val logger = ElmslieConfig.logger private val ioDispatcher: CoroutineDispatcher = ElmslieConfig.ioDispatchers private val canRender - get() = screenLifecycle.currentState.isAtLeast(STARTED) + get() = lifecycle.currentState.isAtLeast(STARTED) init { - with(screenLifecycle) { + with(lifecycle) { coroutineScope.launch { store .effects .flowWithLifecycle( - lifecycle = screenLifecycle, + lifecycle = lifecycle, minActiveState = RESUMED, ) .collect { effect -> catchEffectErrors { delegate.handleEffect(effect) } } @@ -143,7 +39,7 @@ internal class ElmRenderer( store .states .flowWithLifecycle( - lifecycle = screenLifecycle, + lifecycle = lifecycle, minActiveState = STARTED, ) .map { state -> diff --git a/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRendererDelegate.kt b/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRendererDelegate.kt index 718c6e4b..d3526208 100644 --- a/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRendererDelegate.kt +++ b/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRendererDelegate.kt @@ -1,5 +1,20 @@ package money.vivid.elmslie.android.renderer +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.annotation.MainThread +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.withCreated +import androidx.savedstate.SavedStateRegistryOwner +import kotlinx.coroutines.launch +import money.vivid.elmslie.android.elmStore +import money.vivid.elmslie.core.store.Store + @Suppress("OptionalUnit") interface ElmRendererDelegate { fun render(state: State) @@ -7,3 +22,93 @@ interface ElmRendererDelegate { fun mapList(state: State): List = emptyList() fun renderList(state: State, list: List): Unit = Unit } + +/** + * The function makes a connection between the store and the lifecycle owner by collecting states and effects + * and calling corresponds callbacks. + * + * Store creates and connects all required entities when given lifecycle reached CREATED state. + * + * In order to access previously saved state (via [saveState]) in [storeFactory] one must use + * SavedStateHandle.get(StateBundleKey) + */ +@Suppress("LongParameterList") +@MainThread +fun < + Event : Any, + Effect : Any, + State : Any, + > ElmRendererDelegate.connectedElmStore( + key: String = this::class.java.canonicalName ?: this::class.java.simpleName, + defaultArgs: () -> Bundle = { + val args = when (this) { + is Fragment -> arguments + is ComponentActivity -> intent.extras + else -> null + } + args ?: bundleOf() + }, + saveState: Bundle.(State) -> Unit = {}, + storeFactory: SavedStateHandle.() -> Store, +): Lazy> { + require(this is ViewModelStoreOwner) { + "Should implement [ViewModelStoreOwner]" + } + require(this is SavedStateRegistryOwner) { + "Should implement [SavedStateRegistryOwner]" + } + return connectedElmStore( + key = key, + viewModelStoreOwner = { this }, + savedStateRegistryOwner = { this }, + defaultArgs = defaultArgs, + saveState = saveState, + storeFactory = storeFactory, + ) +} + +@Suppress("LongParameterList") +@MainThread +fun < + Event : Any, + Effect : Any, + State : Any, + > ElmRendererDelegate.connectedElmStore( + key: String = this::class.java.canonicalName ?: this::class.java.simpleName, + viewModelStoreOwner: () -> ViewModelStoreOwner, + savedStateRegistryOwner: () -> SavedStateRegistryOwner, + defaultArgs: () -> Bundle = { + val args = when (this) { + is Fragment -> arguments + is ComponentActivity -> intent.extras + else -> null + } + args ?: bundleOf() + }, + saveState: Bundle.(State) -> Unit = {}, + storeFactory: SavedStateHandle.() -> Store, +): Lazy> { + require(this is LifecycleOwner) { + "Should implement [LifecycleOwner]" + } + val lazyStore = elmStore( + storeFactory = storeFactory, + key = key, + viewModelStoreOwner = viewModelStoreOwner, + savedStateRegistryOwner = savedStateRegistryOwner, + saveState = saveState, + defaultArgs = defaultArgs, + ) + with(this) { + lifecycleScope.launch { + withCreated { + ElmRenderer( + store = lazyStore.value, + delegate = this@with, + lifecycle = lifecycle, + ) + } + } + } + return lazyStore +} diff --git a/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainActivity.kt b/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainActivity.kt index c3f78f61..cfb7f34d 100644 --- a/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainActivity.kt +++ b/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainActivity.kt @@ -3,6 +3,7 @@ package money.vivid.elmslie.samples.coroutines.timer import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.commit +import vivid.money.elmslie.samples.coroutines.timer.R import kotlin.random.Random internal class MainActivity : AppCompatActivity(R.layout.activity_main) { diff --git a/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainFragment.kt b/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainFragment.kt index cd2b3814..f45fcf08 100644 --- a/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainFragment.kt +++ b/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainFragment.kt @@ -12,11 +12,12 @@ import androidx.fragment.app.Fragment import com.google.android.material.snackbar.Snackbar import money.vivid.elmslie.android.RetainedElmStore.Companion.StateBundleKey import money.vivid.elmslie.android.renderer.ElmRendererDelegate -import money.vivid.elmslie.android.renderer.elmStoreWithRenderer +import money.vivid.elmslie.android.renderer.connectedElmStore import money.vivid.elmslie.samples.coroutines.timer.elm.Effect import money.vivid.elmslie.samples.coroutines.timer.elm.Event import money.vivid.elmslie.samples.coroutines.timer.elm.State import money.vivid.elmslie.samples.coroutines.timer.elm.storeFactory +import vivid.money.elmslie.samples.coroutines.timer.R internal class MainFragment : Fragment(R.layout.fragment_main), ElmRendererDelegate { @@ -28,16 +29,14 @@ internal class MainFragment : Fragment(R.layout.fragment_main), ElmRendererDeleg MainFragment().apply { arguments = bundleOf(ARG to id) } } - private val store by elmStoreWithRenderer( - elmRenderer = this, - storeFactory = { - storeFactory( - id = get(ARG)!!, - generatedId = get(StateBundleKey)?.getString(GENERATED_ID), - ) - }, + private val store by connectedElmStore( saveState = { state -> putString(GENERATED_ID, state.generatedId) }, - ) + ) { + storeFactory( + id = get(ARG)!!, + generatedId = get(StateBundleKey)?.getString(GENERATED_ID), + ) + } private lateinit var startButton: Button private lateinit var stopButton: Button