As the Android ecosystem grows, Android platform solutions and libraries have evolved with it. In this post, you’ll learn about design patterns, architecture, and essential solutions for Android and how they have evolved over Android’s long history.
We’ve broken down our 2022 Android developer roadmap into a multi-part series, each covering important Android fundamentals and the current ecosystem.
In the last post, we discussed essential elements of modern Android development, including Fragments, App Navigation, Architecture Components, and Android Jetpack libraries.
In part four, you’ll learn the following six sections of our Android Roadmap:
- Design Patterns
- Image Loading
- Local Storage
Let’s get started!
Software design pattern is a reusable solution to solve repeated and common software problems in software engineering. Design patterns can be categorized based on what kind of problem they solve, such as Creational pattern, Behavioral pattern, and Concurrency pattern.
In this section, you’ll explore three major patterns that affect overall architectural designs in modern Android development.
Dependency Injection is one of the most popular patterns in modern Android development, transferring the obligation to create an object out of the class.
By transferring the obligation to create an object, classes don’t need to have dependencies on each other. Therefore you can design loosely coupled dependencies between the classes.
If you utilize dependency injection properly, you can benefit from the advantages below:
- Reduces boilerplate code.
- Loose coupling between classes, so you can write unit tests easily.
- Leverage class reusability.
- Improve code maintainability.
You can implement dependency injection manually following Manual dependency injection guidelines, but it’s highly recommended to use the following efficient solutions: Hilt, Dagger, and Koin(strictly service locator pattern).
- Hilt: Hilt is a compile-time dependency injection tool for Android, which works on top of Dagger. Hilt generates standard Android Dagger components and reduces the amount of boilerplate code needed compared to the Dagger-Android library. Also, Google provides compatibility with ViewModel, Jetpack Compose, Navigation, and WorkManager, and at the time of writing it’s a highly recommended dependency injection tool for modern Android development. For more information, check out the Dependency injection with Hilt documentation.
- Dagger: Dagger is also a compile-time dependency injection tool, based on javax.inject annotations and (JSR 330). You can use Dagger to configure dependency injection for your Android project, but it will require a lot of additional setup costs because Dagger is not an Android-dependent library. It’s highly recommended to use Hilt or Dagger-Android to reduce your setup costs massively.
- Koin: Koin is also a popular dependency injection (strictly service locator pattern) tool for Kotlin projects, it is easy to use and requires low setup costs. For more information, check out insert-koin.io.
If you’re interested in learning more about dependency injection, check out Dependency injection in Android.
Observer pattern is a behavioral design pattern that allows you to build a subscription mechanism to notify observers automatically of any state changes.
The observer pattern is also one of the most frequently used patterns in Android development to build loosely coupled architectures between components. It can also be used to overcome Android platform limitations, such as communicating between independent Android components.
- LiveData: LiveData is a lifecycle-aware, thread-safe, and data-holder observer pattern. LiveData’s observers are bound to Android Lifecycles, so you don’t need to unsubscribe your observers manually, and they will not subscribe to data emissions when the lifecycle is not activated. As a result, it will prevent unpredicted and hard-to-identify memory leaks. It also provides useful operators with LiveData-ktx library, and supports Data Binding and Room compatibility. However, modern Android development prefers Kotlin’s Flows over LiveData since Coroutines has been adopted broadly. If you’re interested in migrating to Flow, check out Migrating from LiveData to Kotlin’s Flow.
- Kotlin Flows: Flows is an asynchronous solution that is cold streams similar to sequences working with Coroutines. They are asynchronous and non-blocking solutions, which are supported at the language level in Kotlin. Flows also provides useful operators, such as Transform operator, Flattening operators, and flowOn operator. You can utilize StateFlow and SharedFlow in Android to implement state-holder observable flow and emit values to multiple consumers.
- RxKotlin(RxJava): RxKotlin originated from the ReactiveX, which is a combination of the observer pattern, iterator pattern, and functional programming. RxKotlin provides many useful operators that allow you to compose asynchronous and event-based programs by using observable sequences. Also, you can easily solve concurrency problems, such as low-level threading, synchronization, and thread safety, with those operators, and there are many useful solutions for Android, such as RxAndroid. However, RxKotlin includes many operators and can be too complex for beginners. Kotlin Flows or LiveData are easier to use, especially if you don’t need to use lots of complicated operations in your project.
The presentation layer uses simple abstractions with interfaces approximating business logic, such as querying data from local databases and fetching remote data from the network. The actual implementation classes perform the heavy lifting and execute the domain-related work.
Repositories conceptually encapsulate a collection of executable domain functions that are related to specific domains and provide more object-oriented aspects to other layers.
In modern Android development, the data layer consists of repositories, which are exposed to other layers as public interfaces and follow the single source-of-truth principle.
So other layers can observe the domain data as a stream, such as Kotlin’s Flow or LiveData, and be guaranteed the source of truth. For more information about the repository pattern and data layer, check out App Architecture.
Now let’s discuss architecture.
Architecture is one of the essential parts of modern Android development, which decides the overall code complexity and expense of your project management.
As the number of features in a project increases, the lines of code and code cohesiveness increase accordingly. Application architecture extensively affects your project’s complexity, scalability, and robustness and makes it easier to test. By defining the boundaries between each layer, you can define their responsibilities clearly and separate each responsibility by modularizing them as dedicated roles.
Historically, the trends of architecture for Android have changed over the last few years, depending on available solutions and best practices.
Seven to eight years ago, Android projects were built with MVC and MVP architecture patterns, but now most projects are built with MVVM and MVI architecture patterns since useful subscription solutions have been introduced, such as RxKotlin, Kotlin Flows, and LiveData.
Nowadays MVVM (Model-View-ViewModel) is one of the most popular architecture designs in modern Android development since Google officially announced Architecture Components, such as ViewModel, LiveData and Data Binding.
Historically it has already been used by WPF developers for more than a decade since The Model-View-ViewModel Pattern was introduced by Microsoft.
The MVVM pattern consists of View, ViewModel, and Model, as shown in the figure below:
Each component has different responsibilities in Android development, as defined below:
- View: Is responsible for constructing the user interfaces of what the user sees on screen. The view consists of Android components that include UI elements, such as
Button, or Jetpack Compose UI. UI elements trigger user events to the ViewModel and configure UI screens by observing data or UI states from the *ViewModel. Ideally, View** includes only UI logic that represents the screen and user interactions, such as listeners, and does not contain business logic.
- ViewModel: This is an independent component that does not have any dependencies on View, and it holds business data or UI states from the Model to propagate them into UI elements. There are typically multiple (one-to-many) relationships between ViewModel and Model, and ViewModel notifies data changes to View as domain data or UI states. In modern Android development, Google suggests using the ViewModel library that helps developers hold business data easily and retain the states while configuration changes happen. But technically, you can say it’s different from Microsoft’s ViewModel because it isn’t close to the original purpose of Microsoft’s design. To make it close to Microsoft’s version, you should utilize other solutions like Data Binding and subscription mechanism solutions, such as RxKotlin, Kotlin Flows, and LiveData. For more details, check out Microsoft’s The Model-View-ViewModel Pattern.
- Model: Encapsulates the app’s domain/data model, which typically includes business logic, complex computational works, and validation logic. Model classes are usually used in conjunction with remote services and local databases in repositories that encapsulate data access like a collection of executable domain functions. The repositories ensure single source of truth from multiple data sources and immutabilities for overall app data.
If you want to explore an open-source project built with MVVM architecture and the design patterns we discussed above, check out Pokedex on GitHub.
MVI (Model-View-Intent) is also a popular architecture in modern Android development since Jetpack Compose has brought declarative programming to our lives.
MVI pattern focuses on the single source of truth principle to provide immutable states to other layers and unidirectional and immutability of states that represent the result of user actions and configure UI screens.
MVI works on top of the other patterns, such as MVP or MVVM, with state management mechanisms, which means MVI architecture can bring the Presenter or ViewModel concepts depending on your architectural design.
Unlike MVVM and MVP, the definition of each component of the MVI (Model-View-Intent) is slightly different:
- Intent: Intent is a definition of interfaces and functions that handle user actions (UI events, such as button click events). The functions transform UI events into Model’s interfaces and deliver the result to Model for manipulating it. As its name implies, we could say we have the intention to perform Model functions.
- Model: The definition of Model in MVI is completely different compared to MVP and MVVM. In MVI, Model is a functional mechanism that takes the output from Intent and manipulates it to UI states that could be rendered in View. The UI states are immutable and come from business logic, which follows single source of truth and unidirectional data flow.
- View: View in MVI has the same responsibilities as MVP and MVVM, which represents the screen and user interactions, such as listeners, and does not contain business logic. One of the biggest differences in implementation from other patterns is that the MVI ensures unidirectional data flow, so View renders UI elements depending on UI states that come from the Model.
If you want to explore an open-source project built with MVI architecture and design patterns discussed above, check out WhatsApp Clone Compose on GitHub.
Clean Architecture was introduced by Robert C. Martin (Uncle Bob) in one of his clean book series, “Clean Architecture: A Craftsman’s Guide to Software Structure and Design”. He theorized and introduced some design approaches to build robust and clean architectures for applications based on OOP (Object-Oriented Programming) paradigms.
The clean architecture has been used widely in modern Android development since dependency injection solutions (like Dagger) and multi-module project environments have been introduced. Also, this theory can be used with other architectures, such as MVP, MVVM, and MVI.
This architecture brings the following advantages: isolating modules, increasing reusability, improving scalability, and making it easy to write unit test cases. But if you’re working on a small project, which doesn’t need complicated business logic, this architecture might be overkill, so you should investigate if the architecture gives advantages to your project.
Before diving into the clean architecture theory, we will discuss the SOLID design principles, which were introduced in one of Uncle Bob’s clean book series. Robert purposed the five software design principles below, which allow you to build understandable, flexible, and maintainable projects:
- Single Responsibility: Each software component, such as a class or module, should have only one reason to change; that means designing unrelated functionalities in the same component, class, or module makes it difficult to comprehend the purpose of the code and makes it unclear.
- Open/Closed: You should be able to extend the functionalities of components (open for extension) without breaking points and modifying the usages (closed for modification).
- Liskov Substitution: Extended classes must be substitutable for the parent class. That means the parent must have minimal interfaces that provide concise purposes to be extended, and the subclasses must implement every abstraction of the parent class.
- Interface Segregation: As you can estimate by name, it’s an atomic-oriented principle and can be linked to the Single Responsibility and Liskov Substitution principles. It’s better to create many smaller interfaces than a huge one, to prevent fat functionalities and violating the Liskov Substitution principle.
- Dependency Inversion: Classes and modules must depend upon abstractions, not upon concretions, to achieve unidirectional and linearized dependencies. Also, this ensures the purity of components, which means each component is responsible for its dedicated role. Don’t confuse this with the Dependency Injection design pattern.
The clean architecture basically follows the SOLID design principles above. Uncle Bob described the clean architecture as you’ve seen in the figure below:
Clean Architecture@Uncle Bob
The center of the circle is the purest scope, which doesn’t have any dependencies on other layers. Each layer must expose abstractions to be used by the outside layers, which have inner circle dependencies. As you may have noticed, this is the maximized combination of the SOLID design principles.
Each layer has its own Single Responsibility and follows the Dependency Inversion principle between them. Now let’s see what each layer is responsible for:
- Entities: Encapsulate a set of business rules and objects of the applications. This layer also follows the highest-level rules and exposes abstractions to other layers to be easily used. In Google’s official architecture guide, you can treat the Entities layer as the data layer.
- Use Cases: Use Cases contains definitions of application business rules, such as the user actions that will trigger functionalities in Entities layer. This layer only has a dependency on the Entities layer and exposes abstractions to outer layers for executing application-specific business logic.
- Presenters (Interface Adapters): This layer performs all of the exposed interfaces, which are definitions of application business rules from the Use Cases layer, and communicates with the UI layer. In MVVM, ViewModel belongs here.
- UI (Frameworks & Drivers): The UI layer represents how the UI is being rendered, including Activity, Fragment, and all of the UI elements, such as Android Widgets to use on your Android screens.
If you want to learn more about this concept and make use of this in your Android project, check out the materials below:
- The Clean Architecture
- Architecting Android…The clean way?
- Clean Architecture Tutorial for Android: Getting Started
Asynchronous and Concurrency
In Android, the system creates the main thread (so-called UI thread) that handles all of the UI-relevant work for your application. The main thread is responsible for rendering the UI elements, dispatching events to the appropriate user interface, and all interactions between components from the Android UI toolkit.
So if you want to perform I/O or expensive computational work, such as network requests and querying the database, you should instead handle them in another thread (so-called worker thread). This ensures that the main thread is dedicated to rendering screens and handling interactions from users.
That all said, writing a heavily multithreaded program is difficult to maintain and debug, there are also other considerations such as avoiding race conditions and managing resources. Luckily there are solutions to execute computationally heavy business work without blocking the main thread, and they make it possible not to need to handle each thread manually one by one.
Now let’s see how you can perform business logic with the following solutions.
We’ve already discussed RxJava before in the Design Patterns section. One of the advantages of using RxJava is that it allows you to easily control multi-threading problems, such as executing business logic in a background thread and getting results back in the UI thread.
RxJava provides a thread pool called Schedulers, and the thread pool contains the following different kind of threads: io, computation, single, or you can create a completely new thread.
If you want to learn more about RxJava and multi-threading, check out Aritra Roy’s Multi-Threading Like a Boss in Android With RxJava 2.
Coroutines is a great concurrency solution that executes code asynchronously at the language level.
Unlike threads, coroutines is purely a user-level language abstraction, so it’s not directly tied to the OS resources and each coroutines object is allocated in the JVM heap. That means coroutines are controllable by the user side, consume much lighter resources, and have lower cost switching contexts expenses than threads.
According to the Android docs, coroutines are lightweight, so you can run many coroutines on a single thread and cause fewer memory leaks because they work based on scope. Google also supports many Jetpack libraries integrations and compatibilities, such as ViewModelScope and LifecycleScope, you gain many advantages if you choose coroutines as a concurrency solution.
If you want to learn more about coroutines for Android, check out Kotlin coroutines on Android.
Network communication is an essential part of any modern-day application. However, building our own network solutions requires massive resources, such as connection pooling, response caching, HTTP (Hypertext Transfer Protocol) specific features, interceptors, and asynchronous call supports.
Seven to eight years ago, Android developers used HttpURLConnection or Apache’s HttpClient to perform HTTP requests. However, those libraries require a lot of boilerplate code and do not support Android platform compatibility, such as connectivity features, security support, and DNS (Domain Name System) resolution.
OkHttp works efficiently by default and helps you to establish an HTTP client quickly. It has its own recovery systems, so you don’t need to handle it manually when the network is troublesome, such as connection problems.
One of the most capable features in OkHttp is Interceptors, which are powerful mechanisms that allow you to log, monitor, modify, rewrite, and retry calls.
Retrofit is a type-safe HTTP client for Android and JVM and it was also developed by Square. Retrofit provides abstraction layers on top of OkHttp, allowing you to define request specifications easily and concisely without handling low-level implementations.
You can also build desired HTTP requests easily by supporting URL, header manipulation, the request method, and the body with Retrofit annotations. Also, by attaching a pluggable Converter.Factory, you can serialize all JSON responses easily without the need to write boilerplate code.
You can also manipulate the network responses by attaching a CallAdapter, which allows you to handle the raw responses and model the response types to your desired types. For more information about this, check out Modeling Retrofit Responses With Sealed Classes and Coroutines.
If you want to learn more about Retrofit, see Retrofit’s official page.
Image loading is also an essential part of modern application development, for example, when loading user profiles or other content from the network.
You can implement your own image loading system, but it requires a lot of features under the hood, such as downloading an image from the network, resizing, caching, rendering, and memory management.
In this section, we will explore popular image libraries for Android.
Glide, developed by bumptech, is one of the most popular image libraries and has been for a long time. It has been used in a lot of global products and open source projects, including Google’s official open source projects.
For more information, check out Glide’s official documentation.
It is completely written in Kotlin, and the exposed APIs are Kotlin-friendly. One notable point is that Coil is lighter than alternatives because it uses other libraries that are already used in Android projects widely, such as OkHttp and Coroutines.
For more information, check out Coil’s official guide.
Unlike other libraries, Fresco focuses on working efficiently with memory, especially targeting below Android version 4.x. However, recent projects target at least a minimum SDK of 21 to 23, and the API is complicated. Unless you’re building memory-sensitive applications, choose Glide or Coil instead.
For more information, check out Fresco’s official guide.
With Jetpack Compose, the UI rendering mechanism has completely changed from the original XML-based. Because of that, Landscapist was developed to support image loading in a generic way using popular image load libraries.
Landscapist supports tracing image states, composing custom implementations, and animations, such as circular reveal and crossfade. It also introduced a new concept called ImagePlugin in a recent version, which allows you to attach and implement image loading behavior easier and faster.
For more information, check out Landscapist on GitHub.
Local storage is another frequently used solution for Android. If you need to persist user input or remote resources in your user’s local device, then you should save and restore them in local storage.
It works based on Annotation Processor (it also supports KSP (Kotlin Symbol Processing)), so the implementation, such as querying and inserting columns, will be generated at compile time.
One of the best advantages of using this library is that developers don’t need to learn SQL queries because the abstraction layer is extremely concise and easy to understand. It also provides useful features, such as Coroutines, RxJava compatibilities, auto migration strategies, and type converters.
To learn more about Room, check out the Save data in a local database using Room training.
DataStore is another Android Jetpack library by Google, which is a data storage solution that allows you to store key-value pairs in your local storage. This library is an alternative solution to SharedPreferences.
DataStore also supports great compatibility with other libraries, such as Coroutines and Flow, to store data asynchronously, and it supports RxJava as well. It also supports storing typed objects with protocol butters.
To learn more about DataStore, check out Google’s official documentation.
This concludes part four of the 2022 Android Developer Roadmap. This installment covered design patterns, architecture, asynchronous, network, image loading, and local storage which are essential parts of modern Android development.
If you missed the previous sections of the Android Roadmap, it would be helpful to read those:
- The Android Platform: The 2022 Android Developer Roadmap — Part 1
- App Components: The Android Developer Roadmap — Part 2
- App Navigation and Jetpack: The Android Developer Roadmap — Part 3
And as always, happy coding!— Jaewoong
Originally published at https://getstream.io.