Showing preview only (329K chars total). Download the full file or copy to clipboard to get everything.
Repository: android/architecture-samples
Branch: main
Commit: ee66e1526b84
Files: 108
Total size: 294.7 KB
Directory structure:
gitextract_6d9xqtrj/
├── .github/
│ ├── ci-gradle.properties
│ └── workflows/
│ ├── build_test.yaml
│ └── copy-branch.yml
├── .gitignore
├── .google/
│ └── packaging.yaml
├── CODEOWNERS
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ ├── proguardTest-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── example/
│ │ └── android/
│ │ └── architecture/
│ │ └── blueprints/
│ │ └── todoapp/
│ │ ├── addedittask/
│ │ │ └── AddEditTaskScreenTest.kt
│ │ ├── data/
│ │ │ └── source/
│ │ │ └── local/
│ │ │ └── TaskDaoTest.kt
│ │ ├── statistics/
│ │ │ └── StatisticsScreenTest.kt
│ │ ├── taskdetail/
│ │ │ └── TaskDetailScreenTest.kt
│ │ └── tasks/
│ │ ├── AppNavigationTest.kt
│ │ ├── TasksScreenTest.kt
│ │ └── TasksTest.kt
│ ├── debug/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── example/
│ │ └── android/
│ │ └── architecture/
│ │ └── blueprints/
│ │ └── todoapp/
│ │ └── HiltTestActivity.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── android/
│ │ │ └── architecture/
│ │ │ └── blueprints/
│ │ │ └── todoapp/
│ │ │ ├── TodoActivity.kt
│ │ │ ├── TodoApplication.kt
│ │ │ ├── TodoNavGraph.kt
│ │ │ ├── TodoNavigation.kt
│ │ │ ├── TodoTheme.kt
│ │ │ ├── addedittask/
│ │ │ │ ├── AddEditTaskScreen.kt
│ │ │ │ └── AddEditTaskViewModel.kt
│ │ │ ├── data/
│ │ │ │ ├── DefaultTaskRepository.kt
│ │ │ │ ├── ModelMappingExt.kt
│ │ │ │ ├── Task.kt
│ │ │ │ ├── TaskRepository.kt
│ │ │ │ └── source/
│ │ │ │ ├── local/
│ │ │ │ │ ├── LocalTask.kt
│ │ │ │ │ ├── TaskDao.kt
│ │ │ │ │ └── ToDoDatabase.kt
│ │ │ │ └── network/
│ │ │ │ ├── NetworkDataSource.kt
│ │ │ │ ├── NetworkTask.kt
│ │ │ │ └── TaskNetworkDataSource.kt
│ │ │ ├── di/
│ │ │ │ ├── CoroutinesModule.kt
│ │ │ │ └── DataModules.kt
│ │ │ ├── statistics/
│ │ │ │ ├── StatisticsScreen.kt
│ │ │ │ ├── StatisticsUtils.kt
│ │ │ │ └── StatisticsViewModel.kt
│ │ │ ├── taskdetail/
│ │ │ │ ├── TaskDetailScreen.kt
│ │ │ │ └── TaskDetailViewModel.kt
│ │ │ ├── tasks/
│ │ │ │ ├── TasksFilterType.kt
│ │ │ │ ├── TasksScreen.kt
│ │ │ │ └── TasksViewModel.kt
│ │ │ └── util/
│ │ │ ├── Async.kt
│ │ │ ├── ComposeUtils.kt
│ │ │ ├── CoroutinesUtils.kt
│ │ │ ├── SimpleCountingIdlingResource.kt
│ │ │ ├── TodoDrawer.kt
│ │ │ └── TopAppBars.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── drawer_item_color.xml
│ │ │ ├── ic_add.xml
│ │ │ ├── ic_assignment_turned_in_24dp.xml
│ │ │ ├── ic_check_circle_96dp.xml
│ │ │ ├── ic_done.xml
│ │ │ ├── ic_edit.xml
│ │ │ ├── ic_filter_list.xml
│ │ │ ├── ic_list.xml
│ │ │ ├── ic_menu.xml
│ │ │ ├── ic_statistics.xml
│ │ │ ├── ic_statistics_100dp.xml
│ │ │ ├── ic_statistics_24dp.xml
│ │ │ ├── ic_verified_user_96dp.xml
│ │ │ ├── list_completed_touch_feedback.xml
│ │ │ └── touch_feedback.xml
│ │ ├── font/
│ │ │ └── opensans_font.xml
│ │ ├── values/
│ │ │ ├── attrs.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ └── values-w820dp/
│ │ └── dimens.xml
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── android/
│ │ └── architecture/
│ │ └── blueprints/
│ │ └── todoapp/
│ │ ├── addedittask/
│ │ │ └── AddEditTaskViewModelTest.kt
│ │ ├── data/
│ │ │ └── DefaultTaskRepositoryTest.kt
│ │ ├── statistics/
│ │ │ ├── StatisticsUtilsTest.kt
│ │ │ └── StatisticsViewModelTest.kt
│ │ ├── taskdetail/
│ │ │ └── TaskDetailViewModelTest.kt
│ │ └── tasks/
│ │ └── TasksViewModelTest.kt
│ └── resources/
│ └── mockito-extensions/
│ └── org.mockito.plugins.MockMaker
├── build.gradle.kts
├── gradle/
│ ├── init.gradle.kts
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── renovate.json
├── settings.gradle.kts
├── shared-test/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── example/
│ └── android/
│ └── architecture/
│ └── blueprints/
│ └── todoapp/
│ ├── CustomTestRunner.kt
│ ├── MainCoroutineRule.kt
│ ├── data/
│ │ ├── FakeTaskRepository.kt
│ │ └── source/
│ │ ├── local/
│ │ │ └── FakeTaskDao.kt
│ │ └── network/
│ │ └── FakeNetworkDataSource.kt
│ └── di/
│ ├── DatabaseTestModule.kt
│ └── RepositoryTestModule.kt
└── spotless/
├── copyright.kt
├── copyright.kts
└── copyright.xml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ci-gradle.properties
================================================
#
# Copyright 2020 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
#
# http://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.
#
org.gradle.daemon=false
org.gradle.parallel=true
org.gradle.workers.max=2
kotlin.incremental=false
# Controls KotlinOptions.allWarningsAsErrors.
# This value used in CI and is currently set to false.
# If you want to treat warnings as errors locally, set this property to true
# in your ~/.gradle/gradle.properties file.
warningsAsErrors=false
================================================
FILE: .github/workflows/build_test.yaml
================================================
name: build_test
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
api-level: [29]
steps:
- uses: actions/checkout@v4
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls /dev/kvm
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set Up JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu' # See 'Supported distributions' for available options
java-version: '17'
cache: 'gradle'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Run instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86
disable-animations: true
script: ./gradlew :app:connectedCheck --stacktrace
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports-${{ matrix.api-level }}
path: ./app/build/reports/androidTests
================================================
FILE: .github/workflows/copy-branch.yml
================================================
# Duplicates default main branch to the old master branch
name: Duplicates main to old master branch
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the main branch
on:
workflow_dispatch:
push:
branches: [ main ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "copy-branch"
copy-branch:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it,
# but specifies master branch (old default).
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: master
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
git merge origin/main
git push
================================================
FILE: .gitignore
================================================
*.iml
.gradle
local.properties
.idea
.DS_Store
build
captures
.externalNativeBuild
================================================
FILE: .google/packaging.yaml
================================================
# Copyright (C) 2020 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.
#
# GOOGLE SAMPLE PACKAGING DATA
#
# This file is used by Google as part of our samples packaging process.
# End users may safely ignore this file. It has no relevance to other systems.
---
status: PUBLISHED
technologies: [Android, JetpackCompose, Coroutines]
categories:
- AndroidTesting
- AndroidArchitecture
- AndroidArchitectureUILayer
- AndroidArchitectureDataLayer
- AndroidArchitectureStateProduction
- AndroidArchitectureStateHolder
- AndroidArchitectureUIEvents
- JetpackComposeTesting
- JetpackComposeArchitectureAndState
- JetpackComposeNavigation
languages: [Kotlin]
solutions:
- Mobile
- Flow
- JetpackHilt
- JetpackRoom
- JetpackNavigation
- JetpackLifecycle
github: android/architecture-samples
level: INTERMEDIATE
license: apache2
================================================
FILE: CODEOWNERS
================================================
* @josealcerreca @dturner
================================================
FILE: CONTRIBUTING.md
================================================
# How to become a contributor and submit your own code
## Contributor License Agreements
We'd love to accept your patches! Before we can take them, we
have to jump a couple of legal hurdles.
### Before you contribute
Before we can use your code, you must sign the
[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
(CLA), which you can do online. The CLA is necessary mainly because you own the
copyright to your changes, even after your contribution becomes part of our
codebase, so we need your permission to use and distribute your code. We also
need to be sure of various other things—for instance that you'll tell us if you
know that your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.
Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on.
### Code reviews
All submissions, including submissions by project members, require review. We
use Github pull requests for this purpose.
### The small print
Contributions made by corporations are covered by a different agreement than
the one above, the
[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 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
http://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.
================================================
FILE: README.md
================================================
# Android Architecture Samples
These samples showcase different architectural approaches to developing Android apps. In its different branches you'll find the same app (a TODO app) implemented with small differences.
In this branch you'll find:
* User Interface built with **[Jetpack Compose](https://developer.android.com/jetpack/compose)**
* A single-activity architecture, using **[Navigation Compose](https://developer.android.com/jetpack/compose/navigation)**.
* A presentation layer that contains a Compose screen (View) and a **ViewModel** per screen (or feature).
* Reactive UIs using **[Flow](https://developer.android.com/kotlin/flow)** and **[coroutines](https://kotlinlang.org/docs/coroutines-overview.html)** for asynchronous operations.
* A **data layer** with a repository and two data sources (local using Room and a fake remote).
* Two **product flavors**, `mock` and `prod`, [to ease development and testing](https://android-developers.googleblog.com/2015/12/leveraging-product-flavors-in-android.html).
* A collection of unit, integration and e2e **tests**, including "shared" tests that can be run on emulator/device.
* Dependency injection using [Hilt](https://developer.android.com/training/dependency-injection/hilt-android).
## Screenshots
<img src="screenshots/screenshots.png" alt="Screenshot">
## Why a to-do app?
The app in this project aims to be simple enough that you can understand it quickly, but complex enough to showcase difficult design decisions and testing scenarios. For more information, see the [app's specification](https://github.com/googlesamples/android-architecture/wiki/To-do-app-specification).
## What is it not?
* A template. Check out the [Architecture Templates](https://github.com/android/architecture-templates) instead.
* A UI/Material Design sample. The interface of the app is deliberately kept simple to focus on architecture. Check out the [Compose Samples](https://github.com/android/compose-samples) instead.
* A real production app with network access, user authentication, etc. Check out the [Now in Android app](https://github.com/android/nowinandroid) instead.
## Who is it for?
* Intermediate developers and beginners looking for a way to structure their app in a testable and maintainable way.
* Advanced developers looking for quick reference.
## Opening a sample in Android Studio
To open one of the samples in Android Studio, begin by checking out one of the sample branches, and then open the root directory in Android Studio. The following series of steps illustrate how to open the sample.
Clone the repository:
```
git clone git@github.com:android/architecture-samples.git
```
Finally open the `architecture-samples/` directory in Android Studio.
### License
```
Copyright 2024 Google, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you 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
http://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.
```
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle.kts
================================================
/*
* Copyright 2020 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.
*/
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
alias(libs.plugins.compose.compiler)
}
android {
namespace = "com.example.android.architecture.blueprints.todoapp"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
applicationId = "com.example.android.architecture.blueprints.main"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "com.example.android.architecture.blueprints.todoapp.CustomTestRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments += "room.incremental" to "true"
}
}
}
buildTypes {
getByName("debug") {
isMinifyEnabled = false
isTestCoverageEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
testProguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguardTest-rules.pro")
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
testProguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguardTest-rules.pro")
}
}
// Always show the result of every unit test, even if it passes.
testOptions.unitTests {
isIncludeAndroidResources = true
all { test ->
with(test) {
testLogging {
events = setOf(
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED,
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_OUT,
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR,
)
}
}
}
}
buildFeatures {
compose = true
buildConfig = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
packaging {
excludes += "META-INF/AL2.0"
excludes += "META-INF/LGPL2.1"
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
freeCompilerArgs += "-opt-in=kotlin.Experimental"
}
}
}
/*
Dependency versions are defined in the top level build.gradle file. This helps keeping track of
all versions in a single place. This improves readability and helps managing project complexity.
*/
dependencies {
// App dependencies
implementation(libs.androidx.annotation)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.timber)
implementation(libs.androidx.test.espresso.idling.resources)
// Architecture Components
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.lifecycle.viewModelCompose)
// Hilt
implementation(libs.hilt.android.core)
implementation(libs.androidx.hilt.navigation.compose)
ksp(libs.hilt.compiler)
// Jetpack Compose
val composeBom = platform(libs.androidx.compose.bom)
implementation(libs.androidx.activity.compose)
implementation(composeBom)
implementation(libs.androidx.compose.foundation.core)
implementation(libs.androidx.compose.foundation.layout)
implementation(libs.androidx.compose.animation)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.lifecycle.viewModelCompose)
implementation(libs.accompanist.appcompat.theme)
implementation(libs.accompanist.swiperefresh)
debugImplementation(composeBom)
debugImplementation(libs.androidx.compose.ui.tooling.core)
debugImplementation(libs.androidx.compose.ui.test.manifest)
// Dependencies for local unit tests
testImplementation(composeBom)
testImplementation(libs.junit4)
testImplementation(libs.androidx.archcore.testing)
testImplementation(libs.kotlinx.coroutines.android)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.androidx.navigation.testing)
testImplementation(libs.androidx.test.espresso.core)
testImplementation(libs.androidx.test.espresso.contrib)
testImplementation(libs.androidx.test.espresso.intents)
testImplementation(libs.google.truth)
testImplementation(libs.androidx.compose.ui.test.junit)
// JVM tests - Hilt
testImplementation(libs.hilt.android.testing)
kspTest(libs.hilt.compiler)
// Dependencies for Android unit tests
androidTestImplementation(composeBom)
androidTestImplementation(libs.junit4)
androidTestImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.compose.ui.test.junit)
// AndroidX Test - JVM testing
testImplementation(libs.androidx.test.core.ktx)
testImplementation(libs.androidx.test.ext)
testImplementation(libs.androidx.test.rules)
testImplementation(project(":shared-test"))
// AndroidX Test - Instrumented testing
androidTestImplementation(libs.androidx.test.core.ktx)
androidTestImplementation(libs.androidx.test.ext)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.room.testing)
androidTestImplementation(libs.androidx.archcore.testing)
androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.androidx.test.espresso.contrib)
androidTestImplementation(libs.androidx.test.espresso.intents)
androidTestImplementation(libs.androidx.test.espresso.idling.resources)
androidTestImplementation(libs.androidx.test.espresso.idling.concurrent)
androidTestImplementation(project(":shared-test"))
// AndroidX Test - Hilt testing
androidTestImplementation(libs.hilt.android.testing)
kspAndroidTest(libs.hilt.compiler)
}
================================================
FILE: app/proguard-rules.pro
================================================
-dontoptimize
# Some methods are only called from tests, so make sure the shrinker keeps them.
-keep class com.example.android.architecture.blueprints.** { *; }
-keep class androidx.drawerlayout.widget.DrawerLayout { *; }
-keep class androidx.test.espresso.**
# keep the class and specified members from being removed or renamed
-keep class androidx.test.espresso.IdlingRegistry { *; }
-keep class androidx.test.espresso.IdlingResource { *; }
-keep class com.google.common.base.Preconditions { *; }
-keep class androidx.room.RoomDataBase { *; }
-keep class androidx.room.Room { *; }
-keep class android.arch.** { *; }
# Proguard rules that are applied to your test apk/code.
-ignorewarnings
-keepattributes *Annotation*
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn androidx.test.**
-dontwarn org.junit.**
-dontwarn org.hamcrest.**
-dontwarn com.squareup.javawriter.JavaWriter
# Uncomment this if you use Mockito
-dontwarn org.mockito.**
================================================
FILE: app/proguardTest-rules.pro
================================================
# Proguard rules that are applied to your test apk/code.
-ignorewarnings
-dontoptimize
-keepattributes *Annotation*
-keep class androidx.test.espresso.**
# keep the class and specified members from being removed or renamed
-keep class androidx.test.espresso.IdlingRegistry { *; }
-keep class androidx.test.espresso.IdlingResource { *; }
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn androidx.test.**
-dontwarn org.junit.**
-dontwarn org.hamcrest.**
-dontwarn com.squareup.javawriter.JavaWriter
================================================
FILE: app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt
================================================
/*
* 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.addedittask
import androidx.compose.material3.Surface
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.lifecycle.SavedStateHandle
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.TodoTheme
import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
/**
* Integration test for the Add Task screen.
*/
@RunWith(AndroidJUnit4::class)
@MediumTest
@HiltAndroidTest
@ExperimentalCoroutinesApi
class AddEditTaskScreenTest {
@get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
private val activity get() = composeTestRule.activity
@Inject
lateinit var repository: TaskRepository
@Before
fun setup() {
hiltRule.inject()
// GIVEN - On the "Add Task" screen.
composeTestRule.setContent {
TodoTheme {
Surface {
AddEditTaskScreen(
viewModel = AddEditTaskViewModel(repository, SavedStateHandle()),
topBarTitle = R.string.add_task,
onTaskUpdate = { },
onBack = { },
)
}
}
}
}
@Test
fun emptyTask_isNotSaved() {
// WHEN - Enter invalid title and description combination and click save
findTextField(R.string.title_hint).performTextClearance()
findTextField(R.string.description_hint).performTextClearance()
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
.performClick()
// THEN - Entered Task is still displayed (a correct task would close it).
composeTestRule
.onNodeWithText(activity.getString(R.string.empty_task_message))
.assertIsDisplayed()
}
@Test
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")
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
.performClick()
// THEN - Verify that the repository saved the task
val tasks = repository.getTasks(true)
assertEquals(1, tasks.size)
assertEquals("title", tasks[0].title)
assertEquals("description", tasks[0].description)
}
private fun findTextField(text: Int): SemanticsNodeInteraction {
return composeTestRule.onNode(
hasSetTextAction() and hasText(activity.getString(text))
)
}
}
================================================
FILE: app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt
================================================
/*
* 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.room.Room
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class TaskDaoTest {
// using an in-memory database because the information stored here disappears when the
// process is killed
private lateinit var database: ToDoDatabase
// Ensure that we use a new database for each test.
@Before
fun initDb() {
database = Room.inMemoryDatabaseBuilder(
getApplicationContext(),
ToDoDatabase::class.java
).allowMainThreadQueries().build()
}
@Test
fun insertTaskAndGetById() = runTest {
// GIVEN - insert a task
val task = LocalTask(
title = "title",
description = "description",
id = "id",
isCompleted = false,
)
database.taskDao().upsert(task)
// WHEN - Get the task by id from the database
val loaded = database.taskDao().getById(task.id)
// THEN - The loaded data contains the expected values
assertNotNull(loaded as LocalTask)
assertEquals(task.id, loaded.id)
assertEquals(task.title, loaded.title)
assertEquals(task.description, loaded.description)
assertEquals(task.isCompleted, loaded.isCompleted)
}
@Test
fun insertTaskReplacesOnConflict() = runTest {
// Given that a task is inserted
val task = LocalTask(
title = "title",
description = "description",
id = "id",
isCompleted = false,
)
database.taskDao().upsert(task)
// When a task with the same id is inserted
val newTask = LocalTask(
title = "title2",
description = "description2",
isCompleted = true,
id = task.id
)
database.taskDao().upsert(newTask)
// THEN - The loaded data contains the expected values
val loaded = database.taskDao().getById(task.id)
assertEquals(task.id, loaded?.id)
assertEquals("title2", loaded?.title)
assertEquals("description2", loaded?.description)
assertEquals(true, loaded?.isCompleted)
}
@Test
fun insertTaskAndGetTasks() = runTest {
// GIVEN - insert a task
val task = LocalTask(
title = "title",
description = "description",
id = "id",
isCompleted = false,
)
database.taskDao().upsert(task)
// WHEN - Get tasks from the database
val tasks = database.taskDao().getAll()
// THEN - There is only 1 task in the database, and contains the expected values
assertEquals(1, tasks.size)
assertEquals(tasks[0].id, task.id)
assertEquals(tasks[0].title, task.title)
assertEquals(tasks[0].description, task.description)
assertEquals(tasks[0].isCompleted, task.isCompleted)
}
@Test
fun updateTaskAndGetById() = runTest {
// When inserting a task
val originalTask = LocalTask(
title = "title",
description = "description",
id = "id",
isCompleted = false,
)
database.taskDao().upsert(originalTask)
// When the task is updated
val updatedTask = LocalTask(
title = "new title",
description = "new description",
isCompleted = true,
id = originalTask.id
)
database.taskDao().upsert(updatedTask)
// THEN - The loaded data contains the expected values
val loaded = database.taskDao().getById(originalTask.id)
assertEquals(originalTask.id, loaded?.id)
assertEquals("new title", loaded?.title)
assertEquals("new description", loaded?.description)
assertEquals(true, loaded?.isCompleted)
}
@Test
fun updateCompletedAndGetById() = runTest {
// When inserting a task
val task = LocalTask(
title = "title",
description = "description",
id = "id",
isCompleted = true
)
database.taskDao().upsert(task)
// When the task is updated
database.taskDao().updateCompleted(task.id, false)
// THEN - The loaded data contains the expected values
val loaded = database.taskDao().getById(task.id)
assertEquals(task.id, loaded?.id)
assertEquals(task.title, loaded?.title)
assertEquals(task.description, loaded?.description)
assertEquals(false, loaded?.isCompleted)
}
@Test
fun deleteTaskByIdAndGettingTasks() = runTest {
// Given a task inserted
val task = LocalTask(
title = "title",
description = "description",
id = "id",
isCompleted = false,
)
database.taskDao().upsert(task)
// When deleting a task by id
database.taskDao().deleteById(task.id)
// THEN - The list is empty
val tasks = database.taskDao().getAll()
assertEquals(true, tasks.isEmpty())
}
@Test
fun deleteTasksAndGettingTasks() = runTest {
// Given a task inserted
database.taskDao().upsert(
LocalTask(
title = "title",
description = "description",
id = "id",
isCompleted = false,
)
)
// When deleting all tasks
database.taskDao().deleteAll()
// THEN - The list is empty
val tasks = database.taskDao().getAll()
assertEquals(true, tasks.isEmpty())
}
@Test
fun deleteCompletedTasksAndGettingTasks() = runTest {
// Given a completed task inserted
database.taskDao().upsert(
LocalTask(title = "completed", description = "task", id = "id", isCompleted = true)
)
// When deleting completed tasks
database.taskDao().deleteCompleted()
// THEN - The list is empty
val tasks = database.taskDao().getAll()
assertEquals(true, tasks.isEmpty())
}
}
================================================
FILE: app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt
================================================
/*
* Copyright 2022 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.statistics
import androidx.compose.material3.Surface
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
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.TodoTheme
import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
/**
* Integration test for the statistics screen.
*/
@RunWith(AndroidJUnit4::class)
@MediumTest
@HiltAndroidTest
@ExperimentalCoroutinesApi
class StatisticsScreenTest {
@get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
private val activity get() = composeTestRule.activity
@Inject
lateinit var repository: TaskRepository
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun tasks_showsNonEmptyMessage() = runTest {
// Given some tasks
repository.apply {
createTask("Title1", "Description1")
createTask("Title2", "Description2").also {
completeTask(it)
}
}
composeTestRule.setContent {
TodoTheme {
Surface {
StatisticsScreen(
openDrawer = { },
viewModel = StatisticsViewModel(repository)
)
}
}
}
val expectedActiveTaskText = activity.getString(R.string.statistics_active_tasks, 50.0f)
val expectedCompletedTaskText = activity
.getString(R.string.statistics_completed_tasks, 50.0f)
// check that both info boxes are displayed and contain the correct info
composeTestRule.onNodeWithText(expectedActiveTaskText).assertIsDisplayed()
composeTestRule.onNodeWithText(expectedCompletedTaskText).assertIsDisplayed()
}
}
================================================
FILE: app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt
================================================
/*
* Copyright 2022 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.taskdetail
import androidx.compose.material3.Surface
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.isToggleable
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.lifecycle.SavedStateHandle
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.TodoTheme
import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
/**
* Integration test for the Task Details screen.
*/
@MediumTest
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
@ExperimentalCoroutinesApi
class TaskDetailScreenTest {
@get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
@Inject
lateinit var repository: TaskRepository
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun activeTaskDetails_DisplayedInUi() = runTest {
// GIVEN - Add active (incomplete) task to the DB
val activeTaskId = repository.createTask(
title = "Active Task",
description = "AndroidX Rocks"
)
// WHEN - Details screen is opened
setContent(activeTaskId)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
composeTestRule.onNodeWithText("Active Task").assertIsDisplayed()
composeTestRule.onNodeWithText("AndroidX Rocks").assertIsDisplayed()
// and make sure the "active" checkbox is shown unchecked
composeTestRule.onNode(isToggleable()).assertIsOff()
}
@Test
fun completedTaskDetails_DisplayedInUi() = runTest {
// GIVEN - Add completed task to the DB
val completedTaskId = repository.createTask("Completed Task", "AndroidX Rocks")
repository.completeTask(completedTaskId)
// WHEN - Details screen is opened
setContent(completedTaskId)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
composeTestRule.onNodeWithText("Completed Task").assertIsDisplayed()
composeTestRule.onNodeWithText("AndroidX Rocks").assertIsDisplayed()
// and make sure the "active" checkbox is shown unchecked
composeTestRule.onNode(isToggleable()).assertIsOn()
}
private fun setContent(activeTaskId: String) {
composeTestRule.setContent {
TodoTheme {
Surface {
TaskDetailScreen(
viewModel = TaskDetailViewModel(
repository,
SavedStateHandle(mapOf("taskId" to activeTaskId))
),
onEditTask = { /*TODO*/ },
onBack = { },
onDeleteTask = { },
)
}
}
}
}
}
================================================
FILE: app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt
================================================
/*
* Copyright 2022 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.tasks
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.pressBack
import androidx.test.ext.junit.runners.AndroidJUnit4
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.TodoTheme
import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
/**
* Tests for scenarios that requires navigating within the app.
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
@HiltAndroidTest
class AppNavigationTest {
@get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
// Executes tasks in the Architecture Components in the same thread
@get:Rule(order = 1)
var instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
private val activity get() = composeTestRule.activity
@Inject
lateinit var taskRepository: TaskRepository
@Before
fun init() {
hiltRule.inject()
}
@Test
fun drawerNavigationFromTasksToStatistics() {
setContent()
openDrawer()
// Start statistics screen.
composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title)).performClick()
// Check that statistics screen was opened.
composeTestRule.onNodeWithText(activity.getString(R.string.statistics_no_tasks))
.assertIsDisplayed()
openDrawer()
// Start tasks screen.
composeTestRule.onNodeWithText(activity.getString(R.string.list_title)).performClick()
// Check that tasks screen was opened.
composeTestRule.onNodeWithText(activity.getString(R.string.no_tasks_all))
.assertIsDisplayed()
}
@Test
fun tasksScreen_clickOnAndroidHomeIcon_OpensNavigation() {
setContent()
// Check that left drawer is closed at startup
composeTestRule.onNodeWithText(activity.getString(R.string.list_title))
.assertIsNotDisplayed()
composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title))
.assertIsNotDisplayed()
openDrawer()
// Check if drawer is open
composeTestRule.onNodeWithText(activity.getString(R.string.list_title)).assertIsDisplayed()
composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title))
.assertIsDisplayed()
}
@Test
fun statsScreen_clickOnAndroidHomeIcon_OpensNavigation() {
setContent()
// When the user navigates to the stats screen
openDrawer()
composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title)).performClick()
composeTestRule.onNodeWithText(activity.getString(R.string.list_title))
.assertIsNotDisplayed()
openDrawer()
// Check if drawer is open
composeTestRule.onNodeWithText(activity.getString(R.string.list_title)).assertIsDisplayed()
assertTrue(
composeTestRule.onAllNodesWithText(activity.getString(R.string.statistics_title))
.fetchSemanticsNodes().isNotEmpty()
)
}
@Test
fun taskDetailScreen_doubleUIBackButton() = runTest {
val taskName = "UI <- button"
taskRepository.createTask(taskName, "Description")
setContent()
// Click on the task on the list
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText(taskName).assertIsDisplayed()
composeTestRule.onNodeWithText(taskName).performClick()
// Click on the edit task button
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))
.assertIsDisplayed()
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))
.performClick()
// Confirm that if we click "<-" once, we end up back at the task details page
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
.performClick()
composeTestRule.onNodeWithText(taskName).assertIsDisplayed()
// Confirm that if we click "<-" a second time, we end up back at the home screen
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
.performClick()
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
}
@Test
fun taskDetailScreen_doubleBackButton() = runTest {
val taskName = "Back button"
taskRepository.createTask(taskName, "Description")
setContent()
// Click on the task on the list
composeTestRule.onNodeWithText(taskName).assertIsDisplayed()
composeTestRule.onNodeWithText(taskName).performClick()
// Click on the edit task button
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))
.performClick()
// Confirm that if we click back once, we end up back at the task details page
pressBack()
composeTestRule.onNodeWithText(taskName).assertIsDisplayed()
// Confirm that if we click back a second time, we end up back at the home screen
pressBack()
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
}
private fun setContent() {
composeTestRule.setContent {
TodoTheme {
TodoNavGraph()
}
}
}
private fun openDrawer() {
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.open_drawer))
.performClick()
}
}
================================================
FILE: app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt
================================================
/*
* Copyright 2022 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.tasks
import androidx.annotation.StringRes
import androidx.compose.material3.Surface
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.isToggleable
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.lifecycle.SavedStateHandle
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.TodoTheme
import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
/**
* Integration test for the Task List screen.
*/
// TODO - Move to the sharedTest folder when https://issuetracker.google.com/224974381 is fixed
@RunWith(AndroidJUnit4::class)
@MediumTest
// @LooperMode(LooperMode.Mode.PAUSED)
// @TextLayoutMode(TextLayoutMode.Mode.REALISTIC)
@HiltAndroidTest
@OptIn(ExperimentalCoroutinesApi::class)
class TasksScreenTest {
@get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
private val activity get() = composeTestRule.activity
@Inject
lateinit var repository: TaskRepository
@Before
fun init() {
hiltRule.inject()
}
@Test
fun displayTask_whenRepositoryHasData() = runTest {
// GIVEN - One task already in the repository
repository.createTask("TITLE1", "DESCRIPTION1")
// WHEN - On startup
setContent()
// THEN - Verify task is displayed on screen
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
}
@Test
fun displayActiveTask() = runTest {
repository.createTask("TITLE1", "DESCRIPTION1")
setContent()
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
openFilterAndSelectOption(R.string.nav_active)
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
openFilterAndSelectOption(R.string.nav_completed)
composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
}
@Test
fun displayCompletedTask() = runTest {
repository.apply {
createTask("TITLE1", "DESCRIPTION1").also { completeTask(it) }
}
setContent()
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
openFilterAndSelectOption(R.string.nav_active)
composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
openFilterAndSelectOption(R.string.nav_completed)
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
}
@Test
fun markTaskAsComplete() = runTest {
repository.createTask("TITLE1", "DESCRIPTION1")
setContent()
// Mark the task as complete
composeTestRule.onNode(isToggleable()).performClick()
// Verify task is shown as complete
openFilterAndSelectOption(R.string.nav_all)
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
openFilterAndSelectOption(R.string.nav_active)
composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
openFilterAndSelectOption(R.string.nav_completed)
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
}
@Test
fun markTaskAsActive() = runTest {
repository.apply {
createTask("TITLE1", "DESCRIPTION1").also { completeTask(it) }
}
setContent()
// Mark the task as active
composeTestRule.onNode(isToggleable()).performClick()
// Verify task is shown as active
openFilterAndSelectOption(R.string.nav_all)
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
openFilterAndSelectOption(R.string.nav_active)
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
openFilterAndSelectOption(R.string.nav_completed)
composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
}
@Test
fun showAllTasks() = runTest {
// Add one active task and one completed task
repository.apply {
createTask("TITLE1", "DESCRIPTION1")
createTask("TITLE2", "DESCRIPTION2").also { completeTask(it) }
}
setContent()
// Verify that both of our tasks are shown
openFilterAndSelectOption(R.string.nav_all)
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
composeTestRule.onNodeWithText("TITLE2").assertIsDisplayed()
}
@Test
fun showActiveTasks() = runTest {
// Add 2 active tasks and one completed task
repository.apply {
createTask("TITLE1", "DESCRIPTION1")
createTask("TITLE2", "DESCRIPTION2")
createTask("TITLE3", "DESCRIPTION3").also { completeTask(it) }
}
setContent()
// Verify that the active tasks (but not the completed task) are shown
openFilterAndSelectOption(R.string.nav_active)
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
composeTestRule.onNodeWithText("TITLE2").assertIsDisplayed()
composeTestRule.onNodeWithText("TITLE3").assertDoesNotExist()
}
@Test
fun showCompletedTasks() = runTest {
// Add one active task and 2 completed tasks
repository.apply {
createTask("TITLE1", "DESCRIPTION1")
createTask("TITLE2", "DESCRIPTION2").also { completeTask(it) }
createTask("TITLE3", "DESCRIPTION3").also { completeTask(it) }
}
setContent()
// Verify that the completed tasks (but not the active task) are shown
openFilterAndSelectOption(R.string.nav_completed)
composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
composeTestRule.onNodeWithText("TITLE2").assertIsDisplayed()
composeTestRule.onNodeWithText("TITLE3").assertIsDisplayed()
}
@Test
fun clearCompletedTasks() = runTest {
// Add one active task and one completed task
repository.apply {
createTask("TITLE1", "DESCRIPTION1")
createTask("TITLE2", "DESCRIPTION2").also { completeTask(it) }
}
setContent()
// Click clear completed in menu
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_more))
.performClick()
composeTestRule.onNodeWithText(activity.getString(R.string.menu_clear)).assertIsDisplayed()
composeTestRule.onNodeWithText(activity.getString(R.string.menu_clear)).performClick()
openFilterAndSelectOption(R.string.nav_all)
// Verify that only the active task is shown
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
composeTestRule.onNodeWithText("TITLE2").assertDoesNotExist()
}
@Test
fun noTasks_AllTasksFilter_AddTaskViewVisible() {
setContent()
openFilterAndSelectOption(R.string.nav_all)
// Verify the "You have no tasks!" text is shown
composeTestRule.onNodeWithText("You have no tasks!").assertIsDisplayed()
}
@Test
fun noTasks_CompletedTasksFilter_AddTaskViewNotVisible() {
setContent()
openFilterAndSelectOption(R.string.nav_completed)
// Verify the "You have no completed tasks!" text is shown
composeTestRule.onNodeWithText("You have no completed tasks!").assertIsDisplayed()
}
@Test
fun noTasks_ActiveTasksFilter_AddTaskViewNotVisible() {
setContent()
openFilterAndSelectOption(R.string.nav_active)
// Verify the "You have no active tasks!" text is shown
composeTestRule.onNodeWithText("You have no active tasks!").assertIsDisplayed()
}
private fun setContent() {
composeTestRule.setContent {
TodoTheme {
Surface {
TasksScreen(
viewModel = TasksViewModel(repository, SavedStateHandle()),
userMessage = R.string.successfully_added_task_message,
onUserMessageDisplayed = { },
onAddTask = { },
onTaskClick = { },
openDrawer = { }
)
}
}
}
}
private fun openFilterAndSelectOption(@StringRes option: Int) {
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_filter))
.performClick()
composeTestRule.onNodeWithText(activity.getString(option)).assertIsDisplayed()
composeTestRule.onNodeWithText(activity.getString(option)).performClick()
}
}
================================================
FILE: app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt
================================================
/*
* Copyright 2022 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.tasks
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isToggleable
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTextReplacement
import androidx.test.ext.junit.runners.AndroidJUnit4
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.TodoTheme
import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
/**
* Large End-to-End test for the tasks module.
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
@HiltAndroidTest
@OptIn(ExperimentalCoroutinesApi::class)
class TasksTest {
@get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
// Executes tasks in the Architecture Components in the same thread
@get:Rule(order = 1)
var instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
private val activity get() = composeTestRule.activity
@Inject
lateinit var repository: TaskRepository
@Before
fun init() {
hiltRule.inject()
}
@Test
fun editTask() = runTest {
val originalTaskTitle = "TITLE1"
repository.createTask(originalTaskTitle, "DESCRIPTION")
setContent()
// Click on the task on the list and verify that all the data is correct
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText(originalTaskTitle).assertIsDisplayed()
composeTestRule.onNodeWithText(originalTaskTitle).performClick()
// Task detail screen
composeTestRule.onNodeWithText(activity.getString(R.string.task_details))
.assertIsDisplayed()
composeTestRule.onNodeWithText(originalTaskTitle).assertIsDisplayed()
composeTestRule.onNodeWithText("DESCRIPTION").assertIsDisplayed()
composeTestRule.onNode(isToggleable()).assertIsOff()
// Click on the edit button, edit, and save
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))
.performClick()
composeTestRule.onNodeWithText(activity.getString(R.string.edit_task)).assertIsDisplayed()
findTextField(originalTaskTitle).performTextReplacement("NEW TITLE")
findTextField("DESCRIPTION").performTextReplacement("NEW DESCRIPTION")
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
.performClick()
// Verify task is displayed on screen in the task list.
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText("NEW TITLE").assertIsDisplayed()
// Verify previous task is not displayed
composeTestRule.onNodeWithText(originalTaskTitle).assertDoesNotExist()
}
@Test
fun createOneTask_deleteTask() {
setContent()
val taskTitle = "TITLE1"
// Add active task
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.add_task))
.performClick()
findTextField(R.string.title_hint).performTextInput(taskTitle)
findTextField(R.string.description_hint).performTextInput("DESCRIPTION")
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
.performClick()
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
// Open the task detail screen
composeTestRule.onNodeWithText(taskTitle).performClick()
composeTestRule.onNodeWithText(activity.getString(R.string.task_details))
.assertIsDisplayed()
// Click delete task in menu
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_delete_task))
.performClick()
// Verify it was deleted
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_filter))
.performClick()
composeTestRule.onNodeWithText(activity.getString(R.string.nav_all)).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).assertDoesNotExist()
}
@Test
fun createTwoTasks_deleteOneTask() = runTest {
repository.apply {
createTask("TITLE1", "DESCRIPTION")
createTask("TITLE2", "DESCRIPTION")
}
setContent()
// Open the second task in details view
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText("TITLE2").assertIsDisplayed()
composeTestRule.onNodeWithText("TITLE2").performClick()
// Click delete task in menu
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_delete_task))
.performClick()
// Verify only one task was deleted
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_filter))
.performClick()
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).performClick()
composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
composeTestRule.onNodeWithText("TITLE2").assertDoesNotExist()
}
@Test
fun markTaskAsCompleteOnDetailScreen_taskIsCompleteInList() = runTest {
// Add 1 active task
val taskTitle = "COMPLETED"
repository.createTask(taskTitle, "DESCRIPTION")
setContent()
// Click on the task on the list
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).performClick()
// Click on the checkbox in task details screen
composeTestRule.onNode(isToggleable()).performClick()
// Click on the navigation up button to go back to the list
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
.performClick()
// Check that the task is marked as completed
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNode(isToggleable()).assertIsOn()
}
@Test
fun markTaskAsActiveOnDetailScreen_taskIsActiveInList() = runTest {
// Add 1 completed task
val taskTitle = "ACTIVE"
repository.apply {
createTask(taskTitle, "DESCRIPTION").also { completeTask(it) }
}
setContent()
// Click on the task on the list
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).performClick()
// Click on the checkbox in task details screen
composeTestRule.onNode(isToggleable()).performClick()
// Click on the navigation up button to go back to the list
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
.performClick()
// Check that the task is marked as active
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNode(isToggleable()).assertIsOff()
}
@Test
fun markTaskAsCompleteAndActiveOnDetailScreen_taskIsActiveInList() = runTest {
// Add 1 active task
val taskTitle = "ACT-COMP"
repository.createTask(taskTitle, "DESCRIPTION")
setContent()
// Click on the task on the list
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).performClick()
// Click on the checkbox in task details screen
composeTestRule.onNode(isToggleable()).performClick()
// Click again to restore it to original state
composeTestRule.onNode(isToggleable()).performClick()
// Click on the navigation up button to go back to the list
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
.performClick()
// Check that the task is marked as active
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNode(isToggleable()).assertIsOff()
}
@Test
fun markTaskAsActiveAndCompleteOnDetailScreen_taskIsCompleteInList() = runTest {
// Add 1 completed task
val taskTitle = "COMP-ACT"
repository.apply {
createTask(taskTitle, "DESCRIPTION").also { completeTask(it) }
}
setContent()
// Click on the task on the list
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
composeTestRule.onNodeWithText(taskTitle).performClick()
// Click on the checkbox in task details screen
composeTestRule.onNode(isToggleable()).performClick()
// Click again to restore it to original state
composeTestRule.onNode(isToggleable()).performClick()
// Click on the navigation up button to go back to the list
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
.performClick()
// Check that the task is marked as active
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNode(isToggleable()).assertIsOn()
}
@Test
fun createTask() {
setContent()
// Click on the "+" button, add details, and save
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.add_task))
.performClick()
findTextField(R.string.title_hint).performTextInput("title")
findTextField(R.string.description_hint).performTextInput("description")
composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
.performClick()
// Then verify task is displayed on screen
composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
composeTestRule.onNodeWithText("title").assertIsDisplayed()
}
private fun setContent() {
composeTestRule.setContent {
TodoTheme {
TodoNavGraph()
}
}
}
private fun findTextField(textId: Int): SemanticsNodeInteraction {
return composeTestRule.onNode(
hasSetTextAction() and hasText(activity.getString(textId))
)
}
private fun findTextField(text: String): SemanticsNodeInteraction {
return composeTestRule.onNode(
hasSetTextAction() and hasText(text)
)
}
}
================================================
FILE: app/src/debug/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 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
http://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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.architecture.blueprints.todoapp">
<application>
<activity
android:name=".HiltTestActivity"
android:exported="false" />
</application>
</manifest>
================================================
FILE: app/src/debug/java/com/example/android/architecture/blueprints/todoapp/HiltTestActivity.kt
================================================
/*
* Copyright 2022 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 androidx.activity.ComponentActivity
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class HiltTestActivity : ComponentActivity()
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
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
http://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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application
android:allowBackup="false"
android:name=".TodoApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="com.example.android.architecture.blueprints.todoapp.TodoActivity"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoActivity.kt
================================================
/*
* 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 android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint
/**
* Main activity for the todoapp
*/
@AndroidEntryPoint
class TodoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TodoTheme {
TodoNavGraph()
}
}
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt
================================================
/*
* 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 android.app.Application
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import timber.log.Timber.DebugTree
/**
* Application that sets up Timber in the DEBUG BuildConfig.
* Read Timber's documentation for production setups.
*/
@HiltAndroidApp
class TodoApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavGraph.kt
================================================
/*
* Copyright 2022 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 android.app.Activity
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TASK_ID_ARG
import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TITLE_ARG
import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.USER_MESSAGE_ARG
import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskScreen
import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsScreen
import com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailScreen
import com.example.android.architecture.blueprints.todoapp.tasks.TasksScreen
import com.example.android.architecture.blueprints.todoapp.util.AppModalDrawer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
fun TodoNavGraph(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
coroutineScope: CoroutineScope = rememberCoroutineScope(),
drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed),
startDestination: String = TodoDestinations.TASKS_ROUTE,
navActions: TodoNavigationActions = remember(navController) {
TodoNavigationActions(navController)
}
) {
val currentNavBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentNavBackStackEntry?.destination?.route ?: startDestination
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier
) {
composable(
TodoDestinations.TASKS_ROUTE,
arguments = listOf(
navArgument(USER_MESSAGE_ARG) { type = NavType.IntType; defaultValue = 0 }
)
) { entry ->
AppModalDrawer(drawerState, currentRoute, navActions) {
TasksScreen(
userMessage = entry.arguments?.getInt(USER_MESSAGE_ARG)!!,
onUserMessageDisplayed = { entry.arguments?.putInt(USER_MESSAGE_ARG, 0) },
onAddTask = { navActions.navigateToAddEditTask(R.string.add_task, null) },
onTaskClick = { task -> navActions.navigateToTaskDetail(task.id) },
openDrawer = { coroutineScope.launch { drawerState.open() } }
)
}
}
composable(TodoDestinations.STATISTICS_ROUTE) {
AppModalDrawer(drawerState, currentRoute, navActions) {
StatisticsScreen(openDrawer = { coroutineScope.launch { drawerState.open() } })
}
}
composable(
TodoDestinations.ADD_EDIT_TASK_ROUTE,
arguments = listOf(
navArgument(TITLE_ARG) { type = NavType.IntType },
navArgument(TASK_ID_ARG) { type = NavType.StringType; nullable = true },
)
) { entry ->
val taskId = entry.arguments?.getString(TASK_ID_ARG)
AddEditTaskScreen(
topBarTitle = entry.arguments?.getInt(TITLE_ARG)!!,
onTaskUpdate = {
navActions.navigateToTasks(
if (taskId == null) ADD_EDIT_RESULT_OK else EDIT_RESULT_OK
)
},
onBack = { navController.popBackStack() }
)
}
composable(TodoDestinations.TASK_DETAIL_ROUTE) {
TaskDetailScreen(
onEditTask = { taskId ->
navActions.navigateToAddEditTask(R.string.edit_task, taskId)
},
onBack = { navController.popBackStack() },
onDeleteTask = { navActions.navigateToTasks(DELETE_RESULT_OK) }
)
}
}
}
// Keys for navigation
const val ADD_EDIT_RESULT_OK = Activity.RESULT_FIRST_USER + 1
const val DELETE_RESULT_OK = Activity.RESULT_FIRST_USER + 2
const val EDIT_RESULT_OK = Activity.RESULT_FIRST_USER + 3
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavigation.kt
================================================
/*
* Copyright 2022 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 androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TASK_ID_ARG
import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TITLE_ARG
import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.USER_MESSAGE_ARG
import com.example.android.architecture.blueprints.todoapp.TodoScreens.ADD_EDIT_TASK_SCREEN
import com.example.android.architecture.blueprints.todoapp.TodoScreens.STATISTICS_SCREEN
import com.example.android.architecture.blueprints.todoapp.TodoScreens.TASKS_SCREEN
import com.example.android.architecture.blueprints.todoapp.TodoScreens.TASK_DETAIL_SCREEN
/**
* Screens used in [TodoDestinations]
*/
private object TodoScreens {
const val TASKS_SCREEN = "tasks"
const val STATISTICS_SCREEN = "statistics"
const val TASK_DETAIL_SCREEN = "task"
const val ADD_EDIT_TASK_SCREEN = "addEditTask"
}
/**
* Arguments used in [TodoDestinations] routes
*/
object TodoDestinationsArgs {
const val USER_MESSAGE_ARG = "userMessage"
const val TASK_ID_ARG = "taskId"
const val TITLE_ARG = "title"
}
/**
* Destinations used in the [TodoActivity]
*/
object TodoDestinations {
const val TASKS_ROUTE = "$TASKS_SCREEN?$USER_MESSAGE_ARG={$USER_MESSAGE_ARG}"
const val STATISTICS_ROUTE = STATISTICS_SCREEN
const val TASK_DETAIL_ROUTE = "$TASK_DETAIL_SCREEN/{$TASK_ID_ARG}"
const val ADD_EDIT_TASK_ROUTE = "$ADD_EDIT_TASK_SCREEN/{$TITLE_ARG}?$TASK_ID_ARG={$TASK_ID_ARG}"
}
/**
* Models the navigation actions in the app.
*/
class TodoNavigationActions(private val navController: NavHostController) {
fun navigateToTasks(userMessage: Int = 0) {
val navigatesFromDrawer = userMessage == 0
navController.navigate(
TASKS_SCREEN.let {
if (userMessage != 0) "$it?$USER_MESSAGE_ARG=$userMessage" else it
}
) {
popUpTo(navController.graph.findStartDestination().id) {
inclusive = !navigatesFromDrawer
saveState = navigatesFromDrawer
}
launchSingleTop = true
restoreState = navigatesFromDrawer
}
}
fun navigateToStatistics() {
navController.navigate(TodoDestinations.STATISTICS_ROUTE) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
fun navigateToTaskDetail(taskId: String) {
navController.navigate("$TASK_DETAIL_SCREEN/$taskId")
}
fun navigateToAddEditTask(title: Int, taskId: String?) {
navController.navigate(
"$ADD_EDIT_TASK_SCREEN/$title".let {
if (taskId != null) "$it?$TASK_ID_ARG=$taskId" else it
}
)
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoTheme.kt
================================================
package com.example.android.architecture.blueprints.todoapp
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
@Composable
fun TodoTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = lightColorScheme(
primary = Color(0xFF263238),
secondary = Color(0xFF2E7D32),
tertiary = Color(0xFFCCCCCC),
)
) {
content()
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt
================================================
/*
* Copyright 2022 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.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package com.example.android.architecture.blueprints.todoapp.addedittask
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.dimensionResource
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.collectAsStateWithLifecycle
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.util.AddEditTaskTopAppBar
@Composable
fun AddEditTaskScreen(
@StringRes topBarTitle: Int,
onTaskUpdate: () -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
viewModel: AddEditTaskViewModel = hiltViewModel(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
Scaffold(
modifier = modifier.fillMaxSize(),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = { AddEditTaskTopAppBar(topBarTitle, onBack) },
floatingActionButton = {
SmallFloatingActionButton(onClick = viewModel::saveTask) {
Icon(Icons.Filled.Done, stringResource(id = R.string.cd_save_task))
}
}
) { paddingValues ->
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
AddEditTaskContent(
loading = uiState.isLoading,
title = uiState.title,
description = uiState.description,
onTitleChanged = viewModel::updateTitle,
onDescriptionChanged = viewModel::updateDescription,
modifier = Modifier.padding(paddingValues)
)
// Check if the task is saved and call onTaskUpdate event
LaunchedEffect(uiState.isTaskSaved) {
if (uiState.isTaskSaved) {
onTaskUpdate()
}
}
// Check for user messages to display on the screen
uiState.userMessage?.let { userMessage ->
val snackbarText = stringResource(userMessage)
LaunchedEffect(snackbarHostState, viewModel, userMessage, snackbarText) {
snackbarHostState.showSnackbar(snackbarText)
viewModel.snackbarMessageShown()
}
}
}
}
@Composable
private fun AddEditTaskContent(
loading: Boolean,
title: String,
description: String,
onTitleChanged: (String) -> Unit,
onDescriptionChanged: (String) -> Unit,
modifier: Modifier = Modifier
) {
var isRefreshing by remember { mutableStateOf(false) }
val refreshingState = rememberPullToRefreshState()
if (loading) {
PullToRefreshBox(
isRefreshing = isRefreshing,
state = refreshingState,
onRefresh = { /* DO NOTHING */ },
content = { }
)
} else {
Column(
modifier
.fillMaxWidth()
.padding(all = dimensionResource(id = R.dimen.horizontal_margin))
.verticalScroll(rememberScrollState())
) {
val textFieldColors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onSecondary
)
OutlinedTextField(
value = title,
modifier = Modifier.fillMaxWidth(),
onValueChange = onTitleChanged,
placeholder = {
Text(
text = stringResource(id = R.string.title_hint),
style = MaterialTheme.typography.headlineSmall
)
},
textStyle = MaterialTheme.typography.headlineSmall
.copy(fontWeight = FontWeight.Bold),
maxLines = 1,
colors = textFieldColors
)
OutlinedTextField(
value = description,
onValueChange = onDescriptionChanged,
placeholder = { Text(stringResource(id = R.string.description_hint)) },
modifier = Modifier
.height(350.dp)
.fillMaxWidth(),
colors = textFieldColors
)
}
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt
================================================
/*
* 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.addedittask
import androidx.lifecycle.SavedStateHandle
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.TaskRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* UiState for the Add/Edit screen
*/
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
val isTaskCompleted: Boolean = false,
val isLoading: Boolean = false,
val userMessage: Int? = null,
val isTaskSaved: Boolean = false
)
/**
* ViewModel for the Add/Edit screen.
*/
@HiltViewModel
class AddEditTaskViewModel @Inject constructor(
private val taskRepository: TaskRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val taskId: String? = savedStateHandle[TodoDestinationsArgs.TASK_ID_ARG]
// A MutableStateFlow needs to be created in this ViewModel. The source of truth of the current
// editable Task is the ViewModel, we need to mutate the UI state directly in methods such as
// `updateTitle` or `updateDescription`
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
init {
if (taskId != null) {
loadTask(taskId)
}
}
// Called when clicking on fab.
fun saveTask() {
if (uiState.value.title.isEmpty() || uiState.value.description.isEmpty()) {
_uiState.update {
it.copy(userMessage = R.string.empty_task_message)
}
return
}
if (taskId == null) {
createNewTask()
} else {
updateTask()
}
}
fun snackbarMessageShown() {
_uiState.update {
it.copy(userMessage = null)
}
}
fun updateTitle(newTitle: String) {
_uiState.update {
it.copy(title = newTitle)
}
}
fun updateDescription(newDescription: String) {
_uiState.update {
it.copy(description = newDescription)
}
}
private fun createNewTask() = viewModelScope.launch {
taskRepository.createTask(uiState.value.title, uiState.value.description)
_uiState.update {
it.copy(isTaskSaved = true)
}
}
private fun updateTask() {
if (taskId == null) {
throw RuntimeException("updateTask() was called but task is new.")
}
viewModelScope.launch {
taskRepository.updateTask(
taskId,
title = uiState.value.title,
description = uiState.value.description,
)
_uiState.update {
it.copy(isTaskSaved = true)
}
}
}
private fun loadTask(taskId: String) {
_uiState.update {
it.copy(isLoading = true)
}
viewModelScope.launch {
taskRepository.getTask(taskId).let { task ->
if (task != null) {
_uiState.update {
it.copy(
title = task.title,
description = task.description,
isTaskCompleted = task.isCompleted,
isLoading = false
)
}
} else {
_uiState.update {
it.copy(isLoading = false)
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt
================================================
/*
* 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
import com.example.android.architecture.blueprints.todoapp.data.source.local.TaskDao
import com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkDataSource
import com.example.android.architecture.blueprints.todoapp.di.ApplicationScope
import com.example.android.architecture.blueprints.todoapp.di.DefaultDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* Default implementation of [TaskRepository]. Single entry point for managing tasks' data.
*
* @param networkDataSource - The network data source
* @param localDataSource - The local data source
* @param dispatcher - The dispatcher to be used for long running or complex operations, such as ID
* generation or mapping many models.
* @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,
) : TaskRepository {
override suspend fun createTask(title: String, description: String): String {
// ID creation might be a complex operation so it's executed using the supplied
// coroutine dispatcher
val taskId = withContext(dispatcher) {
UUID.randomUUID().toString()
}
val task = Task(
title = title,
description = description,
id = taskId,
)
localDataSource.upsert(task.toLocal())
saveTasksToNetwork()
return taskId
}
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")
localDataSource.upsert(task.toLocal())
saveTasksToNetwork()
}
override suspend fun getTasks(forceUpdate: Boolean): List<Task> {
if (forceUpdate) {
refresh()
}
return withContext(dispatcher) {
localDataSource.getAll().toExternal()
}
}
override fun getTasksStream(): Flow<List<Task>> {
return localDataSource.observeAll().map { tasks ->
withContext(dispatcher) {
tasks.toExternal()
}
}
}
override suspend fun refreshTask(taskId: String) {
refresh()
}
override fun getTaskStream(taskId: String): Flow<Task?> {
return localDataSource.observeById(taskId).map { it.toExternal() }
}
/**
* Get a Task with the given ID. Will return null if the task cannot be found.
*
* @param taskId - The ID of the task
* @param forceUpdate - true if the task should be updated from the network data source first.
*/
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Task? {
if (forceUpdate) {
refresh()
}
return localDataSource.getById(taskId)?.toExternal()
}
override suspend fun completeTask(taskId: String) {
localDataSource.updateCompleted(taskId = taskId, completed = true)
saveTasksToNetwork()
}
override suspend fun activateTask(taskId: String) {
localDataSource.updateCompleted(taskId = taskId, completed = false)
saveTasksToNetwork()
}
override suspend fun clearCompletedTasks() {
localDataSource.deleteCompleted()
saveTasksToNetwork()
}
override suspend fun deleteAllTasks() {
localDataSource.deleteAll()
saveTasksToNetwork()
}
override suspend fun deleteTask(taskId: String) {
localDataSource.deleteById(taskId)
saveTasksToNetwork()
}
/**
* The following methods load tasks from (refresh), and save tasks to, the network.
*
* Real apps may want to do a proper sync, rather than the "one-way sync everything" approach
* below. See https://developer.android.com/topic/architecture/data-layer/offline-first
* for more efficient and robust synchronisation strategies.
*
* Note that the refresh operation is a suspend function (forces callers to wait) and the save
* operation is not. It returns immediately so callers don't have to wait.
*/
/**
* Delete everything in the local data source and replace it with everything from the network
* data source.
*
* `withContext` is used here in case the bulk `toLocal` mapping operation is complex.
*/
override suspend fun refresh() {
withContext(dispatcher) {
val remoteTasks = networkDataSource.loadTasks()
localDataSource.deleteAll()
localDataSource.upsertAll(remoteTasks.toLocal())
}
}
/**
* Send the tasks from the local data source to the network data source
*
* Returns immediately after launching the job. Real apps may want to suspend here until the
* operation is complete or (better) use WorkManager to schedule this work. Both approaches
* should provide a mechanism for failures to be communicated back to the user so that
* they are aware that their data isn't being backed up.
*/
private fun saveTasksToNetwork() {
scope.launch {
try {
val localTasks = localDataSource.getAll()
val networkTasks = withContext(dispatcher) {
localTasks.toNetwork()
}
networkDataSource.saveTasks(networkTasks)
} catch (e: Exception) {
// In a real app you'd handle the exception e.g. by exposing a `networkStatus` flow
// to an app level UI state holder which could then display a Toast message.
}
}
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/ModelMappingExt.kt
================================================
/*
* 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.network.NetworkTask
import com.example.android.architecture.blueprints.todoapp.data.source.network.TaskStatus
/**
* Data model mapping extension functions. There are three model types:
*
* - Task: External model exposed to other layers in the architecture.
* Obtained using `toExternal`.
*
* - NetworkTask: Internal model used to represent a task from the network. Obtained using
* `toNetwork`.
*
* - LocalTask: Internal model used to represent a task stored locally in a database. Obtained
* using `toLocal`.
*
*/
// External to local
fun Task.toLocal() = LocalTask(
id = id,
title = title,
description = description,
isCompleted = isCompleted,
)
fun List<Task>.toLocal() = map(Task::toLocal)
// Local to External
fun LocalTask.toExternal() = 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("localToExternal")
fun List<LocalTask>.toExternal() = map(LocalTask::toExternal)
// Network to Local
fun NetworkTask.toLocal() = LocalTask(
id = id,
title = title,
description = shortDescription,
isCompleted = (status == TaskStatus.COMPLETE),
)
@JvmName("networkToLocal")
fun List<NetworkTask>.toLocal() = map(NetworkTask::toLocal)
// Local to Network
fun LocalTask.toNetwork() = NetworkTask(
id = id,
title = title,
shortDescription = description,
status = if (isCompleted) { TaskStatus.COMPLETE } else { TaskStatus.ACTIVE }
)
fun List<LocalTask>.toNetwork() = map(LocalTask::toNetwork)
// External to Network
fun Task.toNetwork() = toLocal().toNetwork()
@JvmName("externalToNetwork")
fun List<Task>.toNetwork() = map(Task::toNetwork)
// Network to External
fun NetworkTask.toExternal() = toLocal().toExternal()
@JvmName("networkToExternal")
fun List<NetworkTask>.toExternal() = map(NetworkTask::toExternal)
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt
================================================
/*
* 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
/**
* 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
*
* TODO: The constructor of this class should be `internal` but it is used in previews and tests
* so that's not possible until those previews/tests are refactored.
*/
data class Task(
val title: String = "",
val description: String = "",
val isCompleted: Boolean = false,
val id: String,
) {
val titleForList: String
get() = if (title.isNotEmpty()) title else description
val isActive
get() = !isCompleted
val isEmpty
get() = title.isEmpty() || description.isEmpty()
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/TaskRepository.kt
================================================
/*
* 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
import kotlinx.coroutines.flow.Flow
/**
* Interface to the data layer.
*/
interface TaskRepository {
fun getTasksStream(): Flow<List<Task>>
suspend fun getTasks(forceUpdate: Boolean = false): List<Task>
suspend fun refresh()
fun getTaskStream(taskId: String): Flow<Task?>
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Task?
suspend fun refreshTask(taskId: String)
suspend fun createTask(title: String, description: String): String
suspend fun updateTask(taskId: String, title: String, description: String)
suspend fun completeTask(taskId: String)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/LocalTask.kt
================================================
/*
* 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.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 = "task"
)
data class LocalTask(
@PrimaryKey val id: String,
var title: String,
var description: String,
var isCompleted: Boolean,
)
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDao.kt
================================================
/*
* 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.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
/**
* Data Access Object for the task table.
*/
@Dao
interface TaskDao {
/**
* Observes list of tasks.
*
* @return all tasks.
*/
@Query("SELECT * FROM task")
fun observeAll(): Flow<List<LocalTask>>
/**
* Observes a single task.
*
* @param taskId the task id.
* @return the task with taskId.
*/
@Query("SELECT * FROM task WHERE id = :taskId")
fun observeById(taskId: String): Flow<LocalTask>
/**
* Select all tasks from the tasks table.
*
* @return all tasks.
*/
@Query("SELECT * FROM task")
suspend fun getAll(): List<LocalTask>
/**
* Select a task by id.
*
* @param taskId the task id.
* @return the task with taskId.
*/
@Query("SELECT * FROM task WHERE id = :taskId")
suspend fun getById(taskId: String): LocalTask?
/**
* Insert or update a task in the database. If a task already exists, replace it.
*
* @param task the task to be inserted or updated.
*/
@Upsert
suspend fun upsert(task: LocalTask)
/**
* Insert or update tasks in the database. If a task already exists, replace it.
*
* @param tasks the tasks to be inserted or updated.
*/
@Upsert
suspend fun upsertAll(tasks: List<LocalTask>)
/**
* Update the complete status of a task
*
* @param taskId id of the task
* @param completed status to be updated
*/
@Query("UPDATE task SET isCompleted = :completed WHERE id = :taskId")
suspend fun updateCompleted(taskId: String, completed: Boolean)
/**
* Delete a task by id.
*
* @return the number of tasks deleted. This should always be 1.
*/
@Query("DELETE FROM task WHERE id = :taskId")
suspend fun deleteById(taskId: String): Int
/**
* Delete all tasks.
*/
@Query("DELETE FROM task")
suspend fun deleteAll()
/**
* Delete all completed tasks from the table.
*
* @return the number of tasks deleted.
*/
@Query("DELETE FROM task WHERE isCompleted = 1")
suspend fun deleteCompleted(): Int
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt
================================================
/*
* 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.room.Database
import androidx.room.RoomDatabase
/**
* The Room Database that contains the Task table.
*
* Note that exportSchema should be true in production databases.
*/
@Database(entities = [LocalTask::class], version = 1, exportSchema = false)
abstract class ToDoDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkDataSource.kt
================================================
/*
* 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.network
/**
* Main entry point for accessing tasks data from the network.
*
*/
interface NetworkDataSource {
suspend fun loadTasks(): List<NetworkTask>
suspend fun saveTasks(tasks: List<NetworkTask>)
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkTask.kt
================================================
/*
* 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.network
/**
* 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
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/TaskNetworkDataSource.kt
================================================
/*
* 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.network
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
class TaskNetworkDataSource @Inject constructor() : NetworkDataSource {
// A mutex is used to ensure that reads and writes are thread-safe.
private val accessMutex = Mutex()
private var tasks = listOf(
NetworkTask(
id = "PISA",
title = "Build tower in Pisa",
shortDescription = "Ground looks good, no foundation work required."
),
NetworkTask(
id = "TACOMA",
title = "Finish bridge in Tacoma",
shortDescription = "Found awesome girders at half the cost!"
)
)
override suspend fun loadTasks(): List<NetworkTask> = accessMutex.withLock {
delay(SERVICE_LATENCY_IN_MILLIS)
return tasks
}
override suspend fun saveTasks(newTasks: List<NetworkTask>) = accessMutex.withLock {
delay(SERVICE_LATENCY_IN_MILLIS)
tasks = newTasks
}
}
private const val SERVICE_LATENCY_IN_MILLIS = 2000L
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt
================================================
/*
* Copyright 2022 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.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import javax.inject.Qualifier
import javax.inject.Singleton
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class IoDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DefaultDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope
@Module
@InstallIn(SingletonComponent::class)
object CoroutinesModule {
@Provides
@IoDispatcher
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
@DefaultDispatcher
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@Provides
@Singleton
@ApplicationScope
fun providesCoroutineScope(
@DefaultDispatcher dispatcher: CoroutineDispatcher
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt
================================================
/*
* Copyright 2022 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.di
import android.content.Context
import androidx.room.Room
import com.example.android.architecture.blueprints.todoapp.data.DefaultTaskRepository
import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
import com.example.android.architecture.blueprints.todoapp.data.source.local.TaskDao
import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase
import com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkDataSource
import com.example.android.architecture.blueprints.todoapp.data.source.network.TaskNetworkDataSource
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Singleton
@Binds
abstract fun bindTaskRepository(repository: DefaultTaskRepository): TaskRepository
}
@Module
@InstallIn(SingletonComponent::class)
abstract class DataSourceModule {
@Singleton
@Binds
abstract fun bindNetworkDataSource(dataSource: TaskNetworkDataSource): NetworkDataSource
}
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Singleton
@Provides
fun provideDataBase(@ApplicationContext context: Context): ToDoDatabase {
return Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java,
"Tasks.db"
).build()
}
@Provides
fun provideTaskDao(database: ToDoDatabase): TaskDao = database.taskDao()
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt
================================================
/*
* Copyright 2022 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.statistics
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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.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
@Composable
fun StatisticsScreen(
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: StatisticsViewModel = hiltViewModel(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
Scaffold(
modifier = modifier.fillMaxSize(),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = { StatisticsTopAppBar(openDrawer) },
) { paddingValues ->
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
StatisticsContent(
loading = uiState.isLoading,
empty = uiState.isEmpty,
activeTasksPercent = uiState.activeTasksPercent,
completedTasksPercent = uiState.completedTasksPercent,
onRefresh = { viewModel.refresh() },
modifier = modifier.padding(paddingValues)
)
}
}
@Composable
private fun StatisticsContent(
loading: Boolean,
empty: Boolean,
activeTasksPercent: Float,
completedTasksPercent: Float,
onRefresh: () -> Unit,
modifier: Modifier = Modifier
) {
val commonModifier = modifier
.fillMaxSize()
.padding(all = dimensionResource(id = R.dimen.horizontal_margin))
LoadingContent(
loading = loading,
empty = empty,
onRefresh = onRefresh,
modifier = modifier,
emptyContent = {
Text(
text = stringResource(id = R.string.statistics_no_tasks),
modifier = commonModifier
)
}
) {
Column(
commonModifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
if (!loading) {
Text(stringResource(id = R.string.statistics_active_tasks, activeTasksPercent))
Text(
stringResource(
id = R.string.statistics_completed_tasks,
completedTasksPercent
)
)
}
}
}
}
@Preview
@Composable
fun StatisticsContentPreview() {
Surface {
StatisticsContent(
loading = false,
empty = false,
activeTasksPercent = 80f,
completedTasksPercent = 20f,
onRefresh = { }
)
}
}
@Preview
@Composable
fun StatisticsContentEmptyPreview() {
Surface {
StatisticsScreen({})
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt
================================================
/*
* 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.statistics
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<Task>): StatsResult {
return if (tasks.isEmpty()) {
StatsResult(0f, 0f)
} else {
val totalTasks = tasks.size
val numberOfActiveTasks = tasks.count { it.isActive }
StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
}
}
data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt
================================================
/*
* 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.statistics
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.TaskRepository
import com.example.android.architecture.blueprints.todoapp.util.Async
import com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* UiState for the statistics screen.
*/
data class StatisticsUiState(
val isEmpty: Boolean = false,
val isLoading: Boolean = false,
val activeTasksPercent: Float = 0f,
val completedTasksPercent: Float = 0f
)
/**
* ViewModel for the statistics screen.
*/
@HiltViewModel
class StatisticsViewModel @Inject constructor(
private val taskRepository: TaskRepository
) : ViewModel() {
val uiState: StateFlow<StatisticsUiState> =
taskRepository.getTasksStream()
.map { Async.Success(it) }
.catch<Async<List<Task>>> { emit(Async.Error(R.string.loading_tasks_error)) }
.map { taskAsync -> produceStatisticsUiState(taskAsync) }
.stateIn(
scope = viewModelScope,
started = WhileUiSubscribed,
initialValue = StatisticsUiState(isLoading = true)
)
fun refresh() {
viewModelScope.launch {
taskRepository.refresh()
}
}
private fun produceStatisticsUiState(taskLoad: Async<List<Task>>) =
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
)
}
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt
================================================
/*
* Copyright 2022 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.taskdetail
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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.collectAsStateWithLifecycle
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.util.LoadingContent
import com.example.android.architecture.blueprints.todoapp.util.TaskDetailTopAppBar
@Composable
fun TaskDetailScreen(
onEditTask: (String) -> Unit,
onBack: () -> Unit,
onDeleteTask: () -> Unit,
modifier: Modifier = Modifier,
viewModel: TaskDetailViewModel = hiltViewModel(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
Scaffold(
modifier = modifier.fillMaxSize(),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = { TaskDetailTopAppBar(onBack = onBack, onDelete = viewModel::deleteTask) },
floatingActionButton = {
SmallFloatingActionButton(onClick = { onEditTask(viewModel.taskId) }) {
Icon(Icons.Filled.Edit, stringResource(id = R.string.edit_task))
}
}
) { paddingValues ->
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
EditTaskContent(
loading = uiState.isLoading,
empty = uiState.task == null && !uiState.isLoading,
task = uiState.task,
onRefresh = viewModel::refresh,
onTaskCheck = viewModel::setCompleted,
modifier = Modifier.padding(paddingValues)
)
// Check for user messages to display on the screen
uiState.userMessage?.let { userMessage ->
val snackbarText = stringResource(userMessage)
LaunchedEffect(snackbarHostState, viewModel, userMessage, snackbarText) {
snackbarHostState.showSnackbar(snackbarText)
viewModel.snackbarMessageShown()
}
}
// Check if the task is deleted and call onDeleteTask
LaunchedEffect(uiState.isTaskDeleted) {
if (uiState.isTaskDeleted) {
onDeleteTask()
}
}
}
}
@Composable
private fun EditTaskContent(
loading: Boolean,
empty: Boolean,
task: Task?,
onTaskCheck: (Boolean) -> Unit,
onRefresh: () -> Unit,
modifier: Modifier = Modifier
) {
val screenPadding = Modifier.padding(
horizontal = dimensionResource(id = R.dimen.horizontal_margin),
vertical = dimensionResource(id = R.dimen.vertical_margin),
)
val commonModifier = modifier
.fillMaxWidth()
.then(screenPadding)
LoadingContent(
loading = loading,
empty = empty,
emptyContent = {
Text(
text = stringResource(id = R.string.no_data),
modifier = commonModifier
)
},
onRefresh = onRefresh
) {
Column(commonModifier.verticalScroll(rememberScrollState())) {
Row(
Modifier
.fillMaxWidth()
.then(screenPadding),
) {
if (task != null) {
Checkbox(task.isCompleted, onTaskCheck)
Column {
Text(text = task.title, style = MaterialTheme.typography.headlineSmall)
Text(text = task.description, style = MaterialTheme.typography.bodySmall)
}
}
}
}
}
}
@Preview
@Composable
private fun EditTaskContentPreview() {
Surface {
EditTaskContent(
loading = false,
empty = false,
Task(
title = "Title",
description = "Description",
isCompleted = false,
id = "ID"
),
onTaskCheck = { },
onRefresh = { }
)
}
}
@Preview
@Composable
private fun EditTaskContentTaskCompletedPreview() {
Surface {
EditTaskContent(
loading = false,
empty = false,
Task(
title = "Title",
description = "Description",
isCompleted = false,
id = "ID"
),
onTaskCheck = { },
onRefresh = { }
)
}
}
@Preview
@Composable
private fun EditTaskContentEmptyPreview() {
Surface {
EditTaskContent(
loading = false,
empty = true,
Task(
title = "Title",
description = "Description",
isCompleted = false,
id = "ID"
),
onTaskCheck = { },
onRefresh = { }
)
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt
================================================
/*
* 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.taskdetail
import androidx.lifecycle.SavedStateHandle
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.TaskRepository
import com.example.android.architecture.blueprints.todoapp.util.Async
import com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* UiState for the Details screen.
*/
data class TaskDetailUiState(
val task: Task? = null,
val isLoading: Boolean = false,
val userMessage: Int? = null,
val isTaskDeleted: Boolean = false
)
/**
* ViewModel for the Details screen.
*/
@HiltViewModel
class TaskDetailViewModel @Inject constructor(
private val taskRepository: TaskRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
val taskId: String = savedStateHandle[TodoDestinationsArgs.TASK_ID_ARG]!!
private val _userMessage: MutableStateFlow<Int?> = 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<TaskDetailUiState> = combine(
_userMessage, _isLoading, _isTaskDeleted, _taskAsync
) { userMessage, isLoading, isTaskDeleted, taskAsync ->
when (taskAsync) {
Async.Loading -> {
TaskDetailUiState(isLoading = true)
}
is Async.Error -> {
TaskDetailUiState(
userMessage = taskAsync.errorMessage,
isTaskDeleted = isTaskDeleted
)
}
is Async.Success -> {
TaskDetailUiState(
task = taskAsync.data,
isLoading = isLoading,
userMessage = userMessage,
isTaskDeleted = isTaskDeleted
)
}
}
}
.stateIn(
scope = viewModelScope,
started = WhileUiSubscribed,
initialValue = TaskDetailUiState(isLoading = true)
)
fun deleteTask() = viewModelScope.launch {
taskRepository.deleteTask(taskId)
_isTaskDeleted.value = true
}
fun setCompleted(completed: Boolean) = viewModelScope.launch {
val task = uiState.value.task ?: return@launch
if (completed) {
taskRepository.completeTask(task.id)
showSnackbarMessage(R.string.task_marked_complete)
} else {
taskRepository.activateTask(task.id)
showSnackbarMessage(R.string.task_marked_active)
}
}
fun refresh() {
_isLoading.value = true
viewModelScope.launch {
taskRepository.refreshTask(taskId)
_isLoading.value = false
}
}
fun snackbarMessageShown() {
_userMessage.value = null
}
private fun showSnackbarMessage(message: Int) {
_userMessage.value = message
}
private fun handleTask(task: Task?): Async<Task?> {
if (task == null) {
return Async.Error(R.string.task_not_found)
}
return Async.Success(task)
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFilterType.kt
================================================
/*
* 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.tasks
/**
* Used with the filter spinner in the tasks list.
*/
enum class TasksFilterType {
/**
* Do not filter tasks.
*/
ALL_TASKS,
/**
* Filters only the active (not completed yet) tasks.
*/
ACTIVE_TASKS,
/**
* Filters only the completed tasks.
*/
COMPLETED_TASKS
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt
================================================
/*
* Copyright 2022 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.tasks
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.collectAsStateWithLifecycle
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.TodoTheme
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ACTIVE_TASKS
import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ALL_TASKS
import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.COMPLETED_TASKS
import com.example.android.architecture.blueprints.todoapp.util.LoadingContent
import com.example.android.architecture.blueprints.todoapp.util.TasksTopAppBar
@Composable
fun TasksScreen(
@StringRes userMessage: Int,
onAddTask: () -> Unit,
onTaskClick: (Task) -> Unit,
onUserMessageDisplayed: () -> Unit,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: TasksViewModel = hiltViewModel(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
Scaffold(
modifier = modifier.fillMaxSize(),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
TasksTopAppBar(
openDrawer = openDrawer,
onFilterAllTasks = { viewModel.setFiltering(ALL_TASKS) },
onFilterActiveTasks = { viewModel.setFiltering(ACTIVE_TASKS) },
onFilterCompletedTasks = { viewModel.setFiltering(COMPLETED_TASKS) },
onClearCompletedTasks = { viewModel.clearCompletedTasks() },
onRefresh = { viewModel.refresh() }
)
},
floatingActionButton = {
SmallFloatingActionButton(onClick = onAddTask) {
Icon(Icons.Filled.Add, stringResource(id = R.string.add_task))
}
}
) { paddingValues ->
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
TasksContent(
loading = uiState.isLoading,
tasks = uiState.items,
currentFilteringLabel = uiState.filteringUiInfo.currentFilteringLabel,
noTasksLabel = uiState.filteringUiInfo.noTasksLabel,
noTasksIconRes = uiState.filteringUiInfo.noTaskIconRes,
onRefresh = viewModel::refresh,
onTaskClick = onTaskClick,
onTaskCheckedChange = viewModel::completeTask,
modifier = Modifier.padding(paddingValues)
)
// Check for user messages to display on the screen
uiState.userMessage?.let { message ->
val snackbarText = stringResource(message)
LaunchedEffect(snackbarHostState, viewModel, message, snackbarText) {
snackbarHostState.showSnackbar(snackbarText)
viewModel.snackbarMessageShown()
}
}
// Check if there's a userMessage to show to the user
val currentOnUserMessageDisplayed by rememberUpdatedState(onUserMessageDisplayed)
LaunchedEffect(userMessage) {
if (userMessage != 0) {
viewModel.showEditResultMessage(userMessage)
currentOnUserMessageDisplayed()
}
}
}
}
@Composable
private fun TasksContent(
loading: Boolean,
tasks: List<Task>,
@StringRes currentFilteringLabel: Int,
@StringRes noTasksLabel: Int,
@DrawableRes noTasksIconRes: Int,
onRefresh: () -> Unit,
onTaskClick: (Task) -> Unit,
onTaskCheckedChange: (Task, Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LoadingContent(
loading = loading,
empty = tasks.isEmpty() && !loading,
emptyContent = { TasksEmptyContent(noTasksLabel, noTasksIconRes, modifier) },
onRefresh = onRefresh
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = dimensionResource(id = R.dimen.horizontal_margin))
) {
Text(
text = stringResource(currentFilteringLabel),
modifier = Modifier.padding(
horizontal = dimensionResource(id = R.dimen.list_item_padding),
vertical = dimensionResource(id = R.dimen.vertical_margin)
),
style = MaterialTheme.typography.headlineSmall
)
LazyColumn {
items(tasks) { task ->
TaskItem(
task = task,
onTaskClick = onTaskClick,
onCheckedChange = { onTaskCheckedChange(task, it) }
)
}
}
}
}
}
@Composable
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),
)
.clickable { onTaskClick(task) }
) {
Checkbox(
checked = task.isCompleted,
onCheckedChange = onCheckedChange
)
Text(
text = task.titleForList,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(
start = dimensionResource(id = R.dimen.horizontal_margin)
),
textDecoration = if (task.isCompleted) {
TextDecoration.LineThrough
} else {
null
}
)
}
}
@Composable
private fun TasksEmptyContent(
@StringRes noTasksLabel: Int,
@DrawableRes noTasksIconRes: Int,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = noTasksIconRes),
contentDescription = stringResource(R.string.no_tasks_image_content_description),
modifier = Modifier.size(96.dp)
)
Text(stringResource(id = noTasksLabel))
}
}
@Preview
@Composable
private fun TasksContentPreview() {
MaterialTheme {
Surface {
TasksContent(
loading = false,
tasks = listOf(
Task(
title = "Title 1",
description = "Description 1",
isCompleted = false,
id = "ID 1"
),
Task(
title = "Title 2",
description = "Description 2",
isCompleted = true,
id = "ID 2"
),
Task(
title = "Title 3",
description = "Description 3",
isCompleted = true,
id = "ID 3"
),
Task(
title = "Title 4",
description = "Description 4",
isCompleted = false,
id = "ID 4"
),
Task(
title = "Title 5",
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 = { _, _ -> },
)
}
}
}
@Preview
@Composable
private fun TasksContentEmptyPreview() {
MaterialTheme {
Surface {
TasksContent(
loading = false,
tasks = emptyList(),
currentFilteringLabel = R.string.label_all,
noTasksLabel = R.string.no_tasks_all,
noTasksIconRes = R.drawable.logo_no_fill,
onRefresh = { },
onTaskClick = { },
onTaskCheckedChange = { _, _ -> },
)
}
}
}
@Preview
@Composable
private fun TasksEmptyContentPreview() {
TodoTheme {
Surface {
TasksEmptyContent(
noTasksLabel = R.string.no_tasks_all,
noTasksIconRes = R.drawable.logo_no_fill
)
}
}
}
@Preview
@Composable
private fun TaskItemPreview() {
MaterialTheme {
Surface {
TaskItem(
task = Task(
title = "Title",
description = "Description",
id = "ID"
),
onTaskClick = { },
onCheckedChange = { }
)
}
}
}
@Preview
@Composable
private fun TaskItemCompletedPreview() {
MaterialTheme {
Surface {
TaskItem(
task = Task(
title = "Title",
description = "Description",
isCompleted = true,
id = "ID"
),
onTaskClick = { },
onCheckedChange = { }
)
}
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt
================================================
/*
* 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.tasks
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.android.architecture.blueprints.todoapp.ADD_EDIT_RESULT_OK
import com.example.android.architecture.blueprints.todoapp.DELETE_RESULT_OK
import com.example.android.architecture.blueprints.todoapp.EDIT_RESULT_OK
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.TaskRepository
import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ACTIVE_TASKS
import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ALL_TASKS
import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.COMPLETED_TASKS
import com.example.android.architecture.blueprints.todoapp.util.Async
import com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* UiState for the task list screen.
*/
data class TasksUiState(
val items: List<Task> = emptyList(),
val isLoading: Boolean = false,
val filteringUiInfo: FilteringUiInfo = FilteringUiInfo(),
val userMessage: Int? = null
)
/**
* ViewModel for the task list screen.
*/
@HiltViewModel
class TasksViewModel @Inject constructor(
private val taskRepository: TaskRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _savedFilterType =
savedStateHandle.getStateFlow(TASKS_FILTER_SAVED_STATE_KEY, ALL_TASKS)
private val _filterUiInfo = _savedFilterType.map { getFilterUiInfo(it) }.distinctUntilChanged()
private val _userMessage: MutableStateFlow<Int?> = MutableStateFlow(null)
private val _isLoading = MutableStateFlow(false)
private val _filteredTasksAsync =
combine(taskRepository.getTasksStream(), _savedFilterType) { tasks, type ->
filterTasks(tasks, type)
}
.map { Async.Success(it) }
.catch<Async<List<Task>>> { emit(Async.Error(R.string.loading_tasks_error)) }
val uiState: StateFlow<TasksUiState> = combine(
_filterUiInfo, _isLoading, _userMessage, _filteredTasksAsync
) { filterUiInfo, isLoading, userMessage, tasksAsync ->
when (tasksAsync) {
Async.Loading -> {
TasksUiState(isLoading = true)
}
is Async.Error -> {
TasksUiState(userMessage = tasksAsync.errorMessage)
}
is Async.Success -> {
TasksUiState(
items = tasksAsync.data,
filteringUiInfo = filterUiInfo,
isLoading = isLoading,
userMessage = userMessage
)
}
}
}
.stateIn(
scope = viewModelScope,
started = WhileUiSubscribed,
initialValue = TasksUiState(isLoading = true)
)
fun setFiltering(requestType: TasksFilterType) {
savedStateHandle[TASKS_FILTER_SAVED_STATE_KEY] = requestType
}
fun clearCompletedTasks() {
viewModelScope.launch {
taskRepository.clearCompletedTasks()
showSnackbarMessage(R.string.completed_tasks_cleared)
refresh()
}
}
fun completeTask(task: Task, completed: Boolean) = viewModelScope.launch {
if (completed) {
taskRepository.completeTask(task.id)
showSnackbarMessage(R.string.task_marked_complete)
} else {
taskRepository.activateTask(task.id)
showSnackbarMessage(R.string.task_marked_active)
}
}
fun showEditResultMessage(result: Int) {
when (result) {
EDIT_RESULT_OK -> showSnackbarMessage(R.string.successfully_saved_task_message)
ADD_EDIT_RESULT_OK -> showSnackbarMessage(R.string.successfully_added_task_message)
DELETE_RESULT_OK -> showSnackbarMessage(R.string.successfully_deleted_task_message)
}
}
fun snackbarMessageShown() {
_userMessage.value = null
}
private fun showSnackbarMessage(message: Int) {
_userMessage.value = message
}
fun refresh() {
_isLoading.value = true
viewModelScope.launch {
taskRepository.refresh()
_isLoading.value = false
}
}
private fun filterTasks(tasks: List<Task>, filteringType: TasksFilterType): List<Task> {
val tasksToShow = ArrayList<Task>()
// We filter the tasks based on the requestType
for (task in tasks) {
when (filteringType) {
ALL_TASKS -> tasksToShow.add(task)
ACTIVE_TASKS -> if (task.isActive) {
tasksToShow.add(task)
}
COMPLETED_TASKS -> if (task.isCompleted) {
tasksToShow.add(task)
}
}
}
return tasksToShow
}
private fun getFilterUiInfo(requestType: TasksFilterType): FilteringUiInfo =
when (requestType) {
ALL_TASKS -> {
FilteringUiInfo(
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.drawable.ic_check_circle_96dp
)
}
COMPLETED_TASKS -> {
FilteringUiInfo(
R.string.label_completed, R.string.no_tasks_completed,
R.drawable.ic_verified_user_96dp
)
}
}
}
// Used to save the current filtering in SavedStateHandle.
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,
)
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/Async.kt
================================================
/*
* Copyright 2022 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
/**
* A generic class that holds a loading signal or the result of an async operation.
*/
sealed class Async<out T> {
object Loading : Async<Nothing>()
data class Error(val errorMessage: Int) : Async<Nothing>()
data class Success<out T>(val data: T) : Async<T>()
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt
================================================
/*
* Copyright 2022 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 androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
val primaryDarkColor: Color = Color(0xFF263238)
/**
* Display an initial empty state or swipe to refresh content.
*
* @param loading (state) when true, display a loading spinner over [content]
* @param empty (state) when true, display [emptyContent]
* @param emptyContent (slot) the content to display for the empty state
* @param onRefresh (event) event to request refresh
* @param modifier the modifier to apply to this layout.
* @param content (slot) the main content to show
*/
@Composable
fun LoadingContent(
loading: Boolean,
empty: Boolean,
emptyContent: @Composable () -> Unit,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
if (empty) {
emptyContent()
} else {
SwipeRefresh(
state = rememberSwipeRefreshState(loading),
onRefresh = onRefresh,
modifier = modifier,
content = content,
)
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt
================================================
/*
* Copyright 2022 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 kotlinx.coroutines.flow.SharingStarted
private const val StopTimeoutMillis: Long = 5000
/**
* A [SharingStarted] meant to be used with a [StateFlow] to expose data to the UI.
*
* When the UI stops observing, upstream flows stay active for some time to allow the system to
* come back from a short-lived configuration change (such as rotations). If the UI stops
* observing for longer, the cache is kept but the upstream flows are stopped. When the UI comes
* 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)
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/SimpleCountingIdlingResource.kt
================================================
/*
* 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 androidx.test.espresso.IdlingResource
import java.util.concurrent.atomic.AtomicInteger
/**
* An simple counter implementation of [IdlingResource] that determines idleness by
* maintaining an internal counter. When the counter is 0 - it is considered to be idle, when it is
* non-zero it is not idle. This is very similar to the way a [java.util.concurrent.Semaphore]
* behaves.
*
*
* This class can then be used to wrap up operations that while in progress should block tests from
* accessing the UI.
*/
class SimpleCountingIdlingResource(private val resourceName: String) : IdlingResource {
private val counter = AtomicInteger(0)
// written from main thread, read from any thread.
@Volatile
private var resourceCallback: IdlingResource.ResourceCallback? = null
override fun getName() = resourceName
override fun isIdleNow() = counter.get() == 0
override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) {
this.resourceCallback = resourceCallback
}
/**
* Increments the count of in-flight transactions to the resource being monitored.
*/
fun increment() {
counter.getAndIncrement()
}
/**
* Decrements the count of in-flight transactions to the resource being monitored.
* If this operation results in the counter falling below 0 - an exception is raised.
*
* @throws IllegalStateException if the counter is below 0.
*/
fun decrement() {
val counterVal = counter.decrementAndGet()
if (counterVal == 0) {
// we've gone from non-zero to zero. That means we're idle now! Tell espresso.
resourceCallback?.onTransitionToIdle()
} else if (counterVal < 0) {
throw IllegalStateException("Counter has been corrupted!")
}
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt
================================================
/*
* Copyright 2022 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 androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.DrawerState
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.TodoDestinations
import com.example.android.architecture.blueprints.todoapp.TodoNavigationActions
import com.example.android.architecture.blueprints.todoapp.TodoTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
fun AppModalDrawer(
drawerState: DrawerState,
currentRoute: String,
navigationActions: TodoNavigationActions,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
content: @Composable () -> Unit
) {
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
AppDrawer(
currentRoute = currentRoute,
navigateToTasks = { navigationActions.navigateToTasks() },
navigateToStatistics = { navigationActions.navigateToStatistics() },
closeDrawer = { coroutineScope.launch { drawerState.close() } }
)
}
) {
content()
}
}
@Composable
private fun AppDrawer(
currentRoute: String,
navigateToTasks: () -> Unit,
navigateToStatistics: () -> Unit,
closeDrawer: () -> Unit,
modifier: Modifier = Modifier
) {
Surface(color = MaterialTheme.colorScheme.background) {
Column(modifier = modifier.fillMaxSize()) {
DrawerHeader()
DrawerButton(
painter = painterResource(id = R.drawable.ic_list),
label = stringResource(id = R.string.list_title),
isSelected = currentRoute == TodoDestinations.TASKS_ROUTE,
action = {
navigateToTasks()
closeDrawer()
}
)
DrawerButton(
painter = painterResource(id = R.drawable.ic_statistics),
label = stringResource(id = R.string.statistics_title),
isSelected = currentRoute == TodoDestinations.STATISTICS_ROUTE,
action = {
navigateToStatistics()
closeDrawer()
}
)
}
}
}
@Composable
private fun DrawerHeader(
modifier: Modifier = Modifier
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = modifier
.fillMaxWidth()
.background(primaryDarkColor)
.height(dimensionResource(id = R.dimen.header_height))
.padding(dimensionResource(id = R.dimen.header_padding))
) {
Image(
painter = painterResource(id = R.drawable.logo_no_fill),
contentDescription =
stringResource(id = R.string.tasks_header_image_content_description),
modifier = Modifier.width(dimensionResource(id = R.dimen.header_image_width))
)
Text(
text = stringResource(id = R.string.navigation_view_header_title),
color = MaterialTheme.colorScheme.surface
)
}
}
@Composable
private fun DrawerButton(
painter: Painter,
label: String,
isSelected: Boolean,
action: () -> Unit,
modifier: Modifier = Modifier
) {
val tintColor = if (isSelected) {
MaterialTheme.colorScheme.secondary
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
}
TextButton(
onClick = action,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(id = R.dimen.horizontal_margin))
) {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painter,
contentDescription = null, // decorative
tint = tintColor
)
Spacer(Modifier.width(16.dp))
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = tintColor
)
}
}
}
@Preview("Drawer contents")
@Composable
fun PreviewAppDrawer() {
TodoTheme {
Surface {
AppDrawer(
currentRoute = TodoDestinations.TASKS_ROUTE,
navigateToTasks = {},
navigateToStatistics = {},
closeDrawer = {}
)
}
}
}
================================================
FILE: app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt
================================================
/*
* Copyright 2022 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.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package com.example.android.architecture.blueprints.todoapp.util
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.TodoTheme
@Composable
fun TasksTopAppBar(
openDrawer: () -> Unit,
onFilterAllTasks: () -> Unit,
onFilterActiveTasks: () -> Unit,
onFilterCompletedTasks: () -> Unit,
onClearCompletedTasks: () -> Unit,
onRefresh: () -> Unit
) {
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
navigationIcon = {
IconButton(onClick = openDrawer) {
Icon(Icons.Filled.Menu, stringResource(id = R.string.open_drawer))
}
},
actions = {
FilterTasksMenu(onFilterAllTasks, onFilterActiveTasks, onFilterCompletedTasks)
MoreTasksMenu(onClearCompletedTasks, onRefresh)
},
modifier = Modifier.fillMaxWidth()
)
}
@Composable
private fun FilterTasksMenu(
onFilterAllTasks: () -> Unit,
onFilterActiveTasks: () -> Unit,
onFilterCompletedTasks: () -> Unit
) {
TopAppBarDropdownMenu(
iconContent = {
Icon(
painterResource(id = R.drawable.ic_filter_list),
stringResource(id = R.string.menu_filter)
)
}
) { closeMenu ->
DropdownMenuItem(onClick = { onFilterAllTasks(); closeMenu() },
text = { Text(text = stringResource(id = R.string.nav_all)) }
)
DropdownMenuItem(onClick = { onFilterActiveTasks(); closeMenu() },
text = { Text(text = stringResource(id = R.string.nav_active)) }
)
DropdownMenuItem(onClick = { onFilterCompletedTasks(); closeMenu() },
text = { Text(text = stringResource(id = R.string.nav_completed)) }
)
}
}
@Composable
private fun MoreTasksMenu(
onClearCompletedTasks: () -> Unit,
onRefresh: () -> Unit
) {
TopAppBarDropdownMenu(
iconContent = {
Icon(Icons.Filled.MoreVert, stringResource(id = R.string.menu_more))
}
) { closeMenu ->
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.menu_clear)) },
onClick = { onClearCompletedTasks(); closeMenu() }
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.refresh)) },
o
gitextract_6d9xqtrj/
├── .github/
│ ├── ci-gradle.properties
│ └── workflows/
│ ├── build_test.yaml
│ └── copy-branch.yml
├── .gitignore
├── .google/
│ └── packaging.yaml
├── CODEOWNERS
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ ├── proguardTest-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── example/
│ │ └── android/
│ │ └── architecture/
│ │ └── blueprints/
│ │ └── todoapp/
│ │ ├── addedittask/
│ │ │ └── AddEditTaskScreenTest.kt
│ │ ├── data/
│ │ │ └── source/
│ │ │ └── local/
│ │ │ └── TaskDaoTest.kt
│ │ ├── statistics/
│ │ │ └── StatisticsScreenTest.kt
│ │ ├── taskdetail/
│ │ │ └── TaskDetailScreenTest.kt
│ │ └── tasks/
│ │ ├── AppNavigationTest.kt
│ │ ├── TasksScreenTest.kt
│ │ └── TasksTest.kt
│ ├── debug/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── example/
│ │ └── android/
│ │ └── architecture/
│ │ └── blueprints/
│ │ └── todoapp/
│ │ └── HiltTestActivity.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── android/
│ │ │ └── architecture/
│ │ │ └── blueprints/
│ │ │ └── todoapp/
│ │ │ ├── TodoActivity.kt
│ │ │ ├── TodoApplication.kt
│ │ │ ├── TodoNavGraph.kt
│ │ │ ├── TodoNavigation.kt
│ │ │ ├── TodoTheme.kt
│ │ │ ├── addedittask/
│ │ │ │ ├── AddEditTaskScreen.kt
│ │ │ │ └── AddEditTaskViewModel.kt
│ │ │ ├── data/
│ │ │ │ ├── DefaultTaskRepository.kt
│ │ │ │ ├── ModelMappingExt.kt
│ │ │ │ ├── Task.kt
│ │ │ │ ├── TaskRepository.kt
│ │ │ │ └── source/
│ │ │ │ ├── local/
│ │ │ │ │ ├── LocalTask.kt
│ │ │ │ │ ├── TaskDao.kt
│ │ │ │ │ └── ToDoDatabase.kt
│ │ │ │ └── network/
│ │ │ │ ├── NetworkDataSource.kt
│ │ │ │ ├── NetworkTask.kt
│ │ │ │ └── TaskNetworkDataSource.kt
│ │ │ ├── di/
│ │ │ │ ├── CoroutinesModule.kt
│ │ │ │ └── DataModules.kt
│ │ │ ├── statistics/
│ │ │ │ ├── StatisticsScreen.kt
│ │ │ │ ├── StatisticsUtils.kt
│ │ │ │ └── StatisticsViewModel.kt
│ │ │ ├── taskdetail/
│ │ │ │ ├── TaskDetailScreen.kt
│ │ │ │ └── TaskDetailViewModel.kt
│ │ │ ├── tasks/
│ │ │ │ ├── TasksFilterType.kt
│ │ │ │ ├── TasksScreen.kt
│ │ │ │ └── TasksViewModel.kt
│ │ │ └── util/
│ │ │ ├── Async.kt
│ │ │ ├── ComposeUtils.kt
│ │ │ ├── CoroutinesUtils.kt
│ │ │ ├── SimpleCountingIdlingResource.kt
│ │ │ ├── TodoDrawer.kt
│ │ │ └── TopAppBars.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── drawer_item_color.xml
│ │ │ ├── ic_add.xml
│ │ │ ├── ic_assignment_turned_in_24dp.xml
│ │ │ ├── ic_check_circle_96dp.xml
│ │ │ ├── ic_done.xml
│ │ │ ├── ic_edit.xml
│ │ │ ├── ic_filter_list.xml
│ │ │ ├── ic_list.xml
│ │ │ ├── ic_menu.xml
│ │ │ ├── ic_statistics.xml
│ │ │ ├── ic_statistics_100dp.xml
│ │ │ ├── ic_statistics_24dp.xml
│ │ │ ├── ic_verified_user_96dp.xml
│ │ │ ├── list_completed_touch_feedback.xml
│ │ │ └── touch_feedback.xml
│ │ ├── font/
│ │ │ └── opensans_font.xml
│ │ ├── values/
│ │ │ ├── attrs.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ └── values-w820dp/
│ │ └── dimens.xml
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── android/
│ │ └── architecture/
│ │ └── blueprints/
│ │ └── todoapp/
│ │ ├── addedittask/
│ │ │ └── AddEditTaskViewModelTest.kt
│ │ ├── data/
│ │ │ └── DefaultTaskRepositoryTest.kt
│ │ ├── statistics/
│ │ │ ├── StatisticsUtilsTest.kt
│ │ │ └── StatisticsViewModelTest.kt
│ │ ├── taskdetail/
│ │ │ └── TaskDetailViewModelTest.kt
│ │ └── tasks/
│ │ └── TasksViewModelTest.kt
│ └── resources/
│ └── mockito-extensions/
│ └── org.mockito.plugins.MockMaker
├── build.gradle.kts
├── gradle/
│ ├── init.gradle.kts
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── renovate.json
├── settings.gradle.kts
├── shared-test/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── example/
│ └── android/
│ └── architecture/
│ └── blueprints/
│ └── todoapp/
│ ├── CustomTestRunner.kt
│ ├── MainCoroutineRule.kt
│ ├── data/
│ │ ├── FakeTaskRepository.kt
│ │ └── source/
│ │ ├── local/
│ │ │ └── FakeTaskDao.kt
│ │ └── network/
│ │ └── FakeNetworkDataSource.kt
│ └── di/
│ ├── DatabaseTestModule.kt
│ └── RepositoryTestModule.kt
└── spotless/
├── copyright.kt
├── copyright.kts
└── copyright.xml
Condensed preview — 108 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (323K chars).
[
{
"path": ".github/ci-gradle.properties",
"chars": 947,
"preview": "#\n# Copyright 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n#"
},
{
"path": ".github/workflows/build_test.yaml",
"chars": 1601,
"preview": "name: build_test\n\non:\n workflow_dispatch:\n push:\n branches:\n - main\n pull_request:\n branches:\n - main"
},
{
"path": ".github/workflows/copy-branch.yml",
"chars": 1014,
"preview": "# Duplicates default main branch to the old master branch\n\nname: Duplicates main to old master branch\n\n# Controls when t"
},
{
"path": ".gitignore",
"chars": 83,
"preview": "*.iml\n.gradle\nlocal.properties\n.idea\n.DS_Store\nbuild\ncaptures\n.externalNativeBuild\n"
},
{
"path": ".google/packaging.yaml",
"chars": 1401,
"preview": "# Copyright (C) 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": "CODEOWNERS",
"chars": 25,
"preview": "* @josealcerreca @dturner"
},
{
"path": "CONTRIBUTING.md",
"chars": 1553,
"preview": "# How to become a contributor and submit your own code\n\n## Contributor License Agreements\n\nWe'd love to accept your patc"
},
{
"path": "LICENSE",
"chars": 11361,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 3564,
"preview": "# Android Architecture Samples\n\nThese samples showcase different architectural approaches to developing Android apps. In"
},
{
"path": "app/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "app/build.gradle.kts",
"chars": 7411,
"preview": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/proguard-rules.pro",
"chars": 965,
"preview": "-dontoptimize\n\n# Some methods are only called from tests, so make sure the shrinker keeps them.\n-keep class com.example."
},
{
"path": "app/proguardTest-rules.pro",
"chars": 517,
"preview": "# Proguard rules that are applied to your test apk/code.\n-ignorewarnings\n-dontoptimize\n\n-keepattributes *Annotation*\n\n-k"
},
{
"path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt",
"chars": 4461,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt",
"chars": 7190,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt",
"chars": 3154,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt",
"chars": 4257,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt",
"chars": 7295,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt",
"chars": 9885,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt",
"chars": 12861,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/debug/AndroidManifest.xml",
"chars": 962,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/debug/java/com/example/android/architecture/blueprints/todoapp/HiltTestActivity.kt",
"chars": 831,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 1453,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoActivity.kt",
"chars": 1217,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt",
"chars": 1112,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavGraph.kt",
"chars": 5323,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavigation.kt",
"chars": 3974,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoTheme.kt",
"chars": 536,
"preview": "package com.example.android.architecture.blueprints.todoapp\n\nimport androidx.compose.material3.MaterialTheme\nimport andr"
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt",
"chars": 6421,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt",
"chars": 4604,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt",
"chars": 6888,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/ModelMappingExt.kt",
"chars": 2899,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt",
"chars": 1426,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/TaskRepository.kt",
"chars": 1464,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/LocalTask.kt",
"chars": 1157,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDao.kt",
"chars": 2943,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt",
"chars": 1051,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkDataSource.kt",
"chars": 905,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkTask.kt",
"chars": 1151,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/TaskNetworkDataSource.kt",
"chars": 1782,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt",
"chars": 1789,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt",
"chars": 2340,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt",
"chars": 4291,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt",
"chars": 1385,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt",
"chars": 3167,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt",
"chars": 6666,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt",
"chars": 4520,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFilterType.kt",
"chars": 1004,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt",
"chars": 12010,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt",
"chars": 7176,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/Async.kt",
"chars": 964,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt",
"chars": 1888,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt",
"chars": 1440,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/SimpleCountingIdlingResource.kt",
"chars": 2534,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt",
"chars": 6472,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt",
"chars": 7143,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/res/drawable/drawer_item_color.xml",
"chars": 906,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_add.xml",
"chars": 981,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_assignment_turned_in_24dp.xml",
"chars": 1233,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_check_circle_96dp.xml",
"chars": 1091,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_done.xml",
"chars": 996,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_edit.xml",
"chars": 1112,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_filter_list.xml",
"chars": 990,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_list.xml",
"chars": 1031,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_menu.xml",
"chars": 993,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_statistics.xml",
"chars": 1081,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_statistics_100dp.xml",
"chars": 1108,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_statistics_24dp.xml",
"chars": 1081,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_verified_user_96dp.xml",
"chars": 1095,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/list_completed_touch_feedback.xml",
"chars": 905,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/touch_feedback.xml",
"chars": 838,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/font/opensans_font.xml",
"chars": 983,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values/attrs.xml",
"chars": 834,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 1056,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values/dimens.xml",
"chars": 1100,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 3573,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values/styles.xml",
"chars": 1635,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values-w820dp/dimens.xml",
"chars": 1064,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2019 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModelTest.kt",
"chars": 5612,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt",
"chars": 10442,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt",
"chars": 3217,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModelTest.kt",
"chars": 3896,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt",
"chars": 6230,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt",
"chars": 8341,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker",
"chars": 18,
"preview": "mock-maker-inline\n"
},
{
"path": "build.gradle.kts",
"chars": 921,
"preview": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "gradle/init.gradle.kts",
"chars": 2237,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "gradle/libs.versions.toml",
"chars": 10798,
"preview": "[versions]\naccompanist = \"0.36.0\"\nannotation = \"1.9.1\"\nandroidDesugarJdkLibs = \"2.1.3\"\nandroidGradlePlugin = \"8.7.3\"\nand"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 203,
"preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
},
{
"path": "gradle.properties",
"chars": 922,
"preview": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will o"
},
{
"path": "gradlew",
"chars": 8474,
"preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
},
{
"path": "gradlew.bat",
"chars": 2868,
"preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
},
{
"path": "renovate.json",
"chars": 867,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\n \"config:base\",\n \"group:all\",\n "
},
{
"path": "settings.gradle.kts",
"chars": 939,
"preview": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "shared-test/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "shared-test/build.gradle.kts",
"chars": 1732,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "shared-test/src/main/AndroidManifest.xml",
"chars": 684,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2023 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/CustomTestRunner.kt",
"chars": 1085,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/MainCoroutineRule.kt",
"chars": 1683,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/FakeTaskRepository.kt",
"chars": 4547,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/FakeTaskDao.kt",
"chars": 2306,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/FakeNetworkDataSource.kt",
"chars": 1010,
"preview": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DatabaseTestModule.kt",
"chars": 1475,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/RepositoryTestModule.kt",
"chars": 1276,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "spotless/copyright.kt",
"chars": 618,
"preview": "/*\n * Copyright $YEAR The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "spotless/copyright.kts",
"chars": 617,
"preview": "/*\n * Copyright $YEAR The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "spotless/copyright.xml",
"chars": 672,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright $YEAR The Android Open Source Project\n\n Licensed under th"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the android/architecture-samples GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 108 files (294.7 KB), approximately 70.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.