Designing the solution
The app we’re building may seem simple, but to demonstrate the benefits of layered architecture, we’ll intentionally make it more complex than necessary. Our primary goal is to display a list of movies. However, we’ll still create separate layers with distinct responsibilities. Here’s the order in which we’ll develop them:
In the domain layer, we’ll focus on creating the central “Movie” model, along with the repository contract and data sources.
The repository will be an interface residing in the
domain package, while the data sources, including the libraries for local database storage and REST service integration, will be covered in the upcoming chapter of this series.
This layer will encapsulate all the application logic, independent of the platform it will be implemented on.
For instance, if our app needs to display a list of movies, we’ll create a class responsible for retrieving the movies. This class is commonly referred to as a Use Case or Interactor. Regardless of whether we’re developing for Android or iOS, the functionality of displaying the movie list remains the same.
This highlights the importance of our domain module being decoupled from the specific source code of the Android app. It should be versatile enough to be utilized in other frameworks while preserving its core functionality.
You may think that creating a class with a single function, which simply returns a list of movies from the repository, might not seem particularly useful. It may resemble a handrail or bridge-like structure.
However, consider this: based on the following structure, what do you think the purpose of this example app is?
It’s understandable to assume that the app is a shopping cart application, where users can view products, promotions, add items to their cart, and proceed with a purchase after validating the cart contents.
Encountering such a clear structure in a project saves valuable time when trying to comprehend its functionality. Of course, there may be more complex use cases or interactors involved.
In our app, we’ll have a single implementation of the use case called
GetMoviesInteractor. By the way, I personally prefer using the term “interactor” when naming the use case classes, as an interactor is an object that implements a system’s use case.
The data layer should remain consistent across different platforms and frameworks, whether it’s Android, iOS, web, or desktop. The implementation of repositories and data sources should be identical.
However, we encounter a challenge here. Each platform has its own methods of connecting to REST APIs or saving data locally. Android primarily uses Retrofit, while iOS apps use Alamofire. So, how can we handle data access consistently, both internally and externally?
One option to consider is Retrofit, as it works on any JVM project and Kotlin Multiplatform Mobile (KMM) is built on a pure Kotlin module. However, according to Jake Wharton’s explanation in this Github issue, integrating Retrofit into KMM may take some time, as Retrofit relies on other libraries like OkHttp.
The best option we can find until now (March 2022) is to use Ktor for consuming REST APIs. It provides a reliable way to handle data access in a unified manner within a Kotlin Multiplatform project.
When it comes to a local data source library for Kotlin Multiplatform projects, SqlDelight is an excellent choice. Its implementation in a KMM project is quite simple and straightforward. You’ll need to create the database driver for each platform and place the data source implementations in the
commonMain package within the
shared module. These data sources will depend on the interface of the database driver. This approach ensures that the code remains reusable and platform-independent, making it easy to manage local data storage across different platforms in your project.
The presentation layer in a Kotlin Multiplatform project involves writing the user interface (UI) for each platform, such as using Jetpack Compose for Android or SwiftUI for iOS. However, if you’re looking for a more advanced approach, you can consider adding a
presentation package within the
shared module. Inside this package, you can define contracts for Views, Presenters, and ViewModels, allowing you to implement patterns like MVP (Model-View-Presenter) or MVVM (Model-View-ViewModel). While I haven’t personally tried this approach yet, it presents an intriguing and potentially powerful option for structuring the presentation layer in your project.
This is the final architecture design of our project:
As you can see, we are maximizing the approach of having a unique-cross-business layer that could be used by any platform.
And our project tree should look something like this:
As you may have noticed, we are starting to build the software from the core, from the business logic or domain layer. The rules and needs of the business will guide the development of the software: Domain Driven Development.
Note: I’m currently working on an open-source Kotlin Multiplatform project on GitHub as well. Feel free to check it out!
Github - jflavio11/LayeredKotlinMultiplatform
Example of Mobile Kotlin Multiplatform app for showing about layered architecture
Split of this post series
This has been the second post in which we have designed the solution for our project. In the following, we will talk about the domain layer implementation. This guide will be divided into the following posts:
- Introduction (this post)
- Designing the solution
- Creating the domain layer
- Creating the layer data
- Implementing the presentation layer