April 19, 2024

By: Eli Hart, Ben Schwab, Yvonne Wong

At Airbnb, we now have 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 software state throughout the ViewModel.

Airbnb started growth of Trio greater than two years in the past, and has been utilizing it in manufacturing for over a yr 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 sequence, we are going to take a look at how Mavericks can be utilized in fashionable, Compose based mostly functions. We’ll talk about the challenges of Compose-based structure and the way Trio has tried to resolve them. This can embrace an exploration of ideas equivalent to:

  • Kind-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 assist Trio workflows

This sequence is break up into three elements. 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 know Trio’s structure, it’s necessary 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 a whole lot of screens in Airbnb’s Android app (and by many different corporations 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 probably the most advanced display could be rendered in a manner that’s thread secure, unbiased of the order of occasions main as much as it, and simple to purpose about and check.

To attain this, Mavericks enforces the sample that each one knowledge uncovered by the ViewModel have to 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 are able to 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 modifications are made in a number of locations directly, and ensures that modifications 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 modifications through a coroutine Circulate property. When paired with reactive UI, like Compose, we are able to acquire 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 could 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 growth, 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 modifications from the ViewModel, and name again to the ViewModel to make state modifications. 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 answerable for creating the Exercise ViewModel initially, or for offering the preliminary arguments to it.
  2. Communication — It’s troublesome to share knowledge between Fragments instantly and with kind security. Once more, as a result of ViewModels are injected, it’s onerous to have them talk instantly, 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. Nevertheless, state modifications are completed within the ViewModel. This results in synchronization issues between ViewModel and navigation states. It’s onerous to coordinate if-then situations like making a navigation name solely after updating a state worth within the ViewModel.
  4. Testability — It’s troublesome to isolate the UI for testing as a result of it’s wrapped within the Fragment. Screenshot checks are susceptible to flakiness and numerous indirection is required for mocking the ViewModel state, as a result of ViewModels are injected into the Fragment with property delegates.
  5. Reactivity — Mavericks gives 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 modifications, and it may be troublesome 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 workforce started to discover adopting Jetpack Compose and utterly transitioning away from Fragments. By absolutely embracing Compose, we may higher put together ourselves for future Android developments and get rid of years of amassed tech debt.

Persevering with to make use of Mavericks was necessary to us as a result of we now have 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 chance to rethink how Mavericks may assist a contemporary Android software, and deal with issues we encountered with our earlier structure

With Fragments, we struggled to ensure kind secure 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 seem like between screens.

We additionally didn’t really feel our wants have been absolutely met by the Jetpack Navigation element, particularly given our closely modularized code base and huge app. The Navigation element 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 might present higher kind security and modularization assist.

Lastly, we wished an structure that will enhance testability, equivalent to extra secure screenshot and UI checks, and less complicated navigation testing.

We thought-about 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 suitable with Mavericks and our different pre-existing inner frameworks.

Given these necessities, our choice was to develop our personal answer, 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 foremost courses they comprise. Every Trio has its personal ViewModel, State, and UI, and might talk with and be nested in different Trios. The next diagram represents how these parts work collectively. The ViewModel makes modifications 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.

When you’re already acquainted with Mavericks this sample ought to look very related! 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. “Mother or father” Trios create baby Trios with preliminary arguments via a Router, and retailer these youngsters of their State. The mother or father 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 kind security when navigating and speaking between Trios, particularly throughout module boundaries.

Every Trio could 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 gives 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 growth we are able to seed the Initializer with mock state in order that the display could 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 data, equivalent to IDs used to begin a display, whereas Props permit us to subscribe to knowledge which will change over time.

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

Trio gives an initializer to create a brand new ViewModel occasion, passing obligatory data just like the Trio’s distinctive ID, a Circulate of Props, and a reference to the mother or father Exercise. Dependencies from the applying’s dependency graph will also 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 facility capabilities within the Trio class makes it specific how every class is created, and standardizes the place to look to know dependencies. Nevertheless, it may well additionally really feel like boilerplate. As an enchancment, we frequently 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 a complete 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 reveals. Moreover, the Content material operate has a “TrioRenderScope” receiver kind. It is 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 modifications. The UI directs all UI occasions, equivalent 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 modifications 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 are able to render it by invoking its Content material operate, which makes use of the beforehand talked about manufacturing facility capabilities 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 sort of a bit extra advanced due to extra tooling and edge circumstances we need to assist — equivalent to monitoring the Trio’s lifecycle, managing saved state, and mocking the ViewModel when proven inside a screenshot check 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 parts such because the Trio class and UI class.

In upcoming articles, we are going to proceed this three-part sequence by detailing how navigation works with Trio, and the way Trio’s Props permit dynamic communication between screens. And if this work sounds fascinating to you, try open roles at Airbnb!