@@ -18,40 +18,48 @@ package com.example.android.architecture.blueprints.todoapp.data
18
18
19
19
import com.example.android.architecture.blueprints.todoapp.data.source.local.TaskDao
20
20
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
21
23
import java.util.UUID
24
+ import javax.inject.Inject
25
+ import javax.inject.Singleton
22
26
import kotlinx.coroutines.CoroutineDispatcher
23
- import kotlinx.coroutines.Dispatchers
27
+ import kotlinx.coroutines.CoroutineScope
24
28
import kotlinx.coroutines.flow.Flow
25
29
import kotlinx.coroutines.flow.map
30
+ import kotlinx.coroutines.launch
26
31
import kotlinx.coroutines.withContext
27
32
28
33
/* *
29
34
* Default implementation of [TaskRepository]. Single entry point for managing tasks' data.
30
35
*
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.
36
42
*/
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 ,
41
49
) : TaskRepository {
42
50
43
51
override suspend fun createTask (title : String , description : String ): String {
44
52
// ID creation might be a complex operation so it's executed using the supplied
45
53
// coroutine dispatcher
46
- val taskId = withContext(coroutineDispatcher ) {
54
+ val taskId = withContext(dispatcher ) {
47
55
UUID .randomUUID().toString()
48
56
}
49
57
val task = Task (
50
58
title = title,
51
59
description = description,
52
60
id = taskId,
53
61
)
54
- taskDao .upsert(task.toLocal())
62
+ localDataSource .upsert(task.toLocal())
55
63
saveTasksToNetwork()
56
64
return taskId
57
65
}
@@ -62,37 +70,33 @@ class DefaultTaskRepository(
62
70
description = description
63
71
) ? : throw Exception (" Task (id $taskId ) not found" )
64
72
65
- taskDao .upsert(task.toLocal())
73
+ localDataSource .upsert(task.toLocal())
66
74
saveTasksToNetwork()
67
75
}
68
76
69
77
override suspend fun getTasks (forceUpdate : Boolean ): List <Task > {
70
78
if (forceUpdate) {
71
- loadTasksFromNetwork ()
79
+ refresh ()
72
80
}
73
- return withContext(coroutineDispatcher ) {
74
- taskDao .getAll().toExternal()
81
+ return withContext(dispatcher ) {
82
+ localDataSource .getAll().toExternal()
75
83
}
76
84
}
77
85
78
- override suspend fun refreshTasks () {
79
- loadTasksFromNetwork()
80
- }
81
-
82
86
override fun getTasksStream (): Flow <List <Task >> {
83
- return taskDao .observeAll().map { tasks ->
84
- withContext(coroutineDispatcher ) {
87
+ return localDataSource .observeAll().map { tasks ->
88
+ withContext(dispatcher ) {
85
89
tasks.toExternal()
86
90
}
87
91
}
88
92
}
89
93
90
94
override suspend fun refreshTask (taskId : String ) {
91
- loadTasksFromNetwork ()
95
+ refresh ()
92
96
}
93
97
94
98
override fun getTaskStream (taskId : String ): Flow <Task ?> {
95
- return taskDao .observeById(taskId).map { it.toExternal() }
99
+ return localDataSource .observeById(taskId).map { it.toExternal() }
96
100
}
97
101
98
102
/* *
@@ -103,60 +107,78 @@ class DefaultTaskRepository(
103
107
*/
104
108
override suspend fun getTask (taskId : String , forceUpdate : Boolean ): Task ? {
105
109
if (forceUpdate) {
106
- loadTasksFromNetwork ()
110
+ refresh ()
107
111
}
108
- return taskDao .getById(taskId)?.toExternal()
112
+ return localDataSource .getById(taskId)?.toExternal()
109
113
}
110
114
111
115
override suspend fun completeTask (taskId : String ) {
112
- taskDao .updateCompleted(taskId = taskId, completed = true )
116
+ localDataSource .updateCompleted(taskId = taskId, completed = true )
113
117
saveTasksToNetwork()
114
118
}
115
119
116
120
override suspend fun activateTask (taskId : String ) {
117
- taskDao .updateCompleted(taskId = taskId, completed = false )
121
+ localDataSource .updateCompleted(taskId = taskId, completed = false )
118
122
saveTasksToNetwork()
119
123
}
120
124
121
125
override suspend fun clearCompletedTasks () {
122
- taskDao .deleteCompleted()
126
+ localDataSource .deleteCompleted()
123
127
saveTasksToNetwork()
124
128
}
125
129
126
130
override suspend fun deleteAllTasks () {
127
- taskDao .deleteAll()
131
+ localDataSource .deleteAll()
128
132
saveTasksToNetwork()
129
133
}
130
134
131
135
override suspend fun deleteTask (taskId : String ) {
132
- taskDao .deleteById(taskId)
136
+ localDataSource .deleteById(taskId)
133
137
saveTasksToNetwork()
134
138
}
135
139
136
140
/* *
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.
141
142
*
142
143
* Real apps may want to do a proper sync, rather than the "one-way sync everything" approach
143
144
* below. See https://developer.android.com/topic/architecture/data-layer/offline-first
144
145
* for more efficient and robust synchronisation strategies.
145
146
*
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.
147
149
*/
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())
153
162
}
154
163
}
155
164
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
+ }
160
182
}
161
183
}
162
184
}
0 commit comments