Android Architecture Pattern

Akshay yadav
Team Pratilipi
Published in
11 min readFeb 2, 2022

--

By Robert Martin (a.k.a. Uncle Bob) “By separating the software into layers, and conforming to The Dependency Rule, you will create a system that is intrinsically testable, with all the benefits that implies. When any of the external parts of the system become obsolete, like the database, or the web framework, you can replace those obsolete elements with a minimum of fuss.

Single Activity Architecture

Briefly, the Single-Activity Architecture is the architecture that has only one Activity or a relatively small number of Activities. Instead of having one Activity represent one screen, we view an Activity as a big container with the fragments inside the Activity representing the screen.

Why Single Activity Architecture?

  • Reusability — using fragments give us more reusability over code as the same fragment can be used in different flows.
  • SharedViewModel — using fragment fragment A and fragment B can share ViewModels to share the same data in intent or refetching it.
  • Transitions — transitions in fragments are more refined and controlled.
  • Deep Links — with fragments and nav-graph deeplinks can be defined easily and navigation in multi-modules app can be much more easier than providing class name for intents
  • Localisation and theme — the biggest huddle in changing app wide theme and locale is to restart app to reload resources in updated configurations and setting locale in base activity or in each activity, as for single activity calling recreate will ensure that new configuration is applied.

View State Architecture

Based on MVRX by airbnb, a Kotlin data class is used to define complete ViewState of a UI.

Why ViewState?

  • View complete UI state on a single glance.
  • Define default state at one place rather than review all classes and xml to check default UI state.
  • By using Kotlin state flow always minimises UI rendering by collecting the latest value.
  • ViewSate acts as a single source of truth for defining UI.
  • In Non-ViewState multiple LiveData and flows, or mutable properties are used which are hard to track and a lot of redundant code to make variables getter, setters and their observers.

ViewState

  • ViewSate must be managed by ViewModel wrapped in Kotlin state flow.
  • All properties must be immutable i.e, val.
  • Use copy() method to update ViewState.
  • Use collectLatest to ensure the latest value is rendered on the UI.
  • Don’t use mutex lock for async jobs, it should always be used to set data.

Pending Actions and UI Actions

  • We use Pending actions to submit user actions, managed by ViewModel using kotlin shared flow.
  • Pending actions are the only action in which user action is used to modify data or refresh UI data.
  • Pending actions are private state flow i.e, can only be accessible inside ViewModel
  • Exhaustive in nature as pending actions are defined as sealed class.
  • In some cases we need to perform navigation or ask for user consent which can only be done by either activity, fragment or dialogs. Which ViewModel can not accomplish to overcome this we provide public shared flow defined as pending ui actions.
  • Pending UI actions are collected inside fragment, activity or dialogs
  • Pending UI actions setter is private and can only be accessible from ViewModel

Clean Architecture in Android

Now we have achieved modularisation we will move onto the flow of data in modules.

Why Clean Architecture?

  • Single source of Truth — Providing a single source of truth for data to avoid error-prone code as only one source will be responsible for serving data.
  • Unidirectional flow of data — one-way data flow, which means the data has one, and only one way to be transferred to other parts of the application. In essence, this means child components are not able to update the data that is coming from the parent component.
  • Separation of Concerns — Separation of code in different modules or sections with specific responsibilities making it easier for maintenance and further modification.
  • Loose coupling — flexible code anything can be easily be changed without changing the system
  • Easily Testable

Flow of data

The flow of data in clean architecture is unidirectional, as data provided from data source will not be updated in between, one must re-request data from data source on data change.

  • DataSource — Data source is responsible for serving data from the server.
  • ApiService / GQLClient — a class to provide access to server apis.
  • Mappers — Map data responses to entities
  • DataStore — Data store provides access to databases (dao’s).
  • Repository — A repository layer is responsible for serving data (local/server).
  • Use Case — Domain layer handles all the business logic. Also merges data from different sources can be done here. A use case can access multiple repositories. Checkout all use-cases(Interactors) samples in Tivi.
  • ViewModel — map data in ViewState (represents view state by Kotlin data classes), manages data for the UI layer.
  • UI — User Interaction layer renders ui for given data and manages user actions. Must be free of business logic.

Approach to handle Data

  1. Flow of data :- Data must flow from the data layer to ui layer in one direction as immutable data. No intermediate layer will modify the data. On user action data modification will be requested from data source and on data modification event new data will be provided by data source either by observing, collecting or re-requesting.
  2. Flow of events — Events must flow from the ui layer to the data layer in one direction.
  3. Entities — When interacting with entities it is recommended that view state in the ui layer must encapsulate entity rather than making mapper for entity to ViewState and duplicating entities attributes.
  4. DB first approach :- most of the time to avoid showing blank or loading screens we can serve last fetched data to provide seamless behaviour to user
    - Observe table for query in database
    - Request server for data on empty or stale data.
    - Insert data on response from server
    - New data will be provide through observers
  5. Cache first approach :- in few cases there are some temp data responses which we don’t want to store in db, but on user action we might end up making more than one or two calls for the same response. For such calls we will maintain network caches.

References

Modularisation In Android

We always heard about modularisation and breaking monolithic modules into multiple small library modules, but breaking a monolithic module (:app module) in medium or large scale apps is much more difficult to achieve the required refactor and without proper steps it can also create a huge mess. So why modularisation?

Benefits of modularisation

  • Incremental build time, by using Gradle caching only modules with code changes will execute Gradle tasks.
  • Code separation and better code hygiene
  • Small and focused module — more productivity while working on a particular feature
  • Features are more self-sufficient and less interdependent so more flexibility in design, implementation and testing
  • Potential for dynamic delivery, shipping code on demand

Dependency rule and Code separation in modules

  • Core Module : — Handles the data layer and framework utilities. We will now further break core modules into smaller modules to create better code hygiene.
  • Base — :base module is the Kotlin / java library (no-android dependency), it is the lowest module and imported by every other module. In modularisation the main concern is interacting or navigating one module with other modules. The issue here is cyclic dependencies of Gradle as feature module A cannot inherit feature module B, as one must act as parent module. To solve this problem we will introduce dagger, by using dagger all interfaces will be defined in base and the implementation of interface will be provided by the app module which is parent of all feature modules.
  • Base-android — :base-android module is the lowest android library module, it mainly consists of non-view related utils and extensions, interfaces which depend on android framework.
  • Data :- :data module is also a Kotlin / Java library and as the name suggests it defines the data layer of the app. All the DAO’s, entities, mappers, repositories, data source are defined here. All non-entities classes are marked as internal.
  • Data-Android :- An android library for providing database and GraphQL related modules. It’s recommended to avoid usage of android framework dependent repo’s and data source, but in case of Firestore or firebase related repo’s interface will be defined in :data and implementation will be provided by :data-android.
  • Domain :- :domain is an android library whose sole purpose is to serve use-cases (business logic) that act as an intermediate between data layer and ui layer.
  • Common :- :common modules are further broken into ui, resources and themes. Each of them will contains the resources and ui i.e, is used by more than one or two modules :-
    :common-resources
    :common-ui
    :common-themes
  • FCM :- :fcm is an optional module which we defined for handling notifications and channels.
  • Analytics :- :analytics is an optional module which separates the app metrics and events.
  • Standard Feature Modules :- SFM are standalone modules that define an independent feature/product. It will only contain UI logic. There are times when a feature itself can be a part of a complete flow, such as when an app has a different flow of users. In a reading app one can be an author and one can be a reader, in this case you want to trigger a different flow of navigation for each user that can be done on the basis of condition but to segregate these two flows we can define modules as flow and features.
  • DFM’s (Dynamic Feature Modules) :- DFM’s are dynamic delivery modules. It has reverse dependency as it depends on the app rather than the app depends on DFM. Navigation can be done by reflection or nav-graph.
  • Application module :- App module will now only contain MainActivity, MainApplication and dagger implementation of interfaces.

Now we have very light modules and very good code hygiene. Less build times, more flexibility and scalability.

Google Play Feature Delivery

Overview of Play Feature Delivery | Android Developers

  • Install-time delivery — If the app has certain training activities, such as an interactive guide on how to buy and sell items in the marketplace, you can include that feature at app install, by default. However, to reduce the installed size of the app, the app can request to delete the feature after the user has completed the training
  • On demand delivery — If only 20% of those who use the marketplace app post items for sale, a good strategy to reduce the initial download size for the majority of users is to make the functionality for taking pictures, including an item description, and placing an item for sale available as an on demand download. That is, you can configure the feature module for the selling functionality of the app to be downloaded only when a user shows interest in placing items for sale onto the marketplace. Additionally, if the user no longer sells items after a certain period of time, the app can reduce its installed size by requesting to uninstall the feature.
  • Conditional delivery — If the marketplace app has global reach, you might need to support payment methods that are popular in only certain regions or locals. In order to reduce the initial app download size, you can create separate feature modules for processing certain types of payment methods and have them installed conditionally on a user’s device based on their registered locale.
  • Instant Delivery — Consider a game that includes the first few levels of the game in a lightweight feature module. You can instant-enable that module so that users can instantly experience the game through a URL link or “Try Now” button, without app installation.

References

Server Controlled Experiments

Why server controlled experiments?

Over the past years our company has grown at a high pace and so do our products. We have introduced a lot of new features and experimented with UI to make the user experience more seamless. However without proper architecture after a long journey we found ourselves looking at a lot of conditions, unused code and tech debts.

Another purpose of moving experiments to servers is that we need a generic structure for each and every feature. To make it easily controllable by remote without releasing a new version of APK.

Using RecyclerView for experiments

  • While making variations of the same view, we got lazy and started to make condition based ui instead of defining a separate view. Now looking back we got into the mess where a single ui is based on visibility which is hard to keep track of and more difficult to add more features or experiments and keeps getting more error prone.
  • Using different view holders gives more flexibility of adding more and more variations over view.
  • Code reusability of using same variation in multiple places
  • Code separation as a single view holder is responsible for complete experiment. Code refactoring is even more easy
  • Rather checking multiple conditions a simple when condition is required by recycler-view to decide view type.

Structure for server response

IsEnabled — Whether the whole experiment is enabled or not.

Languages — List of supported languages by the experiment.

Variants — list of experiment variations, contains the bucket id range for which experiment is enabled.

Control — Default variation or current state of ui.

variantFor — for given id and language provide variant or default variant if not-applicable for current bucket id and language.

isLanguageSupported — returns boolean for whether the experiment supports the current language.

Server Controlled Feature Introductions

Feature introductions to make users more familiar with new features and guiding them through new functionality is not a new concept. However when to remove feature introductions, managing a user already watched the intro’s and making it remotely configurable is the next step we want to make in our product.

Current structure and Its Downside

  • SharedPreferences — a local storage which will be cleared on fresh install, leads to re-guide users through all features which is quite annoying.
  • Redundant code as things like into’s are easily forgettable code. It stays there even if the feature is way older.
  • Not server controlled — release is required to disable intro’s.
  • Not a proper track of what all features are showing intro’s.

Server controlled structure

introducedIn, EnabledTill — Build version code

Now instead of local preferences, user watch data will be stored on the server.

--

--