1 year with MvRx. My journey from RecyclerView addiction to Fragment sommelier.
Disclaimer: MvRx (pronounced mavericks) is the Android framework from Airbnb that they use for nearly all product development at Airbnb. I’m not affiliated with them. This month, it’s celebrating 1 year and Airbnb 11 years, happy birthday to both!
Since I became an Android developer in the start of 2017, Fragments, Views and RecyclerViews bothered me. It was (and still is) really easy to make a spaghetti code by coupling everything together. I really couldn’t understand why I was required to write an Adapter, a ViewHolder, bind things manually and then even override the viewCount — my thought was always, how is it going to scale when I have multiple items of different kinds? The painful answer I later discovered: it won’t. Therefore, I became an addicted into finding solutions to this problem.
Journey into RecyclerView
At first, I began using FastAdapter. It provided a lot of simplification over the standard RecyclerView, but also required a lot of internal knowledge and casts — it was a Java library and got too complicated too fast when I began using Kotlin [It’s been fully rewritten recently, so it should be simpler now]. Back then, when I started increasing the complexity of my lists, I got into the same spaghetti code that I was trying to avoid in the first place. It wasn’t easy to deal with headers, animations, ItemDecorations and async (which was still AsyncTask). I lost control over my own codebase when code started to grow faster than expected and maintenance got harder. I just wanted a plug-and-play experience, a RecyclerView that worked like LEGO. Even today, FastAdapter samples are not that easy to understand [radio button sample]. It was a good first try, but ultimately it got so hard to keep everything together that I went to find a better solution for me.
In the beginning of 2018, I started migrating everything to Groupie. It was reliable, powerful and fun. It also worked greatly with Kotlin Extensions, animations and ItemDecorations. Sample code was intuitive and relatively simple, yet very powerful. My apps, including Change Detection, started shipping with it and has been great. There was only one relatively big issue: the main thread was responsible for making the diff operation. Fortunately, there is now a method to update the list asynchronously. Everything else worked fine.
While it was great, it was only the view part of the equation. I still needed to manually take care of Fragments, Lifecycles and ViewModels, and those things started to torment me. For example, I started making long forms and needed to take care of reload, scroll down, items being recycled, lifecycle, checking for invalid inputs and submitting. Not a pleasant experience. Same issue for a Settings screen, I ended up having the stored data and the retrieved data that was being shown: two models instead of one. When I modified one, I needed to update the other; while it worked, it was bug-prone, hard to test and hard to scale. You can see an example of that below with the
updatingGroup, responsible for keeping track of the items.
Groupie also had some maintainability issues: the repository got silent from May to October 2018. It has now transitioned to a new maintainer. This, nowadays, is great, but back then we had no idea what would happen with it. It was complete silence from the author. This, together with my fragments easily bypassing a thousand lines of code and issues keeping everything together, lead me to search for a better solution.
When MvRx was released in August of 2018, I decided to learn it. While I really enjoy all the contributions Airbnb has done, I had no idea how the journey was going to be. I just embarked. A lot of people said they were bringing how React works to Android, but I never understood React beyond their intro page, so it didn’t mean anything to me.
Epoxy was heavily integrated with MvRx. I had heard previously about it and saw many people migrating to it, but I had never tried. Eli Hart was a member of /r/androiddev and constantly answered even the most simple questions in GitHub. I’m glad there are people like him, it was one of the reasons that made me take the risk and try — remember, I was coming from a library that was apparently abandoned. And wow! It was worth it. There was a slightly steep learning curve, but over the months and after many doubts (including four issues and a one contribution) I think I finally grasped it.
Epoxy allowed me to write layouts faster and better than ever. I started using DataBinding, which allowed me to just draw a layout and build the project. A class with corresponding methods is generated and can be used in Epoxy (see image below). You can dynamically update the data and Epoxy will automatically diff and figure out how to update the list, a major win over FastAdapter and Groupie. This was exactly what I wanted when I first thought about the idea of LEGO bricks. I am happy that Compose UI will replace both Epoxy and RecyclerView. Epoxy was ahead of its time and eventually everybody will see the same benefits as I did. Every view in Android will soon work like a LEGO, not just the RecyclerView.
While the RecyclerView being hard to use was solved for me the moment I started using Epoxy, I still had to deal with Fragments, data retrieval and lifecycles. That was precisely what MvRx wrapped or solved. To use it, you (preferably) split a Fragment into a reusable BaseFragment and a Fragment that is unique. Then, you add a ViewModel that has an initial state. Simple as that. As the user interacts, ViewModel updates that state (which contains the data) and Epoxy watches the state for changes, updating the layout automatically.
MvRx allowed me to forget* the about Fragments and focus on the list. You can use other RecyclerView libraries, but it works seamlessly with Epoxy. The sample above shows how simple it is. There is a fact easy to forget: you don’t even need a list to use with MvRx, it works fine with whatever view you use (by using the invalidate method).
* There is a problem: while Epoxy is wonderful, you still live in a world of crazy lifecycles, view inflation and many other things. Therefore, you remember the Android framework exists every time you need to deal with something that is not a list; things start rapidly returning to the way they were previously. For example, in SDK Monitor there were a lot of places that needed to be taken special care of. Most of my apps start with a simple BaseFragment (like this | wiki), but eventually they needed a different BaseFragment for each of the following cases:
- Search toolbar
- Simple toolbar
- Elastic (like Inbox app: scroll up or down to dismiss)
- Elastic variations and Dagger variations
In some cases I created a class that overrides them, in others I made an entirely new/different BaseFragment. In some cases I inflated what I needed in the MainFragment, in others, the BaseFragment would take care of everything.
With Dagger, I included the following snippet in every class. If there is a way to not paste this in every Fragment that needs Dagger, please tell me. The shouldInject helps in debugging. I could have also made every BaseFragment import Dagger and let the app’s fragment enable/disable via shouldInject. This way, I would only have the code below once in the app, but then I would be importing Dagger in every project, even if I don’t need it. I couldn’t find a simple way that served all my issues.
With the strategy mentioned above, I wrapped all my different BaseFragments into a module and shared it with all my apps. But wait! When I say I shared with all my apps, I really mean it.
Android Studio is buggy with symlinks and APFS (Apple File System) is incompatible with hard links for directories, so I was left with no choice. I moved my projects to a HFS+ SSD and hard-linked the shared module in them using hardlink-osx. This helped me develop even faster. When I changed a method from a BaseFragment, it would update in all my projects. Risky? Yep. Any change could stop other projects from compiling? Sure. Would I ever do this if I were working with hundreds of people? No. But it was fun and this methodology helped me make it flexible yet powerful very fast. I would make all the changes in a project, then open another project and see if I needed to take care of any other scenario or not, then open another project and so on in cycles until they all worked fine and had an API that could be extended. This strategy helped me refine and choose the best vintages for my Fragments. The intention was to have every part of it to be maximum reused, optimised, crash-free and leak-free.
It is still hard-linked to this day, but nowadays changes are minimal, usually dependency updates. Both this talk from Airbnb [Scaling an Android App from 1 to 100 developers with modularization] and this talk from Lyft [Creating the Lyft Driver App: Reduce, Reuse, Recycle] helped me in figuring ways to correctly modularise an app and get a bottom-up approach that works.
MvRx relies in ViewModel, ViewModels are not always Dagger-friendly, therefore a lot of people complain on how Dagger works with MvRx. You need to use AssistedInjection and create an AssistedInject.Factory for each ViewModel. You end up with some boilerplate in ViewModels and you need to add inject a corresponding ViewModel.Factory in Fragments. This works fine, but you end up with ViewModels scoped to the Fragment. If you want to use activityViewModel() with Dagger, things start getting tricky quickly.
I personally love how MvRx tries to make every Fragment shine, but at the same time it only makes that fragment shine, it’s hard to share data between two fragments using the same ViewModel. This scenario is useful to me in many places, like selecting an item from a list and updating the other. There, I ended up using two ViewModels, one scoped to Fragment with injection and another scoped to Activity (or NavGraph) with the shared data. It is a simple enough case that could use a single ViewModel, but I needed three, two of them making the same database request, one for each screen, and one to share the data. Worked, but wasn’t a pleasant experience.
Every now and then someone says the reason we have so many different frameworks and architectures is to make testing easier. Yes, MvRx shines in making tests easier [sample]. It is easy to create fake data and test your viewModel states. I personally recommend you make at least one test as you develop your ViewModel. Sometimes things might get slightly unintuitive (like when using Dagger) or the official sample might be “too generic” for you. For example, it merges two dataSources into a single Observable, a beautiful but uncommon solution to me. Either way, developing both together can help you see the bigger picture and avoid any pitfalls, specially if you are new to it.
MvRx heavily depends on RxJava. With Coroutines Flow getting stable, it seems they will release a module with Coroutine [couldn’t find the reference]. There shouldn’t be a performance difference, but it might help those that don’t know or don’t use RxJava.
While I enjoy MvRx most of the time, one of the major drawbacks for me is how it isn’t available anywhere else. They had a plan to open source the iOS version, but it’s been months without any update. They also had a plan to open source Epoxy for iOS, but also no updates so far. With Swift UI getting released, it might not be necessary, but I still miss MvRx. It gets tricky to keep two sources in sync for the same app when you use different architectures.
With the release of Compose UI in a few months or years, it seems Epoxy will not be necessary anymore and I’m really curious to see how both MvRx and Android platform will adapt into the new world. For this reason, while I enjoyed the road and the ride, it might not be the best time for you to learn it.. Unless, you know, you are still suffering with RecyclerView daily and want your life a lot easier right now, please know that everything will probably change soon.
If you want to learn MvRx, I highly recommend this course from one of the developers. It’s short, but worth it. I thought I knew enough, but it still helped me learn some nice tricks.
Happy birthday MvRx and Airbnb!