diff --git a/formula-android/src/main/java/com/instacart/formula/android/FragmentStore.kt b/formula-android/src/main/java/com/instacart/formula/android/FragmentStore.kt index 62d684d8a..8bf5338a2 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/FragmentStore.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/FragmentStore.kt @@ -1,19 +1,18 @@ package com.instacart.formula.android -import com.instacart.formula.RuntimeConfig import com.instacart.formula.android.events.FragmentLifecycleEvent import com.instacart.formula.android.internal.FeatureComponent import com.instacart.formula.android.internal.Features -import com.instacart.formula.android.internal.FragmentStoreFormula import com.instacart.formula.android.utils.MainThreadDispatcher -import com.instacart.formula.rxjava3.toObservable +import com.jakewharton.rxrelay3.PublishRelay import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.disposables.Disposable /** * A FragmentStore is responsible for managing the state of multiple [FragmentKey] instances. */ class FragmentStore @PublishedApi internal constructor( - private val formula: FragmentStoreFormula, + private val featureComponent: FeatureComponent<*>, ) { companion object { val EMPTY = init { } @@ -37,23 +36,116 @@ class FragmentStore @PublishedApi internal constructor( features: Features ): FragmentStore { val featureComponent = FeatureComponent(component, features.bindings) - val formula = FragmentStoreFormula(featureComponent) - return FragmentStore(formula) + return FragmentStore(featureComponent) } } + @Volatile private var disposed = false + private val dispatcher = MainThreadDispatcher() + private val updateRelay = PublishRelay.create() + private var state = FragmentState() + private var runningFeatures = mutableMapOf() + + private lateinit var fragmentEnvironment: FragmentEnvironment + internal fun onLifecycleEffect(event: FragmentLifecycleEvent) { - formula.onLifecycleEffect(event) + val fragmentId = event.fragmentId + when (event) { + is FragmentLifecycleEvent.Added -> handleNewFragment(fragmentId) + is FragmentLifecycleEvent.Removed -> handleRemoveFragment(fragmentId) + } } internal fun onVisibilityChanged(fragmentId: FragmentId, visible: Boolean) { - formula.onVisibilityChanged(fragmentId, visible) + dispatcher.dispatch { + val visibleIds = if (visible) { + state.visibleIds.plus(fragmentId) + } else { + state.visibleIds.minus(fragmentId) + } + state = state.copy(visibleIds = visibleIds) + updateRelay.accept(Unit) + } } + // TODO: should not be an observable. internal fun state(environment: FragmentEnvironment): Observable { - val config = RuntimeConfig( - defaultDispatcher = MainThreadDispatcher(), - ) - return formula.toObservable(environment, config) + // TODO: should be set differently + fragmentEnvironment = environment + return updateRelay.startWithItem(Unit).map { state }.distinctUntilChanged() + } + + fun dispose() { + disposed = true + + dispatcher.dispatch { + for (running in runningFeatures) { + running.value.dispose() + } + + runningFeatures.clear() + } + } + + private fun handleNewFragment(fragmentId: FragmentId) { + dispatcher.dispatch { + if (disposed) return@dispatch + if (state.activeIds.contains(fragmentId)) return@dispatch + + val featureEvent = featureComponent.init(fragmentEnvironment, fragmentId) + state = state.copy( + activeIds = state.activeIds.plus(fragmentId), + features = state.features.plus(featureEvent.id to featureEvent) + ) + + runFeature(fragmentEnvironment, featureEvent) + + updateRelay.accept(Unit) + } + } + + private fun handleRemoveFragment(fragmentId: FragmentId) { + dispatcher.dispatch { + if (disposed) return@dispatch + + runningFeatures.remove(fragmentId)?.dispose() + + if (state.activeIds.contains(fragmentId)) { + state = state.copy( + activeIds = state.activeIds.minus(fragmentId), + features = state.features.minus(fragmentId), + outputs = state.outputs.minus(fragmentId), + ) + updateRelay.accept(Unit) + } + } + } + + private fun runFeature(environment: FragmentEnvironment, event: FeatureEvent) { + val fragmentId = event.id + val feature = (event as? FeatureEvent.Init)?.feature + if (feature != null) { + val observable = feature.stateObservable.onErrorResumeNext { + environment.onScreenError(fragmentId.key, it) + Observable.empty() + } + + runningFeatures[fragmentId] = observable.subscribe { + publishUpdate(fragmentId, it) + } + } + } + + private fun publishUpdate(fragmentId: FragmentId, output: Any) { + dispatcher.dispatch { + if (!state.activeIds.contains(fragmentId)) { + return@dispatch + } + + val keyState = FragmentOutput(fragmentId.key, output) + state = state.copy(outputs = state.outputs.plus(fragmentId to keyState)) + + updateRelay.accept(Unit) + } } } diff --git a/formula-android/src/main/java/com/instacart/formula/android/internal/ActivityManager.kt b/formula-android/src/main/java/com/instacart/formula/android/internal/ActivityManager.kt index c403687be..fab2870f4 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/internal/ActivityManager.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/internal/ActivityManager.kt @@ -93,6 +93,7 @@ internal class ActivityManager( fun dispose() { stateSubscription.dispose() + store.fragmentStore.dispose() store.onCleared?.invoke() } diff --git a/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureObservableAction.kt b/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureObservableAction.kt deleted file mode 100644 index e7093d98c..000000000 --- a/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureObservableAction.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.instacart.formula.android.internal - -import com.instacart.formula.Action -import com.instacart.formula.Cancelable -import com.instacart.formula.android.Feature -import com.instacart.formula.android.FragmentEnvironment -import com.instacart.formula.android.FragmentId -import io.reactivex.rxjava3.core.Observable -import kotlinx.coroutines.CoroutineScope - -class FeatureObservableAction( - private val fragmentEnvironment: FragmentEnvironment, - private val fragmentId: FragmentId, - private val feature: Feature, -) : Action { - - override fun key(): Any = fragmentId - - override fun start(scope: CoroutineScope, send: (Any) -> Unit): Cancelable { - val observable = feature.stateObservable.onErrorResumeNext { - fragmentEnvironment.onScreenError(fragmentId.key, it) - Observable.empty() - } - - val disposable = observable.subscribe(send) - return Cancelable(disposable::dispose) - } -} \ No newline at end of file diff --git a/formula-android/src/main/java/com/instacart/formula/android/internal/FragmentStoreFormula.kt b/formula-android/src/main/java/com/instacart/formula/android/internal/FragmentStoreFormula.kt deleted file mode 100644 index e1943ef8f..000000000 --- a/formula-android/src/main/java/com/instacart/formula/android/internal/FragmentStoreFormula.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.instacart.formula.android.internal - -import com.instacart.formula.Evaluation -import com.instacart.formula.Formula -import com.instacart.formula.Snapshot -import com.instacart.formula.android.FeatureEvent -import com.instacart.formula.android.FragmentEnvironment -import com.instacart.formula.android.FragmentState -import com.instacart.formula.android.FragmentId -import com.instacart.formula.android.FragmentOutput -import com.instacart.formula.android.events.FragmentLifecycleEvent -import com.instacart.formula.rxjava3.RxAction -import com.jakewharton.rxrelay3.PublishRelay - -@PublishedApi -internal class FragmentStoreFormula( - private val featureComponent: FeatureComponent<*>, -) : Formula(){ - private val lifecycleEvents = PublishRelay.create() - private val visibleContractEvents = PublishRelay.create() - private val hiddenContractEvents = PublishRelay.create() - - private val lifecycleEventStream = RxAction.fromObservable { lifecycleEvents } - private val visibleContractEventStream = RxAction.fromObservable { visibleContractEvents } - private val hiddenContractEventStream = RxAction.fromObservable { hiddenContractEvents } - - fun onLifecycleEffect(event: FragmentLifecycleEvent) { - lifecycleEvents.accept(event) - } - - fun onVisibilityChanged(contract: FragmentId, visible: Boolean) { - if (visible) { - visibleContractEvents.accept(contract) - } else { - hiddenContractEvents.accept(contract) - } - } - - override fun initialState(input: FragmentEnvironment): FragmentState = FragmentState() - - override fun Snapshot.evaluate(): Evaluation { - 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), - outputs = state.outputs.minus(fragmentId), - features = state.features.minus(fragmentId) - ) - transition(updated) - } - is FragmentLifecycleEvent.Added -> { - if (!state.activeIds.contains(fragmentId)) { - val feature = featureComponent.init(input, fragmentId) - val updated = state.copy( - activeIds = state.activeIds.plus(fragmentId), - features = state.features.plus(feature.id to feature) - ) - 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 { - val keyState = FragmentOutput(fragmentId.key, it) - transition(state.copy(outputs = state.outputs.plus(fragmentId to keyState))) - } - } - } - } - ) - } -}