提交 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).
差异被折叠。
# 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 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"
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论