-
Notifications
You must be signed in to change notification settings - Fork 3
Project Architecture (EN)
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. |
ViewModel
s retrieve the necessary data, applies the UI logic and then exposes relevant data for the view to consume. ViewModel
s expose streams of events to which the Views can bind to. The views also notify the ViewModels
about different actions. In this project, ViewModel
s 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 ViewModel
s.
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 ViewModel
s. Use cases avoid God ViewModel
s, because the since ViewModel
s 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
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:
- The process starts when the rest of the app calls-in the repository.
- The repository starts by calling the DB module which will fetch the data stored in the DB.
- 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() }
- 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.
- The repository calls the API module to start fetching data from the webservice.
- 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, theChannel
is updated with a "failure" 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)
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.