I'd like to create a sample that demonstrates the power and elegance of Kotlin coroutines. It will use MVVM, Data Binding and Architecture Components (ViewModel and possibly LiveData). I plan to branch it off聽dev-todo-mvvm-live-kotlin. The suggested new branch name is聽dev-todo-mvvm-live-kotlin-coroutines.
The primary objective is to select and consistently apply a small subset of essential Kotlin coroutine idioms.
We would be happy to add this sample to the list of external samples and link it in the Readme file.
The stable variants shouldn't provide every possible combination of implementation but we do consider samples, like the one you suggest, as valuable and we will gladly mention it.
Closing for now, let us know here if you're done with the sample. Thanks!
@JoseAlcerreca @florina-muntenescu hey, can we please re-open this ticket and mark as "IN PROGRESS". I am currently working ontodo鈥憁vp-coroutines. Thanks.
@JoseAlcerreca @florina-muntenescu The sample is complete. That was amazingly easy and straightforward. You can find the source in https://github.com/outware/android-architecture/tree/dev-todo-mvvm-live-kotlin-coroutines.
In the next few days I plan to fix README and upgrade the project dependencies to their latest versions as the work has been interrupted a few months ago. The code is also going through the internal review process here at Outware Mobile. Your feedback would be highly appreciated.
Hey @alex-chiviliov
TasksLocalDataSource. The idea is to only expose suspend functions.fun deleteTask() = launch(u) {
// task.get() imitate long work with SystemClock.sleep(..)
val result = run(io) { task.get() }
result?.let {
tasksRepository.deleteTask(it.id)
deleteTaskCommand.call()
}
}
Job cancellation?Hi @dmytrodanylyk
I DISAGREE as it will lead to "leaking abstraction". The Repository knows what it is doing and what kind of Dispatcher is required to do its work efficiently (CPU bound, I/O bound, long running, etc.). This knowledge should not be leaking to the ViewModel. I AGREE that hard-coding the Dispatcher is not ideal. The better option would be to inject it via the constructor, as we do with ViewModels.
There are no Job cancellations in this project. The reason is:
This is very different from MVP where the Presenter holds a reference to the View, so job cancellations become an important mechanism to avoid lifecycle related leaks and crashes.
@alex-chiviliov ok I see your points. I have checked couple of examples and different examples are doing it differently:
Completable but doesn't specify schedulerNow it your example you are doing kind of mix up - you create coroutine internally, but do not treat it as "fire and forget" because you expose suspend function.
override suspend fun saveTask(task: Task) = run(DefaultDispatcher) { ... }
This force "client" to launch another coroutine just to call your function _(see AddEditTaskViewModel)_.
I think every solution has some pros and cons. I prefer to expose suspend function which basically tells that this is "heavy operation please launch coroutine to deal with it". 馃槈
FYI: new coroutine library is released, run is deprecated now, you may want to migrate to withContext function.
@dmytrodanylyk I think we're on the same page. You say "I prefer to expose suspend function ..."
suspend fun deleteTask(taskId: String)
"which basically tells that this is heavy operation please launch coroutine to deal with it"
(from TaskDetailViewModel)
fun deleteTask() = launch(dispatcher, CoroutineStart.UNDISPATCHED) {
task.get()?.let {
tasksRepository.deleteTask(it.id)
deleteTaskCommand.call()
}
}
@alex-chiviliov I mean suspend function without launching coroutine internally as you do in TasksLocalDataSource#deleteTask.
Anyway, in my MVP example I have followed original todo-mvp example.
mAppExecutors.diskIO().execute(activateRunnable); (fire and forget operations) replaced with coroutine launchsuspend functionsBefore:
interface TasksDataSource {
interface LoadTasksCallback {
fun onTasksLoaded(tasks: List<Task>)
fun onDataNotAvailable()
}
interface GetTaskCallback {
fun onTaskLoaded(task: Task)
fun onDataNotAvailable()
}
fun getTasks(callback: LoadTasksCallback)
fun getTask(taskId: String, callback: GetTaskCallback)
fun saveTask(task: Task)
fun completeTask(task: Task)
fun completeTask(taskId: String)
fun activateTask(task: Task)
fun activateTask(taskId: String)
fun clearCompletedTasks()
fun refreshTasks()
fun deleteAllTasks()
fun deleteTask(taskId: String)
}
After:
interface TasksDataSource {
suspend fun getTasks(): List<Task>?
suspend fun getTask(taskId: String): Task?
fun saveTask(task: Task)
fun completeTask(task: Task)
fun completeTask(taskId: String)
fun activateTask(task: Task)
fun activateTask(taskId: String)
fun clearCompletedTasks()
fun refreshTasks()
fun deleteAllTasks()
fun deleteTask(taskId: String)
}
Before:
override fun saveTask(task: Task) {
appExecutors.diskIO.execute { tasksDao.insertTask(task) }
}
After:
override fun saveTask(task: Task) {
launch(appExecutors.ioContext) { tasksDao.insertTask(task) }
}
@dmytrodanylyk I'd suggest marking all methods that involve any i/o with suspend. Check https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-rxjava/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksDataSource.java where all methods are Observable/Completable.
@alex-chiviliov true, but on other hand original todo-mvp treat operations as "fire and forget", without possibility to provide listener.
So the questions is which example to follow?
@JoseAlcerreca @florina-muntenescu
This Kotlin coroutine variant of dev-todo-mvvm-live-kotlin is COMPLETE. Could you please take a look?
https://github.com/outware/android-architecture/tree/dev-todo-mvvm-live-kotlin-coroutines
I've noticed that the base app has been modified to use Room. Should this change be retrofitted into this variant as well?
Most helpful comment
Hi @dmytrodanylyk
I DISAGREE as it will lead to "leaking abstraction". The Repository knows what it is doing and what kind of Dispatcher is required to do its work efficiently (CPU bound, I/O bound, long running, etc.). This knowledge should not be leaking to the ViewModel. I AGREE that hard-coding the Dispatcher is not ideal. The better option would be to inject it via the constructor, as we do with ViewModels.
There are no Job cancellations in this project. The reason is:
This is very different from MVP where the Presenter holds a reference to the View, so job cancellations become an important mechanism to avoid lifecycle related leaks and crashes.