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 2e08ceb
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 341 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.instacart.formula.android.Feature
import com.instacart.formula.android.FeatureFactory
import com.instacart.formula.android.ViewFactory
import com.instacart.formula.android.events.ActivityResult
import com.instacart.formula.test.TestFragmentActivity
Expand All @@ -29,26 +30,30 @@ class FragmentAndroidEventTest {
initialContract = TestLifecycleKey()
},
contracts = {

bind<TestLifecycleKey> { _, _ ->
Feature(
state = activityResults().flatMap {
activityResults.add(it)
Observable.empty()
},
viewFactory = ViewFactory.fromLayout(R.layout.test_empty_layout) {
featureView { }
}
)
val featureFactory = object : FeatureFactory<Unit, TestLifecycleKey> {
override fun initialize(dependencies: Unit, key: TestLifecycleKey): Feature {
return Feature(
state = activityResults().flatMap {
activityResults.add(it)
Observable.empty()
},
viewFactory = ViewFactory.fromLayout(R.layout.test_empty_layout) {
featureView { }
}
)
}
}

bind(featureFactory)
}
)
}
}
},
cleanUp = {
activityResults.clear()
})
}
)

private val activityRule = ActivityScenarioRule(TestFragmentActivity::class.java)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
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.internal.FragmentFlowStoreFormula
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>() {
private val formula: FragmentFlowStoreFormula<*>,
) {
companion object {
inline fun init(
crossinline init: FragmentStoreBuilder<Unit>.() -> Unit
Expand All @@ -32,119 +25,23 @@ 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)
val formula = FragmentFlowStoreFormula(rootComponent, bindings)
return FragmentFlowStore(formula)
}
}


private val lifecycleEvents = PublishRelay.create<FragmentLifecycleEvent>()
private val visibleContractEvents = PublishRelay.create<FragmentId>()
private val hiddenContractEvents = PublishRelay.create<FragmentId>()

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)
formula.onLifecycleEffect(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()
}
}
}
}
}
)
formula.onVisibilityChanged(contract, visible)
}

internal fun state(environment: FragmentEnvironment): Observable<FragmentFlowState> {
val config = RuntimeConfig(
defaultDispatcher = MainThreadDispatcher(),
)
return toObservable(environment, config)
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,13 +30,14 @@ 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)
}

/**
* A convenience inline function that binds a feature factory for a specific [key][Key].
* Binds a feature factory for a [Key].
*
* @param featureFactory Feature factory that provides state observable and view rendering logic.
*/
Expand All @@ -65,22 +47,6 @@ class FragmentStoreBuilder<Component> {
bind(Key::class, featureFactory)
}

/**
* 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,
) = apply {
val factory = object : FeatureFactory<Component, Key> {
override fun initialize(dependencies: Component, key: Key): Feature {
return initFeature(dependencies, key)
}
}
bind(Key::class, factory)
}

/**
* A convenience inline function that binds a feature factory for a specific [key][Key].
*
Expand All @@ -91,18 +57,19 @@ 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.

Loading

0 comments on commit 2e08ceb

Please sign in to comment.