-
Notifications
You must be signed in to change notification settings - Fork 500
MvRx Launcher
Available from 2.0.0-alpha2 release
Add the mvrx-launcher
artifact as a debug dependency in your app to be able to easily load mocks for each of your screens. See this article for an overview of the launcher.
For more details on the mocking system, see this page.
To allow mocks to work, make sure you have enabled them:
MvRxMocks.install(applicationContext)
Then start MvRxLauncherActivity
to access the launcher. Use MvRxLauncherActivity.show(context)
as an easy shortcut.
The main screen of the Launcher shows a list of all Fragments that were discovered. Clicking a Fragment's name shows all mock entry points that are declared for that Fragment. Pressing back returns to the list of fragment names.
If you click a mock it will launch the Fragment and initialize it to the mocked state. It should then be usable as normal. This works even if the fragment is in the middle of a flow. Pressing back returns to the launcher.
The Launcher does its best to optimize for developer iteration and restore you to the previous page you were on the last time the Launcher was used. This allows you to rebuild the app and have the launcher reopened to the same mock page you were on before.
Additionally, recently used mocks and fragments are moved to the top of their list for easier access.
Every Fragment has three mocks that are always named the same, and are provided by default using the Fragment's default state and arguments.
Default initialization - This creates the ViewModels and States as if the Fragment is being opened for the first time - using the constructor of the State class and passing arguments from the Fragment to the State if any exist.
Default state - This skips the normal initialization and forces the default mock state to be set on the ViewModel.
Default state after process recreation - This mimics what happens when the app process is recreated and State is saved to a Bundle and then restored. This uses the standard MvRx codepath of saving state, which first recreates state from arguments, and then restores any properties annotated with @PersistState
. This mock helps to check that your fragment handles saved state correctly.
All mocks defined below those three defaults are custom named variants that you define in your mock definitions.
You can manually verify that mocks load without crashing by selecting the “Test all” option from the menu.
If selected from the main launcher page it will test all mocks in the flavor. If you click into a Fragment first it will only test the mocks in that fragment.
The “test” opens each mock - checking for crashes. In the future we can support clicking all views on the screen to look for crashes
If all mocks pass without crashing you will see a success dialog. If there is an exception the app will crash and you will have to look at logcat for the issue. Something like this command can be helpful for that - adb logcat -d | grep —color=auto —exclude-dir={.bzr,CVS,.git,.hg,.svn} FATAL -A 50
MvRxLauncherActivity
is exported, so you can open it via adb on the command line.
adb shell am start -n com.your.application.package/com.airbnb.mvrx.launcher.MvRxLauncherActivity
Just change com.your.application.package
to the package of your application.
There is support for two String extras for customization:
viewNameToOpen - Open a specific fragment or mock by name. This doesn't have to match exactly, the first FQN of a view that contains this string will be opened. If a matching view is not found, the first mock name to match is opened. Matching is case insensitive.
adb shell am start -n com.airbnb.sample/com.airbnb.mvrx.launcher.MvRxLauncherActivity --es viewNameToOpen MyFragment
viewNamePatternToTest - Matches all fragments and mocks whose name contains this string. A test flow is started for them to check that they can open without crashing. Matching is case insensitive.
adb shell am start -n com.airbnb.sample/com.airbnb.mvrx.launcher.MvRxLauncherActivity --es viewNamePatternToTest com_my_package
If you would like to pass multiple words in the pattern, you need to wrap the arguments in quotes. You may need to quote the full command for this to work
adb shell "am start -n com.airbnb.sample/com.airbnb.mvrx.launcher.MvRxLauncherActivity --es viewNameToOpen 'Default state'"
You may also find it helpful to define functions in your bash profile to make it easier to call these.
# Open the launcher
function mvrx_launcher() {
adb shell am start -n com.your.application.package/com.airbnb.mvrx.launcher.MvRxLauncherActivity
}
# Test fragments and mocks that match the given pattern.
# ie: mvrx_test my_fragment
function mvrx_test() {
adb shell am start -n com.airbnb.sample/com.airbnb.mvrx.launcher.MvRxLauncherActivity --es viewNamePatternToTest $1
}
# Test all mocks in the app
function mvrx_test_all() {
adb shell am start -n com.airbnb.sample/com.airbnb.mvrx.launcher.MvRxLauncherActivity --es viewNamePatternToTest ""
}
# Open the first fragment that matches the pattern
# ie: mvrx_open MyFragment
function mvrx_open() {
adb shell am start -n com.airbnb.sample/com.airbnb.mvrx.launcher.MvRxLauncherActivity --es viewNameToOpen $1
}
By default the launcher does not work in an app configured for jetpack navigation, since it cannot know how to start a fragment with your navigation graph. You can define a custom activity with your nav graph that the launcher will use instead. See LauncherActivity
in the mvrx sample module for an example.
If you have a specific activity that you would like fragments to launch into you can customize this by setting MvRxLauncherMockActivity.activityToShowMock
.
You can customize the content shown in the MvRxLauncher by subclassing MvRxLauncherActivity
and opening your subclass instead. You can override the buildCustomModels
function to specify additional RecyclerView rows to show with the Epoxy library.
Since each mocked fragment is launched in isolation it must be able to function independently, without assuming any other fragments or activities are present. For example, it must not make assumptions about what its parent fragment or parent activity is.
This means that if you need inter-fragment communication you cannot assume another fragment exists, but instead must use decoupled approaches, such as a Dagger injected class.
Note that the same restriction applies for fragments being run in automatic test suites, so all MvRx fragments should follow this.
MvRx inspects the app Dex files to find all MvRx fragments and their declared mock states and arguments. This discovery process is the same as when tests are run on Fragments. This process can take some time, so Fragment and mock information is saved in shared preferences for faster loading on subsequent app starts.
When a Fragment is selected for display the chosen mock state is applied to it (again, using the same technique as in tests). This is done via a mocked MvRx State Store which allows us to force a certain state, while blocking external calls to set state. This prevents normal ViewModel or Fragment initialization from overriding the mock state.
If the Fragment uses an existingViewModel
then we override that to instead act like activityViewModel
. This is necessary since we synthetically jumped into the middle of a flow so previous view models won't exist.
Network (or other Async
operations) that are started on fragment initialization are blocked so that they don't override the forced mock state. Once initialization is over the mocked ViewModel state store is switched to a functional store so that the screen can be interacted with like normal, and future state updates due to user interaction are allowed.