Skip to content

Commit

Permalink
Refactoring FragmentFlowStore.
Browse files Browse the repository at this point in the history
  • Loading branch information
Laimiux committed Sep 3, 2024
1 parent 2dd6ea3 commit fde869a
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 323 deletions.
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
package com.instacart.formula.android

import com.instacart.formula.Evaluation
import com.instacart.formula.Formula
import com.instacart.formula.RuntimeConfig
import com.instacart.formula.Snapshot
import com.instacart.formula.android.internal.Binding
import com.instacart.formula.android.events.FragmentLifecycleEvent
import com.instacart.formula.android.internal.CompositeBinding
import com.instacart.formula.android.internal.FeatureObservableAction
import com.instacart.formula.android.utils.MainThreadDispatcher
import com.instacart.formula.rxjava3.RxAction
import com.instacart.formula.rxjava3.toObservable
import com.jakewharton.rxrelay3.PublishRelay
import io.reactivex.rxjava3.core.Observable

/**
* A FragmentFlowStore is responsible for managing the state of multiple [FragmentKey] instances.
*/
class FragmentFlowStore @PublishedApi internal constructor(
private val root: CompositeBinding<*>,
) : Formula<FragmentEnvironment, FragmentFlowState, FragmentFlowState>() {
abstract class FragmentFlowStore {
companion object {
inline fun init(
crossinline init: FragmentStoreBuilder<Unit>.() -> Unit
Expand All @@ -32,119 +19,13 @@ class FragmentFlowStore @PublishedApi internal constructor(
crossinline contracts: FragmentStoreBuilder<Component>.() -> Unit
): FragmentFlowStore {
val bindings = FragmentStoreBuilder.build(contracts)
val root = CompositeBinding(rootComponent, bindings.bindings)
return FragmentFlowStore(root)
return FragmentFlowStoreImpl(rootComponent, bindings)
}
}

internal abstract fun onLifecycleEffect(event: FragmentLifecycleEvent)

private val lifecycleEvents = PublishRelay.create<FragmentLifecycleEvent>()
private val visibleContractEvents = PublishRelay.create<FragmentId>()
private val hiddenContractEvents = PublishRelay.create<FragmentId>()
internal abstract fun onVisibilityChanged(contract: FragmentId, visible: Boolean)

private val lifecycleEventStream = RxAction.fromObservable { lifecycleEvents }
private val visibleContractEventStream = RxAction.fromObservable { visibleContractEvents }
private val hiddenContractEventStream = RxAction.fromObservable { hiddenContractEvents }

internal fun onLifecycleEffect(event: FragmentLifecycleEvent) {
lifecycleEvents.accept(event)
}

internal fun onVisibilityChanged(contract: FragmentId, visible: Boolean) {
if (visible) {
visibleContractEvents.accept(contract)
} else {
hiddenContractEvents.accept(contract)
}
}

override fun initialState(input: FragmentEnvironment): FragmentFlowState = FragmentFlowState()

override fun Snapshot<FragmentEnvironment, FragmentFlowState>.evaluate(): Evaluation<FragmentFlowState> {
val rootInput = Binding.Input(
environment = input,
component = Unit,
activeFragments = state.activeIds,
onInitializeFeature = context.onEvent { event ->
val features = state.features.plus(event.id to event)
transition(state.copy(features = features))
}
)
root.bind(context, rootInput)

return Evaluation(
output = state,
actions = context.actions {
lifecycleEventStream.onEvent { event ->
val fragmentId = event.fragmentId
when (event) {
is FragmentLifecycleEvent.Removed -> {
val updated = state.copy(
activeIds = state.activeIds.minus(fragmentId),
states = state.states.minus(fragmentId),
features = state.features.minus(fragmentId)
)
transition(updated)
}
is FragmentLifecycleEvent.Added -> {
if (!state.activeIds.contains(fragmentId)) {
if (root.binds(fragmentId.key)) {
val updated = state.copy(activeIds = state.activeIds.plus(fragmentId))
transition(updated)
} else {
val updated = state.copy(
activeIds = state.activeIds.plus(fragmentId),
features = state.features.plus(fragmentId to FeatureEvent.MissingBinding(fragmentId))
)
transition(updated)
}
} else {
none()
}
}
}
}

visibleContractEventStream.onEvent {
if (state.visibleIds.contains(it)) {
// TODO: should we log this duplicate visibility event?
none()
} else {
transition(state.copy(visibleIds = state.visibleIds.plus(it)))
}
}

hiddenContractEventStream.onEvent {
transition(state.copy(visibleIds = state.visibleIds.minus(it)))
}

state.features.entries.forEach { entry ->
val fragmentId = entry.key
val feature = (entry.value as? FeatureEvent.Init)?.feature
if (feature != null) {
val action = FeatureObservableAction(
fragmentEnvironment = input,
fragmentId = fragmentId,
feature = feature,
)
action.onEvent {
if (state.activeIds.contains(fragmentId)) {
val keyState = FragmentState(fragmentId.key, it)
transition(state.copy(states = state.states.plus(fragmentId to keyState)))
} else {
none()
}
}
}
}
}
)
}

internal fun state(environment: FragmentEnvironment): Observable<FragmentFlowState> {
val config = RuntimeConfig(
defaultDispatcher = MainThreadDispatcher(),
)
return toObservable(environment, config)
}
internal abstract fun state(environment: FragmentEnvironment): Observable<FragmentFlowState>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.instacart.formula.android

import com.instacart.formula.RuntimeConfig
import com.instacart.formula.android.events.FragmentLifecycleEvent
import com.instacart.formula.android.internal.FeatureBinding
import com.instacart.formula.android.internal.FragmentFlowStoreFormula
import com.instacart.formula.android.utils.MainThreadDispatcher
import com.instacart.formula.rxjava3.toObservable
import io.reactivex.rxjava3.core.Observable

/**
* A FragmentFlowStore is responsible for managing the state of multiple [FragmentKey] instances.
*/
@PublishedApi
internal class FragmentFlowStoreImpl<in Component>(
component: Component,
bindings: List<FeatureBinding<Component, *>>,
) : FragmentFlowStore() {
private val formula = FragmentFlowStoreFormula(component, bindings)

override fun onLifecycleEffect(event: FragmentLifecycleEvent) {
formula.onLifecycleEffect(event)
}

override fun onVisibilityChanged(contract: FragmentId, visible: Boolean) {
formula.onVisibilityChanged(contract, visible)
}

override fun state(environment: FragmentEnvironment): Observable<FragmentFlowState> {
val config = RuntimeConfig(
defaultDispatcher = MainThreadDispatcher(),
)
return formula.toObservable(environment, config)
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.instacart.formula.android

import com.instacart.formula.android.internal.Binding
import com.instacart.formula.android.internal.Bindings
import com.instacart.formula.android.internal.FunctionUtils
import com.instacart.formula.android.internal.FeatureBinding
import com.instacart.formula.android.internal.MappedFeatureFactory
import java.lang.IllegalStateException
import kotlin.reflect.KClass

Expand All @@ -13,33 +11,16 @@ import kotlin.reflect.KClass
*/
class FragmentStoreBuilder<Component> {
companion object {

@PublishedApi
internal inline fun <Component> build(
init: FragmentStoreBuilder<Component>.() -> Unit
): Bindings<Component> {
): List<FeatureBinding<Component, *>> {
return FragmentStoreBuilder<Component>().apply(init).build()
}
}

private val types = mutableSetOf<Class<*>>()
private val bindings: MutableList<Binding<Component>> = mutableListOf()

/**
* Binds a [feature factory][FeatureFactory] for a specific [key][type].
*
* @param type The class which describes the [key][Key].
* @param featureFactory Feature factory that provides state observable and view rendering logic.
* @param toDependencies Maps [Component] to feature factory [dependencies][Dependencies].
*/
fun <Dependencies, Key : FragmentKey> bind(
type : KClass<Key>,
featureFactory: FeatureFactory<Dependencies, Key>,
toDependencies: (Component) -> Dependencies
) = apply {
val binding = FeatureBinding(type.java, featureFactory, toDependencies)
bind(binding as Binding<Component>)
}
private val bindings: MutableList<FeatureBinding<Component, *>> = mutableListOf()

/**
* Binds a [feature factory][FeatureFactory] for a specific [key][type].
Expand All @@ -49,9 +30,10 @@ class FragmentStoreBuilder<Component> {
*/
fun <Key : FragmentKey> bind(
type : KClass<Key>,
featureFactory: FeatureFactory<Component, Key>
featureFactory: FeatureFactory<Component, Key>,
) = apply {
bind(type, featureFactory, FunctionUtils.identity())
val binding = FeatureBinding(type.java, featureFactory)
bind(type.java, binding)
}

/**
Expand All @@ -67,8 +49,6 @@ class FragmentStoreBuilder<Component> {

/**
* A convenience inline function that binds a feature factory for a specific [key][Key].
*
* @param featureFactory Feature factory that provides state observable and view rendering logic.
*/
inline fun <reified Key: FragmentKey> bind(
crossinline initFeature: (Component, Key) -> Feature,
Expand All @@ -91,18 +71,20 @@ class FragmentStoreBuilder<Component> {
featureFactory: FeatureFactory<Dependencies, Key>,
noinline toDependencies: (Component) -> Dependencies
) = apply {
bind(Key::class, featureFactory, toDependencies)
val mapped = MappedFeatureFactory(
delegate = featureFactory,
toDependencies = toDependencies,
)

bind(Key::class, mapped)
}

@PublishedApi
internal fun build(): Bindings<Component> {
return Bindings(
bindings = bindings
)
internal fun build(): List<FeatureBinding<Component, *>> {
return bindings
}

private fun bind(binding: Binding<Component>) = apply {
val type = binding.type()
private fun bind(type: Class<*>, binding: FeatureBinding<Component, *>) = apply {
if (types.contains(type)) {
throw IllegalStateException("Binding for $type already exists")
}
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Loading

0 comments on commit fde869a

Please sign in to comment.