Skip to content

Commit

Permalink
Merge pull request #251 from vivid-money/feature/elm-renderer
Browse files Browse the repository at this point in the history
introduce handy way to start Store when lifecycle reached CREATED state
  • Loading branch information
rcmkt authored Feb 5, 2024
2 parents 031d684 + 6bdca6a commit fec3f6f
Show file tree
Hide file tree
Showing 4 changed files with 123 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,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

0 comments on commit fec3f6f

Please sign in to comment.