Building the Coinbase Android App
The Coinbase Android team accomplished two big goals in the last year. First, we shipped a newly redesigned Android app. Second, we’ve made many improvements in how we develop on Android. In this article, I’ll give some insight into the technical improvements that we made and why we made them.
At Coinbase, we strive to ship high quality updates at a rapid pace. At the same time, we need to make sure that the code can effectively scale with the product and team. Because of this, we have to ensure that our code is easy to work with and simple to understand. We also want it to be easily testable and extensible. To accomplish all of this, we took a fresh look at how we structure and write code.
MVP
Like many early stage companies that need to move fast, the initial version of the app didn’t have a lot of thought behind how code should be structured and organized. We wanted to choose an architectural pattern that would allow the code to scale with the product and the team. We experimented with a few options but ultimately decided that the MVP pattern made the most sense for us at this stage. The following reasons explains why:
- Improved testability
- Better separation of concerns
- Changing layouts is easier, important for things like heavy AB testing
- Code structure scales better with a growing team and can reduce code conflicts
- Allows easier migration to Kotlin
RxJava
Some of our goals were to simplify, modernize, and clean up our code. We found that RxJava was a great tool to help us reach that goal. Some of the ways that it helped were:
- A clean alternative for AsyncTask
- Replacement for EventBus which improves speed
- Combining actions is much easier
- Replacement for the listener pattern which helps decouple code and improves reusability
- Better state handling with clearer scope and immutable properties
- Invites more asynchronous coding and simplifies implementation of that
RxJava isn’t without it’s faults. We also found that there were some drawbacks. Namely:
- Learning curve is high and increases ramp up time for those not already familiar with it
- Documentation is decent but examples are lacking online
- Unhandled errors and back pressure can cause exceptions
- Debugging can be tougher with unhelpful stack traces
Ultimately, we decided that the benefits outweighed the costs and are now seeing it pay back in dividends.
Dependency Injection
Our code was originally using the now defunct RoboGuice as our dependency injection framework. Because of this and other reasons which I’ll talk about in a bit, we made the move to Dagger. Among having active community support and being battle tested, some other benefits we saw were:
- Predictable components and component initialization with less boilerplate code
- Composition over inheritance
- More performant because of compile-time injection and elimination of reflection
- Testing advantages, allows easily mocked API responses
It’s clear that moving away from RoboGuice was a no-brainer.
Single Activity and no Fragments
During the redesign of our app, we transitioned from using a side navigation drawer to a bottom navigation bar. This gave us the opportunity to simplify how we worked with views. In particular, we wanted to eliminate the use of Fragments and only have to manage a single Activity for the main portion of our app. Here are the reasons why we did this:
- Lifecycle management is simplified
- Easier view management
- Better control over backstacks including managing multiple backstacks
- For security, it provides a smaller attack surface than compared to multiple Activities
- Easier control over transition animations
We’re now working more efficiently and effectively than ever before. We also understand that we’ll likely revisit these topics as we scale up even more. There’s going to be some interesting challenges for us in the coming days. If you want to help us tackle them, we’re hiring!