The Android Way at Crunchyroll
Why architectural certainty is so important in a constantly changing tech field, and how our team size plays a key role in our process.
A well defined architecture for a large team is a significant journey, even nowadays in 2021. Discussions about which architecture is best never end, and the answer is not trivial. Every team has its long road, priorities and challenges. We at Crunchyroll on the Android team are no exception. In this article we will share our approach and experience.
Everything starts with legacy code, of course :). For any project, one day comes the time when maintenance is just too expensive, painful, and unstable. Fixing the bugs that should not happen is not the job every developer dreams of :)
Rewriting a big codebase is a task that requires patience and care. The first question we had to answer was which architecture to choose, and we did not want to proceed without having it all testable and decoupled.
MVP was the obvious choice for us since it allowed us to break huge components into testable layers and pieces, as well as the team being familiar with it already. It felt like a natural movement for us.
What are priorities and requirements for an architecture, for us, a team of 10 developers?
Clear separation between layers and responsibilities
Every layer should be responsible for their domain of knowledge and concern. When something goes wrong, this easily pops up in class imports. Whenever a bug occurs, it is always easy to figure it out, as the nature of the issue will easily guide you to the layer where it happens.
Consistency
We love when a PR looks familiar and the only thing you should focus on is the business logic it brings to the table. For a new feature, half of the code changes should be trivial to review — it should look familiar and bugs should pop up easily in the eyes.
Stability
There should be an appropriate place for any use case to handle.
Simplicity (easy to build, easy to review)
The code and its integration should look readable and be trivial to understand as well. The applied patterns and architecture should not challenge you and should not compromise readability.
Less collisions between developers
Whatever you work on — fixing a bug, implementing a new feature, extending some functionality — there should be minimal conflicts with other developers. With the majority of cases, developers are working in separate modules, packages, or layers of the app.
Easy unit-testing
The smaller a class responsibility is, the easier it is to test it and the more readable and trivial unit tests look (which we love).
We like dumb views
There’s nothing easier than reviewing changes in a View class, which contains no logic. It is clean and boring :)
Why MVP in 2021?
Well, not pure MVP :) Considering the variety of use cases, modern libraries, and tools, it would be very difficult to integrate all of them in MVP without extending or adapting it.
Our architecture is an adaptation of the MVP pattern to the modern requirements and tools that we enjoy using: Kotlin, Architecture Components, Coroutines. All of them were adopted to solve the common issues that every team faces when it grows:
- Tremendous sizes of Java code (Kotlin to the rescue)
- Android lifecycle handling and leaks (Lifecycle to the rescue)
- Callback hell and async communication with the model layer (Coroutines to the rescue)
Layers and their responsibilities
Model
That layer includes the work with API, repositories, data processing/aggregation and mapping to UI models. Usually it’s an Observable Interface. The public API of its components provides LiveData that is easy to subscribe on and trivial functions that usually deal only with the data. All observables are wrapped in their respective ViewModels to survive screen rotations and be ready to respond whenever a subscription is established.
Presenter
We don’t bother with data in presentation and only consume it to properly render the view according to the business requirements. This is where the subscription to the observable Model Layer is set and whether data was loading, succeeded or failed, the View will be updated accordingly.
This layer suits very well to handle user interactions and deal with what’s next to request from the Model Layer. We found this layer more suitable for View updates than a ViewModel in classical MVVM interpretation for our project that would require a huge amount of observables to handle all various UI use cases.
For example, showing a popup whenever a user presses on a button, which is a simple function call on a View interface that suits our readability and simplicity standards better.
View
Receives UI models to render its components and notifies the presentation layer about user interactions.
We never mix responsibilities between the above layers, as every layer suits the best for its domain of knowledge.
Why don’t we bother much about other architectures?
Every architecture has its strengths and weaknesses. The question is which compromises are we ready to live with?
Our approach is more driven by the size of the team, workflow and quality requirements. Simplicity, readability, easy maintenance and easy onboarding are key components for us as a growing team.
We keep an eye on the most popular and modern architectures and seek reasons to migrate to a better one that is worth the effort. We don’t have big issues with the current one, except Android Framework that still consumes a lot of time to deal with :(
We have big hopes for the Jetpack Compose. Hopefully, the declarative paradigm will sort many things out. That’s why features are designed to consist of multiple custom and reusable components that are not bound to the view implementation.
Maybe one day dealing with UI, navigation, user interaction will be the most trivial in mobile development (fingers crossed).
What are the issues we’re still not happy with the current architecture?
We still rely on the Android architecture components and everything lives in its lifecycle. We’re accommodating when needed.
The majority of issues are mainly related to Android Framework use cases handling:
- Lots of Activities, Fragments, Adapter’s, ViewHolder’s
- Behind every Activity, Fragment or custom view is a special reason for its form of existence. That’s why we have big hopes for Jetpack Compose, where everything is a Composable. We’re tired of such a fragmentation in View layer, and Composable looks to be the consistency we’ve been looking for for a long time.
Sometimes MVP feels boring as the clear structure doesn’t leave room for big mistakes :D (or creativity).
Conclusion
While the community moves forward and adopts Compose, we enjoy following the positive changes happening in Android, but we keep our patience :) We have a lot of work going forward and we’ll definitely share our migration experience and approach for Compose. Make sure to follow us here on Medium so you definitely don’t miss it :)