diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt index 4c0339f38..192f7843f 100644 --- a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt @@ -30,13 +30,12 @@ import androidx.test.filters.LargeTest import com.example.android.architecture.blueprints.todoapp.HiltTestActivity import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.TodoNavGraph -import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository -import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking import com.google.accompanist.appcompattheme.AppCompatTheme import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -129,10 +128,9 @@ class AppNavigationTest { } @Test - fun taskDetailScreen_doubleUIBackButton() { + fun taskDetailScreen_doubleUIBackButton() = runTest { val taskName = "UI <- button" - val task = Task(taskName, "Description") - tasksRepository.saveTaskBlocking(task) + tasksRepository.createTask(taskName, "Description") setContent() @@ -159,10 +157,9 @@ class AppNavigationTest { } @Test - fun taskDetailScreen_doubleBackButton() { + fun taskDetailScreen_doubleBackButton() = runTest { val taskName = "Back button" - val task = Task(taskName, "Description") - tasksRepository.saveTaskBlocking(task) + tasksRepository.createTask(taskName, "Description") setContent() diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt index 428be2ef9..ab15a8a50 100644 --- a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt @@ -29,13 +29,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.example.android.architecture.blueprints.todoapp.HiltTestActivity import com.example.android.architecture.blueprints.todoapp.R -import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository -import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking import com.google.accompanist.appcompattheme.AppCompatTheme import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -50,6 +50,7 @@ import org.junit.runner.RunWith // @LooperMode(LooperMode.Mode.PAUSED) // @TextLayoutMode(TextLayoutMode.Mode.REALISTIC) @HiltAndroidTest +@OptIn(ExperimentalCoroutinesApi::class) class TasksScreenTest { @get:Rule(order = 0) @@ -68,9 +69,9 @@ class TasksScreenTest { } @Test - fun displayTask_whenRepositoryHasData() { + fun displayTask_whenRepositoryHasData() = runTest { // GIVEN - One task already in the repository - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1")) + repository.createTask("TITLE1", "DESCRIPTION1") // WHEN - On startup setContent() @@ -80,8 +81,8 @@ class TasksScreenTest { } @Test - fun displayActiveTask() { - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1")) + fun displayActiveTask() = runTest { + repository.createTask("TITLE1", "DESCRIPTION1") setContent() @@ -96,8 +97,10 @@ class TasksScreenTest { } @Test - fun displayCompletedTask() { - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1", true)) + fun displayCompletedTask() = runTest { + repository.apply { + createTask("TITLE1", "DESCRIPTION1").also { completeTask(it.id) } + } setContent() @@ -111,8 +114,8 @@ class TasksScreenTest { } @Test - fun markTaskAsComplete() { - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1")) + fun markTaskAsComplete() = runTest { + repository.createTask("TITLE1", "DESCRIPTION1") setContent() @@ -129,8 +132,10 @@ class TasksScreenTest { } @Test - fun markTaskAsActive() { - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1", true)) + fun markTaskAsActive() = runTest { + repository.apply { + createTask("TITLE1", "DESCRIPTION1").also { completeTask(it.id) } + } setContent() @@ -147,10 +152,12 @@ class TasksScreenTest { } @Test - fun showAllTasks() { + fun showAllTasks() = runTest { // Add one active task and one completed task - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1")) - repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION2", true)) + repository.apply { + createTask("TITLE1", "DESCRIPTION1") + createTask("TITLE2", "DESCRIPTION2").also { completeTask(it.id) } + } setContent() @@ -161,11 +168,13 @@ class TasksScreenTest { } @Test - fun showActiveTasks() { + fun showActiveTasks() = runTest { // Add 2 active tasks and one completed task - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1")) - repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION2")) - repository.saveTaskBlocking(Task("TITLE3", "DESCRIPTION3", true)) + repository.apply { + createTask("TITLE1", "DESCRIPTION1") + createTask("TITLE2", "DESCRIPTION2") + createTask("TITLE3", "DESCRIPTION3").also { completeTask(it.id) } + } setContent() @@ -177,11 +186,13 @@ class TasksScreenTest { } @Test - fun showCompletedTasks() { + fun showCompletedTasks() = runTest { // Add one active task and 2 completed tasks - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1")) - repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION2", true)) - repository.saveTaskBlocking(Task("TITLE3", "DESCRIPTION3", true)) + repository.apply { + createTask("TITLE1", "DESCRIPTION1") + createTask("TITLE2", "DESCRIPTION2").also { completeTask(it.id) } + createTask("TITLE3", "DESCRIPTION3").also { completeTask(it.id) } + } setContent() @@ -193,10 +204,12 @@ class TasksScreenTest { } @Test - fun clearCompletedTasks() { + fun clearCompletedTasks() = runTest { // Add one active task and one completed task - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1")) - repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION2", true)) + repository.apply { + createTask("TITLE1", "DESCRIPTION1") + createTask("TITLE2", "DESCRIPTION2").also { completeTask(it.id) } + } setContent() diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt index 7994d6401..71d953973 100644 --- a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt @@ -35,13 +35,13 @@ import androidx.test.filters.LargeTest import com.example.android.architecture.blueprints.todoapp.HiltTestActivity import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.TodoNavGraph -import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository -import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking import com.google.accompanist.appcompattheme.AppCompatTheme import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -53,6 +53,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @LargeTest @HiltAndroidTest +@OptIn(ExperimentalCoroutinesApi::class) class TasksTest { @get:Rule(order = 0) @@ -75,9 +76,9 @@ class TasksTest { } @Test - fun editTask() { + fun editTask() = runTest { val originalTaskTitle = "TITLE1" - repository.saveTaskBlocking(Task(originalTaskTitle, "DESCRIPTION")) + repository.createTask(originalTaskTitle, "DESCRIPTION") setContent() @@ -140,9 +141,11 @@ class TasksTest { } @Test - fun createTwoTasks_deleteOneTask() { - repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION")) - repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION")) + fun createTwoTasks_deleteOneTask() = runTest { + repository.apply { + createTask("TITLE1", "DESCRIPTION") + createTask("TITLE2", "DESCRIPTION") + } setContent() @@ -163,10 +166,10 @@ class TasksTest { } @Test - fun markTaskAsCompleteOnDetailScreen_taskIsCompleteInList() { + fun markTaskAsCompleteOnDetailScreen_taskIsCompleteInList() = runTest { // Add 1 active task val taskTitle = "COMPLETED" - repository.saveTaskBlocking(Task(taskTitle, "DESCRIPTION")) + repository.createTask(taskTitle, "DESCRIPTION") setContent() @@ -188,10 +191,12 @@ class TasksTest { } @Test - fun markTaskAsActiveOnDetailScreen_taskIsActiveInList() { + fun markTaskAsActiveOnDetailScreen_taskIsActiveInList() = runTest { // Add 1 completed task val taskTitle = "ACTIVE" - repository.saveTaskBlocking(Task(taskTitle, "DESCRIPTION", true)) + repository.apply { + createTask(taskTitle, "DESCRIPTION").also { completeTask(it.id) } + } setContent() @@ -213,10 +218,10 @@ class TasksTest { } @Test - fun markTaskAsCompleteAndActiveOnDetailScreen_taskIsActiveInList() { + fun markTaskAsCompleteAndActiveOnDetailScreen_taskIsActiveInList() = runTest { // Add 1 active task val taskTitle = "ACT-COMP" - repository.saveTaskBlocking(Task(taskTitle, "DESCRIPTION")) + repository.createTask(taskTitle, "DESCRIPTION") setContent() @@ -240,10 +245,12 @@ class TasksTest { } @Test - fun markTaskAsActiveAndCompleteOnDetailScreen_taskIsCompleteInList() { + fun markTaskAsActiveAndCompleteOnDetailScreen_taskIsCompleteInList() = runTest { // Add 1 completed task val taskTitle = "COMP-ACT" - repository.saveTaskBlocking(Task(taskTitle, "DESCRIPTION", true)) + repository.apply { + createTask(taskTitle, "DESCRIPTION").also { completeTask(it.id) } + } setContent() diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt index 7aa1cfffe..6ebcbd3dd 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs -import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -101,8 +100,7 @@ class AddEditTaskViewModel @Inject constructor( } private fun createNewTask() = viewModelScope.launch { - val newTask = Task(uiState.value.title, uiState.value.description) - tasksRepository.saveTask(newTask) + tasksRepository.createTask(uiState.value.title, uiState.value.description) _uiState.update { it.copy(isTaskSaved = true) } @@ -113,13 +111,11 @@ class AddEditTaskViewModel @Inject constructor( throw RuntimeException("updateTask() was called but task is new.") } viewModelScope.launch { - val updatedTask = Task( + tasksRepository.updateTask( + taskId, title = uiState.value.title, description = uiState.value.description, - isCompleted = uiState.value.isTaskCompleted, - id = taskId ) - tasksRepository.saveTask(updatedTask) _uiState.update { it.copy(isTaskSaved = true) } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/ModelMappingExt.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/ModelMappingExt.kt new file mode 100644 index 000000000..6da6575e3 --- /dev/null +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/ModelMappingExt.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.architecture.blueprints.todoapp.data + +import com.example.android.architecture.blueprints.todoapp.data.source.local.LocalTask +import com.example.android.architecture.blueprints.todoapp.data.source.remote.NetworkTask +import com.example.android.architecture.blueprints.todoapp.data.source.remote.TaskStatus + +/** + * Data model mapping extension functions. There are three model types: + * + * - Task: External model exposed to other layers in the architecture. + * Obtained using `toExternalModel`. + * + * - NetworkTask: Internal model used to represent a task from the network. Obtained using + * `toNetworkModel`. + * + * - LocalTask: Internal model used to represent a task stored locally in a database. Obtained + * using `toLocalModel`. + * + */ + +// External to local +fun Task.toLocalModel() = LocalTask( + id = id, + title = title, + description = description, + isCompleted = isCompleted, +) + +fun List.toLocalModels() = map(Task::toLocalModel) + +// Local to External +fun LocalTask.toExternalModel() = Task( + id = id, + title = title, + description = description, + isCompleted = isCompleted, +) + +// Note: JvmName is used to provide a unique name for each extension function with the same name. +// Without this, type erasure will cause compiler errors because these methods will have the same +// signature on the JVM. +@JvmName("taskEntitiesToExternalModels") +fun List.toExternalModels() = map(LocalTask::toExternalModel) + +// Network to Local +fun NetworkTask.toTaskEntity() = LocalTask( + id = id, + title = title, + description = shortDescription, + isCompleted = (status == TaskStatus.COMPLETE), +) + +@JvmName("networkTasksToTaskEntities") +fun List.toTaskEntities() = map(NetworkTask::toTaskEntity) + +// Local to Network +fun LocalTask.toNetworkModel() = NetworkTask( + id = id, + title = title, + shortDescription = description, + status = if (isCompleted) { TaskStatus.COMPLETE } else { TaskStatus.ACTIVE } +) + +// External to Network +fun Task.toNetworkModel() = toLocalModel().toNetworkModel() + +@JvmName("tasksToNetworkTasks") +fun List.toNetworkModels() = map(Task::toNetworkModel) + +// Network to External +fun NetworkTask.toExternalModel() = toTaskEntity().toExternalModel() + +@JvmName("networkTasksToTasks") +fun List.toExternalModels() = map(NetworkTask::toExternalModel) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt index 771ced8b5..7d84e5351 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt @@ -16,26 +16,21 @@ package com.example.android.architecture.blueprints.todoapp.data -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey import java.util.UUID /** - * Immutable model class for a Task. In order to compile with Room, we can't use @JvmOverloads to - * generate multiple constructors. + * Immutable model class for a Task. * * @param title title of the task * @param description description of the task * @param isCompleted whether or not this task is completed * @param id id of the task */ -@Entity(tableName = "tasks") -data class Task @JvmOverloads constructor( - @ColumnInfo(name = "title") val title: String = "", - @ColumnInfo(name = "description") val description: String = "", - @ColumnInfo(name = "completed") val isCompleted: Boolean = false, - @PrimaryKey @ColumnInfo(name = "entryid") val id: String = UUID.randomUUID().toString() +data class Task internal constructor( + val title: String = "", + val description: String = "", + val isCompleted: Boolean = false, + val id: String = UUID.randomUUID().toString() ) { val titleForList: String diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt index 2cfb0618e..049bcf6d1 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt @@ -17,23 +17,56 @@ package com.example.android.architecture.blueprints.todoapp.data.source import com.example.android.architecture.blueprints.todoapp.data.Task +import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksDao +import com.example.android.architecture.blueprints.todoapp.data.toExternalModel +import com.example.android.architecture.blueprints.todoapp.data.toLocalModel +import com.example.android.architecture.blueprints.todoapp.data.toNetworkModel +import com.example.android.architecture.blueprints.todoapp.data.toTaskEntity import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** * Default implementation of [TasksRepository]. Single entry point for managing tasks' data. */ class DefaultTasksRepository( - private val tasksRemoteDataSource: TasksDataSource, - private val tasksLocalDataSource: TasksDataSource, + private val tasksNetworkDataSource: NetworkDataSource, + private val tasksDao: TasksDao, ) : TasksRepository { + override suspend fun createTask(title: String, description: String): Task { + val task = Task(title = title, description = description) + + coroutineScope { + launch { tasksNetworkDataSource.saveTask(task.toNetworkModel()) } + launch { + tasksDao.insertTask(task.toLocalModel()) + } + } + return task + } + + override suspend fun updateTask(taskId: String, title: String, description: String) { + + val task = getTask(taskId)?.copy( + title = title, + description = description + ) ?: throw Exception("Task (id $taskId) not found") + + coroutineScope { + launch { tasksNetworkDataSource.saveTask(task.toNetworkModel()) } + launch { + tasksDao.insertTask(task.toLocalModel()) + } + } + } + override suspend fun getTasks(forceUpdate: Boolean): List { if (forceUpdate) { updateTasksFromRemoteDataSource() } - return tasksLocalDataSource.getTasks() + return tasksDao.getTasks().map { it.toExternalModel() } } override suspend fun refreshTasks() { @@ -41,7 +74,11 @@ class DefaultTasksRepository( } override fun getTasksStream(): Flow> { - return tasksLocalDataSource.getTasksStream() + return tasksDao.observeTasks().map { tasks -> + tasks.map { task -> + task.toExternalModel() + } + } } override suspend fun refreshTask(taskId: String) { @@ -49,26 +86,28 @@ class DefaultTasksRepository( } private suspend fun updateTasksFromRemoteDataSource() { - val remoteTasks = tasksRemoteDataSource.getTasks() + val remoteTasks = tasksNetworkDataSource.loadTasks() // Real apps might want to do a proper sync, deleting, modifying or adding each task. - tasksLocalDataSource.deleteAllTasks() + tasksDao.deleteTasks() remoteTasks.forEach { task -> - tasksLocalDataSource.saveTask(task) + tasksDao.insertTask(task.toTaskEntity()) } } override fun getTaskStream(taskId: String): Flow { - return tasksLocalDataSource.getTaskStream(taskId) + return tasksDao.observeTaskById(taskId).map { it.toExternalModel() } } private suspend fun updateTaskFromRemoteDataSource(taskId: String) { - val remoteTask = tasksRemoteDataSource.getTask(taskId) + val remoteTask = tasksNetworkDataSource.getTask(taskId) if (remoteTask == null) { - tasksLocalDataSource.deleteTask(taskId) + tasksDao.deleteTaskById(taskId) } else { - tasksLocalDataSource.saveTask(remoteTask) + tasksDao.insertTask( + remoteTask.toTaskEntity() + ) } } @@ -83,48 +122,41 @@ class DefaultTasksRepository( if (forceUpdate) { updateTaskFromRemoteDataSource(taskId) } - return tasksLocalDataSource.getTask(taskId) - } - - override suspend fun saveTask(task: Task) { - coroutineScope { - launch { tasksRemoteDataSource.saveTask(task) } - launch { tasksLocalDataSource.saveTask(task) } - } + return tasksDao.getTaskById(taskId)?.toExternalModel() } override suspend fun completeTask(taskId: String) { coroutineScope { - launch { tasksRemoteDataSource.completeTask(taskId) } - launch { tasksLocalDataSource.completeTask(taskId) } + launch { tasksNetworkDataSource.completeTask(taskId) } + launch { tasksDao.updateCompleted(taskId = taskId, completed = true) } } } override suspend fun activateTask(taskId: String) { coroutineScope { - launch { tasksRemoteDataSource.activateTask(taskId) } - launch { tasksLocalDataSource.activateTask(taskId) } + launch { tasksNetworkDataSource.activateTask(taskId) } + launch { tasksDao.updateCompleted(taskId = taskId, completed = false) } } } override suspend fun clearCompletedTasks() { coroutineScope { - launch { tasksRemoteDataSource.clearCompletedTasks() } - launch { tasksLocalDataSource.clearCompletedTasks() } + launch { tasksNetworkDataSource.clearCompletedTasks() } + launch { tasksDao.deleteCompletedTasks() } } } override suspend fun deleteAllTasks() { coroutineScope { - launch { tasksRemoteDataSource.deleteAllTasks() } - launch { tasksLocalDataSource.deleteAllTasks() } + launch { tasksNetworkDataSource.deleteAllTasks() } + launch { tasksDao.deleteTasks() } } } override suspend fun deleteTask(taskId: String) { coroutineScope { - launch { tasksRemoteDataSource.deleteTask(taskId) } - launch { tasksLocalDataSource.deleteTask(taskId) } + launch { tasksNetworkDataSource.deleteTask(taskId) } + launch { tasksDao.deleteTaskById(taskId) } } } } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksDataSource.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/NetworkDataSource.kt similarity index 70% rename from app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksDataSource.kt rename to app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/NetworkDataSource.kt index 07d9a443d..ec5e1e3ab 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksDataSource.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/NetworkDataSource.kt @@ -16,27 +16,19 @@ package com.example.android.architecture.blueprints.todoapp.data.source -import com.example.android.architecture.blueprints.todoapp.data.Task -import kotlinx.coroutines.flow.Flow +import com.example.android.architecture.blueprints.todoapp.data.source.remote.NetworkTask /** - * Main entry point for accessing tasks data. + * Main entry point for accessing tasks data from the network. + * */ -interface TasksDataSource { - - fun getTasksStream(): Flow> - - suspend fun getTasks(): List - - suspend fun refreshTasks() - - fun getTaskStream(taskId: String): Flow +interface NetworkDataSource { - suspend fun getTask(taskId: String): Task? + suspend fun loadTasks(): List - suspend fun refreshTask(taskId: String) + suspend fun getTask(taskId: String): NetworkTask? - suspend fun saveTask(task: Task) + suspend fun saveTask(task: NetworkTask) suspend fun completeTask(taskId: String) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksRepository.kt index a7243018a..a36dadaa5 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksRepository.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksRepository.kt @@ -36,7 +36,9 @@ interface TasksRepository { suspend fun refreshTask(taskId: String) - suspend fun saveTask(task: Task) + suspend fun createTask(title: String, description: String): Task + + suspend fun updateTask(taskId: String, title: String, description: String) suspend fun completeTask(taskId: String) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/LocalTask.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/LocalTask.kt new file mode 100644 index 000000000..f2a040a26 --- /dev/null +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/LocalTask.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.architecture.blueprints.todoapp.data.source.local + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Internal model used to represent a task stored locally in a Room database. This is used inside + * the data layer only. + * + * See ModelMappingExt.kt for mapping functions used to convert this model to other + * models. + */ +@Entity( + tableName = "tasks" +) +data class LocalTask( + @PrimaryKey @ColumnInfo(name = "entryid") val id: String, + var title: String = "", + var description: String = "", + @ColumnInfo(name = "completed") var isCompleted: Boolean = false, +) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDao.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDao.kt index 38837eb4f..30a3c2d3d 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDao.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDao.kt @@ -21,7 +21,6 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update -import com.example.android.architecture.blueprints.todoapp.data.Task import kotlinx.coroutines.flow.Flow /** @@ -36,7 +35,7 @@ interface TasksDao { * @return all tasks. */ @Query("SELECT * FROM Tasks") - fun observeTasks(): Flow> + fun observeTasks(): Flow> /** * Observes a single task. @@ -45,7 +44,7 @@ interface TasksDao { * @return the task with taskId. */ @Query("SELECT * FROM Tasks WHERE entryid = :taskId") - fun observeTaskById(taskId: String): Flow + fun observeTaskById(taskId: String): Flow /** * Select all tasks from the tasks table. @@ -53,7 +52,7 @@ interface TasksDao { * @return all tasks. */ @Query("SELECT * FROM Tasks") - suspend fun getTasks(): List + suspend fun getTasks(): List /** * Select a task by id. @@ -62,7 +61,7 @@ interface TasksDao { * @return the task with taskId. */ @Query("SELECT * FROM Tasks WHERE entryid = :taskId") - suspend fun getTaskById(taskId: String): Task? + suspend fun getTaskById(taskId: String): LocalTask? /** * Insert a task in the database. If the task already exists, replace it. @@ -70,7 +69,7 @@ interface TasksDao { * @param task the task to be inserted. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertTask(task: Task) + suspend fun insertTask(task: LocalTask) /** * Update a task. @@ -79,7 +78,7 @@ interface TasksDao { * @return the number of tasks updated. This should always be 1. */ @Update - suspend fun updateTask(task: Task): Int + suspend fun updateTask(task: LocalTask): Int /** * Update the complete status of a task diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksLocalDataSource.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksLocalDataSource.kt deleted file mode 100644 index e727748fd..000000000 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksLocalDataSource.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.architecture.blueprints.todoapp.data.source.local - -import com.example.android.architecture.blueprints.todoapp.data.Task -import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource - -/** - * Concrete implementation of a data source as a db. - */ -class TasksLocalDataSource internal constructor( - private val tasksDao: TasksDao -) : TasksDataSource { - - override fun getTasksStream() = tasksDao.observeTasks() - - override fun getTaskStream(taskId: String) = tasksDao.observeTaskById(taskId) - - override suspend fun refreshTask(taskId: String) { - // NO-OP - } - - override suspend fun refreshTasks() { - // NO-OP - } - - override suspend fun getTasks(): List = tasksDao.getTasks() - - override suspend fun getTask(taskId: String): Task? = tasksDao.getTaskById(taskId) - - override suspend fun saveTask(task: Task) = tasksDao.insertTask(task) - - override suspend fun completeTask(taskId: String) = - tasksDao.updateCompleted(taskId, true) - - override suspend fun activateTask(taskId: String) = - tasksDao.updateCompleted(taskId, false) - - override suspend fun clearCompletedTasks() { - tasksDao.deleteCompletedTasks() - } - - override suspend fun deleteAllTasks() = tasksDao.deleteTasks() - - override suspend fun deleteTask(taskId: String) { - tasksDao.deleteTaskById(taskId) - } -} diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt index a9f1f5f38..cf117fb30 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt @@ -18,14 +18,13 @@ package com.example.android.architecture.blueprints.todoapp.data.source.local import androidx.room.Database import androidx.room.RoomDatabase -import com.example.android.architecture.blueprints.todoapp.data.Task /** * The Room Database that contains the Task table. * * Note that exportSchema should be true in production databases. */ -@Database(entities = [Task::class], version = 1, exportSchema = false) +@Database(entities = [LocalTask::class], version = 1, exportSchema = false) abstract class ToDoDatabase : RoomDatabase() { abstract fun taskDao(): TasksDao diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/NetworkTask.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/NetworkTask.kt new file mode 100644 index 000000000..a0c52d5e8 --- /dev/null +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/NetworkTask.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.architecture.blueprints.todoapp.data.source.remote + +/** + * Internal model used to represent a task obtained from the network. This is used inside the data + * layer only. + * + * See ModelMappingExt.kt for mapping functions used to convert this model to other + * models. + */ +data class NetworkTask( + val id: String, + val title: String, + val shortDescription: String, + val priority: Int? = null, + val status: TaskStatus = TaskStatus.ACTIVE +) + +enum class TaskStatus { + ACTIVE, + COMPLETE +} diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/TasksRemoteDataSource.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/TasksNetworkDataSource.kt similarity index 56% rename from app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/TasksRemoteDataSource.kt rename to app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/TasksNetworkDataSource.kt index c87bca43e..f9b177f97 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/TasksRemoteDataSource.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/TasksNetworkDataSource.kt @@ -16,86 +16,70 @@ package com.example.android.architecture.blueprints.todoapp.data.source.remote -import com.example.android.architecture.blueprints.todoapp.data.Task -import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource +import com.example.android.architecture.blueprints.todoapp.data.source.NetworkDataSource import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking /** * Implementation of the data source that adds a latency simulating network. + * */ -object TasksRemoteDataSource : TasksDataSource { +object TasksNetworkDataSource : NetworkDataSource { private const val SERVICE_LATENCY_IN_MILLIS = 2000L - private var TASKS_SERVICE_DATA = LinkedHashMap(2) + private var TASKS_SERVICE_DATA = LinkedHashMap(2) init { - addTask("Build tower in Pisa", "Ground looks good, no foundation work required.") - addTask("Finish bridge in Tacoma", "Found awesome girders at half the cost!") - } - - private val observableTasks = MutableStateFlow(runBlocking { getTasks() }) - - override suspend fun refreshTasks() { - observableTasks.value = getTasks() - } - - override fun getTaskStream(taskId: String): Flow { - return observableTasks.map { tasks -> - tasks.firstOrNull { it.id == taskId } - } - } - - override suspend fun refreshTask(taskId: String) { - refreshTasks() - } - - override fun getTasksStream(): Flow> { - return observableTasks + addTask( + id = "PISA", + title = "Build tower in Pisa", + shortDescription = "Ground looks good, no foundation work required." + ) + addTask( + id = "TACOMA", + title = "Finish bridge in Tacoma", + shortDescription = "Found awesome girders at half the cost!" + ) } - override suspend fun getTasks(): List { + override suspend fun loadTasks(): List { // Simulate network by delaying the execution. val tasks = TASKS_SERVICE_DATA.values.toList() delay(SERVICE_LATENCY_IN_MILLIS) return tasks } - override suspend fun getTask(taskId: String): Task? { + override suspend fun getTask(taskId: String): NetworkTask? { // Simulate network by delaying the execution. delay(SERVICE_LATENCY_IN_MILLIS) return TASKS_SERVICE_DATA[taskId] } - private fun addTask(title: String, description: String) { - val newTask = Task(title, description) + private fun addTask(id: String, title: String, shortDescription: String) { + val newTask = NetworkTask(id = id, title = title, shortDescription = shortDescription) TASKS_SERVICE_DATA[newTask.id] = newTask } - override suspend fun saveTask(task: Task) { + override suspend fun saveTask(task: NetworkTask) { TASKS_SERVICE_DATA[task.id] = task } override suspend fun completeTask(taskId: String) { TASKS_SERVICE_DATA[taskId]?.let { - saveTask(it.copy(isCompleted = true)) + saveTask(it.copy(status = TaskStatus.COMPLETE)) } } override suspend fun activateTask(taskId: String) { TASKS_SERVICE_DATA[taskId]?.let { - saveTask(it.copy(isCompleted = true)) + saveTask(it.copy(status = TaskStatus.ACTIVE)) } } override suspend fun clearCompletedTasks() { TASKS_SERVICE_DATA = TASKS_SERVICE_DATA.filterValues { - !it.isCompleted - } as LinkedHashMap + it.status == TaskStatus.COMPLETE + } as LinkedHashMap } override suspend fun deleteAllTasks() { diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt index cb3ef2aa3..969bad256 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt @@ -19,11 +19,10 @@ package com.example.android.architecture.blueprints.todoapp.di import android.content.Context import androidx.room.Room import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository -import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource +import com.example.android.architecture.blueprints.todoapp.data.source.NetworkDataSource import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository -import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksLocalDataSource import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase -import com.example.android.architecture.blueprints.todoapp.data.source.remote.TasksRemoteDataSource +import com.example.android.architecture.blueprints.todoapp.data.source.remote.TasksNetworkDataSource import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -36,10 +35,6 @@ import javax.inject.Singleton @Retention(AnnotationRetention.RUNTIME) annotation class RemoteTasksDataSource -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class LocalTasksDataSource - @Module @InstallIn(SingletonComponent::class) object RepositoryModule { @@ -47,10 +42,10 @@ object RepositoryModule { @Singleton @Provides fun provideTasksRepository( - @RemoteTasksDataSource remoteDataSource: TasksDataSource, - @LocalTasksDataSource localDataSource: TasksDataSource, + @RemoteTasksDataSource remoteDataSource: NetworkDataSource, + database: ToDoDatabase, ): TasksRepository { - return DefaultTasksRepository(remoteDataSource, localDataSource) + return DefaultTasksRepository(remoteDataSource, database.taskDao()) } } @@ -61,16 +56,7 @@ object DataSourceModule { @Singleton @RemoteTasksDataSource @Provides - fun provideTasksRemoteDataSource(): TasksDataSource = TasksRemoteDataSource - - @Singleton - @LocalTasksDataSource - @Provides - fun provideTasksLocalDataSource( - database: ToDoDatabase - ): TasksDataSource { - return TasksLocalDataSource(database.taskDao()) - } + fun provideTasksRemoteDataSource(): NetworkDataSource = TasksNetworkDataSource } @Module diff --git a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt index 85f145ef4..a87b5b368 100644 --- a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt +++ b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt @@ -33,11 +33,12 @@ import androidx.test.filters.MediumTest import com.example.android.architecture.blueprints.todoapp.HiltTestActivity import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository -import com.example.android.architecture.blueprints.todoapp.util.getTasksBlocking import com.google.accompanist.appcompattheme.AppCompatTheme import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -54,6 +55,7 @@ import org.robolectric.annotation.TextLayoutMode @LooperMode(LooperMode.Mode.PAUSED) @TextLayoutMode(TextLayoutMode.Mode.REALISTIC) @HiltAndroidTest +@ExperimentalCoroutinesApi class AddEditTaskScreenTest { @get:Rule(order = 0) @@ -100,7 +102,7 @@ class AddEditTaskScreenTest { } @Test - fun validTask_isSaved() { + fun validTask_isSaved() = runTest { // WHEN - Valid title and description combination and click save findTextField(R.string.title_hint).performTextInput("title") findTextField(R.string.description_hint).performTextInput("description") @@ -108,7 +110,7 @@ class AddEditTaskScreenTest { .performClick() // THEN - Verify that the repository saved the task - val tasks = repository.getTasksBlocking(true) + val tasks = repository.getTasks(true) assertEquals(tasks.size, 1) assertEquals(tasks[0].title, "title") assertEquals(tasks[0].description, "description") diff --git a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeRepository.kt b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeRepository.kt index 3884513fa..6166c5f4f 100644 --- a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeRepository.kt +++ b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeRepository.kt @@ -56,6 +56,12 @@ class FakeRepository : TasksRepository { refreshTasks() } + override suspend fun createTask(title: String, description: String): Task { + return Task(title = title, description = description).also { + saveTask(it) + } + } + override fun getTasksStream(): Flow> = observableTasks override fun getTaskStream(taskId: String): Flow { @@ -78,7 +84,16 @@ class FakeRepository : TasksRepository { return observableTasks.first() } - override suspend fun saveTask(task: Task) { + override suspend fun updateTask(taskId: String, title: String, description: String) { + val updatedTask = _savedTasks.value[taskId]?.copy( + title = title, + description = description + ) ?: throw Exception("Task (id $taskId) not found") + + saveTask(updatedTask) + } + + private fun saveTask(task: Task) { _savedTasks.update { tasks -> val newTasks = LinkedHashMap(tasks) newTasks[task.id] = task diff --git a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDaoTest.kt b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDaoTest.kt index 4897a47fd..e14fdb573 100644 --- a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDaoTest.kt +++ b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDaoTest.kt @@ -22,7 +22,6 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule -import com.example.android.architecture.blueprints.todoapp.data.Task import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.hamcrest.CoreMatchers.`is` @@ -66,14 +65,14 @@ class TasksDaoTest { @Test fun insertTaskAndGetById() = runTest { // GIVEN - insert a task - val task = Task("title", "description") + val task = LocalTask(title = "title", description = "description", id = "id") database.taskDao().insertTask(task) // WHEN - Get the task by id from the database val loaded = database.taskDao().getTaskById(task.id) // THEN - The loaded data contains the expected values - assertThat(loaded as Task, notNullValue()) + assertThat(loaded as LocalTask, notNullValue()) assertThat(loaded.id, `is`(task.id)) assertThat(loaded.title, `is`(task.title)) assertThat(loaded.description, `is`(task.description)) @@ -83,11 +82,16 @@ class TasksDaoTest { @Test fun insertTaskReplacesOnConflict() = runTest { // Given that a task is inserted - val task = Task("title", "description") + val task = LocalTask(title = "title", description = "description", id = "id") database.taskDao().insertTask(task) // When a task with the same id is inserted - val newTask = Task("title2", "description2", true, task.id) + val newTask = LocalTask( + title = "title2", + description = "description2", + isCompleted = true, + id = task.id + ) database.taskDao().insertTask(newTask) // THEN - The loaded data contains the expected values @@ -101,7 +105,7 @@ class TasksDaoTest { @Test fun insertTaskAndGetTasks() = runTest { // GIVEN - insert a task - val task = Task("title", "description") + val task = LocalTask(title = "title", description = "description", id = "id") database.taskDao().insertTask(task) // WHEN - Get tasks from the database @@ -118,11 +122,16 @@ class TasksDaoTest { @Test fun updateTaskAndGetById() = runTest { // When inserting a task - val originalTask = Task("title", "description") + val originalTask = LocalTask(title = "title", description = "description", id = "id") database.taskDao().insertTask(originalTask) // When the task is updated - val updatedTask = Task("new title", "new description", true, originalTask.id) + val updatedTask = LocalTask( + title = "new title", + description = "new description", + isCompleted = true, + id = originalTask.id + ) database.taskDao().updateTask(updatedTask) // THEN - The loaded data contains the expected values @@ -136,7 +145,12 @@ class TasksDaoTest { @Test fun updateCompletedAndGetById() = runTest { // When inserting a task - val task = Task("title", "description", true) + val task = LocalTask( + title = "title", + description = "description", + id = "id", + isCompleted = true + ) database.taskDao().insertTask(task) // When the task is updated @@ -153,7 +167,7 @@ class TasksDaoTest { @Test fun deleteTaskByIdAndGettingTasks() = runTest { // Given a task inserted - val task = Task("title", "description") + val task = LocalTask(title = "title", description = "description", id = "id") database.taskDao().insertTask(task) // When deleting a task by id @@ -167,7 +181,13 @@ class TasksDaoTest { @Test fun deleteTasksAndGettingTasks() = runTest { // Given a task inserted - database.taskDao().insertTask(Task("title", "description")) + database.taskDao().insertTask( + LocalTask( + title = "title", + description = "description", + id = "id" + ) + ) // When deleting all tasks database.taskDao().deleteTasks() @@ -180,7 +200,9 @@ class TasksDaoTest { @Test fun deleteCompletedTasksAndGettingTasks() = runTest { // Given a completed task inserted - database.taskDao().insertTask(Task("completed", "task", true)) + database.taskDao().insertTask( + LocalTask(title = "completed", description = "task", id = "id", isCompleted = true) + ) // When deleting completed tasks database.taskDao().deleteCompletedTasks() diff --git a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksLocalDataSourceTest.kt b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksLocalDataSourceTest.kt deleted file mode 100644 index 80336acef..000000000 --- a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksLocalDataSourceTest.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.architecture.blueprints.todoapp.data.source.local - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.MediumTest -import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule -import com.example.android.architecture.blueprints.todoapp.data.Task -import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.CoreMatchers.nullValue -import org.hamcrest.MatcherAssert.assertThat -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Integration test for the [TasksDataSource]. - */ -@ExperimentalCoroutinesApi -@RunWith(AndroidJUnit4::class) -@MediumTest -class TasksLocalDataSourceTest { - - private lateinit var localDataSource: TasksLocalDataSource - private lateinit var database: ToDoDatabase - - // Set the main coroutines dispatcher for unit testing. - @ExperimentalCoroutinesApi - @get:Rule - val mainCoroutineRule = MainCoroutineRule() - - // Executes each task synchronously using Architecture Components. - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - - @Before - fun setup() { - // using an in-memory database for testing, since it doesn't survive killing the process - database = Room.inMemoryDatabaseBuilder( - ApplicationProvider.getApplicationContext(), - ToDoDatabase::class.java - ) - .allowMainThreadQueries() - .build() - - localDataSource = TasksLocalDataSource(database.taskDao()) - } - - @After - fun cleanUp() { - database.close() - } - - @Test - fun saveTask_retrievesTask() = runTest { - // GIVEN - a new task saved in the database - val newTask = Task("title", "description", true) - localDataSource.saveTask(newTask) - - // WHEN - Task retrieved by ID - val task = localDataSource.getTask(newTask.id) - - // THEN - Same task is returned - assertThat(task?.title, `is`("title")) - assertThat(task?.description, `is`("description")) - assertThat(task?.isCompleted, `is`(true)) - } - - @Test - fun completeTask_retrievedTaskIsComplete() = runTest { - // Given a new task in the persistent repository - val newTask = Task("title") - localDataSource.saveTask(newTask) - - // When completed in the persistent repository - localDataSource.completeTask(newTask.id) - val task = localDataSource.getTask(newTask.id) - - // Then the task can be retrieved from the persistent repository and is complete - assertThat(task?.title, `is`(newTask.title)) - assertThat(task?.isCompleted, `is`(true)) - } - - @Test - fun activateTask_retrievedTaskIsActive() = runTest { - // Given a new completed task in the persistent repository - val newTask = Task("Some title", "Some description", true) - localDataSource.saveTask(newTask) - - localDataSource.activateTask(newTask.id) - - // Then the task can be retrieved from the persistent repository and is active - val task = localDataSource.getTask(newTask.id) - - assertThat(task?.title, `is`("Some title")) - assertThat(task?.isCompleted, `is`(false)) - } - - @Test - fun clearCompletedTask_taskNotRetrievable() = runTest { - // Given 2 new completed tasks and 1 active task in the persistent repository - val newTask1 = Task("title") - val newTask2 = Task("title2") - val newTask3 = Task("title3") - localDataSource.saveTask(newTask1) - localDataSource.completeTask(newTask1.id) - localDataSource.saveTask(newTask2) - localDataSource.completeTask(newTask2.id) - localDataSource.saveTask(newTask3) - // When completed tasks are cleared in the repository - localDataSource.clearCompletedTasks() - - // Then the completed tasks cannot be retrieved and the active one can - assertThat(localDataSource.getTask(newTask1.id), `is`(nullValue())) - assertThat(localDataSource.getTask(newTask2.id), `is`(nullValue())) - - val result3 = localDataSource.getTask(newTask3.id) - assertThat(result3, `is`(newTask3)) - } - - @Test - fun deleteAllTasks_emptyListOfRetrievedTask() = runTest { - // Given a new task in the persistent repository and a mocked callback - val newTask = Task("title") - - localDataSource.saveTask(newTask) - - // When all tasks are deleted - localDataSource.deleteAllTasks() - - // Then the retrieved tasks is an empty list - val tasks = localDataSource.getTasks() - assertThat(tasks.isEmpty(), `is`(true)) - } - - @Test - fun getTasks_retrieveSavedTasks() = runTest { - // Given 2 new tasks in the persistent repository - val newTask1 = Task("title") - val newTask2 = Task("title") - - localDataSource.saveTask(newTask1) - localDataSource.saveTask(newTask2) - // Then the tasks can be retrieved from the persistent repository - val tasks = localDataSource.getTasks() - assertThat(tasks.size, `is`(2)) - } -} diff --git a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt index 2aae43ac5..3ae5759f1 100644 --- a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt +++ b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt @@ -24,13 +24,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.example.android.architecture.blueprints.todoapp.HiltTestActivity import com.example.android.architecture.blueprints.todoapp.R -import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository -import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking import com.google.accompanist.appcompattheme.AppCompatTheme import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -42,6 +42,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @MediumTest @HiltAndroidTest +@ExperimentalCoroutinesApi class StatisticsScreenTest { @get:Rule(order = 0) @@ -60,11 +61,13 @@ class StatisticsScreenTest { } @Test - fun tasks_showsNonEmptyMessage() { + fun tasks_showsNonEmptyMessage() = runTest { // Given some tasks repository.apply { - saveTaskBlocking(Task("Title1", "Description1", false)) - saveTaskBlocking(Task("Title2", "Description2", true)) + createTask("Title1", "Description1") + createTask("Title2", "Description2").also { + completeTask(it.id) + } } composeTestRule.setContent { diff --git a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt index 678eb98bd..91f6d0b34 100644 --- a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt +++ b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt @@ -29,11 +29,12 @@ import androidx.test.filters.MediumTest import com.example.android.architecture.blueprints.todoapp.HiltTestActivity import com.example.android.architecture.blueprints.todoapp.data.Task import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository -import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking import com.google.accompanist.appcompattheme.AppCompatTheme import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -45,6 +46,7 @@ import org.junit.runner.RunWith @MediumTest @RunWith(AndroidJUnit4::class) @HiltAndroidTest +@ExperimentalCoroutinesApi class TaskDetailScreenTest { @get:Rule(order = 0) @@ -62,10 +64,12 @@ class TaskDetailScreenTest { } @Test - fun activeTaskDetails_DisplayedInUi() { + fun activeTaskDetails_DisplayedInUi() = runTest { // GIVEN - Add active (incomplete) task to the DB - val activeTask = Task("Active Task", "AndroidX Rocks", false) - repository.saveTaskBlocking(activeTask) + val activeTask = repository.createTask( + title = "Active Task", + description = "AndroidX Rocks" + ) // WHEN - Details screen is opened setContent(activeTask) @@ -79,10 +83,10 @@ class TaskDetailScreenTest { } @Test - fun completedTaskDetails_DisplayedInUi() { + fun completedTaskDetails_DisplayedInUi() = runTest { // GIVEN - Add completed task to the DB - val completedTask = Task("Completed Task", "AndroidX Rocks", true) - repository.saveTaskBlocking(completedTask) + val completedTask = repository.createTask("Completed Task", "AndroidX Rocks") + repository.completeTask(completedTask.id) // WHEN - Details screen is opened setContent(completedTask) diff --git a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/util/TasksRepositoryExt.kt b/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/util/TasksRepositoryExt.kt deleted file mode 100644 index 59e758110..000000000 --- a/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/util/TasksRepositoryExt.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.architecture.blueprints.todoapp.util - -import com.example.android.architecture.blueprints.todoapp.data.Task -import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository -import kotlinx.coroutines.runBlocking - -/** - * A blocking version of TasksRepository.saveTask to minimize the number of times we have to - * explicitly add runBlocking { ... } in our tests - */ -fun TasksRepository.saveTaskBlocking(task: Task) = runBlocking { - this@saveTaskBlocking.saveTask(task) -} - -fun TasksRepository.getTasksBlocking(forceUpdate: Boolean) = runBlocking { - this@getTasksBlocking.getTasks(forceUpdate) -} diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/FakeFailingTasksRemoteDataSource.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/FakeFailingTasksRemoteDataSource.kt deleted file mode 100644 index 409a69c20..000000000 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/FakeFailingTasksRemoteDataSource.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.architecture.blueprints.todoapp - -import com.example.android.architecture.blueprints.todoapp.data.Task -import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -object FakeFailingTasksRemoteDataSource : TasksDataSource { - override suspend fun getTasks(): List { - throw Exception("Test") - } - - override suspend fun getTask(taskId: String): Task? { - throw Exception("Test") - } - - override fun getTasksStream(): Flow> { - return flow { emit(getTasks()) } - } - - override suspend fun refreshTasks() { - TODO("not implemented") - } - - override fun getTaskStream(taskId: String): Flow { - return flow { emit(getTask(taskId)) } - } - - override suspend fun refreshTask(taskId: String) { - TODO("not implemented") - } - - override suspend fun saveTask(task: Task) { - TODO("not implemented") - } - - override suspend fun completeTask(taskId: String) { - TODO("not implemented") - } - - override suspend fun activateTask(taskId: String) { - TODO("not implemented") - } - - override suspend fun clearCompletedTasks() { - TODO("not implemented") - } - - override suspend fun deleteAllTasks() { - TODO("not implemented") - } - - override suspend fun deleteTask(taskId: String) { - TODO("not implemented") - } -} diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt index 479c18d1e..1c971a437 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepositoryTest.kt @@ -18,6 +18,10 @@ package com.example.android.architecture.blueprints.todoapp.data.source import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule import com.example.android.architecture.blueprints.todoapp.data.Task +import com.example.android.architecture.blueprints.todoapp.data.toExternalModels +import com.example.android.architecture.blueprints.todoapp.data.toLocalModel +import com.example.android.architecture.blueprints.todoapp.data.toNetworkModel +import com.example.android.architecture.blueprints.todoapp.data.toNetworkModels import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -31,15 +35,16 @@ import org.junit.Test @ExperimentalCoroutinesApi class DefaultTasksRepositoryTest { - private val task1 = Task("Title1", "Description1") - private val task2 = Task("Title2", "Description2") - private val task3 = Task("Title3", "Description3") - private val newTask = Task("Title new", "Description new") - private val remoteTasks = listOf(task1, task2).sortedBy { it.id } - private val localTasks = listOf(task3).sortedBy { it.id } + private val task1 = Task(title = "Title1", description = "Description1") + private val task2 = Task(title = "Title2", description = "Description2") + private val task3 = Task(title = "Title3", description = "Description3") + private val newTask = Task(title = "Title new", description = "Description new") + private val networkTasks = listOf(task1, task2).toNetworkModels().sortedBy { it.id } + private val localTasks = listOf(task3.toLocalModel()).sortedBy { it.id } + private val newTasks = listOf(newTask).sortedBy { it.id } - private lateinit var tasksRemoteDataSource: FakeDataSource - private lateinit var tasksLocalDataSource: FakeDataSource + private lateinit var tasksNetworkDataSource: FakeNetworkDataSource + private lateinit var tasksLocalDataSource: FakeTasksDao // Class under test private lateinit var tasksRepository: DefaultTasksRepository @@ -52,20 +57,22 @@ class DefaultTasksRepositoryTest { @ExperimentalCoroutinesApi @Before fun createRepository() { - tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList()) - tasksLocalDataSource = FakeDataSource(localTasks.toMutableList()) + tasksNetworkDataSource = FakeNetworkDataSource(networkTasks.toMutableList()) + tasksLocalDataSource = FakeTasksDao(localTasks.toMutableList()) // Get a reference to the class under test tasksRepository = DefaultTasksRepository( - tasksRemoteDataSource, tasksLocalDataSource + tasksNetworkDataSource, tasksLocalDataSource ) } @ExperimentalCoroutinesApi @Test fun getTasks_emptyRepositoryAndUninitializedCache() = runTest { - val emptySource = FakeDataSource() + val emptyRemoteSource = FakeNetworkDataSource() + val emptyLocalSource = FakeTasksDao() + val tasksRepository = DefaultTasksRepository( - emptySource, emptySource + emptyRemoteSource, emptyLocalSource ) assertThat(tasksRepository.getTasks().size).isEqualTo(0) @@ -77,7 +84,7 @@ class DefaultTasksRepositoryTest { val initial = tasksRepository.getTasks(forceUpdate = true) // Change the remote data source - tasksRemoteDataSource.tasks = newTasks.toMutableList() + tasksNetworkDataSource.tasks = newTasks.toNetworkModels().toMutableList() // Load the tasks again without forcing a refresh val second = tasksRepository.getTasks() @@ -93,21 +100,21 @@ class DefaultTasksRepositoryTest { val tasks = tasksRepository.getTasks(true) // Then tasks are loaded from the remote data source - assertThat(tasks).isEqualTo(remoteTasks) + assertThat(tasks).isEqualTo(networkTasks.toExternalModels()) } @Test fun saveTask_savesToLocalAndRemote() = runTest { // Make sure newTask is not in the remote or local datasources - assertThat(tasksRemoteDataSource.tasks).doesNotContain(newTask) - assertThat(tasksLocalDataSource.tasks).doesNotContain(newTask) + assertThat(tasksNetworkDataSource.tasks).doesNotContain(newTask.toNetworkModel()) + assertThat(tasksLocalDataSource.tasks).doesNotContain(newTask.toLocalModel()) // When a task is saved to the tasks repository - tasksRepository.saveTask(newTask) + val newTask = tasksRepository.createTask(newTask.title, newTask.description) // Then the remote and local sources are called - assertThat(tasksRemoteDataSource.tasks).contains(newTask) - assertThat(tasksLocalDataSource.tasks).contains(newTask) + assertThat(tasksNetworkDataSource.tasks).contains(newTask.toNetworkModel()) + assertThat(tasksLocalDataSource.tasks?.contains(newTask.toLocalModel())) } @Test @@ -116,7 +123,7 @@ class DefaultTasksRepositoryTest { val tasks = tasksRepository.getTasks() // Set a different list of tasks in REMOTE - tasksRemoteDataSource.tasks = newTasks.toMutableList() + tasksNetworkDataSource.tasks = newTasks.toNetworkModels().toMutableList() // But if tasks are cached, subsequent calls load from cache val cachedTasks = tasksRepository.getTasks() @@ -132,7 +139,7 @@ class DefaultTasksRepositoryTest { @Test(expected = Exception::class) fun getTasks_WithDirtyCache_remoteUnavailable_throwsException() = runTest { // Make remote data source unavailable - tasksRemoteDataSource.tasks = null + tasksNetworkDataSource.tasks = null // Load tasks forcing remote load tasksRepository.getTasks(true) @@ -144,16 +151,16 @@ class DefaultTasksRepositoryTest { fun getTasks_WithRemoteDataSourceUnavailable_tasksAreRetrievedFromLocal() = runTest { // When the remote data source is unavailable - tasksRemoteDataSource.tasks = null + tasksNetworkDataSource.tasks = null // The repository fetches from the local source - assertThat(tasksRepository.getTasks()).isEqualTo(localTasks) + assertThat(tasksRepository.getTasks()).isEqualTo(localTasks.toExternalModels()) } @Test(expected = Exception::class) fun getTasks_WithBothDataSourcesUnavailable_throwsError() = runTest { // When both sources are unavailable - tasksRemoteDataSource.tasks = null + tasksNetworkDataSource.tasks = null tasksLocalDataSource.tasks = null // The repository throws an error @@ -164,18 +171,18 @@ class DefaultTasksRepositoryTest { fun getTasks_refreshesLocalDataSource() = runTest { val initialLocal = tasksLocalDataSource.tasks - // First load will fetch from remote + // Forcing an update will fetch tasks from remote val newTasks = tasksRepository.getTasks(true) - assertThat(newTasks).isEqualTo(remoteTasks) - assertThat(newTasks).isEqualTo(tasksLocalDataSource.tasks) + assertThat(newTasks).isEqualTo(networkTasks.toExternalModels()) + assertThat(newTasks).isEqualTo(tasksLocalDataSource.tasks?.toExternalModels()) assertThat(tasksLocalDataSource.tasks).isEqualTo(initialLocal) } @Test fun completeTask_completesTaskToServiceAPIUpdatesCache() = runTest { // Save a task - tasksRepository.saveTask(newTask) + val newTask = tasksRepository.createTask(newTask.title, newTask.description) // Make sure it's active assertThat(tasksRepository.getTask(newTask.id)?.isCompleted).isFalse() @@ -190,7 +197,7 @@ class DefaultTasksRepositoryTest { @Test fun completeTask_activeTaskToServiceAPIUpdatesCache() = runTest { // Save a task - tasksRepository.saveTask(newTask) + val newTask = tasksRepository.createTask(newTask.title, newTask.description) tasksRepository.completeTask(newTask.id) // Make sure it's completed @@ -206,11 +213,11 @@ class DefaultTasksRepositoryTest { @Test fun getTask_repositoryCachesAfterFirstApiCall() = runTest { // Obtain a task from the local data source - tasksLocalDataSource.tasks = mutableListOf(task1) + tasksLocalDataSource.tasks = mutableListOf(task1.toLocalModel()) val initial = tasksRepository.getTask(task1.id) // Change the tasks on the remote - tasksRemoteDataSource.tasks = newTasks.toMutableList() + tasksNetworkDataSource.tasks = newTasks.toNetworkModels().toMutableList() // Obtain the same task again val second = tasksRepository.getTask(task1.id) @@ -222,12 +229,12 @@ class DefaultTasksRepositoryTest { @Test fun getTask_forceRefresh() = runTest { // Trigger the repository to load data, which loads from remote and caches - tasksRemoteDataSource.tasks = mutableListOf(task1) + tasksNetworkDataSource.tasks = mutableListOf(task1.toNetworkModel()) val task1FirstTime = tasksRepository.getTask(task1.id, forceUpdate = true) assertThat(task1FirstTime?.id).isEqualTo(task1.id) // Configure the remote data source to return a different task - tasksRemoteDataSource.tasks = mutableListOf(task2) + tasksNetworkDataSource.tasks = mutableListOf(task2.toNetworkModel()) // Force refresh val task1SecondTime = tasksRepository.getTask(task1.id, true) @@ -241,7 +248,10 @@ class DefaultTasksRepositoryTest { @Test fun clearCompletedTasks() = runTest { val completedTask = task1.copy(isCompleted = true) - tasksRemoteDataSource.tasks = mutableListOf(completedTask, task2) + tasksNetworkDataSource.tasks = mutableListOf( + completedTask.toNetworkModel(), + task2.toNetworkModel() + ) tasksRepository.clearCompletedTasks() val tasks = tasksRepository.getTasks(true) diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDataSource.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeNetworkDataSource.kt similarity index 64% rename from app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDataSource.kt rename to app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeNetworkDataSource.kt index 4dafff265..7b378f5e9 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeDataSource.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeNetworkDataSource.kt @@ -16,34 +16,36 @@ package com.example.android.architecture.blueprints.todoapp.data.source -import com.example.android.architecture.blueprints.todoapp.data.Task -import kotlinx.coroutines.flow.Flow +import com.example.android.architecture.blueprints.todoapp.data.source.remote.NetworkTask +import com.example.android.architecture.blueprints.todoapp.data.source.remote.TaskStatus -class FakeDataSource(var tasks: MutableList? = mutableListOf()) : TasksDataSource { - override suspend fun getTasks() = tasks ?: throw Exception("Task list is null") +class FakeNetworkDataSource( + var tasks: MutableList? = mutableListOf() +) : NetworkDataSource { + override suspend fun loadTasks() = tasks ?: throw Exception("Task list is null") override suspend fun getTask(taskId: String) = tasks?.firstOrNull { it.id == taskId } - override suspend fun saveTask(task: Task) { + override suspend fun saveTask(task: NetworkTask) { tasks?.add(task) } override suspend fun completeTask(taskId: String) { tasks?.firstOrNull { it.id == taskId }?.let { deleteTask(it.id) - saveTask(it.copy(isCompleted = true)) + saveTask(it.copy(status = TaskStatus.COMPLETE)) } } override suspend fun activateTask(taskId: String) { tasks?.firstOrNull { it.id == taskId }?.let { deleteTask(it.id) - saveTask(it.copy(isCompleted = false)) + saveTask(it.copy(status = TaskStatus.ACTIVE)) } } override suspend fun clearCompletedTasks() { - tasks?.removeIf { it.isCompleted } + tasks?.removeIf { it.status == TaskStatus.COMPLETE } } override suspend fun deleteAllTasks() { @@ -53,20 +55,4 @@ class FakeDataSource(var tasks: MutableList? = mutableListOf()) : TasksDat override suspend fun deleteTask(taskId: String) { tasks?.removeIf { it.id == taskId } } - - override fun getTasksStream(): Flow> { - TODO("not implemented") - } - - override suspend fun refreshTasks() { - TODO("not implemented") - } - - override fun getTaskStream(taskId: String): Flow { - TODO("not implemented") - } - - override suspend fun refreshTask(taskId: String) { - TODO("not implemented") - } } diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeTasksDao.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeTasksDao.kt new file mode 100644 index 000000000..5bee17ac7 --- /dev/null +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/source/FakeTasksDao.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.architecture.blueprints.todoapp.data.source + +import com.example.android.architecture.blueprints.todoapp.data.source.local.LocalTask +import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksDao +import kotlinx.coroutines.flow.Flow + +class FakeTasksDao(var tasks: MutableList? = mutableListOf()) : TasksDao { + + override suspend fun getTasks() = tasks ?: throw Exception("Task list is null") + + override suspend fun getTaskById(taskId: String): LocalTask? = + tasks?.firstOrNull { it.id == taskId } + + override suspend fun insertTask(task: LocalTask) { + tasks?.add(task) + } + + override suspend fun updateTask(task: LocalTask): Int { + tasks?.apply { + val didTaskExist = removeIf { it.id == task.id } + if (didTaskExist) { + if (add(task)) { + return 1 + } + } + } + return 0 + } + + override suspend fun updateCompleted(taskId: String, completed: Boolean) { + tasks?.firstOrNull { it.id == taskId }?.let { it.isCompleted = completed } + } + + override suspend fun deleteTasks() { + tasks?.clear() + } + + override suspend fun deleteTaskById(taskId: String): Int { + val wasDeleted = tasks?.removeIf { it.id == taskId } ?: false + return if (wasDeleted) 1 else 0 + } + + override suspend fun deleteCompletedTasks(): Int { + tasks?.apply { + val originalSize = size + if (removeIf { it.isCompleted }) { + return originalSize - size + } + } + return 0 + } + + override fun observeTasks(): Flow> { + TODO("Not implemented") + } + + override fun observeTaskById(taskId: String): Flow { + TODO("Not implemented") + } +}