diff --git a/.github/workflows/blueprints.yaml b/.github/workflows/blueprints.yaml index bb86bc2fe..41caf6a42 100644 --- a/.github/workflows/blueprints.yaml +++ b/.github/workflows/blueprints.yaml @@ -22,10 +22,10 @@ jobs: - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - uses: actions/cache@v2 with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 457602507..7eb56f5dc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,6 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) - alias(libs.plugins.kapt) alias(libs.plugins.ksp) alias(libs.plugins.hilt) } @@ -78,25 +77,27 @@ android { buildFeatures { compose = true + buildConfig = true } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } - packagingOptions { - excludes += "META-INF/AL2.0" - excludes += "META-INF/LGPL2.1" + packaging { + jniLibs.excludes += "META-INF/AL2.0" + jniLibs.excludes += "META-INF/LGPL2.1" } composeOptions { kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get() } + namespace = "com.example.android.architecture.blueprints.todoapp" tasks.withType().configureEach { kotlinOptions { @@ -128,7 +129,7 @@ dependencies { // Hilt implementation(libs.hilt.android.core) implementation(libs.androidx.hilt.navigation.compose) - kapt(libs.hilt.compiler) + ksp(libs.hilt.compiler) // Jetpack Compose val composeBom = platform(libs.androidx.compose.bom) @@ -166,7 +167,7 @@ dependencies { // JVM tests - Hilt testImplementation(libs.hilt.android.testing) - kaptTest(libs.hilt.compiler) + kspTest(libs.hilt.compiler) // Dependencies for Android unit tests androidTestImplementation(composeBom) @@ -196,5 +197,5 @@ dependencies { // AndroidX Test - Hilt testing androidTestImplementation(libs.hilt.android.testing) - kaptAndroidTest(libs.hilt.compiler) + kspAndroidTest(libs.hilt.compiler) } diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt index f7b68a521..c34a1bd50 100644 --- a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt @@ -76,7 +76,7 @@ class AddEditTaskScreenTest { viewModel = AddEditTaskViewModel(repository, SavedStateHandle()), topBarTitle = R.string.add_task, onTaskUpdate = { }, - onBack = { }, + onBack = { } ) } } diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt index 4575af69e..8927d06ca 100644 --- a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt @@ -45,6 +45,7 @@ class TaskDaoTest { ToDoDatabase::class.java ).allowMainThreadQueries().build() } + @Test fun insertTaskAndGetById() = runTest { // GIVEN - insert a task @@ -52,7 +53,7 @@ class TaskDaoTest { title = "title", description = "description", id = "id", - isCompleted = false, + isCompleted = false ) database.taskDao().upsert(task) @@ -74,7 +75,7 @@ class TaskDaoTest { title = "title", description = "description", id = "id", - isCompleted = false, + isCompleted = false ) database.taskDao().upsert(task) @@ -102,7 +103,7 @@ class TaskDaoTest { title = "title", description = "description", id = "id", - isCompleted = false, + isCompleted = false ) database.taskDao().upsert(task) @@ -124,7 +125,7 @@ class TaskDaoTest { title = "title", description = "description", id = "id", - isCompleted = false, + isCompleted = false ) database.taskDao().upsert(originalTask) @@ -175,7 +176,7 @@ class TaskDaoTest { title = "title", description = "description", id = "id", - isCompleted = false, + isCompleted = false ) database.taskDao().upsert(task) @@ -195,7 +196,7 @@ class TaskDaoTest { title = "title", description = "description", id = "id", - isCompleted = false, + isCompleted = false ) ) diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt index 666f76747..96bbb2946 100644 --- a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt +++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt @@ -109,7 +109,7 @@ class TaskDetailScreenTest { ), onEditTask = { /*TODO*/ }, onBack = { }, - onDeleteTask = { }, + onDeleteTask = { } ) } } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index dbf09ddfc..0c4c62119 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - + AppModalDrawer(drawerState, currentRoute, navActions) { @@ -87,7 +90,10 @@ fun TodoNavGraph( TodoDestinations.ADD_EDIT_TASK_ROUTE, arguments = listOf( navArgument(TITLE_ARG) { type = NavType.IntType }, - navArgument(TASK_ID_ARG) { type = NavType.StringType; nullable = true }, + navArgument(TASK_ID_ARG) { + type = NavType.StringType + nullable = true + } ) ) { entry -> val taskId = entry.arguments?.getString(TASK_ID_ARG) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt index 26e381546..e9eed6e58 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt @@ -46,14 +46,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.util.AddEditTaskTopAppBar import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState -@OptIn(ExperimentalLifecycleComposeApi::class) @Composable fun AddEditTaskScreen( @StringRes topBarTitle: Int, @@ -116,7 +114,7 @@ private fun AddEditTaskContent( // Show the loading spinner—`loading` is `true` in this code path state = rememberSwipeRefreshState(true), onRefresh = { /* DO NOTHING */ }, - content = { }, + content = { } ) } else { Column( 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 247573042..828df5c4c 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 @@ -114,7 +114,7 @@ class AddEditTaskViewModel @Inject constructor( taskRepository.updateTask( taskId, title = uiState.value.title, - description = uiState.value.description, + description = uiState.value.description ) _uiState.update { it.copy(isTaskSaved = true) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt index 9fa32de87..2a29069b1 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt @@ -22,7 +22,6 @@ import com.example.android.architecture.blueprints.todoapp.di.ApplicationScope import com.example.android.architecture.blueprints.todoapp.di.DefaultDispatcher import java.util.UUID import javax.inject.Inject -import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -40,12 +39,11 @@ import kotlinx.coroutines.withContext * @param scope - The coroutine scope used for deferred jobs where the result isn't important, such * as sending data to the network. */ -@Singleton class DefaultTaskRepository @Inject constructor( private val networkDataSource: NetworkDataSource, private val localDataSource: TaskDao, @DefaultDispatcher private val dispatcher: CoroutineDispatcher, - @ApplicationScope private val scope: CoroutineScope, + @ApplicationScope private val scope: CoroutineScope ) : TaskRepository { override suspend fun createTask(title: String, description: String): String { @@ -57,7 +55,7 @@ class DefaultTaskRepository @Inject constructor( val task = Task( title = title, description = description, - id = taskId, + id = taskId ) localDataSource.upsert(task.toLocal()) saveTasksToNetwork() 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 index 38b14282f..67014ec26 100644 --- 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 @@ -39,7 +39,7 @@ fun Task.toLocal() = LocalTask( id = id, title = title, description = description, - isCompleted = isCompleted, + isCompleted = isCompleted ) fun List.toLocal() = map(Task::toLocal) @@ -49,7 +49,7 @@ fun LocalTask.toExternal() = Task( id = id, title = title, description = description, - isCompleted = isCompleted, + isCompleted = isCompleted ) // Note: JvmName is used to provide a unique name for each extension function with the same name. @@ -63,7 +63,7 @@ fun NetworkTask.toLocal() = LocalTask( id = id, title = title, description = shortDescription, - isCompleted = (status == TaskStatus.COMPLETE), + isCompleted = (status == TaskStatus.COMPLETE) ) @JvmName("networkToLocal") @@ -74,7 +74,11 @@ fun LocalTask.toNetwork() = NetworkTask( id = id, title = title, shortDescription = description, - status = if (isCompleted) { TaskStatus.COMPLETE } else { TaskStatus.ACTIVE } + status = if (isCompleted) { + TaskStatus.COMPLETE + } else { + TaskStatus.ACTIVE + } ) fun List.toNetwork() = map(LocalTask::toNetwork) 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 91377071c..2752b3466 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 @@ -31,7 +31,7 @@ data class Task( val title: String = "", val description: String = "", val isCompleted: Boolean = false, - val id: String, + val id: String ) { val titleForList: 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 index 9d9b084e4..0b49b78f8 100644 --- 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 @@ -33,5 +33,5 @@ data class LocalTask( @PrimaryKey val id: String, var title: String, var description: String, - var isCompleted: Boolean, + var isCompleted: Boolean ) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt index 262d52e39..bc88b8c24 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt @@ -54,7 +54,6 @@ object CoroutinesModule { @Provides @Singleton @ApplicationScope - fun providesCoroutineScope( - @DefaultDispatcher dispatcher: CoroutineDispatcher - ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) + fun providesCoroutineScope(@DefaultDispatcher dispatcher: CoroutineDispatcher): CoroutineScope = + CoroutineScope(SupervisorJob() + dispatcher) } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt index cd5b24d71..c04595aa1 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt @@ -34,14 +34,12 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.util.LoadingContent import com.example.android.architecture.blueprints.todoapp.util.StatisticsTopAppBar import com.google.accompanist.appcompattheme.AppCompatTheme -@OptIn(ExperimentalLifecycleComposeApi::class) @Composable fun StatisticsScreen( openDrawer: () -> Unit, diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt index 4db5c1f22..43773f50e 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt @@ -66,23 +66,22 @@ class StatisticsViewModel @Inject constructor( } } - private fun produceStatisticsUiState(taskLoad: Async>) = - when (taskLoad) { - Async.Loading -> { - StatisticsUiState(isLoading = true, isEmpty = true) - } - is Async.Error -> { - // TODO: Show error message? - StatisticsUiState(isEmpty = true, isLoading = false) - } - is Async.Success -> { - val stats = getActiveAndCompletedStats(taskLoad.data) - StatisticsUiState( - isEmpty = taskLoad.data.isEmpty(), - activeTasksPercent = stats.activeTasksPercent, - completedTasksPercent = stats.completedTasksPercent, - isLoading = false - ) - } + private fun produceStatisticsUiState(taskLoad: Async>) = when (taskLoad) { + Async.Loading -> { + StatisticsUiState(isLoading = true, isEmpty = true) } + is Async.Error -> { + // TODO: Show error message? + StatisticsUiState(isEmpty = true, isLoading = false) + } + is Async.Success -> { + val stats = getActiveAndCompletedStats(taskLoad.data) + StatisticsUiState( + isEmpty = taskLoad.data.isEmpty(), + activeTasksPercent = stats.activeTasksPercent, + completedTasksPercent = stats.completedTasksPercent, + isLoading = false + ) + } + } } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatsResult.kt similarity index 99% rename from app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt rename to app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatsResult.kt index 99942a94c..5524f412c 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatsResult.kt @@ -22,7 +22,6 @@ import com.example.android.architecture.blueprints.todoapp.data.Task * Function that does some trivial computation. Used to showcase unit tests. */ internal fun getActiveAndCompletedStats(tasks: List): StatsResult { - return if (tasks.isEmpty()) { StatsResult(0f, 0f) } else { diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt index 814377da3..e76f81a1e 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.data.Task @@ -50,7 +49,6 @@ import com.example.android.architecture.blueprints.todoapp.util.LoadingContent import com.example.android.architecture.blueprints.todoapp.util.TaskDetailTopAppBar import com.google.accompanist.appcompattheme.AppCompatTheme -@OptIn(ExperimentalLifecycleComposeApi::class) @Composable fun TaskDetailScreen( onEditTask: (String) -> Unit, @@ -112,7 +110,7 @@ private fun EditTaskContent( ) { val screenPadding = Modifier.padding( horizontal = dimensionResource(id = R.dimen.horizontal_margin), - vertical = dimensionResource(id = R.dimen.vertical_margin), + vertical = dimensionResource(id = R.dimen.vertical_margin) ) val commonModifier = modifier .fillMaxWidth() @@ -133,7 +131,7 @@ private fun EditTaskContent( Row( Modifier .fillMaxWidth() - .then(screenPadding), + .then(screenPadding) ) { if (task != null) { diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt index 080b22c04..2abb9979a 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt @@ -56,15 +56,18 @@ class TaskDetailViewModel @Inject constructor( val taskId: String = savedStateHandle[TodoDestinationsArgs.TASK_ID_ARG]!! - private val _userMessage: MutableStateFlow = MutableStateFlow(null) - private val _isLoading = MutableStateFlow(false) - private val _isTaskDeleted = MutableStateFlow(false) - private val _taskAsync = taskRepository.getTaskStream(taskId) + private val userMessage: MutableStateFlow = MutableStateFlow(null) + private val isLoading = MutableStateFlow(false) + private val isTaskDeleted = MutableStateFlow(false) + private val taskAsync = taskRepository.getTaskStream(taskId) .map { handleTask(it) } .catch { emit(Async.Error(R.string.loading_task_error)) } val uiState: StateFlow = combine( - _userMessage, _isLoading, _isTaskDeleted, _taskAsync + userMessage, + isLoading, + isTaskDeleted, + taskAsync ) { userMessage, isLoading, isTaskDeleted, taskAsync -> when (taskAsync) { Async.Loading -> { @@ -94,7 +97,7 @@ class TaskDetailViewModel @Inject constructor( fun deleteTask() = viewModelScope.launch { taskRepository.deleteTask(taskId) - _isTaskDeleted.value = true + isTaskDeleted.value = true } fun setCompleted(completed: Boolean) = viewModelScope.launch { @@ -109,19 +112,19 @@ class TaskDetailViewModel @Inject constructor( } fun refresh() { - _isLoading.value = true + isLoading.value = true viewModelScope.launch { taskRepository.refreshTask(taskId) - _isLoading.value = false + isLoading.value = false } } fun snackbarMessageShown() { - _userMessage.value = null + userMessage.value = null } private fun showSnackbarMessage(message: Int) { - _userMessage.value = message + userMessage.value = message } private fun handleTask(task: Task?): Async { diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt index 16cf81e86..2aaf793af 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt @@ -53,7 +53,6 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.android.architecture.blueprints.todoapp.R import com.example.android.architecture.blueprints.todoapp.data.Task @@ -64,7 +63,6 @@ import com.example.android.architecture.blueprints.todoapp.util.LoadingContent import com.example.android.architecture.blueprints.todoapp.util.TasksTopAppBar import com.google.accompanist.appcompattheme.AppCompatTheme -@OptIn(ExperimentalLifecycleComposeApi::class) @Composable fun TasksScreen( @StringRes userMessage: Int, @@ -174,18 +172,14 @@ private fun TasksContent( } @Composable -private fun TaskItem( - task: Task, - onCheckedChange: (Boolean) -> Unit, - onTaskClick: (Task) -> Unit -) { +private fun TaskItem(task: Task, onCheckedChange: (Boolean) -> Unit, onTaskClick: (Task) -> Unit) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .padding( horizontal = dimensionResource(id = R.dimen.horizontal_margin), - vertical = dimensionResource(id = R.dimen.list_item_padding), + vertical = dimensionResource(id = R.dimen.list_item_padding) ) .clickable { onTaskClick(task) } ) { @@ -265,14 +259,14 @@ private fun TasksContentPreview() { description = "Description 5", isCompleted = true, id = "ID 5" - ), + ) ), currentFilteringLabel = R.string.label_all, noTasksLabel = R.string.no_tasks_all, noTasksIconRes = R.drawable.logo_no_fill, onRefresh = { }, onTaskClick = { }, - onTaskCheckedChange = { _, _ -> }, + onTaskCheckedChange = { _, _ -> } ) } } @@ -291,7 +285,7 @@ private fun TasksContentEmptyPreview() { noTasksIconRes = R.drawable.logo_no_fill, onRefresh = { }, onTaskClick = { }, - onTaskCheckedChange = { _, _ -> }, + onTaskCheckedChange = { _, _ -> } ) } } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt index 9868f80a7..b6930e881 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt @@ -60,21 +60,24 @@ class TasksViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle ) : ViewModel() { - private val _savedFilterType = + private val savedFilterType = savedStateHandle.getStateFlow(TASKS_FILTER_SAVED_STATE_KEY, ALL_TASKS) - private val _filterUiInfo = _savedFilterType.map { getFilterUiInfo(it) }.distinctUntilChanged() - private val _userMessage: MutableStateFlow = MutableStateFlow(null) - private val _isLoading = MutableStateFlow(false) - private val _filteredTasksAsync = - combine(taskRepository.getTasksStream(), _savedFilterType) { tasks, type -> + private val filterUiInfo = savedFilterType.map { getFilterUiInfo(it) }.distinctUntilChanged() + private val userMessage: MutableStateFlow = MutableStateFlow(null) + private val isLoading = MutableStateFlow(false) + private val filteredTasksAsync = + combine(taskRepository.getTasksStream(), savedFilterType) { tasks, type -> filterTasks(tasks, type) } .map { Async.Success(it) } .catch>> { emit(Async.Error(R.string.loading_tasks_error)) } val uiState: StateFlow = combine( - _filterUiInfo, _isLoading, _userMessage, _filteredTasksAsync + filterUiInfo, + isLoading, + userMessage, + filteredTasksAsync ) { filterUiInfo, isLoading, userMessage, tasksAsync -> when (tasksAsync) { Async.Loading -> { @@ -130,18 +133,18 @@ class TasksViewModel @Inject constructor( } fun snackbarMessageShown() { - _userMessage.value = null + userMessage.value = null } private fun showSnackbarMessage(message: Int) { - _userMessage.value = message + userMessage.value = message } fun refresh() { - _isLoading.value = true + isLoading.value = true viewModelScope.launch { taskRepository.refresh() - _isLoading.value = false + isLoading.value = false } } @@ -166,19 +169,22 @@ class TasksViewModel @Inject constructor( when (requestType) { ALL_TASKS -> { FilteringUiInfo( - R.string.label_all, R.string.no_tasks_all, + R.string.label_all, + R.string.no_tasks_all, R.drawable.logo_no_fill ) } ACTIVE_TASKS -> { FilteringUiInfo( - R.string.label_active, R.string.no_tasks_active, + R.string.label_active, + R.string.no_tasks_active, R.drawable.ic_check_circle_96dp ) } COMPLETED_TASKS -> { FilteringUiInfo( - R.string.label_completed, R.string.no_tasks_completed, + R.string.label_completed, + R.string.no_tasks_completed, R.drawable.ic_verified_user_96dp ) } @@ -191,5 +197,5 @@ const val TASKS_FILTER_SAVED_STATE_KEY = "TASKS_FILTER_SAVED_STATE_KEY" data class FilteringUiInfo( val currentFilteringLabel: Int = R.string.label_all, val noTasksLabel: Int = R.string.no_tasks_all, - val noTaskIconRes: Int = R.drawable.logo_no_fill, + val noTaskIconRes: Int = R.drawable.logo_no_fill ) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt index 84d641712..7ba7ae60f 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt @@ -50,7 +50,7 @@ fun LoadingContent( state = rememberSwipeRefreshState(loading), onRefresh = onRefresh, modifier = modifier, - content = content, + content = content ) } } diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt index 318b2c90e..ed6c8ea86 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt @@ -18,7 +18,7 @@ package com.example.android.architecture.blueprints.todoapp.util import kotlinx.coroutines.flow.SharingStarted -private const val StopTimeoutMillis: Long = 5000 +private const val STOP_TIMEOUT_MILLIS: Long = 5000 /** * A [SharingStarted] meant to be used with a [StateFlow] to expose data to the UI. @@ -29,4 +29,4 @@ private const val StopTimeoutMillis: Long = 5000 * back, the latest value is replayed and the upstream flows are executed again. This is done to * save resources when the app is in the background but let users switch between apps quickly. */ -val WhileUiSubscribed: SharingStarted = SharingStarted.WhileSubscribed(StopTimeoutMillis) +val WhileUiSubscribed: SharingStarted = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt index 8e5ed06af..18b850b00 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt @@ -106,9 +106,7 @@ private fun AppDrawer( } @Composable -private fun DrawerHeader( - modifier: Modifier = Modifier -) { +private fun DrawerHeader(modifier: Modifier = Modifier) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -158,7 +156,8 @@ private fun DrawerButton( ) { Icon( painter = painter, - contentDescription = null, // decorative + // decorative + contentDescription = null, tint = tintColor ) Spacer(Modifier.width(16.dp)) diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt index abf26cc2e..2ab4bab07 100644 --- a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt +++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt @@ -84,32 +84,44 @@ private fun FilterTasksMenu( ) } ) { closeMenu -> - DropdownMenuItem(onClick = { onFilterAllTasks(); closeMenu() }) { + DropdownMenuItem(onClick = { + onFilterAllTasks() + closeMenu() + }) { Text(text = stringResource(id = R.string.nav_all)) } - DropdownMenuItem(onClick = { onFilterActiveTasks(); closeMenu() }) { + DropdownMenuItem(onClick = { + onFilterActiveTasks() + closeMenu() + }) { Text(text = stringResource(id = R.string.nav_active)) } - DropdownMenuItem(onClick = { onFilterCompletedTasks(); closeMenu() }) { + DropdownMenuItem(onClick = { + onFilterCompletedTasks() + closeMenu() + }) { Text(text = stringResource(id = R.string.nav_completed)) } } } @Composable -private fun MoreTasksMenu( - onClearCompletedTasks: () -> Unit, - onRefresh: () -> Unit -) { +private fun MoreTasksMenu(onClearCompletedTasks: () -> Unit, onRefresh: () -> Unit) { TopAppBarDropdownMenu( iconContent = { Icon(Icons.Filled.MoreVert, stringResource(id = R.string.menu_more)) } ) { closeMenu -> - DropdownMenuItem(onClick = { onClearCompletedTasks(); closeMenu() }) { + DropdownMenuItem(onClick = { + onClearCompletedTasks() + closeMenu() + }) { Text(text = stringResource(id = R.string.menu_clear)) } - DropdownMenuItem(onClick = { onRefresh(); closeMenu() }) { + DropdownMenuItem(onClick = { + onRefresh() + closeMenu() + }) { Text(text = stringResource(id = R.string.refresh)) } } diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt index 1c41c8654..39a69cf22 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt @@ -144,14 +144,13 @@ class DefaultTaskRepositoryTest { } @Test - fun getTasks_WithRemoteDataSourceUnavailable_tasksAreRetrievedFromLocal() = - testScope.runTest { - // When the remote data source is unavailable - networkDataSource.tasks = null - - // The repository fetches from the local source - assertThat(taskRepository.getTasks()).isEqualTo(localTasks.toExternal()) - } + fun getTasks_WithRemoteDataSourceUnavailable_tasksAreRetrievedFromLocal() = testScope.runTest { + // When the remote data source is unavailable + networkDataSource.tasks = null + + // The repository fetches from the local source + assertThat(taskRepository.getTasks()).isEqualTo(localTasks.toExternal()) + } @Test(expected = Exception::class) fun getTasks_WithBothDataSourcesUnavailable_throwsError() = testScope.runTest { diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt index d35aab169..0c5a59482 100644 --- a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt +++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt @@ -33,7 +33,7 @@ class StatisticsUtilsTest { id = "id", title = "title", description = "desc", - isCompleted = false, + isCompleted = false ) ) // When the list of tasks is computed with an active task @@ -51,7 +51,7 @@ class StatisticsUtilsTest { id = "id", title = "title", description = "desc", - isCompleted = true, + isCompleted = true ) ) // When the list of tasks is computed with a completed task @@ -70,7 +70,7 @@ class StatisticsUtilsTest { Task(id = "2", title = "title", description = "desc", isCompleted = true), Task(id = "3", title = "title", description = "desc", isCompleted = true), Task(id = "4", title = "title", description = "desc", isCompleted = false), - Task(id = "5", title = "title", description = "desc", isCompleted = false), + Task(id = "5", title = "title", description = "desc", isCompleted = false) ) // When the list of tasks is computed val result = getActiveAndCompletedStats(tasks) diff --git a/build.gradle.kts b/build.gradle.kts index 79946bd70..232e26416 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,6 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false - alias(libs.plugins.kapt) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.hilt) apply false } diff --git a/gradle.properties b/gradle.properties index 504538451..538c1593d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,7 @@ org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryErr # org.gradle.parallel=true android.enableJetifier=true android.useAndroidX=true -kapt.incremental.apt=true ksp.incremental.apt=true -org.gradle.unsafe.configuration-cache=true \ No newline at end of file +org.gradle.unsafe.configuration-cache=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/init.gradle.kts b/gradle/init.gradle.kts index 82b61b7e4..fe51bef7f 100644 --- a/gradle/init.gradle.kts +++ b/gradle/init.gradle.kts @@ -18,10 +18,10 @@ // Spotless itself is not gradle configuration cache compliant. // Note that the init script needs to be run with the configuration cache turned off. -val ktlintVersion = "0.44.0" +val ktlintVersion = "1.1.1" initscript { - val spotlessVersion = "6.13.0" + val spotlessVersion = "6.25.0" repositories { mavenCentral() @@ -39,7 +39,15 @@ rootProject { kotlin { target("**/*.kt") targetExclude("**/build/**/*.kt") - ktlint(ktlintVersion).userData(mapOf("android" to "true")) + ktlint(ktlintVersion) + .editorConfigOverride( + mapOf( + "ktlint_code_style" to "android_studio", + // Avoid applying naming rules to Composable functions since they + // start with an uppercase letter. + "ktlint_function_naming_ignore_when_annotated_with" to "Composable" + ) + ) licenseHeaderFile(rootProject.file("spotless/copyright.kt")) } format("kts") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c7415dcf8..e7aee083d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,62 +1,62 @@ [versions] -accompanist = "0.28.0" -annotation = "1.5.0" -androidDesugarJdkLibs = "1.2.2" -androidGradlePlugin = "7.4.0" -androidxActivity = "1.6.1" -androidxAppCompat = "1.5.1" -androidxArchCore = "2.1.0" +accompanist = "0.34.0" +annotation = "1.7.1" +androidDesugarJdkLibs = "2.0.4" +androidGradlePlugin = "8.3.1" +androidxActivity = "1.8.2" +androidxAppCompat = "1.6.1" +androidxArchCore = "2.2.0" androidxBrowser = "1.4.0" -androidxComposeBom = "2023.01.00" -androidxComposeCompiler = "1.4.3" +androidxComposeBom = "2024.04.00" +androidxComposeCompiler = "1.5.11" androidxCompose = "1.2.0" -androidxComposeRuntimeTracing = "1.0.0-alpha01" -androidxCore = "1.9.0" -androidxCoreSplashscreen = "1.0.0" +androidxComposeRuntimeTracing = "1.0.0-beta01" +androidxCore = "1.12.0" +androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.0.0" -androidxEspresso = "3.5.0" -androidxHiltNavigationCompose = "1.0.0" -androidxLifecycle = "2.6.0-alpha03" +androidxEspresso = "3.5.1" +androidxHiltNavigationCompose = "1.2.0" +androidxLifecycle = "2.7.0" androidxMacroBenchmark = "1.1.1" -androidxMetrics = "1.0.0-alpha03" -androidxNavigation = "2.5.3" -androidxProfileinstaller = "1.2.1" +androidxMetrics = "1.0.0-beta01" +androidxNavigation = "2.7.7" +androidxProfileinstaller = "1.3.1" androidxStartup = "1.1.1" androidxTestCore = "1.5.0" -androidxTestExt = "1.1.4" +androidxTestExt = "1.1.5" androidxTestRules = "1.5.0" -androidxTestRunner = "1.5.1" -androidxTracing = "1.1.0" -androidxUiAutomator = "2.2.0" -androidxWindowManager = "1.0.0" -androidxWork = "2.7.1" -coil = "2.2.2" +androidxTestRunner = "1.5.2" +androidxTracing = "1.2.0" +androidxUiAutomator = "2.3.0" +androidxWindowManager = "1.2.0" +androidxWork = "2.9.0" +coil = "2.6.0" # @keep -compileSdk = "33" +compileSdk = "34" hamcrest = "1.3" -hilt = "2.44.2" -hiltExt = "1.0.0" +hilt = "2.51" +hiltExt = "1.2.0" jacoco = "0.8.7" junit4 = "4.13.2" -kotlin = "1.8.10" -kotlinxCoroutines = "1.6.4" -kotlinxDatetime = "0.4.0" -kotlinxSerializationJson = "1.5.0" -ksp = "1.8.10-1.0.9" -lint = "30.3.1" +kotlin = "1.9.23" +kotlinxCoroutines = "1.8.0" +kotlinxDatetime = "0.5.0" +kotlinxSerializationJson = "1.6.3" +ksp = "1.9.23-1.0.20" +lint = "31.3.1" # @keep minSdk = "21" okhttp = "4.10.0" protobuf = "3.21.12" -protobufPlugin = "0.8.19" +protobufPlugin = "0.9.1" retrofit = "2.9.0" retrofitKotlinxSerializationJson = "0.8.0" -room = "2.5.0-rc01" +room = "2.6.1" spotless = "5.12.5" timber = "5.0.1" # @keep -targetSdk = "33" -truth = "1.1.2" +targetSdk = "34" +truth = "1.4.2" turbine = "0.12.1" [libraries] @@ -149,6 +149,5 @@ hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c684f..17655d0ef 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/shared-test/build.gradle.kts b/shared-test/build.gradle.kts index 2060fc01b..7d6815085 100644 --- a/shared-test/build.gradle.kts +++ b/shared-test/build.gradle.kts @@ -16,7 +16,6 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) - alias(libs.plugins.kapt) alias(libs.plugins.ksp) alias(libs.plugins.hilt) } @@ -27,6 +26,11 @@ android { defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } } dependencies { @@ -39,7 +43,7 @@ dependencies { implementation(libs.androidx.test.rules) implementation(libs.hilt.android.core) implementation(libs.hilt.android.testing) - kapt(libs.hilt.compiler) + ksp(libs.hilt.compiler) // Room implementation(libs.room.runtime)