-
Notifications
You must be signed in to change notification settings - Fork 499
MvRx at Airbnb
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.
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.
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 includes a default layout with:
- An
AirRecyclerView
hooked up to a defaultEpoxyController
. - An
AirToolbar
. - A footer
- A CoordinatorLayout configured for PopTarts/Snackbars.
These can be accessed in a child fragment with the
recyclerView
,toolbar
andfooterContainer
properties.
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.
override buildFooter()
and return a single epoxy model like this:
override fun EpoxyController.buildFooter() = fixedActionFooter {
val (state) = withState(viewModel1)
buttonText(state.title)
}
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:
- Use the generic
create<A>(...)
function inMvRxFragments
. When you use the generic for, thenewInstance
,newIntent
, andstartActivity
functions will require you to pass in your args in a type-safe manner. - 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.
- 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>()
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
.
Override toolbarMenuRes
with your menu resource. Then you just have to override onOptionsItemSelected
.
Override lightForegroundToolbar
and return true.
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
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
.
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() }
}
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.
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.
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.
The Airbnb specific BaseMvRxViewModel
:
- Adds
execute
extension functions forAirRequest
. - Uses a default
SingleFireRequestExecutor
from theBaseGraph
with which all requests can be executed.
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.