Introducing Trio | Half I. A 3 half collection on how we constructed a… | by Eli Hart | The Airbnb Tech Weblog

By: Eli Hart, Ben Schwab, Yvonne Wong

At Airbnb, now we 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 keep up 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 are going to have 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 embody an exploration of ideas akin to:

  • Kind-safe navigation between characteristic 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 cut up into three elements. Half 1 (this weblog submit) covers Trio’s high-level structure. In Half 2 we element Trio’s navigation system, and Half 3 examines 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 prime 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 perform of state. This ensures that even essentially the most advanced display could be rendered in a means that’s thread secure, unbiased of the order of occasions main as much as it, and simple to cause about and take a look at.

To attain this, Mavericks enforces the sample that every one information uncovered by the ViewModel have to be contained inside a single MavericksState information class. In a easy Counter instance, the state would include the present rely.

information class CounterState(
val rely: Int = 0
) : MavericksState

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

class CounterViewModel : MavericksViewModel<CounterState>(...) 
enjoyable incrementCount()
setState
// this = earlier state
this.copy(rely = rely + 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 without delay, 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 Circulation property. When paired with reactive UI, like Compose, we will accumulate the newest state worth and assure that the UI is up to date with each state change.

counterViewModel.stateFlow.collectAsState().rely

This unidirectional cycle could be visualized with the next diagram:

Challenges with Fragment-based structure

Whereas Mavericks works nicely for state administration, we had been nonetheless experiencing some challenges with Android UI improvement, stemming from the truth that we had been utilizing a Fragment-based structure built-in with Mavericks. With this method, 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.

As a consequence of this structure, we had been operating up towards some ongoing difficulties, which grew to become 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 information between Fragments immediately and with kind security. Once more, as a result of ViewModels are injected, it’s arduous 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 modifications are accomplished within the ViewModel. This results in synchronization issues between ViewModel and navigation states. It’s arduous to coordinate if-then eventualities 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 exams 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 supplies a unidirectional state circulation to the View, which is useful for consistency and testing, however the View system doesn’t lend itself nicely 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 by utilizing a greater Fragment based mostly structure, we discovered that Fragments had been total too limiting with Compose and determined to maneuver away from them solely.

Why we constructed Trio

In 2021, our group 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 eradicate years of amassed tech debt.

Persevering with to make use of Mavericks was essential to us as a result of now we have a considerable amount of inside expertise with it, and we didn’t need to additional complicate an architectural migration by additionally altering our state administration method. We noticed a chance to rethink how Mavericks may help a contemporary Android utility, 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 had been absolutely met by the Jetpack Navigation part, particularly given our closely modularized code base and enormous 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 seemed for a brand new structure that might present higher kind security and modularization help.

Lastly, we wished an structure that may enhance testability, akin to extra steady screenshot and UI exams, 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 suitable with Mavericks and our different pre-existing inside frameworks.

Given these necessities, our determination 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, implementing unidirectional information circulation 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 known as “Trios”, named for the three foremost courses they include. 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 elements work collectively. The ViewModel makes modifications to state through Mavericks reducers, the UI receives the newest state worth to render, and occasions are routed again to the ViewModel for additional state updates.

In case you’re already accustomed to 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 by way of a Router, and retailer these kids of their State. The guardian can then talk dynamically with its kids by way of a circulation of Props, which give information, 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 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 known as the Initializer. In manufacturing, the Initializer will solely include Args handed from one other display, however in improvement we will seed the Initializer with mock state in order that the display could be loaded standalone, unbiased of the conventional 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 information, with the distinction being that Args are static whereas Props are dynamic. Args assure the steadiness of static info, akin to IDs used to start out a display, whereas Props permit us to subscribe to information which will change over time.

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

Trio supplies an initializer to create a brand new ViewModel occasion, passing crucial info just like the Trio’s distinctive ID, a Circulation of Props, and a reference to the guardian Exercise. Dependencies from the applying’s dependency graph will also be additionally handed to the ViewModel by way of 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 circulation of the newest State from the ViewModel, and likewise 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 grasp dependencies. Nonetheless, it could 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 to be like like this:

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

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

The UI Class

The Trio’s UI class implements a single Composable perform named “Content material”, which determines the UI that the Trio exhibits. Moreover, the Content material perform 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.rely,
modifier = Modifier.clickable
viewModel.incrementCount()

)
...


The Content material perform is recomposed each time the State from the ViewModel modifications. The UI directs all UI occasions, akin to clicks, again to the ViewModel for dealing with.

This design enforces unidirectional information circulation, 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 circulation.

Rendering a Trio

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

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

val viewModel = keep in mind
getOrCreateViewModel(exercise)

val ui = keep 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 perform additionally makes use of a TrioRenderScope receiver; this wraps an implementation of Compose’s AnimatedVisibilityScope which shows the Content material. A helper perform 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 follow, the precise implementation of Trio.Content material is kind of a bit extra advanced due to further tooling and edge instances we need to help — akin 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.

Partially 2 of this collection you will notice how navigation works with Trio, and partly 3 we are going to learn the way Trio’s Props permit dynamic communication between screens. And if this work sounds fascinating to you, try open roles at Airbnb!