Over the years, mobile application developers have experimented with various standard architecture patterns like Model View Controller (MVC), Model View Presenter (MVP), Model View ViewModel (MVVM), and clean architecture et al. These patterns need improvisations to implement it for specific requirements of the mobile app. While designing the architecture, the first step is to identify and state the objectives. Below were the objectives identified by us:
- Single source of truth.
- The business logic should be suitable for unit testing and reuse.
- Follow SOLID principles to ensure scalability, flexibility and integration of new components in the future. Such scalability and flexibility should come only at a nominal overhead cost.
- It should adapt to the necessary platform APIs and external SDKs (like Facebook, analytics SDKs etc.) without compromising the fundamentals.
- Each and every class of the project must be in-line with the architectural principles. This helps in seamless initialization and any further reference to the class.
In this blog series, we will discuss the architecture of Dream11 Android app, and future improvements that we plan to undertake:
Dream11 Android Architecture
Our philosophy is to design and maintain a Clean Architecture. We have segregated it in 4 layers, View, Presenter, Model and Services. We are using RxJava for data streaming, Dependency Injection for object accessing and DataBinding library to update the view and obtain events from the view. It is important to note that we are using data binding, but not the MVVM philosophy, where View Models are the most intelligent entities. For us, View models are just POJO classes which hold data for a view and obtain events from view.
Clean Architecture has guidelines for the object dependency (producer-consumer) and the code structure
- Producer-Consumer object dependency
Having a cleanarchitecture means that the producer should not be dependent on the consumer, as in below diagram, where view layer is consuming the observables from the presenter and the presenter is consuming the observables from model layer and so on. In this case, producer layer objects can be created without the dependency on the consumer layer
2. Code structure We can segregate mobile application code in 4 parts.
- Platform specific code (Service Layer, View Layer)
- Enterprise specific code (Feature Layer)
- Application specific code (Feature Layer)
- Interface/Adapters (Presenter Layer)
We have written enterprise and application specific logic in the same layer to reduce communication between layers and centralise all decision making at one place.
View Layer: It comprises of Activities and Fragments. Each flow has one Activity and multiple Fragments for respective screens. For example, LoginActivity manages different screens (fragments) of login flow. This approach provides tremendous flexibility and ease. For instance, whenever we need to discontinue the flow, we can simply close the activity. We also use flow specific activities to trigger prerequisite execution.
Presenter Layer: It is an interface/adapter between view layer and the model layer. It has below responsibilities:-
- It creates ViewModels using the data received from the feature layer and exports them to view layer through observables
- It delegates the events of view layer to respective feature layer class
- While mapping models to view models, it also adds view specific logic, if required
This is the most intelligent layer in our architecture. It contains the enterprise logic and the app logic (deciding the flow of an application). Below are some interesting characteristics of this layer:
- Feature classes are segregated on the basis of business logic types:Since data sharing across different app components needs wiring code, we minimized the number of feature classes by using logic type and not screens like LoginFeature, PaymentFeature. As a result, the logic for all login related screens are handled by Login Feature. Similarly, the logic for all payment related screens is handled by PaymentFeature class, because same business components can share the data with relative ease.
Feature classes can communicate with each other through Request Message Pool, RMP is a special kind of event bus, we will discuss it in detail in the next blog
- Own/Monitor the data: We have a single source of truth for data. But, instead of the single state container, it is fragmented in respective feature classes. For instance, the PaymentFeature class takes care of the Payment state of the app, so all the feature classes are a singleton, created using the Factory-Pattern.
- Communication: There is an input and two output flows on the feature layer. One who initiates the action and gets the output as the return of method flow Observable<FeatureResponse>. Since our feature classes are also maintaining a single source of truth of data, with every change of data, they push it on FeatureUpdate channel. This is subscribed by all the other related views.
Model Layer should be platform independent. This allows all the platform specific functions like making HTTP calls, a message read permission, log in through Facebook and Google, saving or retrieving values from database to be written at the service layer.
Some Android SDK APIs seeks message read permissions while the others (Facebook, etc.) need an instance of the activity. Clean architecture principles do not permit access to the platform object instance in enterprise or app specific code. Additionally, we cannot put them in the view layer because a producer cannot depend on its consumer objects.
To solve this problem, we created UIListener interface. Here, the object is being passed to the service layer class, which provides current activity and application object to service classes.
Things to be improved:
- Java to Kotlin: As of now, we are using Java. But we have also started Kotlin for some components and features. Data classes of Kotlin are very effective for Layered Architecture since every layer has to ensure that its original data objects cannot be changed by the consumer layers. For this, we need to create a copy of data objects while passing it to consumer layer.
- Removing side effects and separating code in pure and impure functions: As of now, we are separating code in pure and impure functions manually. But in the future, we would like to create a framework which enforces pure functions and separates impure logic in an impure function. This will make it easy to write test cases.
- REST to Graphql: As an organisation, we are progressing from REST to Graphql, as it allows enhanced communication between client and server. It has certain advantages like being strictly typed and the best place to write common logic for all the clients. It reduces the complexity of code at the client side, reduces state size and provides view ready data to them. Since it is an abstraction layer between client and server, their architectures can be developed more independently.