May 18, 2024

Half three on how we constructed a Compose based mostly structure with Mavericks within the Airbnb Android app

Eli Hart
The Airbnb Tech Blog

By: Eli Hart, Ben Schwab, and Yvonne Wong

Trio is Airbnb’s framework for Jetpack Compose display structure in Android. It’s constructed on high of Mavericks, Airbnb’s open supply state administration library for Jetpack. On this weblog publish collection, we’ve been breaking down how Trio works to assist clarify our design selections, within the hopes that different groups may profit from points of our method.

We suggest beginning with Half 1, about Trio’s structure, after which studying Half 2, about how navigation works in Trio, earlier than you dive into this text. On this third and closing a part of our collection, we’ll talk about how Props in Trio enable for simplified, type-safe communication between ViewModels. We’ll additionally share an replace on the present adoption of Trio at Airbnb and what’s subsequent.

Trio Props

To raised perceive Props, let’s have a look at an instance of a easy Message Inbox display, composed of two Trios facet by facet. There’s a Record Trio on the left, displaying inbox messages, and a Particulars Trio on the proper, displaying the complete textual content of a specific message.

The 2 Trios are wrapped by a guardian display, which is liable for instantiating the 2 youngsters, passing alongside information to them, and positioning them within the UI. As you may recall from Half 2, Trios might be saved in State; the guardian’s State consists of each the message information in addition to the kid Trios.

information class ParentState(
val inboxMessages: Record<Message>,
val selectedMessage: Message?,
val messageListScreen: Trio<ListProps>,
val messageDetailScreen: Trio<DetailsProps>,
} : MavericksState

The guardian’s UI decides how you can show the kids, which it accesses from the State. With Compose UI, it’s simple to use customized structure logic: we present the screens facet by facet when the system is in panorama mode, and in portrait we present solely a single display, relying on whether or not a message has been chosen.

@Composable 
override enjoyable TrioRenderScope.Content material(state: ParentState)
if (LocalConfiguration.present.orientation == ORIENTATION_LANDSCAPE)
Row(Modifier.fillMaxSize())
ShowTrio(state.listScreen, modifier = Modifier.weight(1f))
ShowTrio(state.detailScreen)

else
if (state.selectedMessage == null)
ShowTrio(state.listScreen)
else
BackHandler viewModel.clearMessageSelection()
ShowTrio(state.detailScreen)


Each little one screens want entry to the most recent message state so that they know which content material to point out. We will present this with Props!

Props are a group of Kotlin properties, held in a knowledge class and handed to a Trio by its guardian.

Not like Arguments, Props can change over time, permitting a guardian to offer up to date information as wanted all through the lifetime of the Trio. Props can embrace Lambda expressions, permitting a display to speak again to its guardian.

A baby Trio can solely be proven in a guardian that helps its Props kind. This ensures compile-time correctness for navigation and communication between Trios.

Defining Props

Let’s see how Props are used to move message information from the guardian Trio to the Record and Particulars Trios. When a guardian defines little one Trios in its State, it should embrace the kind of Props that these youngsters require. For our instance, the Record and Particulars display every have their very own distinctive Props.

The Record display must know the listing of all Messages and whether or not one is chosen. It additionally wants to have the ability to name again to the guardian to inform it when a brand new message has been chosen.

information class ListProps(
val selectedMessage: Message?,
val inboxMessages: Record<Message>,
val onMessageSelected: (Message) -> Unit,
)

The Particulars display simply must know which message to show.

information class DetailProps(
val selectedMessage: Message?
)

The guardian ViewModel holds the kid situations in its State, and is liable for passing the most recent Props worth to the kids.

Passing Props

So, how does a guardian Trio move Props to its little one? In its init block it should use the launchChildInitializer perform — this perform makes use of a lambda to pick out a Trio occasion from the State, specifying which Trio is being focused.

class ParentViewModel: TrioViewModel 

init
launchChildInitializer( messageListScreen ) state ->
ListProps(
state.selectedMessage,
state.inboxMessages,
::showMessageDetails
)

launchChildInitializer( detailScreen ) state ->
DetailProps(state.selectedMessage)

enjoyable showMessageDetails(message: Message?) ...

The second lambda argument receives a State worth and returns a brand new Props occasion to move to the kid. This perform manages the lifecycle of the kid, initializing it with a circulation of Props when it’s first created, and destroying it whether it is ever faraway from the guardian’s state.

The lambda to rebuild Props is re-invoked each time the Mum or dad’s state adjustments, and any new worth of Props is handed alongside to the kid by means of its circulation.

A standard sample we use is to incorporate perform references within the Props, which level to features on the guardian ViewModel. This permits the kid to name again to the guardian for occasion dealing with. Within the instance above we do that with the showMessageDetails perform. Props may also be used to move alongside complicated dependencies, which varieties a dependency graph scoped to the guardian.

Notice that we can not move Props to a Trio when it’s created, like we do with Args. It is because Trios should be capable to be restored after course of dying, and so the Trio class, in addition to the Args used to create it, are Parcelable. Since Props can include lambdas and different arbitrary objects that can’t be safely serialized, we should use the above sample to ascertain a circulation of Props from guardian to little one that may be reestablished even after course of recreation. Navigation and inter-screen communication can be loads easier if we didn’t must deal with course of recreation!

Utilizing Props

To ensure that a toddler Trio to make use of Props information in its UI, it first must be copied to State.

Youngster ViewModels override the perform updateStateFromPropsChange to specify how you can incorporate Prop values into State. The perform is invoked each time the worth of Props adjustments, and the brand new State worth is up to date on the ViewModel. That is how youngsters keep up-to-date with the most recent information from their guardian.

class ListViewModel : TrioViewModel<ListProps, ListState> 

override enjoyable updateStateFromPropsChange(
newProps: ListProps,
thisState: ListState
): ListState
return thisState.copy(
inboxMessages = newProps.inboxMessages,
selectedMessage = newProps.selectedMessage
)

enjoyable onMessageSelected(message: Message)
props.onMessageSelected(message)

For non-state values in Props, equivalent to dependencies or callbacks, the ViewModel can entry the most recent Props worth at any time through the props property. For instance, we do that within the onMessageSelected perform within the pattern code above. The Record UI will invoke this perform when a message is chosen, and the occasion can be propagated to the guardian by means of Props.

There have been a whole lot of complexities when implementing Props — for instance, when dealing with edge circumstances across the Trio lifecycle and restoring state after course of dying. Nonetheless, the internals of Trio conceal many of the complexity from the top person. General, having an opinionated, codified system with kind security for the way Compose screens talk has helped enhance standardization and productiveness throughout our Android engineering workforce.

One of the crucial widespread UI patterns at Airbnb is to coordinate a stack of screens. These screens could share some widespread information, and observe comparable navigation patterns equivalent to pushing, popping, and eradicating all of the screens of the backstack in tandem.

Earlier, we confirmed how a Trio can handle an inventory of kids in its State to perform this, but it surely’s tedious to do this manually. To assist, Trio offers a typical “display circulation” implementation, which consists of a guardian ScreenFlow Trio and associated little one Trio screens. The guardian ScreenFlow routinely manages little one transactions, and renders the highest little one in its UI. It additionally broadcasts a customized Props class to its youngsters, giving entry to shared state and navigation features.

Take into account constructing a Todo app that has a TodoList display, a TaskScreen, and an EditTaskScreen. These screens can all share a single community request that returns a TodoList mannequin. In Trio phrases, the TodoList information mannequin could possibly be the Props for these three screens.

To handle these screens we use ScreenFlow infrastructure to create a TodoScreenFlow Trio. Its state extends ScreenFlowState and overrides a childScreenTransaction property to carry the transactions. On this instance, the circulation’s State was initialized to begin with the TodoListScreen, so will probably be rendered first. The circulation’s State object additionally acts because the supply of fact for different shared state, such because the TodoList information mannequin.

information class TodoFlowState(
@PersistState
override val childScreenTransactions: Record<ScreenTransaction<TodoFlowProps>> = listOf(
ScreenTransaction(Router.TodoListScreen.createFullPaneTrio(NoArgs))
),
// shared state
val todoListQuery: TodoList?,
) : ScreenFlowState<TodoFlowState, TodoFlowProps>

This state is personal to the TodoScreenFlow. Nonetheless, the circulation defines Props to share the TodoList information mannequin, callbacks like a reloadList lambda, and a NavController with its youngsters.

information class TodoFlowProps(
val navController: NavController<TodoFlowProps>,
val todoListQuery: TodoList?,
val reloadList: () -> Unit,
)

The NavController prop can be utilized by the kids screens to push one other sibling display within the circulation. The ScreenFlowViewModel base class implements this NavController interface, managing the complexity of integrating the navigation actions into the display circulation’s state.

interface NavController<PropsT>(
enjoyable push(router: TrioRouter<*, in PropsT>)
enjoyable pop()
)

Lastly, the navigation and shared state is wired right into a circulation of Props when the TodoScreenFlowViewModel overrides createFlowProps. This perform can be invoked anytime the inner state of TodoScreenFlowViewModel adjustments, which means any replace to TodoList mannequin can be propagated to the kids screens.

class TodoScreenFlowViewModel(
initializer: Initializer<NavPopProps, TodoFlowState>
) : ScreenFlowViewModel<NavPopProps, TodoFlowProps, TodoFlowState>(initializer)

override enjoyable createFlowProps(
state: TodoFlowState,
props: NavPopProps
): TodoFlowProps
return TodoFlowProps(
navController = this,
state.todoListQuery,
::reloadList,
)

Inside one of many youngsters display’s ViewModels, we are able to see that it’ll obtain the shared Props:

class TodoListViewModel(
initializer: Initializer<TodoFlowProps, TodoListState>
) : TrioViewModel<TodoFlowProps, TodoListState>(initializer)

override enjoyable updateStateFromPropsChange(
newProps: TodoFlowProps,
thisState: TodoTaskState
): TodoTaskState
// Incorporate the shared information mannequin into this Trio’s personal state handed to its UI:
return thisState.copy(todoListQuery = newProps.todoListQuery)

enjoyable navigateToTodoTask(job: TodoTask)
this.props.navController.push(Router.TodoTaskScreen, TodoTaskArgs(job.id))

In navigateToTodoTask, the NavController ready by the circulation guardian is used to soundly navigate to the subsequent display within the circulation (guaranteeing it’s going to obtain the shared TodoFlowProps). Internally, the NavController updates the ScreenFlow’s childScreenTransactions triggering the ScreenFlow infra to offer the shared TodoFlowProps to the brand new display, and render the brand new display.

Growth historical past and launch

We began designing Trio in late 2021, with the primary Trio screens seeing manufacturing visitors in mid 2022.

As of March 2024, we now have over 230 Trio screens with vital manufacturing visitors at Airbnb.

From surveying our builders, we’ve heard that a lot of them benefit from the general Trio expertise; they like having clear and opinionated patterns and are pleased to be in a pure Compose surroundings. As one developer put it, “Props was an enormous plus by permitting a number of screens to share callbacks, which simplified a few of my code logic loads.” One other mentioned, “Trio makes you unlearn unhealthy habits and undertake finest practices that work for Airbnb based mostly on our previous learnings.” General, our workforce reviews sooner improvement cycles and cleaner code. “It makes Android improvement sooner and extra pleasant,” is how one engineer summed it up.

Dev Tooling

To help our engineers, we have now invested in IDE tooling with an in-house Android Studio Plugin. It features a Trio Technology software that creates all the recordsdata and boilerplate for a brand new Trio, together with routing, mocks, and exams.

The software helps the person select which Arguments and Props to make use of, and helps with different customization equivalent to establishing customized Flows. It additionally permits us to embed instructional experiences to assist newcomers ramp up with Trio.

One piece of suggestions we heard from engineers was that it was tedious to alter a Trio’s Args or Props varieties, since they’re used throughout many various recordsdata.

We leveraged our IDE plugin to offer a software to routinely change these values, making this workflow a lot sooner.

Our workforce leans closely on tooling like this, and we’ve discovered it to be very efficient in enhancing the expertise of engineers at Airbnb. We’ve adopted Compose Multiplatform for our Plugin UI improvement which we imagine made constructing highly effective developer tooling extra possible and pleasant.

General, with greater than 230 of our manufacturing screens carried out as Trios, Trio’s natural adoption at Airbnb has confirmed that a lot of our bets and design selections had been well worth the tradeoffs.

One change we’re anticipating, although, is to include shared factor transitions between screens as soon as the Compose framework offers APIs to help that performance. When Compose APIs for this can be found, we’ll doubtless have to revamp our navigation APIs accordingly.

Thanks for following together with the work we’ve been doing at Airbnb. Our Android Platform workforce works on a wide range of complicated and fascinating initiatives like Trio, and we’re excited to share extra sooner or later.

If this sort of work sounds interesting to you, take a look at our open roles — we’re hiring!