Skip to content

Commit 4aff247

Browse files
dturnermanuelvicnt
andauthored
Add application scope for fire-and-forget jobs (#926)
* Add application scope for fire-and-forget jobs * Update app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt Co-authored-by: Manuel Vivo * Update app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt Co-authored-by: Manuel Vivo * Fix imports * Change createTest to return task ID * Replace provides with binds for simpler Hilt config * Add dispatcher and scope to test dependencies, improve param names * Fix failing test, fix spotless * Catch exceptions and update comments for send tasks job --------- Co-authored-by: Manuel Vivo
1 parent 04924f6 commit 4aff247

File tree

9 files changed

+120
-76
lines changed

9 files changed

+120
-76
lines changed

app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,48 @@ package com.example.android.architecture.blueprints.todoapp.data
1818

1919
import com.example.android.architecture.blueprints.todoapp.data.source.local.TaskDao
2020
import com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkDataSource
21+
import com.example.android.architecture.blueprints.todoapp.di.ApplicationScope
22+
import com.example.android.architecture.blueprints.todoapp.di.DefaultDispatcher
2123
import java.util.UUID
24+
import javax.inject.Inject
25+
import javax.inject.Singleton
2226
import kotlinx.coroutines.CoroutineDispatcher
23-
import kotlinx.coroutines.Dispatchers
27+
import kotlinx.coroutines.CoroutineScope
2428
import kotlinx.coroutines.flow.Flow
2529
import kotlinx.coroutines.flow.map
30+
import kotlinx.coroutines.launch
2631
import kotlinx.coroutines.withContext
2732

2833
/**
2934
* Default implementation of [TaskRepository]. Single entry point for managing tasks' data.
3035
*
31-
* @param tasksNetworkDataSource - The network data source
32-
* @param taskDao - The local data source
33-
* @param coroutineDispatcher - The dispatcher to be used for long running or complex operations,
34-
* such as network calls or mapping many models. This is important to avoid blocking the calling
35-
* thread.
36+
* @param networkDataSource - The network data source
37+
* @param localDataSource - The local data source
38+
* @param dispatcher - The dispatcher to be used for long running or complex operations, such as ID
39+
* generation or mapping many models.
40+
* @param scope - The coroutine scope used for deferred jobs where the result isn't important, such
41+
* as sending data to the network.
3642
*/
37-
class DefaultTaskRepository(
38-
private val tasksNetworkDataSource: NetworkDataSource,
39-
private val taskDao: TaskDao,
40-
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default
43+
@Singleton
44+
class DefaultTaskRepository @Inject constructor(
45+
private val networkDataSource: NetworkDataSource,
46+
private val localDataSource: TaskDao,
47+
@DefaultDispatcher private val dispatcher: CoroutineDispatcher,
48+
@ApplicationScope private val scope: CoroutineScope,
4149
) : TaskRepository {
4250

4351
override suspend fun createTask(title: String, description: String): String {
4452
// ID creation might be a complex operation so it's executed using the supplied
4553
// coroutine dispatcher
46-
val taskId = withContext(coroutineDispatcher) {
54+
val taskId = withContext(dispatcher) {
4755
UUID.randomUUID().toString()
4856
}
4957
val task = Task(
5058
title = title,
5159
description = description,
5260
id = taskId,
5361
)
54-
taskDao.upsert(task.toLocal())
62+
localDataSource.upsert(task.toLocal())
5563
saveTasksToNetwork()
5664
return taskId
5765
}
@@ -62,37 +70,33 @@ class DefaultTaskRepository(
6270
description = description
6371
) ?: throw Exception("Task (id $taskId) not found")
6472

65-
taskDao.upsert(task.toLocal())
73+
localDataSource.upsert(task.toLocal())
6674
saveTasksToNetwork()
6775
}
6876

6977
override suspend fun getTasks(forceUpdate: Boolean): List<Task> {
7078
if (forceUpdate) {
71-
loadTasksFromNetwork()
79+
refresh()
7280
}
73-
return withContext(coroutineDispatcher) {
74-
taskDao.getAll().toExternal()
81+
return withContext(dispatcher) {
82+
localDataSource.getAll().toExternal()
7583
}
7684
}
7785

78-
override suspend fun refreshTasks() {
79-
loadTasksFromNetwork()
80-
}
81-
8286
override fun getTasksStream(): Flow<List<Task>> {
83-
return taskDao.observeAll().map { tasks ->
84-
withContext(coroutineDispatcher) {
87+
return localDataSource.observeAll().map { tasks ->
88+
withContext(dispatcher) {
8589
tasks.toExternal()
8690
}
8791
}
8892
}
8993

9094
override suspend fun refreshTask(taskId: String) {
91-
loadTasksFromNetwork()
95+
refresh()
9296
}
9397

9498
override fun getTaskStream(taskId: String): Flow<Task?> {
95-
return taskDao.observeById(taskId).map { it.toExternal() }
99+
return localDataSource.observeById(taskId).map { it.toExternal() }
96100
}
97101

98102
/**
@@ -103,60 +107,78 @@ class DefaultTaskRepository(
103107
*/
104108
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Task? {
105109
if (forceUpdate) {
106-
loadTasksFromNetwork()
110+
refresh()
107111
}
108-
return taskDao.getById(taskId)?.toExternal()
112+
return localDataSource.getById(taskId)?.toExternal()
109113
}
110114

111115
override suspend fun completeTask(taskId: String) {
112-
taskDao.updateCompleted(taskId = taskId, completed = true)
116+
localDataSource.updateCompleted(taskId = taskId, completed = true)
113117
saveTasksToNetwork()
114118
}
115119

116120
override suspend fun activateTask(taskId: String) {
117-
taskDao.updateCompleted(taskId = taskId, completed = false)
121+
localDataSource.updateCompleted(taskId = taskId, completed = false)
118122
saveTasksToNetwork()
119123
}
120124

121125
override suspend fun clearCompletedTasks() {
122-
taskDao.deleteCompleted()
126+
localDataSource.deleteCompleted()
123127
saveTasksToNetwork()
124128
}
125129

126130
override suspend fun deleteAllTasks() {
127-
taskDao.deleteAll()
131+
localDataSource.deleteAll()
128132
saveTasksToNetwork()
129133
}
130134

131135
override suspend fun deleteTask(taskId: String) {
132-
taskDao.deleteById(taskId)
136+
localDataSource.deleteById(taskId)
133137
saveTasksToNetwork()
134138
}
135139

136140
/**
137-
* The following methods load tasks from, and save tasks to, the network.
138-
*
139-
* Consider these to be long running operations, hence the need for `withContext` which
140-
* can change the coroutine dispatcher so that the caller isn't blocked.
141+
* The following methods load tasks from (refresh), and save tasks to, the network.
141142
*
142143
* Real apps may want to do a proper sync, rather than the "one-way sync everything" approach
143144
* below. See https://developer.android.com/topic/architecture/data-layer/offline-first
144145
* for more efficient and robust synchronisation strategies.
145146
*
146-
* Also, in a real app, these operations could be scheduled using WorkManager.
147+
* Note that the refresh operation is a suspend function (forces callers to wait) and the save
148+
* operation is not. It returns immediately so callers don't have to wait.
147149
*/
148-
private suspend fun loadTasksFromNetwork() {
149-
withContext(coroutineDispatcher) {
150-
val remoteTasks = tasksNetworkDataSource.loadTasks()
151-
taskDao.deleteAll()
152-
taskDao.upsertAll(remoteTasks.toLocal())
150+
151+
/**
152+
* Delete everything in the local data source and replace it with everything from the network
153+
* data source.
154+
*
155+
* `withContext` is used here in case the bulk `toLocal` mapping operation is complex.
156+
*/
157+
override suspend fun refresh() {
158+
withContext(dispatcher) {
159+
val remoteTasks = networkDataSource.loadTasks()
160+
localDataSource.deleteAll()
161+
localDataSource.upsertAll(remoteTasks.toLocal())
153162
}
154163
}
155164

156-
private suspend fun saveTasksToNetwork() {
157-
withContext(coroutineDispatcher) {
158-
val localTasks = taskDao.getAll()
159-
tasksNetworkDataSource.saveTasks(localTasks.toNetwork())
165+
/**
166+
* Send the tasks from the local data source to the network data source
167+
*
168+
* Returns immediately after launching the job. Real apps may want to suspend here until the
169+
* operation is complete or (better) use WorkManager to schedule this work. Both approaches
170+
* should provide a mechanism for failures to be communicated back to the user so that
171+
* they are aware that their data isn't being backed up.
172+
*/
173+
private fun saveTasksToNetwork() {
174+
scope.launch {
175+
try {
176+
val localTasks = localDataSource.getAll()
177+
networkDataSource.saveTasks(localTasks.toNetwork())
178+
} catch (e: Exception) {
179+
// In a real app you'd handle the exception e.g. by exposing a `networkStatus` flow
180+
// to an app level UI state holder which could then display a Toast message.
181+
}
160182
}
161183
}
162184
}

app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/TaskRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface TaskRepository {
2727

2828
suspend fun getTasks(forceUpdate: Boolean = false): List<Task>
2929

30-
suspend fun refreshTasks()
30+
suspend fun refresh()
3131

3232
fun getTaskStream(taskId: String): Flow<Task?>
3333

app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/TaskNetworkDataSource.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@
1616

1717
package com.example.android.architecture.blueprints.todoapp.data.source.network
1818

19+
import javax.inject.Inject
1920
import kotlinx.coroutines.delay
2021

2122
/**
2223
* Implementation of the data source that adds a latency simulating network.
2324
*
2425
*/
25-
object TaskNetworkDataSource : NetworkDataSource {
26+
class TaskNetworkDataSource @Inject constructor() : NetworkDataSource {
2627

27-
private const val SERVICE_LATENCY_IN_MILLIS = 2000L
28+
private val SERVICE_LATENCY_IN_MILLIS = 2000L
2829

2930
private var TASK_SERVICE_DATA = LinkedHashMap<String, NetworkTask>(2)
3031

app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,40 @@ import dagger.Provides
2121
import dagger.hilt.InstallIn
2222
import dagger.hilt.components.SingletonComponent
2323
import javax.inject.Qualifier
24+
import javax.inject.Singleton
2425
import kotlinx.coroutines.CoroutineDispatcher
26+
import kotlinx.coroutines.CoroutineScope
2527
import kotlinx.coroutines.Dispatchers
28+
import kotlinx.coroutines.SupervisorJob
2629

2730
@Qualifier
2831
@Retention(AnnotationRetention.RUNTIME)
2932
annotation class IoDispatcher
3033

34+
@Retention(AnnotationRetention.RUNTIME)
35+
@Qualifier
36+
annotation class DefaultDispatcher
37+
38+
@Retention(AnnotationRetention.RUNTIME)
39+
@Qualifier
40+
annotation class ApplicationScope
41+
3142
@Module
3243
@InstallIn(SingletonComponent::class)
3344
object CoroutinesModule {
3445

3546
@Provides
3647
@IoDispatcher
3748
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
49+
50+
@Provides
51+
@DefaultDispatcher
52+
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
53+
54+
@Provides
55+
@Singleton
56+
@ApplicationScope
57+
fun providesCoroutineScope(
58+
@DefaultDispatcher dispatcher: CoroutineDispatcher
59+
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
3860
}

app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,43 +20,34 @@ import android.content.Context
2020
import androidx.room.Room
2121
import com.example.android.architecture.blueprints.todoapp.data.DefaultTaskRepository
2222
import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
23+
import com.example.android.architecture.blueprints.todoapp.data.source.local.TaskDao
2324
import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase
2425
import com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkDataSource
2526
import com.example.android.architecture.blueprints.todoapp.data.source.network.TaskNetworkDataSource
27+
import dagger.Binds
2628
import dagger.Module
2729
import dagger.Provides
2830
import dagger.hilt.InstallIn
2931
import dagger.hilt.android.qualifiers.ApplicationContext
3032
import dagger.hilt.components.SingletonComponent
31-
import javax.inject.Qualifier
3233
import javax.inject.Singleton
3334

34-
@Qualifier
35-
@Retention(AnnotationRetention.RUNTIME)
36-
annotation class NetworkTaskDataSource
37-
3835
@Module
3936
@InstallIn(SingletonComponent::class)
40-
object RepositoryModule {
37+
abstract class RepositoryModule {
4138

4239
@Singleton
43-
@Provides
44-
fun provideTaskRepository(
45-
@NetworkTaskDataSource remoteDataSource: NetworkDataSource,
46-
database: ToDoDatabase,
47-
): TaskRepository {
48-
return DefaultTaskRepository(remoteDataSource, database.taskDao())
49-
}
40+
@Binds
41+
abstract fun bindTaskRepository(repository: DefaultTaskRepository): TaskRepository
5042
}
5143

5244
@Module
5345
@InstallIn(SingletonComponent::class)
54-
object DataSourceModule {
46+
abstract class DataSourceModule {
5547

5648
@Singleton
57-
@NetworkTaskDataSource
58-
@Provides
59-
fun provideTaskRemoteDataSource(): NetworkDataSource = TaskNetworkDataSource
49+
@Binds
50+
abstract fun bindNetworkDataSource(dataSource: TaskNetworkDataSource): NetworkDataSource
6051
}
6152

6253
@Module
@@ -72,4 +63,7 @@ object DatabaseModule {
7263
"Tasks.db"
7364
).build()
7465
}
66+
67+
@Provides
68+
fun provideTaskDao(database: ToDoDatabase): TaskDao = database.taskDao()
7569
}

app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class StatisticsViewModel @Inject constructor(
6262

6363
fun refresh() {
6464
viewModelScope.launch {
65-
taskRepository.refreshTasks()
65+
taskRepository.refresh()
6666
}
6767
}
6868

app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class TasksViewModel @Inject constructor(
140140
fun refresh() {
141141
_isLoading.value = true
142142
viewModelScope.launch {
143-
taskRepository.refreshTasks()
143+
taskRepository.refresh()
144144
_isLoading.value = false
145145
}
146146
}

app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import com.example.android.architecture.blueprints.todoapp.data.source.network.F
2222
import com.google.common.truth.Truth.assertThat
2323
import junit.framework.TestCase.assertEquals
2424
import kotlinx.coroutines.ExperimentalCoroutinesApi
25+
import kotlinx.coroutines.test.StandardTestDispatcher
26+
import kotlinx.coroutines.test.TestScope
27+
import kotlinx.coroutines.test.advanceUntilIdle
2528
import kotlinx.coroutines.test.runTest
2629
import org.junit.Before
2730
import org.junit.Rule
@@ -63,19 +66,18 @@ class DefaultTaskRepositoryTest {
6366
localDataSource = FakeTaskDao(localTasks)
6467
// Get a reference to the class under test
6568
tasksRepository = DefaultTaskRepository(
66-
networkDataSource, localDataSource
69+
networkDataSource = networkDataSource,
70+
localDataSource = localDataSource,
71+
dispatcher = StandardTestDispatcher(),
72+
scope = TestScope()
6773
)
6874
}
6975

7076
@ExperimentalCoroutinesApi
7177
@Test
7278
fun getTasks_emptyRepositoryAndUninitializedCache() = runTest {
73-
val emptyRemoteSource = FakeNetworkDataSource()
74-
val emptyLocalSource = FakeTaskDao()
75-
76-
val tasksRepository = DefaultTaskRepository(
77-
emptyRemoteSource, emptyLocalSource
78-
)
79+
networkDataSource.tasks?.clear()
80+
localDataSource.deleteAll()
7981

8082
assertThat(tasksRepository.getTasks().size).isEqualTo(0)
8183
}
@@ -110,6 +112,9 @@ class DefaultTaskRepositoryTest {
110112
// When a task is saved to the tasks repository
111113
val newTaskId = tasksRepository.createTask(newTask.title, newTask.description)
112114

115+
// Wait for the network to be updated
116+
advanceUntilIdle()
117+
113118
// Then the remote and local sources contain the new task
114119
assertThat(networkDataSource.tasks?.map { it.id }?.contains(newTaskId))
115120
assertThat(localDataSource.tasks?.map { it.id }?.contains(newTaskId))

0 commit comments

Comments
 (0)