Introducing Trio | Half III. Half three on how we constructed a Compose… | by Eli Hart | The Airbnb Tech Weblog | Apr, 2024

By: Eli Hart, Ben Schwab, and Yvonne Wong

Trio is Airbnb’s framework for Jetpack Compose display screen structure in Android. It’s constructed on high of Mavericks, Airbnb’s open supply state administration library for Jetpack. On this weblog put up sequence, we’ve been breaking down how Trio works to assist clarify our design choices, within the hopes that different groups would possibly profit from facets of our method.

We advocate 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 last a part of our sequence, 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 higher perceive Props, let’s have a look at an instance of a easy Message Inbox display screen, composed of two Trios aspect by aspect. There’s a Record Trio on the left, displaying inbox messages, and a Particulars Trio on the appropriate, displaying the complete textual content of a particular message.

The 2 Trios are wrapped by a dad or mum display screen, which is accountable for instantiating the 2 kids, passing alongside information to them, and positioning them within the UI. As you would possibly recall from Half 2, Trios will be saved in State; the dad or mum’s State contains 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 dad or mum’s UI decides the way to show the kids, which it accesses from the State. With Compose UI, it’s straightforward to use customized format logic: we present the screens aspect by aspect when the machine is in panorama mode, and in portrait we present solely a single display screen, 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 youngster 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 set of Kotlin properties, held in an information class and handed to a Trio by its dad or mum.

In contrast to Arguments, Props can change over time, permitting a dad or mum to offer up to date information as wanted all through the lifetime of the Trio. Props can embody Lambda expressions, permitting a display screen to speak again to its dad or mum.

A baby Trio can solely be proven in a dad or mum 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 dad or mum Trio to the Record and Particulars Trios. When a dad or mum defines youngster Trios in its State, it should embody the kind of Props that these kids require. For our instance, the Record and Particulars display screen every have their very own distinctive Props.

The Record display screen must know the record of all Messages and whether or not one is chosen. It additionally wants to have the ability to name again to the dad or mum 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 screen simply must know which message to show.

information class DetailProps(
val selectedMessage: Message?
)

The dad or mum ViewModel holds the kid situations in its State, and is accountable for passing the most recent Props worth to the kids.

Passing Props

So, how does a dad or mum Trio move Props to its youngster? In its init block it should use the launchChildInitializer operate — this operate 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 operate 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 dad or mum’s state.

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

A typical sample we use is to incorporate operate references within the Props, which level to features on the dad or mum ViewModel. This permits the kid to name again to the dad or mum for occasion dealing with. Within the instance above we do that with the showMessageDetails operate. Props will also be used to move alongside complicated dependencies, which kinds a dependency graph scoped to the dad or mum.

Observe that we can not move Props to a Trio when it’s created, like we do with Args. It is because Trios should have the ability to be restored after course of demise, and so the Trio class, in addition to the Args used to create it, are Parcelable. Since Props can comprise lambdas and different arbitrary objects that can’t be safely serialized, we should use the above sample to ascertain a circulation of Props from dad or mum to youngster that may be reestablished even after course of recreation. Navigation and inter-screen communication could be quite a bit less complicated if we didn’t need to 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.

Baby ViewModels override the operate updateStateFromPropsChange to specify the way to incorporate Prop values into State. The operate is invoked each time the worth of Props modifications, and the brand new State worth is up to date on the ViewModel. That is how kids keep up-to-date with the most recent information from their dad or mum.

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, akin to dependencies or callbacks, the ViewModel can entry the most recent Props worth at any time by way of the props property. For instance, we do that within the onMessageSelected operate within the pattern code above. The Record UI will invoke this operate when a message is chosen, and the occasion might be propagated to the dad or mum by means of Props.

There have been plenty of complexities when implementing Props — for instance, when dealing with edge instances across the Trio lifecycle and restoring state after course of demise. Nevertheless, the internals of Trio conceal many of the complexity from the top consumer. 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 group.

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

Earlier, we confirmed how a Trio can handle a listing of youngsters in its State to perform this, however it’s tedious to try this manually. To assist, Trio supplies a regular “display screen circulation” implementation, which consists of a dad or mum ScreenFlow Trio and associated youngster Trio screens. The dad or mum ScreenFlow mechanically manages youngster transactions, and renders the highest youngster in its UI. It additionally broadcasts a customized Props class to its kids, giving entry to shared state and navigation features.

Take into account constructing a Todo app that has a TodoList display screen, 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 might 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 start out 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. Nevertheless, the circulation defines Props to share the TodoList information mannequin, callbacks like a reloadList lambda, and a NavController with its kids.

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 screen within the circulation. The ScreenFlowViewModel base class implements this NavController interface, managing the complexity of integrating the navigation actions into the display screen 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 operate might be invoked anytime the inner state of TodoScreenFlowViewModel modifications, that means any replace to TodoList mannequin might 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 kids display screen’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(process: TodoTask) {
this.props.navController.push(Router.TodoTaskScreen, TodoTaskArgs(process.id))
}
}

In navigateToTodoTask, the NavController ready by the circulation dad or mum is used to securely navigate to the following display screen within the circulation (guaranteeing it should 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 screen, and render the brand new display screen.

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 important manufacturing visitors at Airbnb.

From surveying our builders, we’ve heard that lots of them benefit from the general Trio expertise; they like having clear and opinionated patterns and are pleased to be in a pure Compose atmosphere. 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 quite a bit.” One other stated, “Trio makes you unlearn dangerous habits and undertake greatest practices that work for Airbnb primarily based on our previous learnings.” General, our group stories sooner improvement cycles and cleaner code. “It makes Android improvement sooner and extra pleasing,” is how one engineer summed it up.

Dev Tooling

To assist our engineers, we’ve invested in IDE tooling with an in-house Android Studio Plugin. It features a Trio Technology instrument that creates the entire information and boilerplate for a brand new Trio, together with routing, mocks, and checks.

The instrument helps the consumer select which Arguments and Props to make use of, and helps with different customization akin 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 vary a Trio’s Args or Props sorts, since they’re used throughout many alternative information.

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

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

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

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

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

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