Skip to content

Remove LayoutViewFactory + refactor FeatureView to make it compose-first.#511

Open
alexanderbezverhni wants to merge 6 commits into
masterfrom
bezverhni/feature_view_refactor
Open

Remove LayoutViewFactory + refactor FeatureView to make it compose-first.#511
alexanderbezverhni wants to merge 6 commits into
masterfrom
bezverhni/feature_view_refactor

Conversation

@alexanderbezverhni
Copy link
Copy Markdown
Collaborator

@alexanderbezverhni alexanderbezverhni commented May 19, 2026

Remove LayoutViewFactory + refactor FeatureView to make it compose-first.

Also, as a side effect of LayoutViewFactory removal:

  • update Tasks sample app to use Compose
  • fold formula-android-compose module into formula-android

Why do we stop supporting Android Views?

Android is Compose-first

@alexanderbezverhni alexanderbezverhni self-assigned this May 19, 2026
@carrotkite
Copy link
Copy Markdown

1 Warning
⚠️ No coverage data found for com/instacart/formula/android/ViewFactory

JaCoCo Code Coverage 93.37% ✅

Class Covered Meta Status
com/instacart/formula/android/ViewFactory No coverage data found : -% No coverage data found : -% 🃏
com/instacart/formula/android/FeatureFactory 100% 0%
com/instacart/formula/android/FeatureView 100% 0%
com/instacart/formula/android/FormulaFragment 62% 0%
com/instacart/formula/android/ComposeViewFactory 0% 0%

Generated by 🚫 Danger

return featureView.view
val state = mutableStateOf(featureView.initialModel)
this.outputState = state
return ComposeView(requireContext()).apply {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the framework's error-handling contract for render failures. routeDelegate.setOutput(...) is still wrapped in try/catch, but the actual UI work now happens later inside ComposeView.setContent { ... }, so exceptions thrown during composition/recomposition will bypass RouteEnvironment.onScreenError. The updated test suite explicitly expects that behavior, which means apps that currently rely on onScreenError for screen-level crash reporting or recovery lose it silently. Was that intended? If not, we need a way to preserve the old reporting path for render-time failures before landing this refactor.

Copy link
Copy Markdown
Collaborator Author

@alexanderbezverhni alexanderbezverhni May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually changes nothing for the upstream app, given it was not using LayoutViewFactory. RouteEnvironment.onScreenError was meaningful in LayoutViewFactory since synchronous and imperative android.view.View setup could throw:

  • null findViewById
  • NPE on a missing field
  • adapter blowing up

ComposeViewFactory, on other hand, both before and after the changes, is just changing a MutableState variable, while the actual rendering happens later, inside Compose recomposition, outside
try/catch.

So onScreenError no longer catches the kind of error it was originally designed for. We should think of an alternative approach here, which is out of scope for this PR.

* }
* ```
*/
abstract class ComposeViewFactory<RenderModel : Any> : ViewFactory<RenderModel> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving ComposeViewFactory into formula-android while deleting the old formula-android-compose module creates an immediate source/binary compatibility break for downstream consumers. Existing imports of com.instacart.formula.android.compose.ComposeViewFactory stop compiling, and the old artifact disappears entirely. If this is meant to ship as a normal library release rather than a major-version break, we should keep a deprecated forwarding type/module for at least one release to provide a migration path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack 👍🏼

@carrotkite
Copy link
Copy Markdown

carrotkite commented May 19, 2026

📊 Benchmark Comparison Report

Summary

  • Regressions: 0 ✅
  • Improvements: 0
  • Unchanged: 25
No significant changes (25 benchmarks)
Benchmark Baseline Current Change
com.instacart.formula.benchmarks.ActionCountBenchmark.stateChanges (actionCount=1) 11.554 ± 0.048 us/op 12.582 ± 0.097 us/op +8.9%
com.instacart.formula.benchmarks.ActionCountBenchmark.stateChanges (actionCount=100) 12.96 ± 0.158 us/op 13.775 ± 0.077 us/op +6.3%
com.instacart.formula.benchmarks.ActionCountBenchmark.stateChanges (actionCount=25) 11.888 ± 0.036 us/op 12.869 ± 0.114 us/op +8.3%
com.instacart.formula.benchmarks.ActionInitializationBenchmark.initializeNewActions (actionCount=1) 0.631 ± 0.058 us/op 0.695 ± 0.056 us/op +10.2%
com.instacart.formula.benchmarks.ActionInitializationBenchmark.initializeNewActions (actionCount=100) 11.076 ± 0.156 us/op 10.693 ± 0.191 us/op -3.5%
com.instacart.formula.benchmarks.ActionInitializationBenchmark.initializeNewActions (actionCount=25) 3.234 ± 0.034 us/op 3.121 ± 0.046 us/op -3.5%
com.instacart.formula.benchmarks.CallbackInitializationBenchmark.initializeNewCallbacks (callbackCount=10) 1.526 ± 0.287 us/op 1.585 ± 0.224 us/op +3.9%
com.instacart.formula.benchmarks.CallbackInitializationBenchmark.initializeNewCallbacks (callbackCount=50) 5.519 ± 1.031 us/op 5.629 ± 0.955 us/op +2.0%
com.instacart.formula.benchmarks.CallbackOverheadBenchmark.transitions (callbackCount=10) 11.956 ± 0.134 us/op 12.88 ± 0.082 us/op +7.7%
com.instacart.formula.benchmarks.CallbackOverheadBenchmark.transitions (callbackCount=50) 13.654 ± 0.45 us/op 14.27 ± 0.373 us/op +4.5%
com.instacart.formula.benchmarks.ChildrenCountBenchmark.stateChanges (childrenCount=1) 11.585 ± 0.042 us/op 12.669 ± 0.147 us/op +9.4%
com.instacart.formula.benchmarks.ChildrenCountBenchmark.stateChanges (childrenCount=100) 13.66 ± 0.182 us/op 14.361 ± 0.074 us/op +5.1%
com.instacart.formula.benchmarks.ChildrenCountBenchmark.stateChanges (childrenCount=25) 12.084 ± 0.092 us/op 12.997 ± 0.145 us/op +7.6%
com.instacart.formula.benchmarks.ChildrenInitializationBenchmark.initializeNewChildren (childrenCount=1) 0.618 ± 0.04 us/op 0.667 ± 0.052 us/op +7.8%
com.instacart.formula.benchmarks.ChildrenInitializationBenchmark.initializeNewChildren (childrenCount=100) 12.28 ± 0.11 us/op 11.76 ± 0.365 us/op -4.2%
com.instacart.formula.benchmarks.ChildrenInitializationBenchmark.initializeNewChildren (childrenCount=25) 3.329 ± 0.084 us/op 3.353 ± 0.047 us/op +0.7%
com.instacart.formula.benchmarks.GlobalEffectQueueBenchmark.measure100Effects 8.122 ± 0.645 us/op 8.773 ± 0.622 us/op +8.0%
com.instacart.formula.benchmarks.GlobalEffectQueueBenchmark.measure10Effects 0.697 ± 0.005 us/op 0.676 ± 0.007 us/op -3.0%
com.instacart.formula.benchmarks.GlobalEffectQueueBenchmark.measure1Effect 0.056 ± 0.0 us/op 0.052 ± 0.001 us/op -7.1%
com.instacart.formula.benchmarks.TransitionBenchmark.transitions (depth=0) 11.479 ± 0.044 us/op 12.508 ± 0.091 us/op +9.0%
com.instacart.formula.benchmarks.TransitionBenchmark.transitions (depth=10) 11.785 ± 0.072 us/op 12.785 ± 0.053 us/op +8.5%
com.instacart.formula.benchmarks.TransitionBenchmark.transitions (depth=20) 12.391 ± 0.079 us/op 13.265 ± 0.117 us/op +7.1%
com.instacart.formula.benchmarks.TransitionQueueBenchmark.measure1 0.32 ± 0.016 us/op 0.347 ± 0.003 us/op +8.4%
com.instacart.formula.benchmarks.TransitionQueueBenchmark.measure100 0.324 ± 0.014 us/op 0.353 ± 0.001 us/op +9.0%
com.instacart.formula.benchmarks.TransitionQueueBenchmark.measure10000 0.325 ± 0.004 us/op 0.351 ± 0.005 us/op +8.2%

Regressions: ±10% with non-overlapping confidence intervals. Improvements: ±10% change only.

Generated by 🚫 Danger

@Jawnnypoo
Copy link
Copy Markdown
Member

Should bump to 0.9.0 with this one

import com.instacart.formula.android.FeatureView
import com.instacart.formula.android.ViewFactory

abstract class ComposeViewFactory<RenderModel : Any> : ViewFactory<RenderModel> {
Copy link
Copy Markdown
Collaborator Author

@alexanderbezverhni alexanderbezverhni May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, when Compose is first class citizen in formula-android, no need to keep a separate formula-android-compose dependency with a single ComposeViewFactory class. Folding it into formula-android.

Comment on lines -17 to +15
val view: View,
val setOutput: (RenderModel) -> Unit,
val content: @Composable (RenderModel) -> Unit,
val initialModel: RenderModel? = null,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing unneeded android.view.View field from FeatureView.

Comment on lines -40 to +44
private var featureView: FeatureView<Any>? = null
private var outputState: MutableState<Any?>? = null
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to keep FeatureView around anymore, for setting new output.

Comment on lines +58 to +62
// Based-on: https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/compose-in-views#compose-in-fragments
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
state.value?.let { featureView.content(it) }
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving androidx.compose.ui.platform.ComposeView creation from ComposeViewFactory directly to FormulaFragment.

Alternative navigation host (Compose Nav 3) doesn't need ComposeView to render the content.

Comment on lines -24 to +27
return flow<Unit> { delay(5.seconds) }.flatMapLatest { localStore }
return flow {
delay(5.seconds)
emit(Unit)
}.flatMapLatest { localStore }
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing unrelated bug that was introduced here: #430

This flow was never emitting, resulting in Tasks app never showing tasks:

import com.examples.todoapp.R
import com.instacart.formula.invoke

private val TodoColors = lightColors(
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migrating Tasks demo app from LayoutViewFactory to ComposeViewFactory.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few screenshots AFTER:

1 2 3
Image Image Image

@alexanderbezverhni alexanderbezverhni marked this pull request as ready for review May 19, 2026 21:26
@alexanderbezverhni alexanderbezverhni changed the title Remove LayoutViewFactory + refactor FeatureView to make it compose-first. Remove LayoutViewFactory + refactor FeatureView to make it compose-first. May 19, 2026
Copy link
Copy Markdown
Collaborator

@FrancoisBlavoet FrancoisBlavoet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

the announcement of Android being Compose first is good timing for formula to remove its view integration 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants