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 submit collection, we’ve been breaking down how Trio works to assist clarify our design choices, within the hopes that different groups may profit from elements 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 remaining a part of our collection, we’ll focus on 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 take a look at an instance of a easy Message Inbox display screen, composed of two Trios facet by facet. There’s a Listing Trio on the left, displaying inbox messages, and a Particulars Trio on the proper, displaying the total textual content of a particular message.

The 2 Trios are wrapped by a dad or mum display screen, which is answerable for instantiating the 2 youngsters, passing alongside knowledge to them, and positioning them within the UI. As you may recall from Half 2, Trios could be saved in State; the dad or mum’s State consists of each the message knowledge in addition to the kid Trios.

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

The dad or mum’s UI decides methods 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 facet by facet when the system 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 little one screens want entry to the newest message state in order that they know which content material to indicate. We will present this with Props!

Props are a group 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 supply up to date knowledge 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 cross message knowledge from the dad or mum Trio to the Listing and Particulars Trios. When a dad or mum defines little one Trios in its State, it should embody the kind of Props that these youngsters require. For our instance, the Listing and Particulars display screen every have their very own distinctive Props.

The Listing display screen 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 dad or mum to inform it when a brand new message has been chosen.

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

The Particulars display screen simply must know which message to show.

knowledge class DetailProps(
val selectedMessage: Message?
)

The dad or mum ViewModel holds the kid cases in its State, and is answerable for passing the newest Props worth to the kids.

Passing Props

So, how does a dad or mum Trio cross Props to its little one? In its init block it should use the launchChildInitializer perform — this perform makes use of a lambda to pick 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 cross 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 dad or mum’s state.

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

A typical sample we use is to incorporate perform references within the Props, which level to capabilities 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 perform. Props will also be used to cross alongside advanced dependencies, which kinds a dependency graph scoped to the dad or mum.

Notice that we can’t cross Props to a Trio when it’s created, like we do with Args. It’s 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 comprise lambdas and different arbitrary objects that can not be safely serialized, we should use the above sample to determine a circulation of Props from dad or mum to little one that may be reestablished even after course of recreation. Navigation and inter-screen communication can be quite a bit less complicated if we didn’t need to deal with course of recreation!

Utilizing Props

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

Little one ViewModels override the perform updateStateFromPropsChange to specify methods to 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 newest knowledge 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, corresponding to dependencies or callbacks, the ViewModel can entry the newest Props worth at any time by way of the props property. For instance, we do that within the onMessageSelected perform within the pattern code above. The Listing UI will invoke this perform when a message is chosen, and the occasion will probably be propagated to the dad or mum by way of Props.

There have been loads of complexities when implementing Props — for instance, when dealing with edge instances across the Trio lifecycle and restoring state after course of dying. Nevertheless, the internals of Trio disguise a lot of the complexity from the tip person. Total, having an opinionated, codified system with kind security for a way Compose screens talk has helped enhance standardization and productiveness throughout our Android engineering group.

One of the crucial widespread UI patterns at Airbnb is to coordinate a stack of screens. These screens might share some widespread knowledge, and observe comparable navigation patterns corresponding 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, however it’s tedious to do this manually. To assist, Trio supplies a normal “display screen circulation” implementation, which consists of a dad or mum ScreenFlow Trio and associated little one Trio screens. The dad or mum 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 capabilities.

Contemplate 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 knowledge 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 it is going to be rendered first. The circulation’s State object additionally acts because the supply of fact for different shared state, such because the TodoList knowledge mannequin.

knowledge class TodoFlowState(
@PersistState
override val childScreenTransactions: Listing<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 knowledge mannequin, callbacks like a reloadList lambda, and a NavController with its youngsters.

knowledge 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 perform will probably be invoked anytime the interior state of TodoScreenFlowViewModel adjustments, that means any replace to TodoList mannequin will probably 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 screen’s ViewModels, we are able to see that it’s going to 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 knowledge 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 dad or mum is used to soundly navigate to the subsequent display screen within the circulation (guaranteeing it can obtain the shared TodoFlowProps). Internally, the NavController updates the ScreenFlow’s childScreenTransactions triggering the ScreenFlow infra to supply 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 site visitors in mid 2022.

As of March 2024, we now have over 230 Trio screens with vital manufacturing site 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 completely satisfied 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 quite a bit.” One other stated, “Trio makes you unlearn unhealthy habits and undertake greatest practices that work for Airbnb primarily based on our previous learnings.” Total, our group studies 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, now we have invested in IDE tooling with an in-house Android Studio Plugin. It features a Trio Era software that creates all the recordsdata and boilerplate for a brand new Trio, together with routing, mocks, and assessments.

The software helps the person select which Arguments and Props to make use of, and helps with different customization corresponding to organising customized Flows. It additionally permits us to embed academic 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 various recordsdata.

We leveraged our IDE plugin to supply a software to routinely 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 enhancing 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 pleasant.

Total, 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 decisions had been well worth the tradeoffs.

One change we’re anticipating, although, is to include shared component transitions between screens as soon as the Compose framework supplies APIs to help 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 advanced and fascinating initiatives like Trio, and we’re excited to share extra sooner or later.

If this type of work sounds interesting to you, try our open roles — we’re hiring!