April 23, 2024

By: Eli Hart, Ben Schwab, Yvonne Wong

At Airbnb, we have now developed an Android framework for Jetpack Compose display structure, which we name Trio. Trio is constructed on our open-source library Mavericks, which it leverages to take care of each navigation and utility state throughout the ViewModel.

Airbnb started improvement of Trio greater than two years in the past, and has been utilizing it in manufacturing for over a 12 months and a half. It’s powering a good portion of our manufacturing screens in Airbnb’s Android app, and has enabled our engineers to create options in 100% Compose UI.

On this weblog submit collection, we’ll take a look at how Mavericks can be utilized in trendy, Compose based mostly purposes. We’ll focus on the challenges of Compose-based structure and the way Trio has tried to resolve them. This can embrace an exploration of ideas comparable to:

  • Sort-safe navigation between function modules
  • Storing navigation state in a ViewModel
  • Communication between Compose-based screens, together with opening screens for outcomes and two-way communication between screens
  • Compile-time validation of navigation and communication interfaces
  • Developer instruments created to help Trio workflows

This collection is break up into three components. Half 1 (this weblog submit) covers Trio’s high-level structure. Keep tuned for Half 2, which is able to element Trio’s navigation system, and Half 3, which is able to study how Trio makes use of Props for communication between screens.

Background on Mavericks

To grasp Trio’s structure, it’s essential to know the fundamentals of Mavericks, which Trio is constructed on high of. Airbnb initially open sourced Mavericks in 2018 to simplify and standardize how state is managed in a Jetpack ViewModel. Take a look at this submit from the preliminary Mavericks (“MvRx”) launch for a deeper dive.

Utilized in nearly all of the tons of of screens in Airbnb’s Android app (and by many different firms too!), Mavericks is a state administration library that’s decoupled from the UI, and can be utilized with any UI system. The core idea is that display UI is modeled as a operate of state. This ensures that even essentially the most advanced display may be rendered in a means that’s thread protected, unbiased of the order of occasions main as much as it, and straightforward to purpose about and take a look at.

To attain this, Mavericks enforces the sample that each one knowledge uncovered by the ViewModel should be contained inside a single MavericksState knowledge class. In a easy Counter instance, the state would comprise the present depend.

knowledge class CounterState(
val depend: Int = 0
) : MavericksState

State properties can solely be up to date within the ViewModel through calls to setState. The setState operate takes a “reducer” lambda, which, given a earlier state, outputs a brand new state. We will use a reducer to increment the depend by merely including 1 to the earlier worth.

class CounterViewModel : MavericksViewModel<CounterState>(...) 
enjoyable incrementCount()
setState
// this = earlier state
this.copy(depend = depend + 1)


The bottom MavericksViewModel enqueues all calls to setState and runs them serially in a background thread. This ensures thread security when adjustments are made in a number of locations without delay, and ensures that adjustments to a number of properties within the state are atomic, so the UI by no means sees a state that’s solely partially up to date.

MavericksViewModel exposes state adjustments through a coroutine Stream property. When paired with reactive UI, like Compose, we will gather the most recent state worth and assure that the UI is up to date with each state change.

counterViewModel.stateFlow.collectAsState().depend

This unidirectional cycle may be visualized with the next diagram:

Challenges with Fragment-based structure

Whereas Mavericks works properly for state administration, we have been nonetheless experiencing some challenges with Android UI improvement, stemming from the truth that we have been utilizing a Fragment-based structure built-in with Mavericks. With this strategy, ViewModels are primarily scoped to the Exercise and shared between Fragments through injection. Fragment views are up to date by state adjustments from the ViewModel, and name again to the ViewModel to make state adjustments. The Fragment Supervisor manages navigation independently when Fragments must be pushed or popped.

Resulting from this structure, we have been operating up in opposition to some ongoing difficulties, which turned the motivation for constructing Trio.

  1. Scoping — Sharing ViewModels between a number of Fragments depends on the implicit injection of the ViewModel. Thus, it isn’t clear which Fragment is chargeable for creating the Exercise ViewModel initially, or for offering the preliminary arguments to it.
  2. Communication — It’s tough to share knowledge between Fragments immediately and with sort security. Once more, as a result of ViewModels are injected, it’s laborious to have them talk immediately, and we don’t have good management over the ordering of their creation.
  3. Navigation — Navigation is finished through the Fragment Supervisor and should occur within the Fragment. Nonetheless, state adjustments are finished within the ViewModel. This results in synchronization issues between ViewModel and navigation states. It’s laborious to coordinate if-then eventualities like making a navigation name solely after updating a state worth within the ViewModel.
  4. Testability — It’s tough to isolate the UI for testing as a result of it’s wrapped within the Fragment. Screenshot assessments are liable to flakiness and a whole lot of indirection is required for mocking the ViewModel state, as a result of ViewModels are injected into the Fragment with property delegates.
  5. Reactivity — Mavericks supplies a unidirectional state circulate to the View, which is useful for consistency and testing, however the View system doesn’t lend itself properly to reactive updates to state adjustments, and it may be tough or inefficient to replace the view incrementally on every state change.

Whereas a few of these issues may have been mitigated through the use of a greater Fragment based mostly structure, we discovered that Fragments have been general too limiting with Compose and determined to maneuver away from them solely.

Why we constructed Trio

In 2021, our crew started to discover adopting Jetpack Compose and fully transitioning away from Fragments. By absolutely embracing Compose, we may higher put together ourselves for future Android developments and remove years of accrued tech debt.

Persevering with to make use of Mavericks was essential to us as a result of we have now a considerable amount of inner expertise with it, and we didn’t need to additional complicate an architectural migration by additionally altering our state administration strategy. We noticed a possibility to rethink how Mavericks may help a contemporary Android utility, and tackle issues we encountered with our earlier structure

With Fragments, we struggled to ensure sort protected communication between screens at runtime. We wished to have the ability to codify the expectations about how ViewModels are used and shared, and what interfaces appear like between screens.

We additionally didn’t really feel our wants have been absolutely met by the Jetpack Navigation part, particularly given our closely modularized code base and huge app. The Navigation part is not type safe, requires defining the navigation graph in a single place, and doesn’t permit us to co-locate state in our ViewModel. We regarded for a brand new structure that would present higher sort security and modularization help.

Lastly, we wished an structure that might enhance testability, comparable to extra secure screenshot and UI assessments, and less complicated navigation testing.

We thought of the open supply libraries Workflow and RIBs, however opted to not use them as a result of they weren’t Compose-first and weren’t appropriate with Mavericks and our different pre-existing inner frameworks.

Given these necessities, our resolution was to develop our personal resolution, which we named Trio.

Trio Structure

Trio is an opinionated framework for constructing options. It helps us to outline and handle boundaries and state in Compose UI. Trio additionally standardizes how state is hoisted from Compose UI and the way occasions are dealt with, imposing unidirectional knowledge circulate with Mavericks. The design was impressed by Sq.’s Workflow library; Trio differs in that it was designed particularly for Compose and makes use of Mavericks ViewModels for managing state and occasions.

Self-contained blocks are referred to as “Trios”, named for the three essential courses they comprise. Every Trio has its personal ViewModel, State, and UI, and may talk with and be nested in different Trios. The next diagram represents how these elements work collectively. The ViewModel makes adjustments to state through Mavericks reducers, the UI receives the most recent state worth to render, and occasions are routed again to the ViewModel for additional state updates.

In the event you’re already conversant in Mavericks this sample ought to look very comparable! The ViewModel and State utilization is similar to what we did with Fragments. What’s new is how we embed the ViewModels in Compose UI and add Routing and Props based mostly communication through Trio.

Trios are nested to type customized, versatile navigation hierarchies. “Dad or mum” Trios create baby Trios with preliminary arguments via a Router, and retailer these youngsters of their State. The dad or mum can then talk dynamically with its youngsters via a circulate of Props, which give knowledge, dependencies, and practical callbacks.

The framework helps us to ensure sort security when navigating and speaking between Trios, particularly throughout module boundaries.

Every Trio may be examined individually by instantiating it with mocked arguments, State, and Props. Coupled with Compose’s state-based rendering and Maverick’s immutable state patterns, this supplies managed and deterministic testing environments.

The Trio Class

Creating a brand new Trio implementation requires subclassing the Trio base class. The Trio class is typed to outline Args, Props, State, ViewModel, and UI; this enables us to ensure type-safe navigation and inter-screen communication.

class CounterScreen : Trio<
CounterArgs,
CounterProps,
CounterState,
CounterViewModel,
CounterUI
>

A Trio is created with both an preliminary set of arguments or an preliminary state, that are wrapped in a sealed class referred to as the Initializer. In manufacturing, the Initializer will solely comprise Args handed from one other display, however in improvement we will seed the Initializer with mock state in order that the display may be loaded standalone, unbiased of the traditional navigation hierarchy.

class CounterScreen(
initializer: Initializer<CounterArgs, CounterState>
)

Then, in our subclass physique, we outline how we need to create our State, ViewModel, and UI, given the beginning values of Args and Props.

Args and Props each present enter knowledge, with the distinction being that Args are static whereas Props are dynamic. Args assure the steadiness of static info, comparable to IDs used to begin a display, whereas Props permit us to subscribe to knowledge that will change over time.

override enjoyable createInitialState(args: CounterArgs, props:  CounterProps) 
return CounterState(args.depend)

Trio supplies an initializer to create a brand new ViewModel occasion, passing essential info just like the Trio’s distinctive ID, a Stream of Props, and a reference to the dad or mum Exercise. Dependencies from the applying’s dependency graph can be additionally handed to the ViewModel via its constructor.

override enjoyable createViewModel(
initializer: Initializer<CounterProps, CounterState>
)
return CounterViewModel(initializer)

Lastly, the UI class wraps the composable code used to render the Trio. The UI class receives a circulate of the most recent State from the ViewModel, and in addition makes use of the ViewModel reference to name again to it when dealing with UI occasions.

override enjoyable createUI(viewModel: CounterViewModel ): CounterUI 
return CounterUI(viewModel)

We like that grouping all of those manufacturing unit features within the Trio class makes it specific how every class is created, and standardizes the place to look to know dependencies. Nonetheless, it will possibly additionally really feel like boilerplate. As an enchancment, we regularly use reflection to create the UI class, and we use assisted inject to automate creation of the ViewModel with Dagger dependencies.

The ensuing Trio declaration as an entire appears like this:

class CounterScreen(
initializer: Initializer<CounterArgs, CounterState>
) : Trio<
CounterArgs,
CounterProps,
CounterState,
CounterViewModel,
CounterUI
>(initializer)

override enjoyable createInitialState(CounterArgs, CounterProps)
return CounterState(args.depend)

The UI Class

The Trio’s UI class implements a single Composable operate named “Content material”, which determines the UI that the Trio exhibits. Moreover, the Content material operate has a “TrioRenderScope” receiver sort. This can be a Compose animation scope that permits us to customise the Trio’s animations when it’s displayed.

class CounterUI(
override val viewModel: CounterViewModel
) : UI<CounterState, CounterViewModel>

@Composable
override enjoyable TrioRenderScope.Content material(state: CounterState)
Column
TopAppBar()
Button(
textual content = state.depend,
modifier = Modifier.clickable
viewModel.incrementCount()

)
...


The Content material operate is recomposed each time the State from the ViewModel adjustments. The UI directs all UI occasions, comparable to clicks, again to the ViewModel for dealing with.

This design enforces unidirectional knowledge circulate, and testing the UI is simple as a result of it’s decoupled from the logic of state adjustments and occasion dealing with. It additionally standardizes how Compose state is hoisted for consistency throughout screens, whereas eradicating the boilerplate of organising entry to the ViewModel’s state circulate.

Rendering a Trio

Given a Trio occasion, we will render it by invoking its Content material operate, which makes use of the beforehand talked about manufacturing unit features to create preliminary values of the ViewModel, State, and UI. The state circulate is collected from the ViewModel and handed to the UI’s Content material operate. The UI is wrapped in a Field to respect the constraints and modifier of the caller.

@Composable
inner enjoyable TrioRenderScope.Content material(modifier: Modifier = Modifier)
key(trioId)
val exercise = LocalContext.present as ComponentActivity

val viewModel = bear in mind
getOrCreateViewModel(exercise)

val ui = bear in mind createUI(viewModel)

val state = viewModel.stateFlow
.collectAsState(viewModel.currentState).worth

Field(propagateMinConstraints = true, modifier = modifier)
ui.Content material(state = state)


To allow customizing entry and exit animations, the Content material operate additionally makes use of a TrioRenderScope receiver; this wraps an implementation of Compose’s AnimatedVisibilityScope which shows the Content material. A helper operate is used to coordinate this.

@Composable
enjoyable ShowTrio(trio: Trio, modifier: Modifier)
AnimatedVisibility(
seen = true,
enter = EnterTransition.None,
exit = ExitTransition.None
)
val animationScope = TrioRenderScopeImpl(this)
trio.Content material(modifier, animationScope)

In apply, the precise implementation of Trio.Content material is kind of a bit extra advanced due to extra tooling and edge circumstances we need to help — comparable to monitoring the Trio’s lifecycle, managing saved state, and mocking the ViewModel when proven inside a screenshot take a look at or IDE preview.

Conclusion

On this introduction to Trio we mentioned Airbnb’s background with Mavericks and Fragments, and why we constructed Trio to transition to a Jetpack Compose-based structure. We offered an outline of Trio’s structure, and checked out core elements such because the Trio class and UI class.

In upcoming articles, we’ll proceed this three-part collection by detailing how navigation works with Trio, and the way Trio’s Props permit dynamic communication between screens. And if this work sounds attention-grabbing to you, take a look at open roles at Airbnb!