Skip to content

Project Architecture (EN)

Thanh-Son-Philippe Lam edited this page May 26, 2019 · 44 revisions

This document is a WIP.

The project has three main modules:

Module Description
android The android module contains code used for the Android app.
ios The ios module contains code used for the iOS app.
shared The shared module contains common code used by the android module and the ios module. To access platform APIs, the code in this module make use of the expect/actual mechanism provided by Kotlin. The code in this module rely on multiplatform libraries such as SQLDelight and Kotlin Coroutines.

Architecture Diagram

ViewModel

ViewModels retrieve the necessary data, applies the UI logic and then exposes relevant data for the view to consume. ViewModels expose streams of events to which the Views can bind to. The views also notify the ViewModels about different actions. In this project, ViewModels expose Coroutines Channels to the View.

The view has a reference to its ViewModel but the ViewModel has no information about the View. A ViewModel must never reference a view.

Android

A viewModelScope extension is available for the ViewModel class. We can use it to launch coroutines inside our ViewModels.

UseCase

Use cases define the operations that the app needs. To do so, they retrieve data from Repository modules, process it and return it to the ViewModels. Use cases avoid God ViewModels, because the since ViewModels will only execute Use cases.

Furthermore, use cases depends only from repositories and/or other use cases.

Finally, each use case has a single task responsability. The name should be meaningful and starts with an action verb. For example, an use case that fetches the current session should have a name like the following: FetchCurrentSessionUseCase.

Here is video that talks a little about use cases : https://youtu.be/Sy6ZdgqrQp0?t=1102

Repository

The repository module implements the repository pattern. The repository module serves as a clean API so that that the rest of the app can retrieve data easily. It acts as a mediator for between the DB and the API modules. The module handle data operations by using Coroutines Suspend Functions.

The repository creates and returns a Coroutine Channel that the app "subscribes" to in order to be notified of further updates. The Channel will be updated with relevant data and a status (loading, success or failure). The status allows the UI to update its state accordingly. For example, for the loading status, the UI can display a loading bar.

Here are the operations that are generally done by the repository module:

  1. The process starts when the rest of the app calls-in the repository.
  2. The repository starts by calling the DB module which will fetch the data stored in the DB.
  3. The DB module returns a Query which is a a listenable, typed query generated by SQLDelight. Notice that, if needed, this object can be easily converted to a Coroutine Channel by using the asChannel extension function.

Here is an example of usage with the map operator:

        val query = queries.selectAll()

        query.asChannel().map(context) { it.executeAsList() }
  1. The Query finishes and the channel is updated with cached data and a "loading" status. The app should have displayed a loading state by now.
  2. The repository calls the API module to start fetching data from the webservice.
  3. The webservice returns a response. If the call succeeded, the repository updates the DB with the new data. The Channel is also updated with a "success status" and new data coming from the DB. If the call failed, the Channel is updated with a "failure" status.

How to deliver the data and its status?

To do so, use the Resource class. It encapsulates both the data and its state.

class Resource<T> private constructor(val status: Status, val data: T?, val message: String?) {
    enum class Status {
        SUCCESS,
        ERROR,
        LOADING
    }

    companion object {
        fun <T> success(data: T): Resource<T> = Resource(Status.SUCCESS, data, null)
        fun <T> error(msg: String, data: T?): Resource<T> = Resource(Status.ERROR, data, msg)
        fun <T> loading(data: T?): Resource<T> = Resource(Status.LOADING, data, null)
    }

    ...
}

Use one of the static methods to create a Resource instance. For example, to encapsulate a data called "myData" and a loading status, you can do like so:

Resource.loading(myData)

DB

The database module uses SQLDelight to persist the app's data. The library let you write write SQL statements from which their Gradle plugin will generate APIs to run your queries. Please refer to their documentation for more information.

API

https://ktor.io/clients/http-client/multiplatform.html