提交 dc6ec7d3 创建 作者: 宋海霞's avatar 宋海霞

modify

上级
version: 2
config_android:
docker:
- image: circleci/android:api-28
working_directory: ~/project
environment:
JAVA_TOOL_OPTIONS: "-Xmx1024m -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2 -Djava.util.concurrent.ForkJoinPool.common.parallelism=2 -Dkotlin.incremental=false"
TERM: dumb
setup_ftl:
- run:
name: Authorize gcloud and set config defaults
command: |
echo $GCLOUD_SERVICE_KEY | base64 -di > ${HOME}/gcloud-service-key.json
sudo gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
sudo gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
jobs:
build_and_setup:
docker:
- image: circleci/android:api-28
working_directory: ~/project
environment:
- JAVA_TOOL_OPTIONS: "-Xmx1024m -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
- GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2 -Djava.util.concurrent.ForkJoinPool.common.parallelism=2 -Dkotlin.incremental=false"
- TERM: dumb
steps:
- checkout
- run:
name: Build test and lint
command: |
./gradlew assembleMockDebug assembleProdDebug assembleMockDebugAndroidTest testMockDebug testProdDebug lintMockDebug lintProdDebug
- run:
name: Save test results
command: |
mkdir -p ~/junit/
find . -type f -regex "./.*/build/test-results/.*xml" -exec cp {} ~/junit/ \;
when: always
- store_test_results:
path: ~/junit
- store_artifacts:
path: ~/junit
destination: tests
- store_artifacts:
path: ./app/build/reports
destination: reports/
- persist_to_workspace:
root: .
paths:
- ./app/build
run_ftl:
docker:
- image: circleci/android:api-28
working_directory: ~/project
environment:
- JAVA_TOOL_OPTIONS: -Xmx1024m
- GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2 -Dkotlin.incremental=false
- TERM: dumb
steps:
- attach_workspace:
at: .
- run:
name: Authorize gcloud and set config defaults
command: |
echo $GCLOUD_SERVICE_KEY | base64 -di > ${HOME}/gcloud-service-key.json
sudo gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
sudo gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
- run:
name: Test with Firebase Test Lab
command: |
BUILD_DIR=build_${CIRCLE_BUILD_NUM}
sudo gcloud firebase test android run \
--app app/build/outputs/apk/mock/debug/app-mock-debug.apk \
--test app/build/outputs/apk/androidTest/mock/debug/app-mock-debug-androidTest.apk \
--results-bucket cloud-test-${GOOGLE_PROJECT_ID}-blueprints \
--results-dir=${BUILD_DIR}
- run:
name: Download results
command: |
BUILD_DIR=build_${CIRCLE_BUILD_NUM}
sudo pip install -U crcmod
mkdir firebase_test_results
sudo gsutil -m mv -r -U `sudo gsutil ls gs://cloud-test-${GOOGLE_PROJECT_ID}-blueprints/${BUILD_DIR} | tail -1` firebase_test_results/ | true
- store_artifacts:
path: firebase_test_results
workflows:
version: 2
build_and_test:
jobs:
- build_and_setup
- run_ftl:
requires:
- build_and_setup
*.iml
.gradle
local.properties
.idea
.DS_Store
build
captures
.externalNativeBuild
language: android
android:
components:
- tools
- platform-tools
- build-tools-28.0.3
- android-28
- extra-android-m2repository
- extra-google-m2repository
jdk:
- oraclejdk8
script:
- ./gradlew test
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
directories:
- $HOME/.m2
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
# 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).
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.
# Android Architecture Blueprints v2
<p align="center">
<img src="https://github.com/googlesamples/android-architecture/wiki/images/aab-logov2.png" alt="Illustration by Virginia Poltrack"/>
</p>
Android Architecture Blueprints is a project to 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:
* Kotlin **[Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)** for background operations.
* A single-activity architecture, using the **[Navigation component](https://developer.android.com/guide/navigation/navigation-getting-started)** to manage fragment operations.
* A presentation layer that contains a fragment (View) and a **ViewModel** per screen (or feature).
* Reactive UIs using **LiveData** observables and **Data Binding**.
* A **data layer** with a repository and two data sources (local using Room and remote) that are queried with one-shot operations (no listeners or data streams).
* Two **product flavors**, `mock` and `prod`, [to ease development and testing](https://android-developers.googleblog.com/2015/12/leveraging-product-flavors-in-android.html) (except in the Dagger branch).
* A collection of unit, integration and e2e **tests**, including "shared" tests that can be run on emulator/device or Robolectric.
## Variations
This project hosts each sample app in separate repository branches. For more information, see the `README.md` file in each branch.
### Stable samples - Kotlin
| Sample | Description |
| ------------- | ------------- |
| [master](https://github.com/googlesamples/android-architecture/tree/master) | The base for the rest of the branches. <br/>Uses Kotlin, Architecture Components, coroutines, Data Binding, etc. and uses Room as source of truth, with a reactive UI. |
| [dagger-android](https://github.com/googlesamples/android-architecture/tree/dagger-android)<br/>[[compare](https://github.com/googlesamples/android-architecture/compare/dagger-android#files_bucket)] | A simple Dagger setup that uses `dagger-android` and removes the two flavors. |
| [usecases](https://github.com/googlesamples/android-architecture/tree/usecases)<br/>[[compare](https://github.com/googlesamples/android-architecture/compare/usecases#files_bucket)] | Adds a new domain layer that uses UseCases for business logic. |
### Old samples - Kotlin and Java
Blueprints v1 had a collection of samples that are not maintained anymore, but can still be useful. See [all project branches](https://github.com/googlesamples/android-architecture/branches).
## Why a to-do app?
<img align="right" src="https://github.com/googlesamples/android-architecture/wiki/images/todoapp.gif" alt="A demo illustraating the UI of the app" width="288" height="512" style="display: inline; float: right"/>
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 UI/Material Design sample. The interface of the app is deliberately kept simple to focus on architecture. Check out [Plaid](https://github.com/android/plaid) instead.
* A complete Jetpack sample covering all libraries. Check out [Android Sunflower](https://github.com/googlesamples/android-sunflower) or the advanced [Github Browser Sample](https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample) instead.
* A real production app with network access, user authentication, etc. Check out the [Google I/O app](https://github.com/google/iosched), [Santa Tracker](https://github.com/google/santa-tracker-android) or [Tivi](https://github.com/chrisbanes/tivi) for that.
## 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 [usecases](tree/usecases/) sample.
Clone the repository:
```
git clone git@github.com:googlesamples/android-architecture.git
```
This step checks out the master branch. If you want to change to a different sample:
```
git checkout usecases
```
**Note:** To review a different sample, replace `usecases` with the name of sample you want to check out.
Finally open the `android-architecture/` directory in Android Studio.
### License
```
Copyright 2019 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.
```
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
android {
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
applicationId "com.example.android.architecture.blueprints.master"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
android {
sourceSets {
String sharedTestDir = 'src/sharedTest/java'
test {
java.srcDirs += sharedTestDir
}
androidTest {
java.srcDirs += sharedTestDir
}
}
}
buildTypes {
debug {
minifyEnabled false
testCoverageEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguardTest-rules.pro'
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguardTest-rules.pro'
}
}
flavorDimensions "default"
// If you need to add more flavors, consider using flavor dimensions.
productFlavors {
mock {
dimension "default"
applicationIdSuffix = ".mock"
}
prod {
dimension "default"
}
}
// Remove mockRelease as it's not needed.
android.variantFilter { variant ->
if (variant.buildType.name == 'release'
&& variant.getFlavors().get(0).name == 'mock') {
variant.setIgnore(true)
}
}
// Always show the result of every unit test, even if it passes.
testOptions.unitTests {
includeAndroidResources = true
all {
testLogging {
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
}
}
}
dataBinding {
enabled = true
enabledForTests = true
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
/*
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 "androidx.appcompat:appcompat:$appCompatVersion"
implementation "androidx.cardview:cardview:$cardVersion"
implementation "com.google.android.material:material:$materialVersion"
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
implementation "androidx.annotation:annotation:$androidXAnnotations"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "com.jakewharton.timber:timber:$timberVersion"
implementation "androidx.legacy:legacy-support-v4:$androidXLegacySupport"
implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion"
implementation "androidx.room:room-runtime:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
// Architecture Components
implementation "androidx.room:room-runtime:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
implementation "androidx.lifecycle:lifecycle-extensions:$archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$archLifecycleVersion"
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
// Dependencies for local unit tests
testImplementation "junit:junit:$junitVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
testImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
testImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
testImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
testImplementation "com.google.truth:truth:$truthVersion"
// Dependencies for Android unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// AndroidX Test - JVM testing
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:rules:$androidXTestRulesVersion"
// Once https://issuetracker.google.com/127986458 is fixed this can be testImplementation
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
implementation "androidx.fragment:fragment:$fragmentVersion"
// AndroidX Test - Instrumented testing
androidTestImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
androidTestImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
androidTestImplementation "androidx.test:rules:$androidXTestRulesVersion"
androidTestImplementation "androidx.room:room-testing:$roomVersion"
androidTestImplementation "androidx.arch.core:core-testing:$archTestingVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion"
androidTestImplementation "org.robolectric:annotations:$robolectricVersion"
implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion"
// Resolve conflicts between main and test APK:
androidTestImplementation "androidx.annotation:annotation:$androidXAnnotations"
androidTestImplementation "androidx.legacy:legacy-support-v4:$androidXLegacySupport"
androidTestImplementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
androidTestImplementation "androidx.appcompat:appcompat:$appCompatVersion"
androidTestImplementation "com.google.android.material:material:$materialVersion"
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion"
}
-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.**
# 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
# Uncomment this if you use Mockito
-dontwarn org.mockito.**
\ No newline at end of file
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import android.view.Gravity
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.findNavController
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.DrawerActions.open
import androidx.test.espresso.contrib.DrawerMatchers.isClosed
import androidx.test.espresso.contrib.DrawerMatchers.isOpen
import androidx.test.espresso.contrib.NavigationViewActions.navigateTo
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import com.example.android.architecture.blueprints.todoapp.util.DataBindingIdlingResource
import com.example.android.architecture.blueprints.todoapp.util.EspressoIdlingResource
import com.example.android.architecture.blueprints.todoapp.util.monitorActivity
import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* Tests for the [DrawerLayout] layout component in [TasksActivity] which manages
* navigation within the app.
*
* UI tests usually use [ActivityTestRule] but there's no API to perform an action before
* each test. The workaround is to use `ActivityScenario.launch()` and `ActivityScenario.close()`.
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class AppNavigationTest {
private lateinit var tasksRepository: TasksRepository
// An Idling Resource that waits for Data Binding to have no pending bindings
private val dataBindingIdlingResource = DataBindingIdlingResource()
@Before
fun init() {
tasksRepository = ServiceLocator.provideTasksRepository(getApplicationContext())
}
@After
fun reset() {
ServiceLocator.resetRepository()
}
/**
* Idling resources tell Espresso that the app is idle or busy. This is needed when operations
* are not scheduled in the main Looper (for example when executed on a different thread).
*/
@Before
fun registerIdlingResource() {
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
IdlingRegistry.getInstance().register(dataBindingIdlingResource)
}
/**
* Unregister your Idling Resource so it can be garbage collected and does not leak any memory.
*/
@After
fun unregisterIdlingResource() {
IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
IdlingRegistry.getInstance().unregister(dataBindingIdlingResource)
}
@Test
fun drawerNavigationFromTasksToStatistics() {
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
onView(withId(R.id.drawer_layout))
.check(matches(isClosed(Gravity.START))) // Left Drawer should be closed.
.perform(open()) // Open Drawer
// Start statistics screen.
onView(withId(R.id.nav_view))
.perform(navigateTo(R.id.statistics_fragment_dest))
// Check that statistics screen was opened.
onView(withId(R.id.statistics_layout)).check(matches(isDisplayed()))
onView(withId(R.id.drawer_layout))
.check(matches(isClosed(Gravity.START))) // Left Drawer should be closed.
.perform(open()) // Open Drawer
// Start tasks screen.
onView(withId(R.id.nav_view))
.perform(navigateTo(R.id.tasks_fragment_dest))
// Check that tasks screen was opened.
onView(withId(R.id.tasks_container_layout)).check(matches(isDisplayed()))
// When using ActivityScenario.launch, always call close()
activityScenario.close()
}
@Test
fun tasksScreen_clickOnAndroidHomeIcon_OpensNavigation() {
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Check that left drawer is closed at startup
onView(withId(R.id.drawer_layout))
.check(matches(isClosed(Gravity.START))) // Left Drawer should be closed.
// Open Drawer
onView(
withContentDescription(
activityScenario
.getToolbarNavigationContentDescription()
)
).perform(click())
// Check if drawer is open
onView(withId(R.id.drawer_layout))
.check(matches(isOpen(Gravity.START))) // Left drawer is open open.
// When using ActivityScenario.launch, always call close()
activityScenario.close()
}
@Test
fun statsScreen_clickOnAndroidHomeIcon_OpensNavigation() {
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// When the user navigates to the stats screen
activityScenario.onActivity {
it.findNavController(R.id.nav_host_fragment).navigate(R.id.statistics_fragment_dest)
}
// Then check that left drawer is closed at startup
onView(withId(R.id.drawer_layout))
.check(matches(isClosed(Gravity.START))) // Left Drawer should be closed.
// When the drawer is opened
onView(
withContentDescription(
activityScenario
.getToolbarNavigationContentDescription()
)
).perform(click())
// Then check that the drawer is open
onView(withId(R.id.drawer_layout))
.check(matches(isOpen(Gravity.START))) // Left drawer is open open.
// When using ActivityScenario.launch, always call close()
activityScenario.close()
}
@Test
fun taskDetailScreen_doubleUIBackButton() {
val task = Task("UI <- button", "Description")
tasksRepository.saveTaskBlocking(task)
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Click on the task on the list
onView(withText("UI <- button")).perform(click())
// Click on the edit task button
onView(withId(R.id.edit_task_fab)).perform(click())
// Confirm that if we click "<-" once, we end up back at the task details page
onView(
withContentDescription(
activityScenario
.getToolbarNavigationContentDescription()
)
).perform(click())
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
// Confirm that if we click "<-" a second time, we end up back at the home screen
onView(
withContentDescription(
activityScenario
.getToolbarNavigationContentDescription()
)
).perform(click())
onView(withId(R.id.tasks_container_layout)).check(matches(isDisplayed()))
// When using ActivityScenario.launch, always call close()
activityScenario.close()
}
@Test
fun taskDetailScreen_doubleBackButton() {
val task = Task("Back button", "Description")
tasksRepository.saveTaskBlocking(task)
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Click on the task on the list
onView(withText("Back button")).perform(click())
// Click on the edit task button
onView(withId(R.id.edit_task_fab)).perform(click())
// Confirm that if we click back once, we end up back at the task details page
pressBack()
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
// Confirm that if we click back a second time, we end up back at the home screen
pressBack()
onView(withId(R.id.tasks_container_layout)).check(matches(isDisplayed()))
// When using ActivityScenario.launch, always call close()
activityScenario.close()
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.R.string
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import com.example.android.architecture.blueprints.todoapp.util.DataBindingIdlingResource
import com.example.android.architecture.blueprints.todoapp.util.EspressoIdlingResource
import com.example.android.architecture.blueprints.todoapp.util.deleteAllTasksBlocking
import com.example.android.architecture.blueprints.todoapp.util.monitorActivity
import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking
import org.hamcrest.Matchers.allOf
import org.hamcrest.core.IsNot.not
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* Large End-to-End test for the tasks module.
*
* UI tests usually use [ActivityTestRule] but there's no API to perform an action before
* each test. The workaround is to use `ActivityScenario.launch()` and `ActivityScenario.close()`.
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class TasksActivityTest {
private lateinit var repository: TasksRepository
// An Idling Resource that waits for Data Binding to have no pending bindings
private val dataBindingIdlingResource = DataBindingIdlingResource()
@Before
fun init() {
repository = ServiceLocator.provideTasksRepository(getApplicationContext())
repository.deleteAllTasksBlocking()
}
@After
fun reset() {
ServiceLocator.resetRepository()
}
/**
* Idling resources tell Espresso that the app is idle or busy. This is needed when operations
* are not scheduled in the main Looper (for example when executed on a different thread).
*/
@Before
fun registerIdlingResource() {
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
IdlingRegistry.getInstance().register(dataBindingIdlingResource)
}
/**
* Unregister your Idling Resource so it can be garbage collected and does not leak any memory.
*/
@After
fun unregisterIdlingResource() {
IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
IdlingRegistry.getInstance().unregister(dataBindingIdlingResource)
}
@Test
fun editTask() {
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION"))
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Click on the task on the list and verify that all the data is correct
onView(withText("TITLE1")).perform(click())
onView(withId(R.id.task_detail_title_text)).check(matches(withText("TITLE1")))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("DESCRIPTION")))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
// Click on the edit button, edit, and save
onView(withId(R.id.edit_task_fab)).perform(click())
onView(withId(R.id.add_task_title_edit_text)).perform(replaceText("NEW TITLE"))
onView(withId(R.id.add_task_description_edit_text)).perform(replaceText("NEW DESCRIPTION"))
onView(withId(R.id.save_task_fab)).perform(click())
// Verify task is displayed on screen in the task list.
onView(withText("NEW TITLE")).check(matches(isDisplayed()))
// Verify previous task is not displayed
onView(withText("TITLE1")).check(doesNotExist())
// Make sure the activity is closed before resetting the db:
activityScenario.close()
}
@Test
fun createOneTask_deleteTask() {
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Add active task
onView(withId(R.id.add_task_fab)).perform(click())
onView(withId(R.id.add_task_title_edit_text))
.perform(typeText("TITLE1"), closeSoftKeyboard())
onView(withId(R.id.add_task_description_edit_text)).perform(typeText("DESCRIPTION"))
onView(withId(R.id.save_task_fab)).perform(click())
// Open it in details view
onView(withText("TITLE1")).perform(click())
// Click delete task in menu
onView(withId(R.id.menu_delete)).perform(click())
// Verify it was deleted
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(string.nav_all)).perform(click())
onView(withText("TITLE1")).check(doesNotExist())
// Make sure the activity is closed before resetting the db:
activityScenario.close()
}
@Test
fun createTwoTasks_deleteOneTask() {
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION"))
repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION"))
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Open the second task in details view
onView(withText("TITLE2")).perform(click())
// Click delete task in menu
onView(withId(R.id.menu_delete)).perform(click())
// Verify only one task was deleted
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(string.nav_all)).perform(click())
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withText("TITLE2")).check(doesNotExist())
// Make sure the activity is closed before resetting the db:
activityScenario.close()
}
@Test
fun markTaskAsCompleteOnDetailScreen_taskIsCompleteInList() {
// Add 1 active task
val taskTitle = "COMPLETED"
repository.saveTaskBlocking(Task(taskTitle, "DESCRIPTION"))
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Click on the task on the list
onView(withText(taskTitle)).perform(click())
// Click on the checkbox in task details screen
onView(withId(R.id.task_detail_complete_checkbox)).perform(click())
// Click on the navigation up button to go back to the list
onView(
withContentDescription(
activityScenario.getToolbarNavigationContentDescription()
)
).perform(click())
// Check that the task is marked as completed
onView(allOf(withId(R.id.complete_checkbox), hasSibling(withText(taskTitle))))
.check(matches(isChecked()))
// Make sure the activity is closed before resetting the db:
activityScenario.close()
}
@Test
fun markTaskAsActiveOnDetailScreen_taskIsActiveInList() {
// Add 1 completed task
val taskTitle = "ACTIVE"
repository.saveTaskBlocking(Task(taskTitle, "DESCRIPTION", true))
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Click on the task on the list
onView(withText(taskTitle)).perform(click())
// Click on the checkbox in task details screen
onView(withId(R.id.task_detail_complete_checkbox)).perform(click())
// Click on the navigation up button to go back to the list
onView(
withContentDescription(
activityScenario.getToolbarNavigationContentDescription()
)
).perform(click())
// Check that the task is marked as active
onView(allOf(withId(R.id.complete_checkbox), hasSibling(withText(taskTitle))))
.check(matches(not(isChecked())))
// Make sure the activity is closed before resetting the db:
activityScenario.close()
}
@Test
fun markTaskAsCompleteAndActiveOnDetailScreen_taskIsActiveInList() {
// Add 1 active task
val taskTitle = "ACT-COMP"
repository.saveTaskBlocking(Task(taskTitle, "DESCRIPTION"))
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Click on the task on the list
onView(withText(taskTitle)).perform(click())
// Click on the checkbox in task details screen
onView(withId(R.id.task_detail_complete_checkbox)).perform(click())
// Click again to restore it to original state
onView(withId(R.id.task_detail_complete_checkbox)).perform(click())
// Click on the navigation up button to go back to the list
onView(
withContentDescription(
activityScenario.getToolbarNavigationContentDescription()
)
).perform(click())
// Check that the task is marked as active
onView(allOf(withId(R.id.complete_checkbox), hasSibling(withText(taskTitle))))
.check(matches(not(isChecked())))
// Make sure the activity is closed before resetting the db:
activityScenario.close()
}
@Test
fun markTaskAsActiveAndCompleteOnDetailScreen_taskIsCompleteInList() {
// Add 1 completed task
val taskTitle = "COMP-ACT"
repository.saveTaskBlocking(Task(taskTitle, "DESCRIPTION", true))
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Click on the task on the list
onView(withText(taskTitle)).perform(click())
// Click on the checkbox in task details screen
onView(withId(R.id.task_detail_complete_checkbox)).perform(click())
// Click again to restore it to original state
onView(withId(R.id.task_detail_complete_checkbox)).perform(click())
// Click on the navigation up button to go back to the list
onView(
withContentDescription(
activityScenario.getToolbarNavigationContentDescription()
)
).perform(click())
// Check that the task is marked as active
onView(allOf(withId(R.id.complete_checkbox), hasSibling(withText(taskTitle))))
.check(matches(isChecked()))
// Make sure the activity is closed before resetting the db:
activityScenario.close()
}
@Test
fun createTask() {
// start up Tasks screen
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario)
// Click on the "+" button, add details, and save
onView(withId(R.id.add_task_fab)).perform(click())
onView(withId(R.id.add_task_title_edit_text))
.perform(typeText("title"), closeSoftKeyboard())
onView(withId(R.id.add_task_description_edit_text)).perform(typeText("description"))
onView(withId(R.id.save_task_fab)).perform(click())
// Then verify task is displayed on screen
onView(withText("title")).check(matches(isDisplayed()))
// Make sure the activity is closed before resetting the db:
activityScenario.close()
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import android.app.Activity
import androidx.appcompat.widget.Toolbar
import androidx.test.core.app.ActivityScenario
import com.example.android.architecture.blueprints.todoapp.R
fun <T : Activity> ActivityScenario<T>.getToolbarNavigationContentDescription(): String {
var description = ""
onActivity {
description =
it.findViewById<Toolbar>(R.id.toolbar).navigationContentDescription as String
}
return description
}
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 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"
package="com.example.android.architecture.blueprints.todoapp">
<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.tasks.TasksActivity"
android:windowSoftInputMode="adjustResize"
android:theme="@style/AppTheme.OverlapSystemBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp
import androidx.lifecycle.Observer
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
@Suppress("MemberVisibilityCanBePrivate")
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
/**
* An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
* already been handled.
*
* [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
*/
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let {
onEventUnhandledContent(it)
}
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
/**
* Extends [SwipeRefreshLayout] to support non-direct descendant scrolling views.
*
*
* [SwipeRefreshLayout] works as expected when a scroll view is a direct child: it triggers
* the refresh only when the view is on top. This class adds a way (@link #setScrollUpChild} to
* define which view controls this behavior.
*/
class ScrollChildSwipeRefreshLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : SwipeRefreshLayout(context, attrs) {
var scrollUpChild: View? = null
override fun canChildScrollUp() =
scrollUpChild?.canScrollVertically(-1) ?: super.canChildScrollUp()
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp
import android.app.Application
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import timber.log.Timber
import timber.log.Timber.DebugTree
/**
* An application that lazily provides a repository. Note that this Service Locator pattern is
* used to simplify the sample. Consider a Dependency Injection framework.
*
* Also, sets up Timber in the DEBUG BuildConfig. Read Timber's documentation for production setups.
*/
class TodoApplication : Application() {
// Depends on the flavor,
val taskRepository: TasksRepository
get() = ServiceLocator.provideTasksRepository(this)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp
import android.app.Application
import android.os.Bundle
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.savedstate.SavedStateRegistryOwner
import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskViewModel
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsViewModel
import com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailViewModel
import com.example.android.architecture.blueprints.todoapp.tasks.TasksViewModel
/**
* Factory for all ViewModels.
*/
@Suppress("UNCHECKED_CAST")
class ViewModelFactory constructor(
private val tasksRepository: TasksRepository,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = with(modelClass) {
when {
isAssignableFrom(StatisticsViewModel::class.java) ->
StatisticsViewModel(tasksRepository)
isAssignableFrom(TaskDetailViewModel::class.java) ->
TaskDetailViewModel(tasksRepository)
isAssignableFrom(AddEditTaskViewModel::class.java) ->
AddEditTaskViewModel(tasksRepository)
isAssignableFrom(TasksViewModel::class.java) ->
TasksViewModel(tasksRepository, handle)
else ->
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.addedittask
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.example.android.architecture.blueprints.todoapp.EventObserver
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.databinding.AddtaskFragBinding
import com.example.android.architecture.blueprints.todoapp.tasks.ADD_EDIT_RESULT_OK
import com.example.android.architecture.blueprints.todoapp.util.getViewModelFactory
import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout
import com.example.android.architecture.blueprints.todoapp.util.setupSnackbar
import com.google.android.material.snackbar.Snackbar
/**
* Main UI for the add task screen. Users can enter a task title and description.
*/
class AddEditTaskFragment : Fragment() {
private lateinit var viewDataBinding: AddtaskFragBinding
private val args: AddEditTaskFragmentArgs by navArgs()
private val viewModel by viewModels<AddEditTaskViewModel> { getViewModelFactory() }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.addtask_frag, container, false)
viewDataBinding = AddtaskFragBinding.bind(root).apply {
this.viewmodel = viewModel
}
// Set the lifecycle owner to the lifecycle of the view
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupSnackbar()
setupNavigation()
this.setupRefreshLayout(viewDataBinding.refreshLayout)
viewModel.start(args.taskId)
}
private fun setupSnackbar() {
view?.setupSnackbar(this, viewModel.snackbarText, Snackbar.LENGTH_SHORT)
}
private fun setupNavigation() {
viewModel.taskUpdatedEvent.observe(this, EventObserver {
val action = AddEditTaskFragmentDirections
.actionAddEditTaskFragmentToTasksFragment(ADD_EDIT_RESULT_OK)
findNavController().navigate(action)
})
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.addedittask
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.android.architecture.blueprints.todoapp.Event
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import kotlinx.coroutines.launch
/**
* ViewModel for the Add/Edit screen.
*/
class AddEditTaskViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() {
// Two-way databinding, exposing MutableLiveData
val title = MutableLiveData<String>()
// Two-way databinding, exposing MutableLiveData
val description = MutableLiveData<String>()
private val _dataLoading = MutableLiveData<Boolean>()
val dataLoading: LiveData<Boolean> = _dataLoading
private val _snackbarText = MutableLiveData<Event<Int>>()
val snackbarText: LiveData<Event<Int>> = _snackbarText
private val _taskUpdatedEvent = MutableLiveData<Event<Unit>>()
val taskUpdatedEvent: LiveData<Event<Unit>> = _taskUpdatedEvent
private var taskId: String? = null
private var isNewTask: Boolean = false
private var isDataLoaded = false
private var taskCompleted = false
fun start(taskId: String?) {
if (_dataLoading.value == true) {
return
}
this.taskId = taskId
if (taskId == null) {
// No need to populate, it's a new task
isNewTask = true
return
}
if (isDataLoaded) {
// No need to populate, already have data.
return
}
isNewTask = false
_dataLoading.value = true
viewModelScope.launch {
tasksRepository.getTask(taskId).let { result ->
if (result is Success) {
onTaskLoaded(result.data)
} else {
onDataNotAvailable()
}
}
}
}
private fun onTaskLoaded(task: Task) {
title.value = task.title
description.value = task.description
taskCompleted = task.isCompleted
_dataLoading.value = false
isDataLoaded = true
}
private fun onDataNotAvailable() {
_dataLoading.value = false
}
// Called when clicking on fab.
fun saveTask() {
val currentTitle = title.value
val currentDescription = description.value
if (currentTitle == null || currentDescription == null) {
_snackbarText.value = Event(R.string.empty_task_message)
return
}
if (Task(currentTitle, currentDescription).isEmpty) {
_snackbarText.value = Event(R.string.empty_task_message)
return
}
val currentTaskId = taskId
if (isNewTask || currentTaskId == null) {
createTask(Task(currentTitle, currentDescription))
} else {
val task = Task(currentTitle, currentDescription, taskCompleted, currentTaskId)
updateTask(task)
}
}
private fun createTask(newTask: Task) = viewModelScope.launch {
tasksRepository.saveTask(newTask)
_taskUpdatedEvent.value = Event(Unit)
}
private fun updateTask(task: Task) {
if (isNewTask) {
throw RuntimeException("updateTask() was called but task is new.")
}
viewModelScope.launch {
tasksRepository.saveTask(task)
_taskUpdatedEvent.value = Event(Unit)
}
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
/**
* A generic class that holds a value with its loading status.
* @param <T>
*/
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
Loading -> "Loading"
}
}
}
/**
* `true` if [Result] is of type [Success] & holds non-null [Success.data].
*/
val Result<*>.succeeded
get() = this is Success && data != null
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.UUID
/**
* Immutable model class for a Task. In order to compile with Room, we can't use @JvmOverloads to
* generate multiple constructors.
*
* @param title title of the task
* @param description description of the task
* @param isCompleted whether or not this task is completed
* @param id id of the task
*/
@Entity(tableName = "tasks")
data class Task @JvmOverloads constructor(
@ColumnInfo(name = "title") var title: String = "",
@ColumnInfo(name = "description") var description: String = "",
@ColumnInfo(name = "completed") var isCompleted: Boolean = false,
@PrimaryKey @ColumnInfo(name = "entryid") var id: String = UUID.randomUUID().toString()
) {
val titleForList: String
get() = if (title.isNotEmpty()) title else description
val isActive
get() = !isCompleted
val isEmpty
get() = title.isEmpty() || description.isEmpty()
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source
import androidx.lifecycle.LiveData
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.util.wrapEspressoIdlingResource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Default implementation of [TasksRepository]. Single entry point for managing tasks' data.
*/
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksRepository {
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
// Set app as busy while this function executes.
wrapEspressoIdlingResource {
if (forceUpdate) {
try {
updateTasksFromRemoteDataSource()
} catch (ex: Exception) {
return Result.Error(ex)
}
}
return tasksLocalDataSource.getTasks()
}
}
override suspend fun refreshTasks() {
updateTasksFromRemoteDataSource()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return tasksLocalDataSource.observeTasks()
}
override suspend fun refreshTask(taskId: String) {
updateTaskFromRemoteDataSource(taskId)
}
private suspend fun updateTasksFromRemoteDataSource() {
val remoteTasks = tasksRemoteDataSource.getTasks()
if (remoteTasks is Success) {
// Real apps might want to do a proper sync, deleting, modifying or adding each task.
tasksLocalDataSource.deleteAllTasks()
remoteTasks.data.forEach { task ->
tasksLocalDataSource.saveTask(task)
}
} else if (remoteTasks is Result.Error) {
throw remoteTasks.exception
}
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
return tasksLocalDataSource.observeTask(taskId)
}
private suspend fun updateTaskFromRemoteDataSource(taskId: String) {
val remoteTask = tasksRemoteDataSource.getTask(taskId)
if (remoteTask is Success) {
tasksLocalDataSource.saveTask(remoteTask.data)
}
}
/**
* Relies on [getTasks] to fetch data and picks the task with the same ID.
*/
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
// Set app as busy while this function executes.
wrapEspressoIdlingResource {
if (forceUpdate) {
updateTaskFromRemoteDataSource(taskId)
}
return tasksLocalDataSource.getTask(taskId)
}
}
override suspend fun saveTask(task: Task) {
coroutineScope {
launch { tasksRemoteDataSource.saveTask(task) }
launch { tasksLocalDataSource.saveTask(task) }
}
}
override suspend fun completeTask(task: Task) {
coroutineScope {
launch { tasksRemoteDataSource.completeTask(task) }
launch { tasksLocalDataSource.completeTask(task) }
}
}
override suspend fun completeTask(taskId: String) {
withContext(ioDispatcher) {
(getTaskWithId(taskId) as? Success)?.let { it ->
completeTask(it.data)
}
}
}
override suspend fun activateTask(task: Task) = withContext<Unit>(ioDispatcher) {
coroutineScope {
launch { tasksRemoteDataSource.activateTask(task) }
launch { tasksLocalDataSource.activateTask(task) }
}
}
override suspend fun activateTask(taskId: String) {
withContext(ioDispatcher) {
(getTaskWithId(taskId) as? Success)?.let { it ->
activateTask(it.data)
}
}
}
override suspend fun clearCompletedTasks() {
coroutineScope {
launch { tasksRemoteDataSource.clearCompletedTasks() }
launch { tasksLocalDataSource.clearCompletedTasks() }
}
}
override suspend fun deleteAllTasks() {
withContext(ioDispatcher) {
coroutineScope {
launch { tasksRemoteDataSource.deleteAllTasks() }
launch { tasksLocalDataSource.deleteAllTasks() }
}
}
}
override suspend fun deleteTask(taskId: String) {
coroutineScope {
launch { tasksRemoteDataSource.deleteTask(taskId) }
launch { tasksLocalDataSource.deleteTask(taskId) }
}
}
private suspend fun getTaskWithId(id: String): Result<Task> {
return tasksLocalDataSource.getTask(id)
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source
import androidx.lifecycle.LiveData
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Task
/**
* Main entry point for accessing tasks data.
*/
interface TasksDataSource {
fun observeTasks(): LiveData<Result<List<Task>>>
suspend fun getTasks(): Result<List<Task>>
suspend fun refreshTasks()
fun observeTask(taskId: String): LiveData<Result<Task>>
suspend fun getTask(taskId: String): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source
import androidx.lifecycle.LiveData
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Task
/**
* Interface to the data layer.
*/
interface TasksRepository {
fun observeTasks(): LiveData<Result<List<Task>>>
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun refreshTasks()
fun observeTask(taskId: String): LiveData<Result<Task>>
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun refreshTask(taskId: String)
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source.local
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.android.architecture.blueprints.todoapp.data.Task
/**
* Data Access Object for the tasks table.
*/
@Dao
interface TasksDao {
/**
* Observes list of tasks.
*
* @return all tasks.
*/
@Query("SELECT * FROM Tasks")
fun observeTasks(): LiveData<List<Task>>
/**
* Observes a single task.
*
* @param taskId the task id.
* @return the task with taskId.
*/
@Query("SELECT * FROM Tasks WHERE entryid = :taskId")
fun observeTaskById(taskId: String): LiveData<Task>
/**
* Select all tasks from the tasks table.
*
* @return all tasks.
*/
@Query("SELECT * FROM Tasks")
suspend fun getTasks(): List<Task>
/**
* Select a task by id.
*
* @param taskId the task id.
* @return the task with taskId.
*/
@Query("SELECT * FROM Tasks WHERE entryid = :taskId")
suspend fun getTaskById(taskId: String): Task?
/**
* Insert a task in the database. If the task already exists, replace it.
*
* @param task the task to be inserted.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: Task)
/**
* Update a task.
*
* @param task task to be updated
* @return the number of tasks updated. This should always be 1.
*/
@Update
suspend fun updateTask(task: Task): Int
/**
* Update the complete status of a task
*
* @param taskId id of the task
* @param completed status to be updated
*/
@Query("UPDATE tasks SET completed = :completed WHERE entryid = :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 Tasks WHERE entryid = :taskId")
suspend fun deleteTaskById(taskId: String): Int
/**
* Delete all tasks.
*/
@Query("DELETE FROM Tasks")
suspend fun deleteTasks()
/**
* Delete all completed tasks from the table.
*
* @return the number of tasks deleted.
*/
@Query("DELETE FROM Tasks WHERE completed = 1")
suspend fun deleteCompletedTasks(): Int
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source.local
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Concrete implementation of a data source as a db.
*/
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource {
override fun observeTasks(): LiveData<Result<List<Task>>> {
return tasksDao.observeTasks().map {
Success(it)
}
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
return tasksDao.observeTaskById(taskId).map {
Success(it)
}
}
override suspend fun refreshTask(taskId: String) {
// NO-OP
}
override suspend fun refreshTasks() {
// NO-OP
}
override suspend fun getTasks(): Result<List<Task>> = withContext(ioDispatcher) {
return@withContext try {
Success(tasksDao.getTasks())
} catch (e: Exception) {
Error(e)
}
}
override suspend fun getTask(taskId: String): Result<Task> = withContext(ioDispatcher) {
try {
val task = tasksDao.getTaskById(taskId)
if (task != null) {
return@withContext Success(task)
} else {
return@withContext Error(Exception("Task not found!"))
}
} catch (e: Exception) {
return@withContext Error(e)
}
}
override suspend fun saveTask(task: Task) = withContext(ioDispatcher) {
tasksDao.insertTask(task)
}
override suspend fun completeTask(task: Task) = withContext(ioDispatcher) {
tasksDao.updateCompleted(task.id, true)
}
override suspend fun completeTask(taskId: String) {
tasksDao.updateCompleted(taskId, true)
}
override suspend fun activateTask(task: Task) = withContext(ioDispatcher) {
tasksDao.updateCompleted(task.id, false)
}
override suspend fun activateTask(taskId: String) {
tasksDao.updateCompleted(taskId, false)
}
override suspend fun clearCompletedTasks() = withContext<Unit>(ioDispatcher) {
tasksDao.deleteCompletedTasks()
}
override suspend fun deleteAllTasks() = withContext(ioDispatcher) {
tasksDao.deleteTasks()
}
override suspend fun deleteTask(taskId: String) = withContext<Unit>(ioDispatcher) {
tasksDao.deleteTaskById(taskId)
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source.local
import androidx.room.Database
import androidx.room.RoomDatabase
import com.example.android.architecture.blueprints.todoapp.data.Task
/**
* The Room Database that contains the Task table.
*
* Note that exportSchema should be true in production databases.
*/
@Database(entities = [Task::class], version = 1, exportSchema = false)
abstract class ToDoDatabase : RoomDatabase() {
abstract fun taskDao(): TasksDao
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.statistics
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.databinding.StatisticsFragBinding
import com.example.android.architecture.blueprints.todoapp.util.getViewModelFactory
import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout
/**
* Main UI for the statistics screen.
*/
class StatisticsFragment : Fragment() {
private lateinit var viewDataBinding: StatisticsFragBinding
private val viewModel by viewModels<StatisticsViewModel> { getViewModelFactory() }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = DataBindingUtil.inflate(
inflater, R.layout.statistics_frag, container,
false
)
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewDataBinding.viewmodel = viewModel
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
this.setupRefreshLayout(viewDataBinding.refreshLayout)
}
}
/*
* Copyright (C) 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.
*/
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 == null || 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)
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.statistics
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import kotlinx.coroutines.launch
/**
* ViewModel for the statistics screen.
*/
class StatisticsViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() {
private val tasks: LiveData<Result<List<Task>>> = tasksRepository.observeTasks()
private val _dataLoading = MutableLiveData<Boolean>(false)
private val stats: LiveData<StatsResult?> = tasks.map {
if (it is Success) {
getActiveAndCompletedStats(it.data)
} else {
null
}
}
val activeTasksPercent = stats.map {
it?.activeTasksPercent ?: 0f }
val completedTasksPercent: LiveData<Float> = stats.map { it?.completedTasksPercent ?: 0f }
val dataLoading: LiveData<Boolean> = _dataLoading
val error: LiveData<Boolean> = tasks.map { it is Error }
val empty: LiveData<Boolean> = tasks.map { (it as? Success)?.data.isNullOrEmpty() }
fun refresh() {
_dataLoading.value = true
viewModelScope.launch {
tasksRepository.refreshTasks()
_dataLoading.value = false
}
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.taskdetail
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.example.android.architecture.blueprints.todoapp.EventObserver
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.databinding.TaskdetailFragBinding
import com.example.android.architecture.blueprints.todoapp.tasks.DELETE_RESULT_OK
import com.example.android.architecture.blueprints.todoapp.util.getViewModelFactory
import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout
import com.example.android.architecture.blueprints.todoapp.util.setupSnackbar
import com.google.android.material.snackbar.Snackbar
/**
* Main UI for the task detail screen.
*/
class TaskDetailFragment : Fragment() {
private lateinit var viewDataBinding: TaskdetailFragBinding
private val args: TaskDetailFragmentArgs by navArgs()
private val viewModel by viewModels<TaskDetailViewModel> { getViewModelFactory() }
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupFab()
view?.setupSnackbar(this, viewModel.snackbarText, Snackbar.LENGTH_SHORT)
setupNavigation()
this.setupRefreshLayout(viewDataBinding.refreshLayout)
}
private fun setupNavigation() {
viewModel.deleteTaskEvent.observe(this, EventObserver {
val action = TaskDetailFragmentDirections
.actionTaskDetailFragmentToTasksFragment(DELETE_RESULT_OK)
findNavController().navigate(action)
})
viewModel.editTaskEvent.observe(this, EventObserver {
val action = TaskDetailFragmentDirections
.actionTaskDetailFragmentToAddEditTaskFragment(
args.taskId,
resources.getString(R.string.edit_task)
)
findNavController().navigate(action)
})
}
private fun setupFab() {
activity?.findViewById<View>(R.id.edit_task_fab)?.setOnClickListener {
viewModel.editTask()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.taskdetail_frag, container, false)
viewDataBinding = TaskdetailFragBinding.bind(view).apply {
viewmodel = viewModel
}
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
viewModel.start(args.taskId)
setHasOptionsMenu(true)
return view
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_delete -> {
viewModel.deleteTask()
true
}
else -> false
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.taskdetail_fragment_menu, menu)
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.taskdetail
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import com.example.android.architecture.blueprints.todoapp.Event
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import kotlinx.coroutines.launch
/**
* ViewModel for the Details screen.
*/
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() {
private val _taskId = MutableLiveData<String>()
private val _task = _taskId.switchMap { taskId ->
tasksRepository.observeTask(taskId).map { computeResult(it) }
}
val task: LiveData<Task?> = _task
val isDataAvailable: LiveData<Boolean> = _task.map { it != null }
private val _dataLoading = MutableLiveData<Boolean>()
val dataLoading: LiveData<Boolean> = _dataLoading
private val _editTaskEvent = MutableLiveData<Event<Unit>>()
val editTaskEvent: LiveData<Event<Unit>> = _editTaskEvent
private val _deleteTaskEvent = MutableLiveData<Event<Unit>>()
val deleteTaskEvent: LiveData<Event<Unit>> = _deleteTaskEvent
private val _snackbarText = MutableLiveData<Event<Int>>()
val snackbarText: LiveData<Event<Int>> = _snackbarText
// This LiveData depends on another so we can use a transformation.
val completed: LiveData<Boolean> = _task.map { input: Task? ->
input?.isCompleted ?: false
}
fun deleteTask() = viewModelScope.launch {
_taskId.value?.let {
tasksRepository.deleteTask(it)
_deleteTaskEvent.value = Event(Unit)
}
}
fun editTask() {
_editTaskEvent.value = Event(Unit)
}
fun setCompleted(completed: Boolean) = viewModelScope.launch {
val task = _task.value ?: return@launch
if (completed) {
tasksRepository.completeTask(task)
showSnackbarMessage(R.string.task_marked_complete)
} else {
tasksRepository.activateTask(task)
showSnackbarMessage(R.string.task_marked_active)
}
}
fun start(taskId: String?) {
// If we're already loading or already loaded, return (might be a config change)
if (_dataLoading.value == true || taskId == _taskId.value) {
return
}
// Trigger the load
_taskId.value = taskId
}
private fun computeResult(taskResult: Result<Task>): Task? {
return if (taskResult is Success) {
taskResult.data
} else {
showSnackbarMessage(R.string.loading_tasks_error)
null
}
}
fun refresh() {
// Refresh the repository and the task will be updated automatically.
_task.value?.let {
_dataLoading.value = true
viewModelScope.launch {
tasksRepository.refreshTask(it.id)
_dataLoading.value = false
}
}
}
private fun showSnackbarMessage(@StringRes message: Int) {
_snackbarText.value = Event(message)
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import android.app.Activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.example.android.architecture.blueprints.todoapp.R
import com.google.android.material.navigation.NavigationView
/**
* Main activity for the todoapp. Holds the Navigation Host Fragment and the Drawer, Toolbar, etc.
*/
class TasksActivity : AppCompatActivity() {
private lateinit var drawerLayout: DrawerLayout
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.tasks_act)
setupNavigationDrawer()
setSupportActionBar(findViewById(R.id.toolbar))
val navController: NavController = findNavController(R.id.nav_host_fragment)
appBarConfiguration =
AppBarConfiguration.Builder(R.id.tasks_fragment_dest, R.id.statistics_fragment_dest)
.setDrawerLayout(drawerLayout)
.build()
setupActionBarWithNavController(navController, appBarConfiguration)
findViewById<NavigationView>(R.id.nav_view)
.setupWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration) ||
super.onSupportNavigateUp()
}
private fun setupNavigationDrawer() {
drawerLayout = (findViewById<DrawerLayout>(R.id.drawer_layout))
.apply {
setStatusBarBackground(R.color.colorPrimaryDark)
}
}
}
// 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
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.databinding.TaskItemBinding
import com.example.android.architecture.blueprints.todoapp.tasks.TasksAdapter.ViewHolder
/**
* Adapter for the task list. Has a reference to the [TasksViewModel] to send actions back to it.
*/
class TasksAdapter(private val viewModel: TasksViewModel) :
ListAdapter<Task, ViewHolder>(TaskDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(viewModel, item)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
class ViewHolder private constructor(val binding: TaskItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(viewModel: TasksViewModel, item: Task) {
binding.viewmodel = viewModel
binding.task = item
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = TaskItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
/**
* Callback for calculating the diff between two non-null items in a list.
*
* Used by ListAdapter to calculate the minimum number of changes between and old list and a new
* list that's been passed to `submitList`.
*/
class TaskDiffCallback : DiffUtil.ItemCallback<Task>() {
override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
return oldItem == newItem
}
}
/*
* Copyright (C) 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.
*/
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
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.example.android.architecture.blueprints.todoapp.EventObserver
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.databinding.TasksFragBinding
import com.example.android.architecture.blueprints.todoapp.util.getViewModelFactory
import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout
import com.example.android.architecture.blueprints.todoapp.util.setupSnackbar
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import timber.log.Timber
/**
* Display a grid of [Task]s. User can choose to view all, active or completed tasks.
*/
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel> { getViewModelFactory() }
private val args: TasksFragmentArgs by navArgs()
private lateinit var viewDataBinding: TasksFragBinding
private lateinit var listAdapter: TasksAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = TasksFragBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
setHasOptionsMenu(true)
return viewDataBinding.root
}
override fun onOptionsItemSelected(item: MenuItem) =
when (item.itemId) {
R.id.menu_clear -> {
viewModel.clearCompletedTasks()
true
}
R.id.menu_filter -> {
showFilteringPopUpMenu()
true
}
R.id.menu_refresh -> {
viewModel.loadTasks(true)
true
}
else -> false
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.tasks_fragment_menu, menu)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Set the lifecycle owner to the lifecycle of the view
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
setupSnackbar()
setupListAdapter()
setupRefreshLayout(viewDataBinding.refreshLayout, viewDataBinding.tasksList)
setupNavigation()
setupFab()
}
private fun setupNavigation() {
viewModel.openTaskEvent.observe(this, EventObserver {
openTaskDetails(it)
})
viewModel.newTaskEvent.observe(this, EventObserver {
navigateToAddNewTask()
})
}
private fun setupSnackbar() {
view?.setupSnackbar(this, viewModel.snackbarText, Snackbar.LENGTH_SHORT)
arguments?.let {
viewModel.showEditResultMessage(args.userMessage)
}
}
private fun showFilteringPopUpMenu() {
val view = activity?.findViewById<View>(R.id.menu_filter) ?: return
PopupMenu(requireContext(), view).run {
menuInflater.inflate(R.menu.filter_tasks, menu)
setOnMenuItemClickListener {
viewModel.setFiltering(
when (it.itemId) {
R.id.active -> TasksFilterType.ACTIVE_TASKS
R.id.completed -> TasksFilterType.COMPLETED_TASKS
else -> TasksFilterType.ALL_TASKS
}
)
true
}
show()
}
}
private fun setupFab() {
activity?.findViewById<FloatingActionButton>(R.id.add_task_fab)?.let {
it.setOnClickListener {
navigateToAddNewTask()
}
}
}
private fun navigateToAddNewTask() {
val action = TasksFragmentDirections
.actionTasksFragmentToAddEditTaskFragment(
null,
resources.getString(R.string.add_task)
)
findNavController().navigate(action)
}
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
private fun setupListAdapter() {
val viewModel = viewDataBinding.viewmodel
if (viewModel != null) {
listAdapter = TasksAdapter(viewModel)
viewDataBinding.tasksList.adapter = listAdapter
} else {
Timber.w("ViewModel not initialized when attempting to set up adapter.")
}
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import android.graphics.Paint
import android.widget.TextView
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.android.architecture.blueprints.todoapp.data.Task
/**
* [BindingAdapter]s for the [Task]s list.
*/
@BindingAdapter("app:items")
fun setItems(listView: RecyclerView, items: List<Task>?) {
items?.let {
(listView.adapter as TasksAdapter).submitList(items)
}
}
@BindingAdapter("app:completedTask")
fun setStyle(textView: TextView, enabled: Boolean) {
if (enabled) {
textView.paintFlags = textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
textView.paintFlags = textView.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import com.example.android.architecture.blueprints.todoapp.Event
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
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 kotlinx.coroutines.launch
/**
* ViewModel for the task list screen.
*/
class TasksViewModel(
private val tasksRepository: TasksRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _forceUpdate = MutableLiveData<Boolean>(false)
private val _items: LiveData<List<Task>> = _forceUpdate.switchMap { forceUpdate ->
if (forceUpdate) {
_dataLoading.value = true
viewModelScope.launch {
tasksRepository.refreshTasks()
_dataLoading.value = false
}
}
tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) }
}
val items: LiveData<List<Task>> = _items
private val _dataLoading = MutableLiveData<Boolean>()
val dataLoading: LiveData<Boolean> = _dataLoading
private val _currentFilteringLabel = MutableLiveData<Int>()
val currentFilteringLabel: LiveData<Int> = _currentFilteringLabel
private val _noTasksLabel = MutableLiveData<Int>()
val noTasksLabel: LiveData<Int> = _noTasksLabel
private val _noTaskIconRes = MutableLiveData<Int>()
val noTaskIconRes: LiveData<Int> = _noTaskIconRes
private val _tasksAddViewVisible = MutableLiveData<Boolean>()
val tasksAddViewVisible: LiveData<Boolean> = _tasksAddViewVisible
private val _snackbarText = MutableLiveData<Event<Int>>()
val snackbarText: LiveData<Event<Int>> = _snackbarText
// Not used at the moment
private val isDataLoadingError = MutableLiveData<Boolean>()
private val _openTaskEvent = MutableLiveData<Event<String>>()
val openTaskEvent: LiveData<Event<String>> = _openTaskEvent
private val _newTaskEvent = MutableLiveData<Event<Unit>>()
val newTaskEvent: LiveData<Event<Unit>> = _newTaskEvent
private var resultMessageShown: Boolean = false
// This LiveData depends on another so we can use a transformation.
val empty: LiveData<Boolean> = Transformations.map(_items) {
it.isEmpty()
}
init {
// Set initial state
setFiltering(getSavedFilterType())
loadTasks(true)
}
/**
* Sets the current task filtering type.
*
* @param requestType Can be [TasksFilterType.ALL_TASKS],
* [TasksFilterType.COMPLETED_TASKS], or
* [TasksFilterType.ACTIVE_TASKS]
*/
fun setFiltering(requestType: TasksFilterType) {
savedStateHandle.set(TASKS_FILTER_SAVED_STATE_KEY, requestType)
// Depending on the filter type, set the filtering label, icon drawables, etc.
when (requestType) {
ALL_TASKS -> {
setFilter(
R.string.label_all, R.string.no_tasks_all,
R.drawable.logo_no_fill, true
)
}
ACTIVE_TASKS -> {
setFilter(
R.string.label_active, R.string.no_tasks_active,
R.drawable.ic_check_circle_96dp, false
)
}
COMPLETED_TASKS -> {
setFilter(
R.string.label_completed, R.string.no_tasks_completed,
R.drawable.ic_verified_user_96dp, false
)
}
}
// Refresh list
loadTasks(false)
}
private fun setFilter(
@StringRes filteringLabelString: Int,
@StringRes noTasksLabelString: Int,
@DrawableRes noTaskIconDrawable: Int,
tasksAddVisible: Boolean
) {
_currentFilteringLabel.value = filteringLabelString
_noTasksLabel.value = noTasksLabelString
_noTaskIconRes.value = noTaskIconDrawable
_tasksAddViewVisible.value = tasksAddVisible
}
fun clearCompletedTasks() {
viewModelScope.launch {
tasksRepository.clearCompletedTasks()
showSnackbarMessage(R.string.completed_tasks_cleared)
}
}
fun completeTask(task: Task, completed: Boolean) = viewModelScope.launch {
if (completed) {
tasksRepository.completeTask(task)
showSnackbarMessage(R.string.task_marked_complete)
} else {
tasksRepository.activateTask(task)
showSnackbarMessage(R.string.task_marked_active)
}
}
/**
* Called by the Data Binding library and the FAB's click listener.
*/
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
/**
* Called by Data Binding.
*/
fun openTask(taskId: String) {
_openTaskEvent.value = Event(taskId)
}
fun showEditResultMessage(result: Int) {
if (resultMessageShown) return
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)
}
resultMessageShown = true
}
private fun showSnackbarMessage(message: Int) {
_snackbarText.value = Event(message)
}
private fun filterTasks(tasksResult: Result<List<Task>>): LiveData<List<Task>> {
// TODO: This is a good case for liveData builder. Replace when stable.
val result = MutableLiveData<List<Task>>()
if (tasksResult is Success) {
isDataLoadingError.value = false
viewModelScope.launch {
result.value = filterItems(tasksResult.data, getSavedFilterType())
}
} else {
result.value = emptyList()
showSnackbarMessage(R.string.loading_tasks_error)
isDataLoadingError.value = true
}
return result
}
/**
* @param forceUpdate Pass in true to refresh the data in the [TasksDataSource]
*/
fun loadTasks(forceUpdate: Boolean) {
_forceUpdate.value = forceUpdate
}
private fun filterItems(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
}
fun refresh() {
_forceUpdate.value = true
}
private fun getSavedFilterType() : TasksFilterType {
return savedStateHandle.get(TASKS_FILTER_SAVED_STATE_KEY) ?: ALL_TASKS
}
}
// Used to save the current filtering in SavedStateHandle.
const val TASKS_FILTER_SAVED_STATE_KEY = "TASKS_FILTER_SAVED_STATE_KEY"
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.util
import androidx.test.espresso.IdlingResource
/**
* Contains a static reference to [IdlingResource], only available in the 'mock' build type.
*/
object EspressoIdlingResource {
private const val RESOURCE = "GLOBAL"
@JvmField
val countingIdlingResource = SimpleCountingIdlingResource(RESOURCE)
fun increment() {
countingIdlingResource.increment()
}
fun decrement() {
if (!countingIdlingResource.isIdleNow) {
countingIdlingResource.decrement()
}
}
}
inline fun <T> wrapEspressoIdlingResource(function: () -> T): T {
// Espresso does not work well with coroutines yet. See
// https://github.com/Kotlin/kotlinx.coroutines/issues/982
EspressoIdlingResource.increment() // Set app as busy.
return try {
function()
} finally {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.util
/**
* Extension functions for Fragment.
*/
import androidx.fragment.app.Fragment
import com.example.android.architecture.blueprints.todoapp.TodoApplication
import com.example.android.architecture.blueprints.todoapp.ViewModelFactory
fun Fragment.getViewModelFactory(): ViewModelFactory {
val repository = (requireContext().applicationContext as TodoApplication).taskRepository
return ViewModelFactory(repository, this)
}
/*
* Copyright (C) 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.
*/
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!")
}
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.util
/**
* Extension functions and Binding Adapters.
*/
import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.example.android.architecture.blueprints.todoapp.Event
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
/**
* Transforms static java function Snackbar.make() to an extension function on View.
*/
fun View.showSnackbar(snackbarText: String, timeLength: Int) {
Snackbar.make(this, snackbarText, timeLength).run {
addCallback(object : Snackbar.Callback() {
override fun onShown(sb: Snackbar?) {
EspressoIdlingResource.increment()
}
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
EspressoIdlingResource.decrement()
}
})
show()
}
}
/**
* Triggers a snackbar message when the value contained by snackbarTaskMessageLiveEvent is modified.
*/
fun View.setupSnackbar(
lifecycleOwner: LifecycleOwner,
snackbarEvent: LiveData<Event<Int>>,
timeLength: Int
) {
snackbarEvent.observe(lifecycleOwner, Observer { event ->
event.getContentIfNotHandled()?.let {
showSnackbar(context.getString(it), timeLength)
}
})
}
fun Fragment.setupRefreshLayout(
refreshLayout: ScrollChildSwipeRefreshLayout,
scrollUpChild: View? = null
) {
refreshLayout.setColorSchemeColors(
ContextCompat.getColor(requireActivity(), R.color.colorPrimary),
ContextCompat.getColor(requireActivity(), R.color.colorAccent),
ContextCompat.getColor(requireActivity(), R.color.colorPrimaryDark)
)
// Set the scrolling view in the custom SwipeRefreshLayout.
scrollUpChild?.let {
refreshLayout.scrollUpChild = it
}
}
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/colorAccent" android:state_checked="true" />
<item android:color="@color/colorGrey" android:state_checked="false" />
</selector>
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 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.
-->
<vector android:alpha="0.49"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FF000000"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zm-7,0c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zm-2,14l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z" />
</vector>
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:alpha="0.49"
android:width="96dp"
android:height="96dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#2E7D32"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm-2,15l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
</vector>
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#2E7D32"
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18V6H3zm3,7h12v-2H6v2z" />
</vector>
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#2E7D32"
android:pathData="M3,13h2v-2H3v2zm0,4h2v-2H3v2zm0,-8h2V7H3v2zm4,4h14v-2H7v2zm0,4h14v-2H7v2zM7,7v2h14V7H7z" />
</vector>
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,18h18v-2H3v2zm0,-5h18v-2H3v2zm0,-7v2h18V6H3z" />
</vector>
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zm4,0h-2V7h2v10zm4,0h-2v-4h2v4z" />
</vector>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 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.
-->
<vector android:alpha="0.80"
android:height="100dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="100dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FF000000"
android:pathData="M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zm4,0h-2V7h2v10zm4,0h-2v-4h2v4z" />
</vector>
<!--
~ Copyright (C) 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zm4,0h-2V7h2v10zm4,0h-2v-4h2v4z" />
</vector>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 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.
-->
<vector
android:alpha="0.49"
android:height="96dp"
android:width="96dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#2E7D32"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4zm-2,16l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/touchFeedback" />
<item android:drawable="@drawable/completedTaskBackground" />
</selector>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/touchFeedback" />
</selector>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 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.
-->
<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
<font
app:fontStyle="normal"
app:fontWeight="400"
app:font="@font/opensans_regular" />
<font
app:fontStyle="normal"
app:fontWeight="700"
app:font="@font/opensans_semibold" />
</font-family>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewmodel"
type="com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:enabled="@{viewmodel.dataLoading}"
app:refreshing="@{viewmodel.dataLoading}">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:visibility="@{viewmodel.dataLoading ? View.GONE : View.VISIBLE}">
<EditText
android:id="@+id/add_task_title_edit_text"
android:background="@null"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title_hint"
android:imeOptions="flagNoExtractUi"
android:maxLines="1"
android:text="@={viewmodel.title}"
android:textStyle="bold"
android:textAppearance="@style/TextAppearance.AppCompat.Title" />
<EditText
android:id="@+id/add_task_description_edit_text"
android:background="@null"
android:layout_width="match_parent"
android:layout_height="350dp"
android:gravity="top"
android:hint="@string/description_hint"
android:imeOptions="flagNoExtractUi"
android:text="@={viewmodel.description}" />
</LinearLayout>
</ScrollView>
</com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/save_task_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_done"
android:onClick="@{() -> viewmodel.saveTask()}"
app:fabSize="normal"
app:layout_anchor="@id/refresh_layout"
app:layout_anchorGravity="bottom|right|end" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/header_height"
android:background="?attr/colorPrimaryDark"
android:gravity="bottom"
android:orientation="vertical"
android:padding="@dimen/header_padding"
android:theme="@style/ThemeOverlay.AppCompat.Dark">
<ImageView
android:layout_width="@dimen/header_image_width"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/logo_no_fill"
android:contentDescription="@string/tasks_header_image_content_description" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:text="@string/navigation_view_header_title"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewmodel"
type="com.example.android.architecture.blueprints.todoapp.statistics.StatisticsViewModel" />
</data>
<com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout
android:id="@+id/refresh_layout"
app:refreshing="@{viewmodel.dataLoading}"
app:onRefreshListener="@{viewmodel::refresh}"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
<LinearLayout
android:id="@+id/statistics_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="@{viewmodel.dataLoading ? View.GONE : View.VISIBLE}">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/statistics_no_tasks"
android:textAppearance="?android:attr/textAppearanceMedium"
android:visibility="@{viewmodel.empty ? View.VISIBLE : View.GONE}" />
<TextView
android:id="@+id/stats_active_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="1dp"
android:text="@{@string/statistics_active_tasks(viewmodel.activeTasksPercent)}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:visibility="@{viewmodel.empty ? View.GONE : View.VISIBLE}" />
<!-- android:paddingTop specified to temporarily work around -->
<!-- https://github.com/robolectric/robolectric/issues/4588 -->
<TextView
android:id="@+id/stats_completed_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="1dp"
android:text="@{@string/statistics_completed_tasks(viewmodel.completedTasksPercent)}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:visibility="@{viewmodel.empty ? View.GONE : View.VISIBLE}" />
</LinearLayout>
</LinearLayout>
</com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.widget.CompoundButton" />
<variable
name="task"
type="com.example.android.architecture.blueprints.todoapp.data.Task" />
<variable
name="viewmodel"
type="com.example.android.architecture.blueprints.todoapp.tasks.TasksViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:orientation="horizontal"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/list_item_padding"
android:paddingTop="@dimen/list_item_padding"
android:onClick="@{() -> viewmodel.openTask(task.id)}">
<CheckBox
android:id="@+id/complete_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:onClick="@{(view) -> viewmodel.completeTask(task, ((CompoundButton)view).isChecked())}"
android:checked="@{task.completed}" />
<TextView
android:id="@+id/title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
android:text="@{task.titleForList}"
app:completedTask="@{task.completed}" />
</LinearLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="android.widget.CompoundButton" />
<variable
name="viewmodel"
type="com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:onRefreshListener="@{viewmodel::refresh}"
app:refreshing="@{viewmodel.dataLoading}">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:visibility="@{viewmodel.isDataAvailable ? View.GONE : View.VISIBLE}">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_data"
android:textAppearance="?android:attr/textAppearanceLarge"
android:visibility="@{viewmodel.dataLoading ? View.GONE : View.VISIBLE}" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:visibility="@{viewmodel.isDataAvailable ? View.VISIBLE : View.GONE}">
<!-- android:paddingTop specified to temporarily work around -->
<!-- https://github.com/robolectric/robolectric/issues/4588 -->
<CheckBox
android:id="@+id/task_detail_complete_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:checked="@{viewmodel.completed}"
android:onClick="@{(view) -> viewmodel.setCompleted(((CompoundButton)view).isChecked())}"
android:paddingTop="1dp" />
<TextView
android:id="@+id/task_detail_title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/task_detail_complete_checkbox"
android:paddingTop="1dp"
android:text="@{viewmodel.task.title}"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/task_detail_description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/task_detail_title_text"
android:layout_toRightOf="@id/task_detail_complete_checkbox"
android:paddingTop="1dp"
android:text="@{viewmodel.task.description}"
android:textAppearance="?android:attr/textAppearanceMedium" />
</RelativeLayout>
</LinearLayout>
</com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/edit_task_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_edit"
app:fabSize="normal"
app:layout_anchor="@id/refresh_layout"
app:layout_anchorGravity="bottom|right|end" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".tasks.TasksActivity"
tools:openDrawer="start">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/Toolbar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</LinearLayout>
<!-- Navigation Drawer -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header"
app:itemIconTint="@drawable/drawer_item_color"
app:itemTextColor="@drawable/drawer_item_color"
app:menu="@menu/drawer_actions" />
</androidx.drawerlayout.widget.DrawerLayout>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="androidx.core.content.ContextCompat" />
<variable
name="viewmodel"
type="com.example.android.architecture.blueprints.todoapp.tasks.TasksViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:onRefreshListener="@{viewmodel::refresh}"
app:refreshing="@{viewmodel.dataLoading}">
<RelativeLayout
android:id="@+id/tasks_container_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:orientation="vertical">
<LinearLayout
android:id="@+id/tasks_linear_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="@{viewmodel.empty ? View.GONE : View.VISIBLE}">
<TextView
android:id="@+id/filtering_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/list_item_padding"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginRight="@dimen/list_item_padding"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:gravity="center_vertical"
android:text="@{context.getString(viewmodel.currentFilteringLabel)}"
android:textAppearance="@style/TextAppearance.AppCompat.Title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tasks_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:items="@{viewmodel.items}" />
</LinearLayout>
<LinearLayout
android:id="@+id/no_tasks_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical"
android:visibility="@{viewmodel.empty ? View.VISIBLE : View.GONE}">
<ImageView
android:id="@+id/no_tasks_icon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center"
android:src="@{ContextCompat.getDrawable(context, viewmodel.noTaskIconRes)}"
android:contentDescription="@string/no_tasks_image_content_description" />
<TextView
android:id="@+id/no_tasks_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/list_item_padding"
android:text="@{context.getString(viewmodel.noTasksLabel)}" />
</LinearLayout>
</RelativeLayout>
</com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_task_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_add"
app:fabSize="normal"
app:layout_anchor="@id/refresh_layout"
app:layout_anchorGravity="bottom|right|end" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@id/tasks_fragment_dest"
android:icon="@drawable/ic_list"
android:title="@string/list_title" />
<item
android:id="@id/statistics_fragment_dest"
android:icon="@drawable/ic_statistics"
android:title="@string/statistics_title" />
</menu>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/all"
android:title="@string/nav_all" />
<item
android:id="@+id/active"
android:title="@string/nav_active" />
<item
android:id="@+id/completed"
android:title="@string/nav_completed" />
</menu>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_delete"
android:icon="@drawable/trash_icon"
android:title="@string/menu_delete_task"
app:showAsAction="always" />
</menu>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_filter"
android:title="@string/menu_filter"
android:icon="@drawable/ic_filter_list"
app:showAsAction="always" />
<item
android:id="@+id/menu_clear"
android:title="@string/menu_clear"
app:showAsAction="never" />
<item
android:id="@+id/menu_refresh"
android:title="@string/refresh"
app:showAsAction="never" />
</menu>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 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.
-->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/tasks_fragment_dest">
<fragment
android:id="@+id/task_detail_fragment_dest"
android:name="com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailFragment"
android:label="Task Details">
<action
android:id="@+id/action_taskDetailFragment_to_addEditTaskFragment"
app:destination="@id/add_edit_task_fragment_dest" />
<argument
android:name="taskId"
app:argType="string" />
<action
android:id="@+id/action_taskDetailFragment_to_tasksFragment"
app:destination="@id/tasks_fragment_dest" />
</fragment>
<fragment
android:id="@+id/statistics_fragment_dest"
android:name="com.example.android.architecture.blueprints.todoapp.statistics.StatisticsFragment"
android:label="@string/app_name">
<action
android:id="@+id/action_statisticsFragment_to_tasksFragment"
app:destination="@id/tasks_fragment_dest"
/>
</fragment>
<fragment
android:id="@+id/tasks_fragment_dest"
android:name="com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment"
android:label="@string/app_name">
<action
android:id="@+id/action_tasksFragment_to_statisticsFragment"
app:destination="@id/statistics_fragment_dest" />
<action
android:id="@+id/action_tasksFragment_to_taskDetailFragment"
app:destination="@id/task_detail_fragment_dest" />
<action
android:id="@+id/action_tasksFragment_to_addEditTaskFragment"
app:destination="@id/add_edit_task_fragment_dest" />
<argument
android:name="userMessage"
app:argType="integer"
android:defaultValue="0" />
</fragment>
<fragment
android:id="@+id/add_edit_task_fragment_dest"
android:name="com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskFragment"
android:label="{title}">
<argument
android:name="taskId"
app:argType="string"
app:nullable="true" />
<argument
android:name="title"
app:argType="string"
app:nullable="false" />
<action
android:id="@+id/action_addEditTaskFragment_to_tasksFragment"
app:destination="@id/tasks_fragment_dest"
/>
</fragment>
<argument
android:name="userMessage"
android:defaultValue="0" />
</navigation>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<resources>
<style name="AppTheme" parent="Base.AppTheme" />
<style name="AppTheme.OverlapSystemBar" parent="Base.AppTheme">
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:fitsSystemWindows">true</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
<dimen name="fab_margin">24dp</dimen>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 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.
-->
<resources>
<declare-styleable name="ScrollChildSwipeRefreshLayout">
<attr name="refreshing" format="boolean" />
</declare-styleable>
</resources>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<resources>
<color name="colorPrimary">#FFFFF0</color>
<color name="colorPrimaryDark">#263238</color>
<color name="colorAccent">#2E7D32</color>
<color name="colorTextPrimary">#000000</color>
<color name="colorGrey">#757575</color>
<drawable name="completedTaskBackground">#CCCCCC</drawable>
<drawable name="touchFeedback">#CFD8DC</drawable>
</resources>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="list_item_padding">8dp</dimen>
<dimen name="header_height">192dp</dimen>
<dimen name="header_padding">16dp</dimen>
<dimen name="header_image_width">100dp</dimen>
</resources>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<resources>
<string name="app_name">Todo</string>
<string name="add_task">New Task</string>
<string name="edit_task">Edit Task</string>
<string name="task_marked_complete">Task marked complete</string>
<string name="task_marked_active">Task marked active</string>
<string name="loading_tasks_error">Error while loading tasks</string>
<string name="completed_tasks_cleared">Completed tasks cleared</string>
<string name="menu_filter">Filter</string>
<string name="menu_clear">Clear completed</string>
<string name="menu_delete_task">Delete task</string>
<string name="navigation_view_header_title">Todo</string>
<string name="title_hint">Title</string>
<string name="description_hint">Enter your task here.</string>
<string name="empty_task_message">Tasks cannot be empty</string>
<string name="successfully_saved_task_message">Task saved</string>
<string name="list_title">Task List</string>
<string name="statistics_title">Statistics</string>
<string name="statistics_no_tasks">You have no tasks.</string>
<string name="statistics_active_tasks">Active tasks: %.1f%%</string>
<string name="statistics_completed_tasks">Completed tasks: %.1f%%</string>
<string name="statistics_error">Error loading statistics.</string>
<string name="no_data">No data</string>
<string name="loading">LOADING</string>
<string-array name="list_tasks_array">
<item>@string/nav_all</item>
<item>@string/nav_active</item>
<item>@string/nav_completed</item>
</string-array>
<string name="nav_all">All</string>
<string name="nav_active">Active</string>
<string name="nav_completed">Completed</string>
<string name="label_all">All Tasks</string>
<string name="label_active">Active Tasks</string>
<string name="label_completed">Completed Tasks</string>
<string name="no_tasks_all">You have no tasks!</string>
<string name="no_tasks_active">You have no active tasks!</string>
<string name="no_tasks_completed">You have no completed tasks!</string>
<string name="refresh">Refresh</string>
<string name="successfully_deleted_task_message">Task was deleted</string>
<string name="successfully_added_task_message">Task added</string>
<string name="no_data_description" />
<!-- Content Descriptions -->
<string name="tasks_header_image_content_description">Tasks header image</string>
<string name="no_tasks_image_content_description">No tasks image</string>
</resources>
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --><style name="AppTheme" parent="Base.AppTheme" />
<style name="AppTheme.OverlapSystemBar" parent="Base.AppTheme" />
<style name="Base.AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<!-- For Android sdk versions 26+. -->
<item name="android:fontFamily" tools:targetApi="jelly_bean">@font/opensans_font</item>
<!-- Target Android sdk versions < 26 and > 14. -->
<item name="fontFamily">@font/opensans_font</item>
<!-- sets the style for the overflow button -->
<item name="actionOverflowButtonStyle">@style/actionOverflowButtonStyle</item>
</style>
<!-- This style defines the tint color of the overflow menu button -->
<style name="actionOverflowButtonStyle" parent="@style/Widget.AppCompat.ActionButton.Overflow">
<item name="android:tint">@color/colorAccent</item>
</style>
<style name="Toolbar" parent="ThemeOverlay.AppCompat.Light" />
</resources>
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.room.Room
import com.example.android.architecture.blueprints.todoapp.data.FakeTasksRemoteDataSource
import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksLocalDataSource
import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase
import kotlinx.coroutines.runBlocking
/**
* A Service Locator for the [TasksRepository]. This is the mock version, with a
* [FakeTasksRemoteDataSource].
*/
object ServiceLocator {
private val lock = Any()
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
fun provideTasksRepository(context: Context): TasksRepository {
synchronized(this) {
return tasksRepository ?: tasksRepository ?: createTasksRepository(context)
}
}
private fun createTasksRepository(context: Context): TasksRepository {
val newRepo = DefaultTasksRepository(FakeTasksRemoteDataSource, createTaskLocalDataSource(context))
tasksRepository = newRepo
return newRepo
}
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
private fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, "Tasks.db"
).build()
database = result
return result
}
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
FakeTasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
import java.util.LinkedHashMap
/**
* Implementation of a remote data source with static access to the data for easy testing.
*/
object FakeTasksRemoteDataSource : TasksDataSource {
private var TASKS_SERVICE_DATA: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
override suspend fun refreshTasks() {
observableTasks.postValue(getTasks())
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTask(taskId: String): Result<Task> {
TASKS_SERVICE_DATA[taskId]?.let {
return Success(it)
}
return Error(Exception("Could not find task"))
}
override suspend fun getTasks(): Result<List<Task>> {
return Success(TASKS_SERVICE_DATA.values.toList())
}
override suspend fun saveTask(task: Task) {
TASKS_SERVICE_DATA[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
TASKS_SERVICE_DATA[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source.
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
TASKS_SERVICE_DATA[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
// Not required for the remote data source.
}
override suspend fun clearCompletedTasks() {
TASKS_SERVICE_DATA = TASKS_SERVICE_DATA.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteTask(taskId: String) {
TASKS_SERVICE_DATA.remove(taskId)
refreshTasks()
}
override suspend fun deleteAllTasks() {
TASKS_SERVICE_DATA.clear()
refreshTasks()
}
}
/*
* Copyright (C) 2017 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.
*/
package com.example.android.architecture.blueprints.todoapp
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.room.Room
import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksLocalDataSource
import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase
import com.example.android.architecture.blueprints.todoapp.data.source.remote.TasksRemoteDataSource
import kotlinx.coroutines.runBlocking
/**
* A Service Locator for the [TasksRepository]. This is the prod version, with a
* the "real" [TasksRemoteDataSource].
*/
object ServiceLocator {
private val lock = Any()
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
fun provideTasksRepository(context: Context): TasksRepository {
synchronized(this) {
return tasksRepository ?: tasksRepository ?: createTasksRepository(context)
}
}
private fun createTasksRepository(context: Context): TasksRepository {
val newRepo =
DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
tasksRepository = newRepo
return newRepo
}
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
private fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, DB_NAME
).build()
database = result
return result
}
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}
}
private const val DB_NAME = "Tasks.db"
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source.remote
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
import kotlinx.coroutines.delay
/**
* Implementation of the data source that adds a latency simulating network.
*/
object TasksRemoteDataSource : TasksDataSource {
private const val SERVICE_LATENCY_IN_MILLIS = 2000L
private var TASKS_SERVICE_DATA = LinkedHashMap<String, Task>(2)
init {
addTask("Build tower in Pisa", "Ground looks good, no foundation work required.")
addTask("Finish bridge in Tacoma", "Found awesome girders at half the cost!")
}
private val observableTasks = MutableLiveData<Result<List<Task>>>()
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTasks(): Result<List<Task>> {
// Simulate network by delaying the execution.
val tasks = TASKS_SERVICE_DATA.values.toList()
delay(SERVICE_LATENCY_IN_MILLIS)
return Success(tasks)
}
override suspend fun getTask(taskId: String): Result<Task> {
// Simulate network by delaying the execution.
delay(SERVICE_LATENCY_IN_MILLIS)
TASKS_SERVICE_DATA[taskId]?.let {
return Success(it)
}
return Error(Exception("Task not found"))
}
private fun addTask(title: String, description: String) {
val newTask = Task(title, description)
TASKS_SERVICE_DATA[newTask.id] = newTask
}
override suspend fun saveTask(task: Task) {
TASKS_SERVICE_DATA[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
TASKS_SERVICE_DATA[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
TASKS_SERVICE_DATA[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
// Not required for the remote data source
}
override suspend fun clearCompletedTasks() {
TASKS_SERVICE_DATA = TASKS_SERVICE_DATA.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteAllTasks() {
TASKS_SERVICE_DATA.clear()
}
override suspend fun deleteTask(taskId: String) {
TASKS_SERVICE_DATA.remove(taskId)
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import kotlin.coroutines.ContinuationInterceptor
/**
* Sets the main coroutines dispatcher to a [TestCoroutineScope] for unit testing. A
* [TestCoroutineScope] provides control over the execution of coroutines.
*
* Declare it as a JUnit Rule:
*
* ```
* @get:Rule
* var mainCoroutineRule = MainCoroutineRule()
* ```
*
* Use it directly as a [TestCoroutineScope]:
*
* ```
* mainCoroutineRule.pauseDispatcher()
* ...
* mainCoroutineRule.resumeDispatcher()
* ...
* mainCoroutineRule.runBlockingTest { }
* ...
*
* ```
*/
@ExperimentalCoroutinesApi
class MainCoroutineRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.addedittask
import android.content.Context
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.clearText
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import com.example.android.architecture.blueprints.todoapp.tasks.ADD_EDIT_RESULT_OK
import com.example.android.architecture.blueprints.todoapp.util.getTasksBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.robolectric.annotation.LooperMode
import org.robolectric.annotation.TextLayoutMode
/**
* Integration test for the Add Task screen.
*/
@RunWith(AndroidJUnit4::class)
@MediumTest
@LooperMode(LooperMode.Mode.PAUSED)
@TextLayoutMode(TextLayoutMode.Mode.REALISTIC)
@ExperimentalCoroutinesApi
class AddEditTaskFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun emptyTask_isNotSaved() {
// GIVEN - On the "Add Task" screen.
val bundle = AddEditTaskFragmentArgs(
null,
getApplicationContext<Context>().getString(R.string.add_task)
).toBundle()
launchFragmentInContainer<AddEditTaskFragment>(bundle, R.style.AppTheme)
// WHEN - Enter invalid title and description combination and click save
onView(withId(R.id.add_task_title_edit_text)).perform(clearText())
onView(withId(R.id.add_task_description_edit_text)).perform(clearText())
onView(withId(R.id.save_task_fab)).perform(click())
// THEN - Entered Task is still displayed (a correct task would close it).
onView(withId(R.id.add_task_title_edit_text)).check(matches(isDisplayed()))
}
@Test
fun validTask_navigatesBack() {
// GIVEN - On the "Add Task" screen.
val navController = mock(NavController::class.java)
launchFragment(navController)
// WHEN - Valid title and description combination and click save
onView(withId(R.id.add_task_title_edit_text)).perform(replaceText("title"))
onView(withId(R.id.add_task_description_edit_text)).perform(replaceText("description"))
onView(withId(R.id.save_task_fab)).perform(click())
// THEN - Verify that we navigated back to the tasks screen.
verify(navController).navigate(
AddEditTaskFragmentDirections
.actionAddEditTaskFragmentToTasksFragment(ADD_EDIT_RESULT_OK)
)
}
private fun launchFragment(navController: NavController?) {
val bundle = AddEditTaskFragmentArgs(
null,
getApplicationContext<Context>().getString(R.string.add_task)
).toBundle()
val scenario = launchFragmentInContainer<AddEditTaskFragment>(bundle, R.style.AppTheme)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
}
@Test
fun validTask_isSaved() {
// GIVEN - On the "Add Task" screen.
val navController = mock(NavController::class.java)
launchFragment(navController)
// WHEN - Valid title and description combination and click save
onView(withId(R.id.add_task_title_edit_text)).perform(replaceText("title"))
onView(withId(R.id.add_task_description_edit_text)).perform(replaceText("description"))
onView(withId(R.id.save_task_fab)).perform(click())
// THEN - Verify that the repository saved the task
val tasks = (repository.getTasksBlocking(true) as Result.Success).data
assertEquals(tasks.size, 1)
assertEquals(tasks[0].title, "title")
assertEquals(tasks[0].description, "description")
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap
/**
* Implementation of a remote data source with static access to the data for easy testing.
*/
class FakeRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private var shouldReturnError = false
private val observableTasks = MutableLiveData<Result<List<Task>>>()
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { refreshTasks() }
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
tasksServiceData[taskId]?.let {
return Success(it)
}
return Error(Exception("Could not find task"))
}
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
return Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
tasksServiceData[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source.
throw NotImplementedError()
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
tasksServiceData[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
throw NotImplementedError()
}
override suspend fun clearCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
refreshTasks()
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
refreshTasks()
}
@VisibleForTesting
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source.local
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class TasksDaoTest {
private lateinit var database: ToDoDatabase
// Set the main coroutines dispatcher for unit testing.
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
// Executes each task synchronously using Architecture Components.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun initDb() {
// using an in-memory database because the information stored here disappears when the
// process is killed
database = Room.inMemoryDatabaseBuilder(
getApplicationContext(),
ToDoDatabase::class.java
).allowMainThreadQueries().build()
}
@After
fun closeDb() = database.close()
@Test
fun insertTaskAndGetById() = runBlockingTest {
// GIVEN - insert a task
val task = Task("title", "description")
database.taskDao().insertTask(task)
// WHEN - Get the task by id from the database
val loaded = database.taskDao().getTaskById(task.id)
// THEN - The loaded data contains the expected values
assertThat<Task>(loaded as Task, notNullValue())
assertThat(loaded.id, `is`(task.id))
assertThat(loaded.title, `is`(task.title))
assertThat(loaded.description, `is`(task.description))
assertThat(loaded.isCompleted, `is`(task.isCompleted))
}
@Test
fun insertTaskReplacesOnConflict() = runBlockingTest {
// Given that a task is inserted
val task = Task("title", "description")
database.taskDao().insertTask(task)
// When a task with the same id is inserted
val newTask = Task("title2", "description2", true, task.id)
database.taskDao().insertTask(newTask)
// THEN - The loaded data contains the expected values
val loaded = database.taskDao().getTaskById(task.id)
assertThat(loaded?.id, `is`(task.id))
assertThat(loaded?.title, `is`("title2"))
assertThat(loaded?.description, `is`("description2"))
assertThat(loaded?.isCompleted, `is`(true))
}
@Test
fun insertTaskAndGetTasks() = runBlockingTest {
// GIVEN - insert a task
val task = Task("title", "description")
database.taskDao().insertTask(task)
// WHEN - Get tasks from the database
val tasks = database.taskDao().getTasks()
// THEN - There is only 1 task in the database, and contains the expected values
assertThat(tasks.size, `is`(1))
assertThat(tasks[0].id, `is`(task.id))
assertThat(tasks[0].title, `is`(task.title))
assertThat(tasks[0].description, `is`(task.description))
assertThat(tasks[0].isCompleted, `is`(task.isCompleted))
}
@Test
fun updateTaskAndGetById() = runBlockingTest {
// When inserting a task
val originalTask = Task("title", "description")
database.taskDao().insertTask(originalTask)
// When the task is updated
val updatedTask = Task("new title", "new description", true, originalTask.id)
database.taskDao().updateTask(updatedTask)
// THEN - The loaded data contains the expected values
val loaded = database.taskDao().getTaskById(originalTask.id)
assertThat(loaded?.id, `is`(originalTask.id))
assertThat(loaded?.title, `is`("new title"))
assertThat(loaded?.description, `is`("new description"))
assertThat(loaded?.isCompleted, `is`(true))
}
@Test
fun updateCompletedAndGetById() = runBlockingTest {
// When inserting a task
val task = Task("title", "description", true)
database.taskDao().insertTask(task)
// When the task is updated
database.taskDao().updateCompleted(task.id, false)
// THEN - The loaded data contains the expected values
val loaded = database.taskDao().getTaskById(task.id)
assertThat(loaded?.id, `is`(task.id))
assertThat(loaded?.title, `is`(task.title))
assertThat(loaded?.description, `is`(task.description))
assertThat(loaded?.isCompleted, `is`(false))
}
@Test
fun deleteTaskByIdAndGettingTasks() = runBlockingTest {
// Given a task inserted
val task = Task("title", "description")
database.taskDao().insertTask(task)
// When deleting a task by id
database.taskDao().deleteTaskById(task.id)
// THEN - The list is empty
val tasks = database.taskDao().getTasks()
assertThat(tasks.isEmpty(), `is`(true))
}
@Test
fun deleteTasksAndGettingTasks() = runBlockingTest {
// Given a task inserted
database.taskDao().insertTask(Task("title", "description"))
// When deleting all tasks
database.taskDao().deleteTasks()
// THEN - The list is empty
val tasks = database.taskDao().getTasks()
assertThat(tasks.isEmpty(), `is`(true))
}
@Test
fun deleteCompletedTasksAndGettingTasks() = runBlockingTest {
// Given a completed task inserted
database.taskDao().insertTask(Task("completed", "task", true))
// When deleting completed tasks
database.taskDao().deleteCompletedTasks()
// THEN - The list is empty
val tasks = database.taskDao().getTasks()
assertThat(tasks.isEmpty(), `is`(true))
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source.local
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
import com.example.android.architecture.blueprints.todoapp.data.succeeded
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.CoreMatchers.`is`
import org.junit.After
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Integration test for the [TasksDataSource].
*/
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@MediumTest
class TasksLocalDataSourceTest {
private lateinit var localDataSource: TasksLocalDataSource
private lateinit var database: ToDoDatabase
// Set the main coroutines dispatcher for unit testing.
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
// Executes each task synchronously using Architecture Components.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun setup() {
// using an in-memory database for testing, since it doesn't survive killing the process
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
ToDoDatabase::class.java
)
.allowMainThreadQueries()
.build()
localDataSource = TasksLocalDataSource(database.taskDao(), Dispatchers.Main)
}
@After
fun cleanUp() {
database.close()
}
@Test
fun saveTask_retrievesTask() = runBlockingTest {
// GIVEN - a new task saved in the database
val newTask = Task("title", "description", true)
localDataSource.saveTask(newTask)
// WHEN - Task retrieved by ID
val result = localDataSource.getTask(newTask.id)
// THEN - Same task is returned
assertThat(result.succeeded, `is`(true))
result as Success
assertThat(result.data.title, `is`("title"))
assertThat(result.data.description, `is`("description"))
assertThat(result.data.isCompleted, `is`(true))
}
@Test
fun completeTask_retrievedTaskIsComplete() = runBlockingTest {
// Given a new task in the persistent repository
val newTask = Task("title")
localDataSource.saveTask(newTask)
// When completed in the persistent repository
localDataSource.completeTask(newTask)
val result = localDataSource.getTask(newTask.id)
// Then the task can be retrieved from the persistent repository and is complete
assertThat(result.succeeded, `is`(true))
result as Success
assertThat(result.data.title, `is`(newTask.title))
assertThat(result.data.isCompleted, `is`(true))
}
@Test
fun activateTask_retrievedTaskIsActive() = runBlockingTest {
// Given a new completed task in the persistent repository
val newTask = Task("Some title", "Some description", true)
localDataSource.saveTask(newTask)
localDataSource.activateTask(newTask)
// Then the task can be retrieved from the persistent repository and is active
val result = localDataSource.getTask(newTask.id)
assertThat(result.succeeded, `is`(true))
result as Success
assertThat(result.data.title, `is`("Some title"))
assertThat(result.data.isCompleted, `is`(false))
}
@Test
fun clearCompletedTask_taskNotRetrievable() = runBlockingTest {
// Given 2 new completed tasks and 1 active task in the persistent repository
val newTask1 = Task("title")
val newTask2 = Task("title2")
val newTask3 = Task("title3")
localDataSource.saveTask(newTask1)
localDataSource.completeTask(newTask1)
localDataSource.saveTask(newTask2)
localDataSource.completeTask(newTask2)
localDataSource.saveTask(newTask3)
// When completed tasks are cleared in the repository
localDataSource.clearCompletedTasks()
// Then the completed tasks cannot be retrieved and the active one can
assertThat(localDataSource.getTask(newTask1.id).succeeded, `is`(false))
assertThat(localDataSource.getTask(newTask2.id).succeeded, `is`(false))
val result3 = localDataSource.getTask(newTask3.id)
assertThat(result3.succeeded, `is`(true))
result3 as Success
assertThat(result3.data, `is`(newTask3))
}
@Test
fun deleteAllTasks_emptyListOfRetrievedTask() = runBlockingTest {
// Given a new task in the persistent repository and a mocked callback
val newTask = Task("title")
localDataSource.saveTask(newTask)
// When all tasks are deleted
localDataSource.deleteAllTasks()
// Then the retrieved tasks is an empty list
val result = localDataSource.getTasks() as Success
assertThat(result.data.isEmpty(), `is`(true))
}
@Test
fun getTasks_retrieveSavedTasks() = runBlockingTest {
// Given 2 new tasks in the persistent repository
val newTask1 = Task("title")
val newTask2 = Task("title")
localDataSource.saveTask(newTask1)
localDataSource.saveTask(newTask2)
// Then the tasks can be retrieved from the persistent repository
val results = localDataSource.getTasks() as Success<List<Task>>
val tasks = results.data
assertThat(tasks.size, `is`(2))
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.statistics
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import com.example.android.architecture.blueprints.todoapp.util.DataBindingIdlingResource
import com.example.android.architecture.blueprints.todoapp.util.monitorFragment
import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* Integration test for the statistics screen.
*/
@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class StatisticsFragmentTest {
private lateinit var repository: TasksRepository
// An Idling Resource that waits for Data Binding to have no pending bindings
private val dataBindingIdlingResource = DataBindingIdlingResource()
@Before
fun initRepository() {
repository = FakeRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
/**
* Idling resources tell Espresso that the app is idle or busy. This is needed when operations
* are not scheduled in the main Looper (for example when executed on a different thread).
*/
@Before
fun registerIdlingResource() {
IdlingRegistry.getInstance().register(dataBindingIdlingResource)
}
/**
* Unregister your Idling Resource so it can be garbage collected and does not leak any memory.
*/
@After
fun unregisterIdlingResource() {
IdlingRegistry.getInstance().unregister(dataBindingIdlingResource)
}
@Test
fun tasks_showsNonEmptyMessage() {
// Given some tasks
repository.apply {
saveTaskBlocking(Task("Title1", "Description1", false))
saveTaskBlocking(Task("Title2", "Description2", true))
}
val scenario = launchFragmentInContainer<StatisticsFragment>(Bundle(), R.style.AppTheme)
dataBindingIdlingResource.monitorFragment(scenario)
val expectedActiveTaskText = getApplicationContext<Context>()
.getString(R.string.statistics_active_tasks, 50.0f)
val expectedCompletedTaskText = getApplicationContext<Context>()
.getString(R.string.statistics_completed_tasks, 50.0f)
// check that both info boxes are displayed and contain the correct info
onView(withId(R.id.stats_active_text)).check(matches(isDisplayed()))
onView(withId(R.id.stats_active_text)).check(matches(withText(expectedActiveTaskText)))
onView(withId(R.id.stats_completed_text)).check(matches(isDisplayed()))
onView(withId(R.id.stats_completed_text))
.check(matches(withText(expectedCompletedTaskText)))
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.taskdetail
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsNot.not
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* Integration test for the Task Details screen.
*/
@MediumTest
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class TaskDetailFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun activeTaskDetails_DisplayedInUi() {
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTaskBlocking(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}
@Test
fun completedTaskDetails_DisplayedInUi() {
// GIVEN - Add completed task to the DB
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
repository.saveTaskBlocking(completedTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.ServiceLocator
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import com.example.android.architecture.blueprints.todoapp.util.saveTaskBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher
import org.hamcrest.core.IsNot.not
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.robolectric.annotation.LooperMode
import org.robolectric.annotation.TextLayoutMode
/**
* Integration test for the Task List screen.
*/
// TODO - Use FragmentScenario, see: https://github.com/android/android-test/issues/291
@RunWith(AndroidJUnit4::class)
@MediumTest
@LooperMode(LooperMode.Mode.PAUSED)
@TextLayoutMode(TextLayoutMode.Mode.REALISTIC)
@ExperimentalCoroutinesApi
class TasksFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun displayTask_whenRepositoryHasData() {
// GIVEN - One task already in the repository
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))
// WHEN - On startup
launchActivity()
// THEN - Verify task is displayed on screen
onView(withText("TITLE1")).check(matches(isDisplayed()))
}
@Test
fun displayActiveTask() {
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))
launchActivity()
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_active)).perform(click())
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_completed)).perform(click())
onView(withText("TITLE1")).check(matches(not(isDisplayed())))
}
@Test
fun displayCompletedTask() {
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1", true))
launchActivity()
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_active)).perform(click())
onView(withText("TITLE1")).check(matches(not(isDisplayed())))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_completed)).perform(click())
onView(withText("TITLE1")).check(matches(isDisplayed()))
}
@Test
fun deleteOneTask() {
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))
launchActivity()
// Open it in details view
onView(withText("TITLE1")).perform(click())
// Click delete task in menu
onView(withId(R.id.menu_delete)).perform(click())
// Verify it was deleted
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_all)).perform(click())
onView(withText("TITLE1")).check(doesNotExist())
}
@Test
fun deleteOneOfTwoTasks() {
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))
repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION2"))
launchActivity()
// Open it in details view
onView(withText("TITLE1")).perform(click())
// Click delete task in menu
onView(withId(R.id.menu_delete)).perform(click())
// Verify it was deleted
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_all)).perform(click())
onView(withText("TITLE1")).check(doesNotExist())
// but not the other one
onView(withText("TITLE2")).check(matches(isDisplayed()))
}
@Test
fun markTaskAsComplete() {
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))
launchActivity()
// Mark the task as complete
onView(checkboxWithText("TITLE1")).perform(click())
// Verify task is shown as complete
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_all)).perform(click())
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_active)).perform(click())
onView(withText("TITLE1")).check(matches(not(isDisplayed())))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_completed)).perform(click())
onView(withText("TITLE1")).check(matches(isDisplayed()))
}
@Test
fun markTaskAsActive() {
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1", true))
launchActivity()
// Mark the task as active
onView(checkboxWithText("TITLE1")).perform(click())
// Verify task is shown as active
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_all)).perform(click())
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_active)).perform(click())
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_completed)).perform(click())
onView(withText("TITLE1")).check(matches(not(isDisplayed())))
}
@Test
fun showAllTasks() {
// Add one active task and one completed task
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))
repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION2", true))
launchActivity()
// Verify that both of our tasks are shown
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_all)).perform(click())
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withText("TITLE2")).check(matches(isDisplayed()))
}
@Test
fun showActiveTasks() {
// Add 2 active tasks and one completed task
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))
repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION2"))
repository.saveTaskBlocking(Task("TITLE3", "DESCRIPTION3", true))
launchActivity()
// Verify that the active tasks (but not the completed task) are shown
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_active)).perform(click())
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withText("TITLE2")).check(matches(isDisplayed()))
onView(withText("TITLE3")).check(doesNotExist())
}
@Test
fun showCompletedTasks() {
// Add one active task and 2 completed tasks
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))
repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION2", true))
repository.saveTaskBlocking(Task("TITLE3", "DESCRIPTION3", true))
launchActivity()
// Verify that the completed tasks (but not the active task) are shown
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_completed)).perform(click())
onView(withText("TITLE1")).check(doesNotExist())
onView(withText("TITLE2")).check(matches(isDisplayed()))
onView(withText("TITLE3")).check(matches(isDisplayed()))
}
@Test
fun clearCompletedTasks() {
// Add one active task and one completed task
repository.saveTaskBlocking(Task("TITLE1", "DESCRIPTION1"))
repository.saveTaskBlocking(Task("TITLE2", "DESCRIPTION2", true))
launchActivity()
// Click clear completed in menu
openActionBarOverflowOrOptionsMenu(getApplicationContext())
onView(withText(R.string.menu_clear)).perform(click())
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_all)).perform(click())
// Verify that only the active task is shown
onView(withText("TITLE1")).check(matches(isDisplayed()))
onView(withText("TITLE2")).check(doesNotExist())
}
@Test
fun noTasks_AllTasksFilter_AddTaskViewVisible() {
launchActivity()
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_all)).perform(click())
// Verify the "You have no tasks!" text is shown
onView(withText("You have no tasks!")).check(matches(isDisplayed()))
}
@Test
fun noTasks_CompletedTasksFilter_AddTaskViewNotVisible() {
launchActivity()
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_completed)).perform(click())
// Verify the "You have no completed tasks!" text is shown
onView(withText("You have no completed tasks!")).check(matches((isDisplayed())))
}
@Test
fun noTasks_ActiveTasksFilter_AddTaskViewNotVisible() {
launchActivity()
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.nav_active)).perform(click())
// Verify the "You have no active tasks!" text is shown
onView(withText("You have no active tasks!")).check(matches((isDisplayed())))
}
@Test
fun clickAddTaskButton_navigateToAddEditFragment() {
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the "+" button
onView(withId(R.id.add_task_fab)).perform(click())
// THEN - Verify that we navigate to the add screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null, getApplicationContext<Context>().getString(R.string.add_task)
)
)
}
private fun launchActivity(): ActivityScenario<TasksActivity>? {
val activityScenario = launch(TasksActivity::class.java)
activityScenario.onActivity { activity ->
// Disable animations in RecyclerView
(activity.findViewById(R.id.tasks_list) as RecyclerView).itemAnimator = null
}
return activityScenario
}
private fun checkboxWithText(text: String): Matcher<View> {
return allOf(withId(R.id.complete_checkbox), hasSibling(withText(text)))
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.util
import android.view.View
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.testing.FragmentScenario
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.IdlingResource
import java.util.UUID
/**
* An espresso idling resource implementation that reports idle status for all data binding
* layouts. Data Binding uses a mechanism to post messages which Espresso doesn't track yet.
*
* Since this application only uses fragments, the resource only checks the fragments and their
* children instead of the whole view tree.
*/
class DataBindingIdlingResource : IdlingResource {
// list of registered callbacks
private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()
// give it a unique id to workaround an espresso bug where you cannot register/unregister
// an idling resource w/ the same name.
private val id = UUID.randomUUID().toString()
// holds whether isIdle is called and the result was false. We track this to avoid calling
// onTransitionToIdle callbacks if Espresso never thought we were idle in the first place.
private var wasNotIdle = false
lateinit var activity: FragmentActivity
override fun getName() = "DataBinding $id"
override fun isIdleNow(): Boolean {
val idle = !getBindings().any { it.hasPendingBindings() }
@Suppress("LiftReturnOrAssignment")
if (idle) {
if (wasNotIdle) {
// notify observers to avoid espresso race detector
idlingCallbacks.forEach { it.onTransitionToIdle() }
}
wasNotIdle = false
} else {
wasNotIdle = true
// check next frame
activity.findViewById<View>(android.R.id.content).postDelayed({
isIdleNow
}, 16)
}
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
idlingCallbacks.add(callback)
}
/**
* Find all binding classes in all currently available fragments.
*/
private fun getBindings(): List<ViewDataBinding> {
val fragments = (activity as? FragmentActivity)
?.supportFragmentManager
?.fragments
val bindings =
fragments?.mapNotNull {
it.view?.getBinding()
} ?: emptyList()
val childrenBindings = fragments?.flatMap { it.childFragmentManager.fragments }
?.mapNotNull { it.view?.getBinding() } ?: emptyList()
return bindings + childrenBindings
}
}
private fun View.getBinding(): ViewDataBinding? = DataBindingUtil.getBinding(this)
/**
* Sets the activity from an [ActivityScenario] to be used from [DataBindingIdlingResource].
*/
fun DataBindingIdlingResource.monitorActivity(
activityScenario: ActivityScenario<out FragmentActivity>
) {
activityScenario.onActivity {
this.activity = it
}
}
/**
* Sets the fragment from a [FragmentScenario] to be used from [DataBindingIdlingResource].
*/
fun DataBindingIdlingResource.monitorFragment(fragmentScenario: FragmentScenario<out Fragment>) {
fragmentScenario.onFragment {
this.activity = it.requireActivity()
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.util
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
import kotlinx.coroutines.runBlocking
/**
* A blocking version of TasksRepository.saveTask to minimize the number of times we have to
* explicitly add <code>runBlocking { ... }</code> in our tests
*/
fun TasksRepository.saveTaskBlocking(task: Task) = runBlocking {
this@saveTaskBlocking.saveTask(task)
}
fun TasksRepository.getTasksBlocking(forceUpdate: Boolean) = runBlocking {
this@getTasksBlocking.getTasks(forceUpdate)
}
fun TasksRepository.deleteAllTasksBlocking() = runBlocking {
this@deleteAllTasksBlocking.deleteAllTasks()
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp
import androidx.lifecycle.LiveData
import androidx.lifecycle.liveData
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource
object FakeFailingTasksRemoteDataSource : TasksDataSource {
override suspend fun getTasks(): Result<List<Task>> {
return Result.Error(Exception("Test"))
}
override suspend fun getTask(taskId: String): Result<Task> {
return Result.Error(Exception("Test"))
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return liveData { emit(getTasks()) }
}
override suspend fun refreshTasks() {
TODO("not implemented")
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
return liveData { emit(getTask(taskId)) }
}
override suspend fun refreshTask(taskId: String) {
TODO("not implemented")
}
override suspend fun saveTask(task: Task) {
TODO("not implemented")
}
override suspend fun completeTask(task: Task) {
TODO("not implemented")
}
override suspend fun completeTask(taskId: String) {
TODO("not implemented")
}
override suspend fun activateTask(task: Task) {
TODO("not implemented")
}
override suspend fun activateTask(taskId: String) {
TODO("not implemented")
}
override suspend fun clearCompletedTasks() {
TODO("not implemented")
}
override suspend fun deleteAllTasks() {
TODO("not implemented")
}
override suspend fun deleteTask(taskId: String) {
TODO("not implemented")
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
this.removeObserver(observer)
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
/**
* Observes a [LiveData] until the `block` is done executing.
*/
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
val observer = Observer<T> { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp
import androidx.lifecycle.LiveData
import org.junit.Assert.assertEquals
fun assertLiveDataEventTriggered(
liveData: LiveData<Event<String>>,
taskId: String
) {
val value = liveData.getOrAwaitValue()
assertEquals(value.getContentIfNotHandled(), taskId)
}
fun assertSnackbarMessage(snackbarLiveData: LiveData<Event<Int>>, messageId: Int) {
val value: Event<Int> = snackbarLiveData.getOrAwaitValue()
assertEquals(value.getContentIfNotHandled(), messageId)
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.addedittask
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
import com.example.android.architecture.blueprints.todoapp.R.string
import com.example.android.architecture.blueprints.todoapp.assertSnackbarMessage
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
import org.junit.Rule
import org.junit.Test
/**
* Unit tests for the implementation of [AddEditTaskViewModel].
*/
@ExperimentalCoroutinesApi
class AddEditTaskViewModelTest {
// Subject under test
private lateinit var addEditTaskViewModel: AddEditTaskViewModel
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeRepository
// Set the main coroutines dispatcher for unit testing.
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
// Executes each task synchronously using Architecture Components.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
private val task = Task("Title1", "Description1")
@Before
fun setupViewModel() {
// We initialise the repository with no tasks
tasksRepository = FakeRepository()
// Create class under test
addEditTaskViewModel = AddEditTaskViewModel(tasksRepository)
}
@Test
fun saveNewTaskToRepository_showsSuccessMessageUi() {
val newTitle = "New Task Title"
val newDescription = "Some Task Description"
(addEditTaskViewModel).apply {
title.value = newTitle
description.value = newDescription
}
addEditTaskViewModel.saveTask()
val newTask = tasksRepository.tasksServiceData.values.first()
// Then a task is saved in the repository and the view updated
assertThat(newTask.title).isEqualTo(newTitle)
assertThat(newTask.description).isEqualTo(newDescription)
}
@Test
fun loadTasks_loading() {
// Pause dispatcher so we can verify initial values
mainCoroutineRule.pauseDispatcher()
// Load the task in the viewmodel
addEditTaskViewModel.start(task.id)
// Then progress indicator is shown
assertThat(addEditTaskViewModel.dataLoading.getOrAwaitValue()).isTrue()
// Execute pending coroutines actions
mainCoroutineRule.resumeDispatcher()
// Then progress indicator is hidden
assertThat(addEditTaskViewModel.dataLoading.getOrAwaitValue()).isFalse()
}
@Test
fun loadTasks_taskShown() {
// Add task to repository
tasksRepository.addTasks(task)
// Load the task with the viewmodel
addEditTaskViewModel.start(task.id)
// Verify a task is loaded
assertThat(addEditTaskViewModel.title.getOrAwaitValue()).isEqualTo(task.title)
assertThat(addEditTaskViewModel.description.getOrAwaitValue()).isEqualTo(task.description)
assertThat(addEditTaskViewModel.dataLoading.getOrAwaitValue()).isFalse()
}
@Test
fun saveNewTaskToRepository_emptyTitle_error() {
saveTaskAndAssertSnackbarError("", "Some Task Description")
}
@Test
fun saveNewTaskToRepository_nullTitle_error() {
saveTaskAndAssertSnackbarError(null, "Some Task Description")
}
@Test
fun saveNewTaskToRepository_emptyDescription_error() {
saveTaskAndAssertSnackbarError("Title", "")
}
@Test
fun saveNewTaskToRepository_nullDescription_error() {
saveTaskAndAssertSnackbarError("Title", null)
}
@Test
fun saveNewTaskToRepository_nullDescriptionNullTitle_error() {
saveTaskAndAssertSnackbarError(null, null)
}
@Test
fun saveNewTaskToRepository_emptyDescriptionEmptyTitle_error() {
saveTaskAndAssertSnackbarError("", "")
}
private fun saveTaskAndAssertSnackbarError(title: String?, description: String?) {
(addEditTaskViewModel).apply {
this.title.value = title
this.description.value = description
}
// When saving an incomplete task
addEditTaskViewModel.saveTask()
// Then the snackbar shows an error
assertSnackbarMessage(addEditTaskViewModel.snackbarText, string.empty_task_message)
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
/**
* Unit tests for the implementation of the in-memory repository with cache.
*/
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val newTask = Task("Title new", "Description new")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
// Set the main coroutines dispatcher for unit testing.
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
@ExperimentalCoroutinesApi
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Main
)
}
@ExperimentalCoroutinesApi
@Test
fun getTasks_emptyRepositoryAndUninitializedCache() = mainCoroutineRule.runBlockingTest {
val emptySource = FakeDataSource()
val tasksRepository = DefaultTasksRepository(
emptySource, emptySource, Dispatchers.Main
)
assertThat(tasksRepository.getTasks() is Success).isTrue()
}
@Test
fun getTasks_repositoryCachesAfterFirstApiCall() = mainCoroutineRule.runBlockingTest {
// Trigger the repository to load data, which loads from remote and caches
val initial = tasksRepository.getTasks()
tasksRemoteDataSource.tasks = newTasks.toMutableList()
val second = tasksRepository.getTasks()
// Initial and second should match because we didn't force a refresh
assertThat(second).isEqualTo(initial)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = mainCoroutineRule.runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data).isEqualTo(remoteTasks)
}
@Test
fun saveTask_savesToLocalAndRemote() = mainCoroutineRule.runBlockingTest {
// Make sure newTask is not in the remote or local datasources
assertThat(tasksRemoteDataSource.tasks).doesNotContain(newTask)
assertThat(tasksLocalDataSource.tasks).doesNotContain(newTask)
// When a task is saved to the tasks repository
tasksRepository.saveTask(newTask)
// Then the remote and local sources are called
assertThat(tasksRemoteDataSource.tasks).contains(newTask)
assertThat(tasksLocalDataSource.tasks).contains(newTask)
}
@Test
fun getTasks_WithDirtyCache_tasksAreRetrievedFromRemote() = mainCoroutineRule.runBlockingTest {
// First call returns from REMOTE
val tasks = tasksRepository.getTasks()
// Set a different list of tasks in REMOTE
tasksRemoteDataSource.tasks = newTasks.toMutableList()
// But if tasks are cached, subsequent calls load from cache
val cachedTasks = tasksRepository.getTasks()
assertThat(cachedTasks).isEqualTo(tasks)
// Now force remote loading
val refreshedTasks = tasksRepository.getTasks(true) as Success
// Tasks must be the recently updated in REMOTE
assertThat(refreshedTasks.data).isEqualTo(newTasks)
}
@Test
fun getTasks_WithDirtyCache_remoteUnavailable_error() = mainCoroutineRule.runBlockingTest {
// Make remote data source unavailable
tasksRemoteDataSource.tasks = null
// Load tasks forcing remote load
val refreshedTasks = tasksRepository.getTasks(true)
// Result should be an error
assertThat(refreshedTasks).isInstanceOf(Result.Error::class.java)
}
@Test
fun getTasks_WithRemoteDataSourceUnavailable_tasksAreRetrievedFromLocal() =
mainCoroutineRule.runBlockingTest {
// When the remote data source is unavailable
tasksRemoteDataSource.tasks = null
// The repository fetches from the local source
assertThat((tasksRepository.getTasks() as Success).data).isEqualTo(localTasks)
}
@Test
fun getTasks_WithBothDataSourcesUnavailable_returnsError() = mainCoroutineRule.runBlockingTest {
// When both sources are unavailable
tasksRemoteDataSource.tasks = null
tasksLocalDataSource.tasks = null
// The repository returns an error
assertThat(tasksRepository.getTasks()).isInstanceOf(Result.Error::class.java)
}
@Test
fun getTasks_refreshesLocalDataSource() = mainCoroutineRule.runBlockingTest {
val initialLocal = tasksLocalDataSource.tasks
// First load will fetch from remote
val newTasks = (tasksRepository.getTasks(true) as Success).data
assertThat(newTasks).isEqualTo(remoteTasks)
assertThat(newTasks).isEqualTo(tasksLocalDataSource.tasks)
assertThat(tasksLocalDataSource.tasks).isEqualTo(initialLocal)
}
@Test
fun completeTask_completesTaskToServiceAPIUpdatesCache() = mainCoroutineRule.runBlockingTest {
// Save a task
tasksRepository.saveTask(newTask)
// Make sure it's active
assertThat((tasksRepository.getTask(newTask.id) as Success).data.isCompleted).isFalse()
// Mark is as complete
tasksRepository.completeTask(newTask.id)
// Verify it's now completed
assertThat((tasksRepository.getTask(newTask.id) as Success).data.isCompleted).isTrue()
}
@Test
fun completeTask_activeTaskToServiceAPIUpdatesCache() = mainCoroutineRule.runBlockingTest {
// Save a task
tasksRepository.saveTask(newTask)
tasksRepository.completeTask(newTask.id)
// Make sure it's completed
assertThat((tasksRepository.getTask(newTask.id) as Success).data.isActive).isFalse()
// Mark is as active
tasksRepository.activateTask(newTask.id)
// Verify it's now activated
val result = tasksRepository.getTask(newTask.id) as Success
assertThat(result.data.isActive).isTrue()
}
@Test
fun getTask_repositoryCachesAfterFirstApiCall() = mainCoroutineRule.runBlockingTest {
// Trigger the repository to load data, which loads from remote
tasksRemoteDataSource.tasks = mutableListOf(task1)
tasksRepository.getTask(task1.id, true)
// Configure the remote data source to store a different task
tasksRemoteDataSource.tasks = mutableListOf(task2)
val task1SecondTime = tasksRepository.getTask(task1.id, true) as Success
val task2SecondTime = tasksRepository.getTask(task2.id, true) as Success
// Both work because one is in remote and the other in cache
assertThat(task1SecondTime.data.id).isEqualTo(task1.id)
assertThat(task2SecondTime.data.id).isEqualTo(task2.id)
}
@Test
fun getTask_forceRefresh() = mainCoroutineRule.runBlockingTest {
// Trigger the repository to load data, which loads from remote and caches
tasksRemoteDataSource.tasks = mutableListOf(task1)
tasksRepository.getTask(task1.id)
// Configure the remote data source to return a different task
tasksRemoteDataSource.tasks = mutableListOf(task2)
// Force refresh
val task1SecondTime = tasksRepository.getTask(task1.id, true)
val task2SecondTime = tasksRepository.getTask(task2.id, true)
// Only task2 works because the cache and local were invalidated
assertThat((task1SecondTime as? Success)?.data?.id).isNull()
assertThat((task2SecondTime as? Success)?.data?.id).isEqualTo(task2.id)
}
@Test
fun clearCompletedTasks() = mainCoroutineRule.runBlockingTest {
val completedTask = task1.copy().apply { isCompleted = true }
tasksRemoteDataSource.tasks = mutableListOf(completedTask, task2)
tasksRepository.clearCompletedTasks()
val tasks = (tasksRepository.getTasks(true) as? Success)?.data
assertThat(tasks).hasSize(1)
assertThat(tasks).contains(task2)
assertThat(tasks).doesNotContain(completedTask)
}
@Test
fun deleteAllTasks() = mainCoroutineRule.runBlockingTest {
val initialTasks = (tasksRepository.getTasks() as? Success)?.data
// Delete all tasks
tasksRepository.deleteAllTasks()
// Fetch data again
val afterDeleteTasks = (tasksRepository.getTasks() as? Success)?.data
// Verify tasks are empty now
assertThat(initialTasks).isNotEmpty()
assertThat(afterDeleteTasks).isEmpty()
}
@Test
fun deleteSingleTask() = mainCoroutineRule.runBlockingTest {
val initialTasks = (tasksRepository.getTasks(true) as? Success)?.data
// Delete first task
tasksRepository.deleteTask(task1.id)
// Fetch data again
val afterDeleteTasks = (tasksRepository.getTasks(true) as? Success)?.data
// Verify only one task was deleted
assertThat(afterDeleteTasks?.size).isEqualTo(initialTasks!!.size - 1)
assertThat(afterDeleteTasks).doesNotContain(task1)
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.data.source
import androidx.lifecycle.LiveData
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource {
override suspend fun getTasks(): Result<List<Task>> {
tasks?.let { return Success(ArrayList(it)) }
return Error(
Exception("Tasks not found")
)
}
override suspend fun getTask(taskId: String): Result<Task> {
tasks?.firstOrNull { it.id == taskId }?.let { return Success(it) }
return Error(
Exception("Task not found")
)
}
override suspend fun saveTask(task: Task) {
tasks?.add(task)
}
override suspend fun completeTask(task: Task) {
tasks?.firstOrNull { it.id == task.id }?.let { it.isCompleted = true }
}
override suspend fun completeTask(taskId: String) {
tasks?.firstOrNull { it.id == taskId }?.let { it.isCompleted = true }
}
override suspend fun activateTask(task: Task) {
tasks?.firstOrNull { it.id == task.id }?.let { it.isCompleted = false }
}
override suspend fun activateTask(taskId: String) {
tasks?.firstOrNull { it.id == taskId }?.let { it.isCompleted = false }
}
override suspend fun clearCompletedTasks() {
tasks?.removeIf { it.isCompleted }
}
override suspend fun deleteAllTasks() {
tasks?.clear()
}
override suspend fun deleteTask(taskId: String) {
tasks?.removeIf { it.id == taskId }
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
TODO("not implemented")
}
override suspend fun refreshTasks() {
TODO("not implemented")
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
TODO("not implemented")
}
override suspend fun refreshTask(taskId: String) {
TODO("not implemented")
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.statistics
import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.core.Is.`is`
import org.junit.Assert.assertThat
import org.junit.Test
/**
* Unit tests for [getActiveAndCompletedStats].
*/
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted() {
val tasks = listOf(
Task("title", "desc", isCompleted = false)
)
// When the list of tasks is computed with an active task
val result = getActiveAndCompletedStats(tasks)
// Then the percentages are 100 and 0
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
}
@Test
fun getActiveAndCompletedStats_noActive() {
val tasks = listOf(
Task("title", "desc", isCompleted = true)
)
// When the list of tasks is computed with a completed task
val result = getActiveAndCompletedStats(tasks)
// Then the percentages are 0 and 100
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(100f))
}
@Test
fun getActiveAndCompletedStats_both() {
// Given 3 completed tasks and 2 active tasks
val tasks = listOf(
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = false),
Task("title", "desc", isCompleted = false)
)
// When the list of tasks is computed
val result = getActiveAndCompletedStats(tasks)
// Then the result is 40-60
assertThat(result.activeTasksPercent, `is`(40f))
assertThat(result.completedTasksPercent, `is`(60f))
}
@Test
fun getActiveAndCompletedStats_error() {
// When there's an error loading stats
val result = getActiveAndCompletedStats(null)
// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}
@Test
fun getActiveAndCompletedStats_empty() {
// When there are no tasks
val result = getActiveAndCompletedStats(emptyList())
// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.statistics
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.example.android.architecture.blueprints.todoapp.FakeFailingTasksRemoteDataSource
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
/**
* Unit tests for the implementation of [StatisticsViewModel]
*/
@ExperimentalCoroutinesApi
class StatisticsViewModelTest {
// Executes each task synchronously using Architecture Components.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Subject under test
private lateinit var statisticsViewModel: StatisticsViewModel
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeRepository
// Set the main coroutines dispatcher for unit testing.
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
@Before
fun setupStatisticsViewModel() {
tasksRepository = FakeRepository()
statisticsViewModel = StatisticsViewModel(tasksRepository)
}
@Test
fun loadEmptyTasksFromRepository_EmptyResults() = mainCoroutineRule.runBlockingTest {
// Given an initialized StatisticsViewModel with no tasks
// Then the results are empty
assertThat(statisticsViewModel.empty.getOrAwaitValue()).isTrue()
}
@Test
fun loadNonEmptyTasksFromRepository_NonEmptyResults() {
// We initialise the tasks to 3, with one active and two completed
val task1 = Task("Title1", "Description1")
val task2 = Task("Title2", "Description2", true)
val task3 = Task("Title3", "Description3", true)
val task4 = Task("Title4", "Description4", true)
tasksRepository.addTasks(task1, task2, task3, task4)
// Then the results are not empty
assertThat(statisticsViewModel.empty.getOrAwaitValue())
.isFalse()
assertThat(statisticsViewModel.activeTasksPercent.getOrAwaitValue())
.isEqualTo(25f)
assertThat(statisticsViewModel.completedTasksPercent.getOrAwaitValue())
.isEqualTo(75f)
}
@Test
fun loadStatisticsWhenTasksAreUnavailable_CallErrorToDisplay() {
val errorViewModel = StatisticsViewModel(
DefaultTasksRepository(
FakeFailingTasksRemoteDataSource,
FakeFailingTasksRemoteDataSource,
Dispatchers.Main // Main is set in MainCoroutineRule
)
)
// Then an error message is shown
assertThat(errorViewModel.empty.getOrAwaitValue()).isTrue()
assertThat(errorViewModel.error.getOrAwaitValue()).isTrue()
}
@Test
fun loadTasks_loading() {
// Pause dispatcher so we can verify initial values
mainCoroutineRule.pauseDispatcher()
// Load the task in the viewmodel
statisticsViewModel.refresh()
// Then progress indicator is shown
assertThat(statisticsViewModel.dataLoading.getOrAwaitValue()).isTrue()
// Execute pending coroutines actions
mainCoroutineRule.resumeDispatcher()
// Then progress indicator is hidden
assertThat(statisticsViewModel.dataLoading.getOrAwaitValue()).isFalse()
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.taskdetail
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.assertSnackbarMessage
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import com.example.android.architecture.blueprints.todoapp.observeForTesting
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
/**
* Unit tests for the implementation of [TaskDetailViewModel]
*/
@ExperimentalCoroutinesApi
class TaskDetailViewModelTest {
// Subject under test
private lateinit var taskDetailViewModel: TaskDetailViewModel
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeRepository
// Set the main coroutines dispatcher for unit testing.
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
// Executes each task synchronously using Architecture Components.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
val task = Task("Title1", "Description1")
@Before
fun setupViewModel() {
tasksRepository = FakeRepository()
tasksRepository.addTasks(task)
taskDetailViewModel = TaskDetailViewModel(tasksRepository)
}
@Test
fun getActiveTaskFromRepositoryAndLoadIntoView() {
taskDetailViewModel.start(task.id)
// Then verify that the view was notified
assertThat(taskDetailViewModel.task.getOrAwaitValue()?.title).isEqualTo(task.title)
assertThat(taskDetailViewModel.task.getOrAwaitValue()?.description)
.isEqualTo(task.description)
}
@Test
fun completeTask() {
// Load the ViewModel
taskDetailViewModel.start(task.id)
// Start observing to compute transformations
taskDetailViewModel.task.getOrAwaitValue()
// Verify that the task was active initially
assertThat(tasksRepository.tasksServiceData[task.id]?.isCompleted).isFalse()
// When the ViewModel is asked to complete the task
taskDetailViewModel.setCompleted(true)
// Then the task is completed and the snackbar shows the correct message
assertThat(tasksRepository.tasksServiceData[task.id]?.isCompleted).isTrue()
assertSnackbarMessage(taskDetailViewModel.snackbarText, R.string.task_marked_complete)
}
@Test
fun activateTask() {
task.isCompleted = true
// Load the ViewModel
taskDetailViewModel.start(task.id)
// Start observing to compute transformations
taskDetailViewModel.task.observeForTesting {
// Verify that the task was completed initially
assertThat(tasksRepository.tasksServiceData[task.id]?.isCompleted).isTrue()
// When the ViewModel is asked to complete the task
taskDetailViewModel.setCompleted(false)
mainCoroutineRule.runBlockingTest {
// Then the task is not completed and the snackbar shows the correct message
val newTask = (tasksRepository.getTask(task.id) as Success).data
assertTrue(newTask.isActive)
assertSnackbarMessage(taskDetailViewModel.snackbarText, R.string.task_marked_active)
}
}
}
@Test
fun taskDetailViewModel_repositoryError() {
// Given a repository that returns errors
tasksRepository.setReturnError(true)
// Given an initialized ViewModel with an active task
taskDetailViewModel.start(task.id)
// Get the computed LiveData value
taskDetailViewModel.task.observeForTesting {
// Then verify that data is not available
assertThat(taskDetailViewModel.isDataAvailable.getOrAwaitValue()).isFalse()
}
}
@Test
fun updateSnackbar_nullValue() {
// Before setting the Snackbar text, get its current value
val snackbarText = taskDetailViewModel.snackbarText.value
// Check that the value is null
assertThat(snackbarText).isNull()
}
@Test
fun clickOnEditTask_SetsEvent() {
// When opening a new task
taskDetailViewModel.editTask()
// Then the event is triggered
val value = taskDetailViewModel.editTaskEvent.getOrAwaitValue()
assertThat(value.getContentIfNotHandled()).isNotNull()
}
@Test
fun deleteTask() {
assertThat(tasksRepository.tasksServiceData.containsValue(task)).isTrue()
taskDetailViewModel.start(task.id)
// When the deletion of a task is requested
taskDetailViewModel.deleteTask()
assertThat(tasksRepository.tasksServiceData.containsValue(task)).isFalse()
}
@Test
fun loadTask_loading() {
// Pause dispatcher so we can verify initial values
mainCoroutineRule.pauseDispatcher()
// Load the task in the viewmodel
taskDetailViewModel.start(task.id)
// Start observing to compute transformations
taskDetailViewModel.task.observeForTesting {
// Force a refresh to show the loading indicator
taskDetailViewModel.refresh()
// Then progress indicator is shown
assertThat(taskDetailViewModel.dataLoading.getOrAwaitValue()).isTrue()
// Execute pending coroutines actions
mainCoroutineRule.resumeDispatcher()
// Then progress indicator is hidden
assertThat(taskDetailViewModel.dataLoading.getOrAwaitValue()).isFalse()
}
}
}
/*
* Copyright (C) 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.
*/
package com.example.android.architecture.blueprints.todoapp.tasks
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.SavedStateHandle
import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
import com.example.android.architecture.blueprints.todoapp.R
import com.example.android.architecture.blueprints.todoapp.assertLiveDataEventTriggered
import com.example.android.architecture.blueprints.todoapp.assertSnackbarMessage
import com.example.android.architecture.blueprints.todoapp.data.Task
import com.example.android.architecture.blueprints.todoapp.data.source.FakeRepository
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import com.example.android.architecture.blueprints.todoapp.observeForTesting
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
/**
* Unit tests for the implementation of [TasksViewModel]
*/
@ExperimentalCoroutinesApi
class TasksViewModelTest {
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeRepository
// Set the main coroutines dispatcher for unit testing.
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
// Executes each task synchronously using Architecture Components.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun setupViewModel() {
// We initialise the tasks to 3, with one active and two completed
tasksRepository = FakeRepository()
val task1 = Task("Title1", "Description1")
val task2 = Task("Title2", "Description2", true)
val task3 = Task("Title3", "Description3", true)
tasksRepository.addTasks(task1, task2, task3)
tasksViewModel = TasksViewModel(tasksRepository, SavedStateHandle())
}
@Test
fun loadAllTasksFromRepository_loadingTogglesAndDataLoaded() {
// Pause dispatcher so we can verify initial values
mainCoroutineRule.pauseDispatcher()
// Given an initialized TasksViewModel with initialized tasks
// When loading of Tasks is requested
tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
// Trigger loading of tasks
tasksViewModel.loadTasks(true)
// Observe the items to keep LiveData emitting
tasksViewModel.items.observeForTesting {
// Then progress indicator is shown
assertThat(tasksViewModel.dataLoading.getOrAwaitValue()).isTrue()
// Execute pending coroutines actions
mainCoroutineRule.resumeDispatcher()
// Then progress indicator is hidden
assertThat(tasksViewModel.dataLoading.getOrAwaitValue()).isFalse()
// And data correctly loaded
assertThat(tasksViewModel.items.getOrAwaitValue()).hasSize(3)
}
}
@Test
fun loadActiveTasksFromRepositoryAndLoadIntoView() {
// Given an initialized TasksViewModel with initialized tasks
// When loading of Tasks is requested
tasksViewModel.setFiltering(TasksFilterType.ACTIVE_TASKS)
// Load tasks
tasksViewModel.loadTasks(true)
// Observe the items to keep LiveData emitting
tasksViewModel.items.observeForTesting {
// Then progress indicator is hidden
assertThat(tasksViewModel.dataLoading.getOrAwaitValue()).isFalse()
// And data correctly loaded
assertThat(tasksViewModel.items.getOrAwaitValue()).hasSize(1)
}
}
@Test
fun loadCompletedTasksFromRepositoryAndLoadIntoView() {
// Given an initialized TasksViewModel with initialized tasks
// When loading of Tasks is requested
tasksViewModel.setFiltering(TasksFilterType.COMPLETED_TASKS)
// Load tasks
tasksViewModel.loadTasks(true)
// Observe the items to keep LiveData emitting
tasksViewModel.items.observeForTesting {
// Then progress indicator is hidden
assertThat(tasksViewModel.dataLoading.getOrAwaitValue()).isFalse()
// And data correctly loaded
assertThat(tasksViewModel.items.getOrAwaitValue()).hasSize(2)
}
}
@Test
fun loadTasks_error() {
// Make the repository return errors
tasksRepository.setReturnError(true)
// Load tasks
tasksViewModel.loadTasks(true)
// Observe the items to keep LiveData emitting
tasksViewModel.items.observeForTesting {
// Then progress indicator is hidden
assertThat(tasksViewModel.dataLoading.getOrAwaitValue()).isFalse()
// And the list of items is empty
assertThat(tasksViewModel.items.getOrAwaitValue()).isEmpty()
// And the snackbar updated
assertSnackbarMessage(tasksViewModel.snackbarText, R.string.loading_tasks_error)
}
}
@Test
fun clickOnFab_showsAddTaskUi() {
// When adding a new task
tasksViewModel.addNewTask()
// Then the event is triggered
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
assertThat(value.getContentIfNotHandled()).isNotNull()
}
@Test
fun clickOnOpenTask_setsEvent() {
// When opening a new task
val taskId = "42"
tasksViewModel.openTask(taskId)
// Then the event is triggered
assertLiveDataEventTriggered(tasksViewModel.openTaskEvent, taskId)
}
@Test
fun clearCompletedTasks_clearsTasks() = mainCoroutineRule.runBlockingTest {
// When completed tasks are cleared
tasksViewModel.clearCompletedTasks()
// Fetch tasks
tasksViewModel.loadTasks(true)
// Fetch tasks
val allTasks = tasksViewModel.items.getOrAwaitValue()
val completedTasks = allTasks.filter { it.isCompleted }
// Verify there are no completed tasks left
assertThat(completedTasks).isEmpty()
// Verify active task is not cleared
assertThat(allTasks).hasSize(1)
// Verify snackbar is updated
assertSnackbarMessage(
tasksViewModel.snackbarText, R.string.completed_tasks_cleared
)
}
@Test
fun showEditResultMessages_editOk_snackbarUpdated() {
// When the viewmodel receives a result from another destination
tasksViewModel.showEditResultMessage(EDIT_RESULT_OK)
// The snackbar is updated
assertSnackbarMessage(
tasksViewModel.snackbarText, R.string.successfully_saved_task_message
)
}
@Test
fun showEditResultMessages_addOk_snackbarUpdated() {
// When the viewmodel receives a result from another destination
tasksViewModel.showEditResultMessage(ADD_EDIT_RESULT_OK)
// The snackbar is updated
assertSnackbarMessage(
tasksViewModel.snackbarText, R.string.successfully_added_task_message
)
}
@Test
fun showEditResultMessages_deleteOk_snackbarUpdated() {
// When the viewmodel receives a result from another destination
tasksViewModel.showEditResultMessage(DELETE_RESULT_OK)
// The snackbar is updated
assertSnackbarMessage(tasksViewModel.snackbarText, R.string.successfully_deleted_task_message)
}
@Test
fun completeTask_dataAndSnackbarUpdated() {
// With a repository that has an active task
val task = Task("Title", "Description")
tasksRepository.addTasks(task)
// Complete task
tasksViewModel.completeTask(task, true)
// Verify the task is completed
assertThat(tasksRepository.tasksServiceData[task.id]?.isCompleted).isTrue()
// The snackbar is updated
assertSnackbarMessage(
tasksViewModel.snackbarText, R.string.task_marked_complete
)
}
@Test
fun activateTask_dataAndSnackbarUpdated() {
// With a repository that has a completed task
val task = Task("Title", "Description", true)
tasksRepository.addTasks(task)
// Activate task
tasksViewModel.completeTask(task, false)
// Verify the task is active
assertThat(tasksRepository.tasksServiceData[task.id]?.isActive).isTrue()
// The snackbar is updated
assertSnackbarMessage(
tasksViewModel.snackbarText, R.string.task_marked_active
)
}
@Test
fun getTasksAddViewVisible() {
// When the filter type is ALL_TASKS
tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
// Then the "Add task" action is visible
assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue()).isTrue()
}
}
buildscript {
ext.kotlinVersion = '1.3.61'
ext.navigationVersion = '2.2.0-rc02'
ext.ktlintVersion = '0.33.0'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
plugins {
id "com.diffplug.gradle.spotless" version "3.24.0"
}
allprojects {
repositories {
google()
jcenter()
}
apply plugin: 'com.diffplug.gradle.spotless'
spotless {
kotlin {
target "**/*.kt"
targetExclude ".idea/"
ktlint(ktlintVersion)
}
}
}
// Define versions in a single place
ext {
// Sdk and tools
// Support library and architecture components support minSdk 14 and above.
minSdkVersion = 14
targetSdkVersion = 28
compileSdkVersion = 28
// App dependencies
androidXVersion = '1.0.0'
androidXTestCoreVersion = '1.2.0'
androidXTestExtKotlinRunnerVersion = '1.1.1'
androidXTestRulesVersion = '1.2.0'
androidXAnnotations = '1.1.0'
androidXLegacySupport = '1.0.0'
appCompatVersion = '1.1.0'
archLifecycleVersion = '2.2.0-rc02'
archTestingVersion = '2.1.0'
cardVersion = '1.0.0'
coroutinesVersion = '1.2.1'
dexMakerVersion = '2.12.1'
espressoVersion = '3.2.0'
fragmentVersion = '1.2.0-rc02'
fragmentKtxVersion = '1.1.0-rc01'
hamcrestVersion = '1.3'
junitVersion = '4.12'
materialVersion = '1.0.0'
mockitoVersion = '2.19.0'
recyclerViewVersion = '1.1.0'
robolectricVersion = '4.3.1'
roomVersion = '2.2.2'
rulesVersion = '1.0.1'
timberVersion = '4.7.1'
truthVersion = '1.0'
}
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.enableJetifier=true
android.useAndroidX=true
# android.enableUnitTestBinaryResources=true
android.enableR8=true
kapt.incremental.apt=true
#Sun Jul 26 18:58:02 GMT+08:00 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
include ':app'
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论