App Architecture — All layers explained — Kotlin Multiplatform Shared UI Series
Welcome to the first part of the Kotlin Multiplatform Shared UI Series, where we dive deep into the architecture of mobile applications and explore the best practices for developing highly scalable and maintainable applications. In this series, we will cover all the layers of app architecture, explaining each layer in detail and how they work together to create a robust and efficient application.
As mobile application development continues to evolve, the need for cross-platform solutions becomes more apparent. Kotlin Multiplatform provides a unified development experience that allows developers to write code for multiple platforms such as Android, iOS, and Web using a single codebase. By sharing code across platforms, development time is reduced, and application maintenance becomes more manageable.
In this series, we will focus on the architecture of mobile applications and how to structure code to maximize code reuse, minimize duplication, and improve maintainability. We will cover topics such as the presentation layer, data layer, domain layer, and infrastructure layer and explore how each layer interacts with the others to create a cohesive application.
By the end of this series, you will have a deep understanding of mobile application architecture and the best practices for developing scalable and maintainable applications using Kotlin Multiplatform. Let’s dive in!
As we said in the introduction, to create our new supper-dupper app, we need an attack plan. So I came up with an initial App Architecture:
Are you ready to explore the fascinating world of mobile application architecture? Get excited because we’re about to take a deep dive into the layers that make up our two amazing products: the API and the KMM App!
- Our API is a Ktor backend that utilizes the power of MongoDB to store data. This dynamic duo provides the foundation for our application’s data layer, which is responsible for managing data storage and retrieval.
- Next up, we have the KMM App, which is available on both Android and iOS platforms. This versatile and multi-platform app is built using Kotlin Multiplatform, which allows us to share code across platforms and reduce development time.
Now, let’s turn our attention to the layers that make up these incredible products. We’ll examine each layer in detail, from the presentation layer to the data layer, and explore how they work together to create a seamless and efficient user experience.
So buckle up and get ready for an exciting journey as we take a closer look at the layers that make up our amazing products. Trust us; it’s going to be a wild ride!
Platforms
Here we have our two platforms, Android and iOS. Each of them itself will be very small.
Android App will contain:
- MainActivity — is the beating heart of our app, where we call upon our RootComponent and initialize Koin to bring our app to life. It’s the starting point for all our app’s functionality, providing the foundation for everything from our presentation layer to our data layer.
- Manifest — the behind-the-scenes file that adds essential permissions and references our MainActivity. Without the Manifest, our app wouldn’t be able to function correctly, so it’s a crucial component of our development process.
iOS App won't be huge too:
- AppDelegate — is the key to unlocking the full potential of our iOS App, calling upon our Kotlin Application with RootComponent to bring our app to life. It’s the starting point for all our app’s functionality, providing the foundation for everything from our presentation layer to our data layer.
- info.plist — the essential file that sets up the basic settings and permissions for our app. This powerful file is like a road map, guiding users through the app’s features and ensuring that they can access all the essential functionalities seamlessly.
Thanks to KMM for letting us keep nearly all our code in the common Kotlin code layer.
Shared UI
Shared UI, a Kotlin Multiplatform module that is dedicated to UI development. Here, all UI Composables are kept, Theming is controlled, and Koin modules are grouped together. Additionally, the Shared UI module depends on all components required in UI components, making it a one-stop-shop for all UI-related needs.
One of the significant benefits of Shared UI is its ability to promote consistency across different platforms. By centralizing UI development in a single module, developers can ensure that their apps look and feel the same across all platforms. This not only enhances the user experience but also saves time and resources by reducing the need for redundant code.
UI Components
The UI Components layer is where all the visual magic happens. Although our app may be small, this architecture allows us to scale easily, with all components being reusable and easily testable.
- Root Component — we’ll initiate Koin and host the Router Component, observing the user authentication state to navigate back to the Login screen if the user signs out.
- Router Component — is responsible for handling all navigation in the app. The login screen will be the initial Screen, with the user typing in their email and password to log in. If they need to register, they’ll need to put confirmation for the password. On successful login, the user will be navigated to the BT Devices Screen.
- Login Component —is our Home screen, displaying a list of detected BT devices. Each list item will have an onClick action that takes the user to the BT Device Detail screen. If the list item is in the favourites list, the user will see the heart icon displayed.
- BT Device list Component — This is our Home screen displaying a list of detected BT devices. Each list item will have onClick action that will take the user to the BT Device Detail screen. If the list item is in the favourites list, the user will see the ❤️ icon displayed.
- Bt device detail Component — this is where users can find comprehensive information about the BT device they’ve selected, including technical specifications and other relevant details.
What are those Components?
Let’s discuss an MVI library called Ballast, which we’ll be using to build all of our UI components.
Ballast is an opinionated Application State Management framework for Kotlin Multiplatform, and it provides a solid foundation for building robust and scalable UI components.
Each component we build will have a similar structure, with the following four essential components:
- Contract — where we’ll be writing our UI using Jetbrains Compose. Compose is a modern UI toolkit that simplifies the process of building reactive, declarative, and composable UIs.
- EventHandler — will be responsible for sending events back to the UI, such as error messages, error logs, and messages for the Snackbar. The EventHandler acts as a bridge between the UI and the business logic, allowing us to communicate changes and respond to user actions.
- InputHandler —will be handling all events coming from the UI. We call these Inputs to indicate the direction of the event. Examples of Inputs include onClick events, receiving a text from a TextField, and sign-in events that initiate our authentication calls. We’ll see many more real-world examples later as we progress in building our app.
- ViewModel —ViewModels in Ballast are different from Android ViewModels. In Ballast, the ViewModel works as a glue between the InputHandler, EventHandler, logging, dispatchers, SavedStateHandle, and many more.
Why MVI, not MVVM, for state management?
MVI (Model-View-Intent) and MVVM (Model-View-ViewModel) are both popular patterns for managing the state of an application. However, MVI is gaining more popularity over MVVM due to several reasons.
One of the main differences between MVI and MVVM is how they handle state changes. In MVI, the state of the application is modeled as a single immutable object that is passed between the components of the architecture. This makes it easier to reason about the state of the application and reduces the likelihood of bugs caused by race conditions or inconsistent state.
On the other hand, MVVM relies on mutable ViewModel objects that are tightly coupled to the View layer. This can make it harder to reason about the state of the application and increases the likelihood of bugs due to mutable state.
Another advantage of MVI is that it provides a clear separation between the different layers of the application. The Model layer is responsible for fetching data, the View layer is responsible for rendering the UI, and the Intent layer is responsible for handling user interactions and triggering state changes. This separation of concerns makes the application more modular and easier to test.
Overall, MVI is gaining more popularity over MVVM due to its emphasis on an immutable state and clear separation of concerns.
Usecases
One particularly useful module for Kotlin Multiplatform development is Usecases, which is designed to streamline component development. With Usecases, developers can keep their components lean and focused, making it easier to manage and maintain codebases over time.
Usecases provide a range of benefits for developers. One of its key features is its ability to promote modularity and code reuse. By defining and encapsulating the logic required for specific use cases, Usecases make it easier to separate concerns and reuse code across multiple components.
Another advantage of Usecases is its ability to promote cleaner and more readable code. By abstracting complex business logic into dedicated use cases, developers can reduce clutter and improve the overall readability of their code. This can also make it easier to test and debug components, saving time and resources over the long term.
Device hardware
One of the main advantages of Kotlin Multiplatform is the ability to write code once and deploy it across multiple platforms. However, sometimes it’s necessary to write platform-specific code to access device hardware features that aren’t available on all platforms. To do this, Kotlin Multiplatform provides the Actual/Expect feature, which enables developers to write platform-specific code that can be used by shared code.
In this module, we’ll be using Actual/Expect to access device hardware features using the BluetoothService and LocationService modules. BluetoothService allows developers to trigger device scanning and connect to Bluetooth devices, while LocationService provides location data from Android and iOS devices.
To use Actual/Expect, we’ll write platform-specific implementations of the BluetoothService and LocationService modules in the Android and iOS projects, respectively. These platform-specific implementations will then be accessed by the shared code using the expect keyword, allowing the shared code to interact with the device hardware features in a platform-agnostic way.
Data layer
Kotlin Multiplatform’s data layer is where data is stored and managed, making it a crucial component of any app. In this module, we’ll be using SqlDelight to save Bluetooth data into a local database, which will then be synced with a remote API service. Additionally, we’ll be using the SDK module to access device preferences on both Android and iOS.
SqlDelight is a powerful database management tool that allows developers to write SQL queries in Kotlin code. By using SqlDelight in the data layer, we can easily save and manage Bluetooth data in a local database. This data can then be synced with a remote API service, ensuring that the app always has the most up-to-date information.
In conclusion, this Kotlin Multiplatform Shared UI Series provides a comprehensive overview of app architecture and best practices for developing scalable and maintainable mobile applications. By exploring all the layers of app architecture, including the presentation, data, domain, infrastructure, and hardware layers, developers can gain a deep understanding of how each layer interacts with the others to create a cohesive and efficient application. The series emphasizes the use of Kotlin Multiplatform to reduce development time and improve application maintenance, and also highlights the benefits of tools such as Shared UI, Usecases, and SqlDelight in promoting code reuse and modularity. With this knowledge, developers can create robust and efficient mobile applications that provide a seamless and enjoyable user experience.
Navigation:
- Table of Contents
- ⏮ — React Native killer — Introduction.
- Current — App Architecture — All layers explained
- ⏭ — Creating and setting up Kotlin Multiplatform projects for Android and iOS.
Thank you for reading! I hope you found this post helpful. If you enjoyed it, please consider sharing it with your friends and colleagues. You can also follow me on Medium or Twitter to stay up-to-date on my latest posts. As always, I welcome your feedback and comments. Thank you again for your support!
Twitter — https://twitter.com/adrianwita
Github — https://github.com/charlee-dev