Skip to content

MvRx at Airbnb

Eli Hart edited this page Sep 14, 2018 · 19 revisions

Non-Airbnb engineers: this page serves as a reference for what MvRx looks like for us. Feel free to pick and choose parts that are applicable for you.

Airbnb engineers: this page contains documentation for the Airbnb-specific APIs added on top of MvRx.

Activies

You don't need to create your own Activity with MvRx. When you register your Fragment in MvRxFragments (detailed below), the factory it returns includes a newInstance, newIntent, and startActivity function.

Under the hood, it uses a default MvRxActivity class that acts as a host for your Fragments.

MvRxFragments (registering and creating a Fragment)

Fragments are registered as functions inside of a class that extends Fragments. If your Fragment should be accessible to other modules, put it in MvRxFragments.kt in the intents module. Otherwise, create a Fragments class in your module.

object YourFeature : Fragments("com.airbnb.android.yourfeature") {
    fun yourFragment() = create(".YourFragment")
    fun yourFragmentWithArgs() = create<YourArgs>(".YourArgsFragment")
}

The create function returns a factory with newInstance, newIntent, and startActivity functions. If create has a generic type, each of the helper functions will take type-safe args.

MvRx will concatenate the prefix passed to Fragments with the suffix passed to create() and will create it with reflection. This enables you to launch features from any module without having to add a hard dependency on it.

If arguments are used they should be a Parcelable Kotlin data class. Create a file to declare all of your arg classes in the intents/args directory. For example:

// HelperCenterArgs.kt

@Parcelize
data class HelpCenterTopicArgs(
    val topicId: Int,
    val isGuestMode: Boolean
) : Parcelable

@Parcelize
data class HelpCenterChannelArgs(
    val channels: List<String>
) : Parcelable

MvRxFragment

Default Layout

MvRxFragment includes a default layout with:

  • An AirRecyclerView hooked up to a default EpoxyController.
  • An AirToolbar.
  • A footer
  • A CoordinatorLayout configured for PopTarts/Snackbars. These can be accessed in a child fragment with the recyclerView, toolbar and footerContainer properties.

Setting your EpoxyModels.

Override epoxyController() in your Fragment and provide the EpoxyController you would like to use to build models with. If your models are simple you can do it inline with the simpleController function. You can also provide a boolean for whether auto dividers should be disabled, and any view models who's state should be used.

override fun epoxyController() = simpleController(viewModel, disableAutoDividers = true) { state ->

}

When you use the MvRx ViewModel delegates (like fragmentViewModel), it will automatically subscribe to any changes and request model build any time it changes.

Setting a footer

override buildFooter() and return a single epoxy model like this:

override fun EpoxyController.buildFooter() = fixedActionFooter {
    val (state) = withState(viewModel1)
    buttonText(state.title)
}

Fragment Args

With MvRx, you never have to manually set your arguments bundle or create arguments keys. Instead, you should create a single @Parcelize Kotlin data class to hold the args for your Fragment. Then:

  1. Use the generic create<A>(...) function in MvRxFragments. When you use the generic for, the newInstance, newIntent, and startActivity functions will require you to pass in your args in a type-safe manner.
  2. Give any state class used by a ViewModel in your fragment a secondary constructor that takes your args class. It will automatically be used to create the initial state for your ViewModel.
  3. If possible, your args should only be used for creating the initial state (see #2). However, if you do need to access them in your Fragment, you may do so with the args delegate:
private val args by args<YourArgs>()

Custom Layout

If you want to use a custom layout, override the layout() function and return your own. If you use R.id.toolbar, R.id.recycler_view, and R.id.footer_container, MvRxFragment will wire them up as if you used the default layout so it can save you that overhead. The properties mentioned above will also be wired up. There are optional versions of the properties prefixed with optional.

Setting a custom menu

Override toolbarMenuRes with your menu resource. Then you just have to override onOptionsItemSelected.

Light Toolbar

Override lightForegroundToolbar and return true.

Navigating to another Fragment

Call showFragment to have a fragment slide in from the right or showModal to have it slide up from the bottom. If you use showModal

Initialization

You shouldn't need to use any actual Fragment lifecycles. However, if you need to do any initialization, override initView. At that point, the view will have been created and the Fragment will be attached to an Activity.

Wiring up a failure PopTart

To have a retry-able and swipe-able PopTart appear for the failure of a network request, add this to initView:

registerFailurePoptart(viewModel, MyState::listing) { fetchListing() }

Alternatively, you can register multiple properties for the same view model like this

registerFailurePoptarts(viewModel) {
     property(MyState::listing) { fetchListing() }
     property(MyState::translation) { fetchTranslation() } 
}

Logging

MvRx includes a simple way to track impression and TTI logging on a page. Override loggingConfig and return an object that defines your logging parameters.

Use it like this:

override fun loggingConfig() = withState(viewModel) { state ->
        LoggingConfig(
            pageName = PageName.PdpHomeMarketplace,
            tti = Tti(
                name = "p3_tti",
                dependencies = listOf(state.listingDetails, state.bookingDetails),
                metadata = {
                    kv("search_id", state.searchSessionId)
                    kv("id_listing", state.listingId)
                }
            )
        )
    }

If a LoggingConfig object is provided then an impression will be logged for the given pageName. If a Tti object is provided then a Page Interactive event will be fired when all of the dependencies are in the Success state. These dependencies must be Async properties in your MvRxState.

If TTI is present then the impression will be logged once the page is loaded, otherwise it is logged on first view invalidation.

onBackPressed

Handling the back button normally requires setting a listener on AirActivity. As a convenience, MvRx does this automatically. You should think twice before using this but it is there if you need it.

MvRx Debug View

Swipe down from the middle of the toolbar and it will toggle the MvRx debug view. It will flash and increment every time the view is invalidated as well as show the TTI time if it was overridden.

MvRxViewModel

The Airbnb specific BaseMvRxViewModel:

  • Adds execute extension functions for AirRequest.
  • Uses a default SingleFireRequestExecutor from the BaseGraph with which all requests can be executed.

MvRxLauncher

If you annotate create() functions in MvRxFragments with @Launchable, then it will be accessible via the MvRxLauncher which can be accessed by long pressing the app icon and launching its shortcut.

If your Fragment has arguments, annotate functions in its companion object with @MockArgs and it will use those in the launcher instead. You can create multiple @MockArgs and give them names. Their function name will show up in the launcher.