Skip to content

Commit

Permalink
introduce handy way to start Store when lifecycle reached CREATED state
Browse files Browse the repository at this point in the history
  • Loading branch information
Dmitriy Berdnikov committed Feb 5, 2024
1 parent 031d684 commit 3177128
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -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<Effect, State>,
viewModelStoreOwner: () -> ViewModelStoreOwner,
savedStateRegistryOwner: () -> SavedStateRegistryOwner,
defaultArgs: () -> Bundle,
saveState: Bundle.(State) -> Unit = {},
storeFactory: SavedStateHandle.() -> Store<Event, Effect, State>,
): Lazy<Store<Event, Effect, State>> {
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<Effect, State>,
viewModelStoreOwner: () -> ViewModelStoreOwner = { this },
savedStateRegistryOwner: () -> SavedStateRegistryOwner = { this },
defaultArgs: () -> Bundle = { arguments ?: bundleOf() },
saveState: Bundle.(State) -> Unit = {},
storeFactory: SavedStateHandle.() -> Store<Event, Effect, State>,
): Lazy<Store<Event, Effect, State>> {
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<Effect, State>,
viewModelStoreOwner: () -> ViewModelStoreOwner = { this },
savedStateRegistryOwner: () -> SavedStateRegistryOwner = { this },
defaultArgs: () -> Bundle = { intent?.extras ?: bundleOf() },
saveState: Bundle.(State) -> Unit = {},
storeFactory: SavedStateHandle.() -> Store<Event, Effect, State>,
): Lazy<Store<Event, Effect, State>> {
return elmStoreWithRenderer(
lifecycleOwner = this,
key = key,
elmRenderer = elmRenderer,
viewModelStoreOwner = viewModelStoreOwner,
savedStateRegistryOwner = savedStateRegistryOwner,
defaultArgs = defaultArgs,
saveState = saveState,
storeFactory = storeFactory,
)
}

internal class ElmRenderer<Effect : Any, State : Any>(
private val store: Store<*, Effect, State>,
private val delegate: ElmRendererDelegate<Effect, State>,
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) } }
Expand All @@ -143,7 +39,7 @@ internal class ElmRenderer<Effect : Any, State : Any>(
store
.states
.flowWithLifecycle(
lifecycle = screenLifecycle,
lifecycle = lifecycle,
minActiveState = STARTED,
)
.map { state ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,114 @@
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<Effect : Any, State : Any> {
fun render(state: State)
fun handleEffect(effect: Effect): Unit? = Unit
fun mapList(state: State): List<Any> = emptyList()
fun renderList(state: State, list: List<Any>): 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<Bundle>(StateBundleKey)
*/
@Suppress("LongParameterList")
@MainThread
fun <
Event : Any,
Effect : Any,
State : Any,
> ElmRendererDelegate<Effect, State>.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<Event, Effect, State>,
): Lazy<Store<Event, Effect, State>> {
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<Effect, State>.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<Event, Effect, State>,
): Lazy<Store<Event, Effect, State>> {
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Effect, State> {

Expand All @@ -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<Bundle>(StateBundleKey)?.getString(GENERATED_ID),
)
},
private val store by connectedElmStore(
saveState = { state -> putString(GENERATED_ID, state.generatedId) },
)
) {
storeFactory(
id = get(ARG)!!,
generatedId = get<Bundle>(StateBundleKey)?.getString(GENERATED_ID),
)
}

private lateinit var startButton: Button
private lateinit var stopButton: Button
Expand Down

0 comments on commit 3177128

Please sign in to comment.