Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce handy way to start Store when lifecycle reached CREATED state #251

Merged
merged 1 commit into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,117 @@
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)
*
* NOTE: If you implement your own ElmRendererDelegate, you should also implement the following interfaces:
* [ViewModelStoreOwner], [SavedStateRegistryOwner], [LifecycleOwner].
*/
@Suppress("LongParameterList")
@MainThread
fun <
Event : Any,
Effect : Any,
State : Any,
> ElmRendererDelegate<Effect, State>.androidElmStore(
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 androidElmStore(
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>.androidElmStore(
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 money.vivid.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.androidElmStore
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 money.vivid.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 androidElmStore(
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
Loading