Skip to content

Latest commit

 

History

History
169 lines (108 loc) · 11.3 KB

GENERAL_ARCHITECTURE.md

File metadata and controls

169 lines (108 loc) · 11.3 KB

Architecture Overview

In this guide, we'll provide you with a clear understanding of the app's structure, the usage of libraries, and the locations of important files and directories.

Structure of the Project

KaMP Kit is organized into three main directories:

  • shared
  • app
  • ios

The app directory contains the Android version of the app, complete with Android-specific code. The name "app" is the default name assigned by Android Studio during project creation

Similarly, the ios directory houses the iOS version of the app. This directory includes an Xcode project and workspace. For better compatibility, it's recommended to use the workspace as it incorporates the shared library.

The shared directory is crucial as it contains the shared codebase. The shared directory is actually a library project that is referenced from the app project. Within this library, you'll find separate directories for various platforms and testing:

  • androidMain
  • iosMain
  • commonMain
  • androidUnitTest
  • iosTest
  • commonTest

Each of these directories maintains a consistent structure: the programming language followed by the package name (e.g., "kotlin/co/touchlab/kampkit/").

Overall Architecture

Platform

KaMP Kit app, whether running in Android or iOS, starts with the platforms View (MainActivity / ViewController). These components serve as the standard UI interfaces for each platform and initiate upon app launch. They handle all aspects of the user interface, including RecyclerView/UITableView, user input, and view lifecycle management.

ViewModel

From the platforms views we then have the ViewModel layer that bridges our shared data with the views.

If you want your shared viewmodel to be an androidx.lifecycle.ViewModel on the Android side, you can take either a composition or inheritence approach.

For this project we chose the inheritence approach, because Android can use the common viewmodel directly. To enable sharing of presentation logic between platforms, we define expect abstract class ViewModel in commonMain, with platform specific implementations provided in androidMain and iosMain. The android implementation simply extends the Jetpack ViewModel, while an equivalent is implemented for iOS.

ViewModel sharing used to a bit more convoluted but now with Touchlab's Skie tool, iOS code can reference the common BreedViewModel directly.

Repository

The BreedRepository resides in the common Multiplatform code and handles data access functions. This repository references the Multiplatform-Settings library, as well as two auxiliary classes: DogApiImpl (implementing DogApi) and DatabaseHelper. Both DatabaseHelper and DogApiImpl utilize Multiplatform libraries to fetch and manage data, forwarding it to the BreedRepository.

Note that the BreedRepository references the interface DogApi. This is so we can test the Model using a Mock Api

In this implementation the ViewModel listens to the database as a flow, so that when any changes occur to the database it will then call the callback it was passed. When breed data is requested, the model fetches it from the network and saves it to the database. This, in turn, triggers the database flow to update the platform for display updates.

In Short: Platform -> BreedViewModel -> BreedRepository -> DogApiImpl -> BreedModel -> DatabaseHelper -> BreedRepository -> BreedViewModel -> Platform

You may be asking where the Multiplatform-settings comes in. When the BreedModel is told to get breeds from the network, it first checks to see if it's done a network request within the past hour. If it has then it decides not to update the breeds.

Kotlinx Coroutines

We use a new version of Kotlinx Coroutines that uses a new memory model that resolves the multithreading and object freezing concerns. To learn more, refer to the Migration Guide and our Blogpost.

Explore the implementations in DogApiImpl.kt and BreedModel.kt

Libraries and Dependencies

If you're familiar with Android projects then you know that the apps dependencies are stored in the build.gradle.kts. Since shared is a library project, it also contains its own build.gradle.kts where it defines its own dependencies. If you open shared/build.gradle.kts you will see sourceSets corresponding to the directories in the shared project.

Each part of the shared library can declare its own dependencies in these source sets. For example the multiplatform-settings library is only declared for the commonMain and commonTest, since the multiplatform gradle plugin uses hierarchical project structure to pull in the correct platform specific dependencies. Other libraries like SqlDelight, which necessitate platform-specific variables, require distinct platform dependencies. Consider the example of commonMain using sqlDelight.runtime, while androidMain utilizes sqlDelight.driverAndroid.

Below is some information about some of the libraries used in the project.

SKIE

Documentation: https://skie.touchlab.co/intro

SKIE is setup as a Gradle plugin. SKIE runs during compile-time, generating Kotlin IR and Swift code. The Swift code is compiled and linked directly into the Xcode Framework produced by the Kotlin compiler, requiring no changes for your code distribution.

SKIE streamlines iOS code, reducing the preceding boilerplate. Suspend functions and flows are automatically translated into Swift-style async functions or streams. Additionally, SKIE simplifies the conversion between Kotlin sealed classes and Swift enums, facilitating more idiomatic and exhaustive switches in Swift.

Kermit

Documentation: https://kermit.touchlab.co/

Kermit is a Kotlin Multiplatform logging library. It's as easy as it can get logging library. The default platform LogWriter is readily available without any setup hassles.

SqlDelight

Documentation: https://github.com/cashapp/sqldelight

Usage in the project: commonMain/kotlin/co/touchlab/kampkit/DatabaseHelper.kt

SQL Location in the project: commonMain/sqldelight/co/touchlab/kampkit/Table.sq

SqlDelight is a multiplatform SQL library that generates type-safe APIs from SQL Statements. Since it is a multiplatform library, it naturally uses code stored in commonMain. SQL statements are stored in the sqldelight directory, in .sq files. ex: "commonMain/sqldelight/co/touchlab/kampkit/Table.sq"

Even though the SQL queries and main bulk of the library are in the common code, there are some platform specific drivers required from Android and iOS in order to work correctly on each platform. These are the AndroidSqliteDriver and the NativeSqliteDriver(for iOS). These are passed in from platform specific code, in this case injected into the BreedModel. The APIs are stored in the build folder, and referenced from the DatabaseHelper (also in commonMain).

Flow

Normally sql queries are called, and a result is given, but what if you want to get sql query as a flow? We've added Coroutine Extensions to the shared code, which adds the asFlow function that converts queries into flows. Behind the scenes this creates a Query Listener that when a query result has changed, emits the new value to the flow.

Ktor

Documentation: https://ktor.io/

Usage in the project: commonMain/kotlin/co/touchlab/kampkit/ktor/DogApiImpl.kt

Ktor, a multiplatform networking library, facilitates asynchronous client creation. Although the entirety of Ktor's code is housed in commonMain, specific platform dependencies are necessary for proper functionality. These dependencies are outlined in the build.gradle.

Multiplatform Settings

Documentation: https://github.com/russhwolf/multiplatform-settings

Usage in the project: commonMain/kotlin/co/touchlab/kampkit/models/BreedModel.kt

Multiplatform settings really speaks for itself. It persists data by storing it in settings. It is being used in the BreedModel, and acts similarly to a HashMap or Dictionary. Much like SqlDelight the actual internals of the settings are platform specific, so the settings are passed in from the platform and all of the actual saving and loading is in the common code.

Koin

Documentation: https://insert-koin.io/

Usage in the project: commonMain/kotlin/co/touchlab/kampkit/Koin.kt

Koin is a lightweight dependency injection framework. It is being used in the koin.kt file to inject modules into the BreedModel.

Injected variables within the BreedModel are marked using by inject(). We've structured injections into two modules: coreModule and platformModule. The former houses Ktor and Database Helper implementations, while the latter encompasses platform-specific dependencies (SqlDelight and Multiplatform Settings).

Testing

With KMP, tests can be shared across platforms. However, due to platform-specific drivers and dependencies, tests must be executed on individual platforms. In essence, while tests can be shared, they must be run separately for Android and iOS.

The shared tests can be found in the commonTest directory, while the implementations can be found in the androidTest and iosTest directories.

Dependency injection for testing is managed in the TestUtil.kt file in commonTest. This file facilitates the injection of platform-specific libraries (for instance, SqlDelight requiring a platform driver) into the BreedRepository to enable effective testing.

For running tests we use kotlinx.coroutines.test.runTest. For specifying a test runner we use @RunWith() annotation. Platform-specific implementations of testDbConnection() are stored in TestUtilAndroid.kt and TestUtilIOS.kt.

Turbine

Check out the Repository for more info.

Turbine is a small testing library for kotlinx.coroutines Flow. A practical example can be found in BreedViewModelTest.kt.

Android

On the android side we are using AndroidRunner to run the tests because we want to use android specifics in our tests. If you're not using android specific methods then you don't need to use AndroidRunner. The android tests are run can be easily run in Android Studio by right clicking on the folder, and selecting Run 'All Tests'.

iOS

iOS tests have their own gradle task allowing them to run with an iOS simulator. You can simply go to the terminal and run ./gradlew iosTest.