├── .github ├── ISSUE_TEMPLATE │ └── advanced-android-issue-template-for-testing-codelab.md ├── ci-gradle.properties ├── renovate.json └── workflows │ └── build_test.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── android │ │ └── architecture │ │ └── blueprints │ │ └── todoapp │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── architecture │ │ │ └── blueprints │ │ │ └── todoapp │ │ │ ├── Event.kt │ │ │ ├── ScrollChildSwipeRefreshLayout.kt │ │ │ ├── TodoApplication.kt │ │ │ ├── addedittask │ │ │ ├── AddEditTaskFragment.kt │ │ │ └── AddEditTaskViewModel.kt │ │ │ ├── data │ │ │ ├── Result.kt │ │ │ ├── Task.kt │ │ │ └── source │ │ │ │ ├── DefaultTasksRepository.kt │ │ │ │ ├── TasksDataSource.kt │ │ │ │ ├── local │ │ │ │ ├── TasksDao.kt │ │ │ │ ├── TasksLocalDataSource.kt │ │ │ │ └── ToDoDatabase.kt │ │ │ │ └── remote │ │ │ │ └── TasksRemoteDataSource.kt │ │ │ ├── statistics │ │ │ ├── StatisticsFragment.kt │ │ │ ├── StatisticsUtils.kt │ │ │ └── StatisticsViewModel.kt │ │ │ ├── taskdetail │ │ │ ├── TaskDetailFragment.kt │ │ │ └── TaskDetailViewModel.kt │ │ │ ├── tasks │ │ │ ├── TasksActivity.kt │ │ │ ├── TasksAdapter.kt │ │ │ ├── TasksFilterType.kt │ │ │ ├── TasksFragment.kt │ │ │ ├── TasksListBindings.kt │ │ │ └── TasksViewModel.kt │ │ │ └── util │ │ │ └── ViewExt.kt │ └── res │ │ ├── drawable │ │ ├── drawer_item_color.xml │ │ ├── ic_add.xml │ │ ├── ic_assignment_turned_in_24dp.xml │ │ ├── ic_check_circle_96dp.xml │ │ ├── ic_done.xml │ │ ├── ic_edit.xml │ │ ├── ic_filter_list.xml │ │ ├── ic_list.xml │ │ ├── ic_menu.xml │ │ ├── ic_statistics.xml │ │ ├── ic_statistics_100dp.xml │ │ ├── ic_statistics_24dp.xml │ │ ├── ic_verified_user_96dp.xml │ │ ├── list_completed_touch_feedback.xml │ │ ├── logo_no_fill.png │ │ ├── touch_feedback.xml │ │ └── trash_icon.png │ │ ├── font │ │ ├── opensans_font.xml │ │ ├── opensans_regular.ttf │ │ └── opensans_semibold.ttf │ │ ├── layout │ │ ├── addtask_frag.xml │ │ ├── nav_header.xml │ │ ├── statistics_frag.xml │ │ ├── task_item.xml │ │ ├── taskdetail_frag.xml │ │ ├── tasks_act.xml │ │ └── tasks_frag.xml │ │ ├── menu │ │ ├── drawer_actions.xml │ │ ├── filter_tasks.xml │ │ ├── taskdetail_fragment_menu.xml │ │ └── tasks_fragment_menu.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── example │ └── android │ └── architecture │ └── blueprints │ └── todoapp │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot.png └── settings.gradle /.github/ISSUE_TEMPLATE/advanced-android-issue-template-for-testing-codelab.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Advanced Android Issue Template for Testing Codelab 3 | about: Report problems with Advanced Android in Kotlin Testing codelab 4 | title: "[Codelab Issue] Testing Codelab 5.#, Step # - Issue description" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the problem** 11 | A clear and concise description of what the problem is. 12 | 13 | **In which lesson and step of the codelab can this issue be found?** 14 | Lesson number + step number. (ex. 5.2, Step 7) 15 | 16 | **How to reproduce?** 17 | What are the exact steps to reproduce the problem? 18 | 19 | **Versions** 20 | 1. What version of Android Studio are you using? 21 | 22 | **Additional information** 23 | Add any other context about the problem here. 24 | 25 | **codelab:** advanced-android-kotlin 26 | -------------------------------------------------------------------------------- /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | org.gradle.daemon=false 18 | org.gradle.parallel=true 19 | org.gradle.jvmargs=-Xmx5120m 20 | org.gradle.workers.max=2 21 | 22 | kotlin.incremental=false 23 | kotlin.compiler.execution.strategy=in-process -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>android/.github:renovate-config" 5 | ], 6 | 7 | "baseBranches": [ 8 | "starter_code", 9 | "end_codelab_1", 10 | "end_codelab_2", 11 | "end_codelab_3" 12 | ] 13 | 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/build_test.yaml: -------------------------------------------------------------------------------- 1 | name: testingcodelabs 2 | 3 | on: 4 | push: 5 | branches: 6 | - starter_code 7 | - end_codelab_1 8 | - end_codelab_2 9 | - end_codelab_3 10 | pull_request: 11 | branches: 12 | - starter_code 13 | - end_codelab_1 14 | - end_codelab_2 15 | - end_codelab_3 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 30 21 | strategy: 22 | matrix: 23 | api-level: [29] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Enable KVM group perms 29 | run: | 30 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 31 | sudo udevadm control --reload-rules 32 | sudo udevadm trigger --name-match=kvm 33 | ls /dev/kvm 34 | 35 | - name: Set Up JDK 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: 'zulu' # See 'Supported distributions' for available options 39 | java-version: '17' 40 | cache: 'gradle' 41 | 42 | - name: Setup Gradle 43 | uses: gradle/actions/setup-gradle@v4 44 | 45 | - name: Setup Android SDK 46 | uses: android-actions/setup-android@v3 47 | 48 | - name: Run instrumentation tests 49 | uses: reactivecircus/android-emulator-runner@v2 50 | with: 51 | api-level: ${{ matrix.api-level }} 52 | arch: x86 53 | disable-animations: true 54 | script: ./gradlew connectedCheck --stacktrace 55 | 56 | - name: Upload test reports 57 | if: always() 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: test-reports-${{ matrix.api-level }} 61 | path: ./app/build/reports/androidTests 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | local.properties 4 | build 5 | .gradle 6 | # Eclipse project files 7 | .project 8 | .settings/ 9 | .classpath 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | 8 | ### Before you contribute 9 | Before we can use your code, you must sign the 10 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 11 | (CLA), which you can do online. The CLA is necessary mainly because you own the 12 | copyright to your changes, even after your contribution becomes part of our 13 | codebase, so we need your permission to use and distribute your code. We also 14 | need to be sure of various other things—for instance that you'll tell us if you 15 | know that your code infringes on other people's patents. You don't have to sign 16 | the CLA until after you've submitted your code for review and a member has 17 | approved it, but you must do it before we can put your code into our codebase. 18 | Before you start working on a larger contribution, you should get in touch with 19 | us first through the issue tracker with your idea so that we can help out and 20 | possibly guide you. Coordinating up front makes it much easier to avoid 21 | frustration later on. 22 | 23 | ### Code reviews 24 | All submissions, including submissions by project members, require review. We 25 | use Github pull requests for this purpose. 26 | 27 | ### The small print 28 | Contributions made by corporations are covered by a different agreement than 29 | the one above, the 30 | [Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 The Android Open Source Project 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | (Deprecated) TO-DO Notes - Code for 5.1-5.3 Testing Codelab 2 | ============================================================================ 3 | 4 | > [!CAUTION] 5 | > This codelab is deprecated and it will be removed soon. 6 | 7 | Code for the Advanced Android Kotlin Testing Codelab 5.1-5.3 8 | 9 | Introduction 10 | ------------ 11 | 12 | TO-DO Notes is an app where you to write down tasks to complete. The app displays them in a list. 13 | You can then mark them as completed or not, filter them and delete them. 14 | 15 | ![App main screen, screenshot](screenshot.png) 16 | 17 | This codelab has four branches, representing different code states: 18 | 19 | * [starter_code](https://github.com/googlecodelabs/android-testing/tree/starter_code) 20 | * [end_codelab_1](https://github.com/googlecodelabs/android-testing/tree/end_codelab_1) 21 | * [end_codelab_2](https://github.com/googlecodelabs/android-testing/tree/end_codelab_2) 22 | * [end_codelab_3](https://github.com/googlecodelabs/android-testing/tree/end_codelab_3) 23 | 24 | The codelabs in this series are: 25 | * [Testing Basics](https://codelabs.developers.google.com/codelabs/advanced-android-kotlin-training-testing-basics) 26 | * [Introduction to Test Doubles and Dependency Injection](https://codelabs.developers.google.com/codelabs/advanced-android-kotlin-training-testing-test-doubles) 27 | * [Survey of Testing Topics](https://codelabs.developers.google.com/codelabs/advanced-android-kotlin-training-testing-survey) 28 | 29 | 30 | Pre-requisites 31 | -------------- 32 | 33 | You should be familiar with: 34 | 35 | * The Kotlin programming language, including [Kotlin coroutines](https://developer.android.com/kotlin/coroutines) and their interaction with [Android Jetpack components](https://developer.android.com/topic/libraries/architecture/coroutines). 36 | * The following core Android Jetpack libraries: [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel), 37 | [LiveData](https://developer.android.com/topic/libraries/architecture/livedata), 38 | [Navigation Component](https://developer.android.com/guide/navigation) and 39 | [Data Binding](https://developer.android.com/topic/libraries/data-binding). 40 | * Application architecture, following the pattern from the [Guide to app architecture](https://developer.android.com/jetpack/docs/guide) and [Android Fundamentals codelabs](https://developer.android.com/courses/kotlin-android-fundamentals/toc). 41 | 42 | 43 | Getting Started 44 | --------------- 45 | 46 | 1. Download and run the app. 47 | 2. Check out one of the codelabs mentioned above. 48 | 49 | License 50 | ------- 51 | 52 | Copyright 2019 Google, Inc. 53 | 54 | Licensed to the Apache Software Foundation (ASF) under one or more contributor 55 | license agreements. See the NOTICE file distributed with this work for 56 | additional information regarding copyright ownership. The ASF licenses this 57 | file to you under the Apache License, Version 2.0 (the "License"); you may not 58 | use this file except in compliance with the License. You may obtain a copy of 59 | the License at 60 | 61 | http://www.apache.org/licenses/LICENSE-2.0 62 | 63 | Unless required by applicable law or agreed to in writing, software 64 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 65 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 66 | License for the specific language governing permissions and limitations under 67 | the License. 68 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: "androidx.navigation.safeargs.kotlin" 5 | 6 | android { 7 | compileSdkVersion rootProject.compileSdkVersion 8 | 9 | defaultConfig { 10 | applicationId "com.example.android.architecture.blueprints.reactive" 11 | minSdkVersion rootProject.minSdkVersion 12 | targetSdkVersion rootProject.targetSdkVersion 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | dataBinding { 25 | enabled = true 26 | enabledForTests = true 27 | } 28 | } 29 | 30 | dependencies { 31 | 32 | // App dependencies 33 | implementation "androidx.appcompat:appcompat:$appCompatVersion" 34 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:$swipeRefreshLayoutVersion" 35 | implementation "com.google.android.material:material:$materialVersion" 36 | implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion" 37 | implementation "androidx.annotation:annotation:$androidXAnnotations" 38 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" 39 | implementation "com.jakewharton.timber:timber:$timberVersion" 40 | 41 | // Architecture Components 42 | implementation "androidx.room:room-runtime:$roomVersion" 43 | kapt "androidx.room:room-compiler:$roomVersion" 44 | implementation "androidx.room:room-ktx:$roomVersion" 45 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion" 46 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$archLifecycleVersion" 47 | kapt "androidx.lifecycle:lifecycle-compiler:$archLifecycleVersion" 48 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion" 49 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$archLifecycleVersion" 50 | implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" 51 | implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" 52 | 53 | // Dependencies for local unit tests 54 | testImplementation "junit:junit:$junitVersion" 55 | 56 | // AndroidX Test - Instrumented testing 57 | androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion" 58 | androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" 59 | 60 | // Kotlin 61 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 62 | implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion" 63 | } 64 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.android.architecture.blueprints.reactive", 23 | appContext.packageName) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/Event.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp 17 | 18 | import androidx.lifecycle.Observer 19 | 20 | /** 21 | * Used as a wrapper for data that is exposed via a LiveData that represents an event. 22 | */ 23 | open class Event(private val content: T) { 24 | 25 | @Suppress("MemberVisibilityCanBePrivate") 26 | var hasBeenHandled = false 27 | private set // Allow external read but not write 28 | 29 | /** 30 | * Returns the content and prevents its use again. 31 | */ 32 | fun getContentIfNotHandled(): T? { 33 | return if (hasBeenHandled) { 34 | null 35 | } else { 36 | hasBeenHandled = true 37 | content 38 | } 39 | } 40 | 41 | /** 42 | * Returns the content, even if it's already been handled. 43 | */ 44 | fun peekContent(): T = content 45 | } 46 | 47 | /** 48 | * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has 49 | * already been handled. 50 | * 51 | * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled. 52 | */ 53 | class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { 54 | override fun onChanged(event: Event?) { 55 | event?.getContentIfNotHandled()?.let { 56 | onEventUnhandledContent(it) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/ScrollChildSwipeRefreshLayout.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp 17 | 18 | import android.content.Context 19 | import android.util.AttributeSet 20 | import android.view.View 21 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 22 | 23 | /** 24 | * Extends [SwipeRefreshLayout] to support non-direct descendant scrolling views. 25 | * 26 | * 27 | * [SwipeRefreshLayout] works as expected when a scroll view is a direct child: it triggers 28 | * the refresh only when the view is on top. This class adds a way (@link #setScrollUpChild} to 29 | * define which view controls this behavior. 30 | */ 31 | class ScrollChildSwipeRefreshLayout @JvmOverloads constructor( 32 | context: Context, 33 | attrs: AttributeSet? = null 34 | ) : SwipeRefreshLayout(context, attrs) { 35 | 36 | var scrollUpChild: View? = null 37 | 38 | override fun canChildScrollUp() = 39 | scrollUpChild?.canScrollVertically(-1) ?: super.canChildScrollUp() 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp 18 | 19 | import android.app.Application 20 | import timber.log.Timber 21 | import timber.log.Timber.DebugTree 22 | 23 | /** 24 | * An application that lazily provides a repository. Note that this Service Locator pattern is 25 | * used to simplify the sample. Consider a Dependency Injection framework. 26 | * 27 | * Also, sets up Timber in the DEBUG BuildConfig. Read Timber's documentation for production setups. 28 | */ 29 | class TodoApplication : Application() { 30 | 31 | override fun onCreate() { 32 | super.onCreate() 33 | if (BuildConfig.DEBUG) Timber.plant(DebugTree()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.addedittask 17 | 18 | import android.os.Bundle 19 | import android.view.LayoutInflater 20 | import android.view.View 21 | import android.view.ViewGroup 22 | import androidx.fragment.app.Fragment 23 | import androidx.fragment.app.viewModels 24 | import androidx.navigation.fragment.findNavController 25 | import androidx.navigation.fragment.navArgs 26 | import com.example.android.architecture.blueprints.todoapp.EventObserver 27 | import com.example.android.architecture.blueprints.todoapp.R 28 | import com.example.android.architecture.blueprints.todoapp.databinding.AddtaskFragBinding 29 | import com.example.android.architecture.blueprints.todoapp.tasks.ADD_EDIT_RESULT_OK 30 | import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout 31 | import com.example.android.architecture.blueprints.todoapp.util.setupSnackbar 32 | import com.google.android.material.snackbar.Snackbar 33 | 34 | /** 35 | * Main UI for the add task screen. Users can enter a task title and description. 36 | */ 37 | class AddEditTaskFragment : Fragment() { 38 | 39 | private lateinit var viewDataBinding: AddtaskFragBinding 40 | 41 | private val args: AddEditTaskFragmentArgs by navArgs() 42 | 43 | private val viewModel by viewModels() 44 | 45 | override fun onCreateView( 46 | inflater: LayoutInflater, container: ViewGroup?, 47 | savedInstanceState: Bundle? 48 | ): View { 49 | val root = inflater.inflate(R.layout.addtask_frag, container, false) 50 | viewDataBinding = AddtaskFragBinding.bind(root).apply { 51 | this.viewmodel = viewModel 52 | } 53 | // Set the lifecycle owner to the lifecycle of the view 54 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 55 | return viewDataBinding.root 56 | } 57 | 58 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 59 | setupSnackbar() 60 | setupNavigation() 61 | this.setupRefreshLayout(viewDataBinding.refreshLayout) 62 | viewModel.start(args.taskId) 63 | } 64 | 65 | private fun setupSnackbar() { 66 | view?.setupSnackbar(this, viewModel.snackbarText, Snackbar.LENGTH_SHORT) 67 | } 68 | 69 | private fun setupNavigation() { 70 | viewModel.taskUpdatedEvent.observe(viewLifecycleOwner, EventObserver { 71 | val action = AddEditTaskFragmentDirections 72 | .actionAddEditTaskFragmentToTasksFragment(ADD_EDIT_RESULT_OK) 73 | findNavController().navigate(action) 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.addedittask 18 | 19 | import android.app.Application 20 | import androidx.lifecycle.* 21 | import com.example.android.architecture.blueprints.todoapp.Event 22 | import com.example.android.architecture.blueprints.todoapp.R 23 | import com.example.android.architecture.blueprints.todoapp.data.Result.Success 24 | import com.example.android.architecture.blueprints.todoapp.data.Task 25 | import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository 26 | import kotlinx.coroutines.launch 27 | 28 | /** 29 | * ViewModel for the Add/Edit screen. 30 | */ 31 | class AddEditTaskViewModel(application: Application) : AndroidViewModel(application) { 32 | 33 | // Note, for testing and architecture purposes, it's bad practice to construct the repository 34 | // here. We'll show you how to fix this during the codelab 35 | private val tasksRepository = DefaultTasksRepository.getRepository(application) 36 | 37 | // Two-way databinding, exposing MutableLiveData 38 | val title = MutableLiveData() 39 | 40 | // Two-way databinding, exposing MutableLiveData 41 | val description = MutableLiveData() 42 | 43 | private val _dataLoading = MutableLiveData() 44 | val dataLoading: LiveData = _dataLoading 45 | 46 | private val _snackbarText = MutableLiveData>() 47 | val snackbarText: LiveData> = _snackbarText 48 | 49 | private val _taskUpdatedEvent = MutableLiveData>() 50 | val taskUpdatedEvent: LiveData> = _taskUpdatedEvent 51 | 52 | private var taskId: String? = null 53 | 54 | private var isNewTask: Boolean = false 55 | 56 | private var isDataLoaded = false 57 | 58 | private var taskCompleted = false 59 | 60 | fun start(taskId: String?) { 61 | if (_dataLoading.value == true) { 62 | return 63 | } 64 | 65 | this.taskId = taskId 66 | if (taskId == null) { 67 | // No need to populate, it's a new task 68 | isNewTask = true 69 | return 70 | } 71 | if (isDataLoaded) { 72 | // No need to populate, already have data. 73 | return 74 | } 75 | 76 | isNewTask = false 77 | _dataLoading.value = true 78 | 79 | viewModelScope.launch { 80 | tasksRepository.getTask(taskId).let { result -> 81 | if (result is Success) { 82 | onTaskLoaded(result.data) 83 | } else { 84 | onDataNotAvailable() 85 | } 86 | } 87 | } 88 | } 89 | 90 | private fun onTaskLoaded(task: Task) { 91 | title.value = task.title 92 | description.value = task.description 93 | taskCompleted = task.isCompleted 94 | _dataLoading.value = false 95 | isDataLoaded = true 96 | } 97 | 98 | private fun onDataNotAvailable() { 99 | _dataLoading.value = false 100 | } 101 | 102 | // Called when clicking on fab. 103 | fun saveTask() { 104 | val currentTitle = title.value 105 | val currentDescription = description.value 106 | 107 | if (currentTitle == null || currentDescription == null) { 108 | _snackbarText.value = Event(R.string.empty_task_message) 109 | return 110 | } 111 | if (Task(currentTitle, currentDescription).isEmpty) { 112 | _snackbarText.value = Event(R.string.empty_task_message) 113 | return 114 | } 115 | 116 | val currentTaskId = taskId 117 | if (isNewTask || currentTaskId == null) { 118 | createTask(Task(currentTitle, currentDescription)) 119 | } else { 120 | val task = Task(currentTitle, currentDescription, taskCompleted, currentTaskId) 121 | updateTask(task) 122 | } 123 | } 124 | 125 | private fun createTask(newTask: Task) = viewModelScope.launch { 126 | tasksRepository.saveTask(newTask) 127 | _taskUpdatedEvent.value = Event(Unit) 128 | } 129 | 130 | private fun updateTask(task: Task) { 131 | if (isNewTask) { 132 | throw RuntimeException("updateTask() was called but task is new.") 133 | } 134 | viewModelScope.launch { 135 | tasksRepository.saveTask(task) 136 | _taskUpdatedEvent.value = Event(Unit) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Result.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.data 18 | 19 | import com.example.android.architecture.blueprints.todoapp.data.Result.Success 20 | 21 | /** 22 | * A generic class that holds a value with its loading status. 23 | * @param 24 | */ 25 | sealed class Result { 26 | 27 | data class Success(val data: T) : Result() 28 | data class Error(val exception: Exception) : Result() 29 | object Loading : Result() 30 | 31 | override fun toString(): String { 32 | return when (this) { 33 | is Success<*> -> "Success[data=$data]" 34 | is Error -> "Error[exception=$exception]" 35 | Loading -> "Loading" 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * `true` if [Result] is of type [Success] & holds non-null [Success.data]. 42 | */ 43 | val Result<*>.succeeded 44 | get() = this is Success && data != null 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.data 17 | 18 | import androidx.room.ColumnInfo 19 | import androidx.room.Entity 20 | import androidx.room.PrimaryKey 21 | import java.util.UUID 22 | 23 | /** 24 | * Immutable model class for a Task. In order to compile with Room, we can't use @JvmOverloads to 25 | * generate multiple constructors. 26 | * 27 | * @param title title of the task 28 | * @param description description of the task 29 | * @param isCompleted whether or not this task is completed 30 | * @param id id of the task 31 | */ 32 | @Entity(tableName = "tasks") 33 | data class Task @JvmOverloads constructor( 34 | @ColumnInfo(name = "title") var title: String = "", 35 | @ColumnInfo(name = "description") var description: String = "", 36 | @ColumnInfo(name = "completed") var isCompleted: Boolean = false, 37 | @PrimaryKey @ColumnInfo(name = "entryid") var id: String = UUID.randomUUID().toString() 38 | ) { 39 | 40 | val titleForList: String 41 | get() = if (title.isNotEmpty()) title else description 42 | 43 | 44 | val isActive 45 | get() = !isCompleted 46 | 47 | val isEmpty 48 | get() = title.isEmpty() || description.isEmpty() 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/DefaultTasksRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.data.source 17 | 18 | import android.app.Application 19 | import androidx.lifecycle.LiveData 20 | import androidx.room.Room 21 | import com.example.android.architecture.blueprints.todoapp.data.Result 22 | import com.example.android.architecture.blueprints.todoapp.data.Result.Success 23 | import com.example.android.architecture.blueprints.todoapp.data.Task 24 | import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksLocalDataSource 25 | import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase 26 | import com.example.android.architecture.blueprints.todoapp.data.source.remote.TasksRemoteDataSource 27 | import kotlinx.coroutines.CoroutineDispatcher 28 | import kotlinx.coroutines.Dispatchers 29 | import kotlinx.coroutines.coroutineScope 30 | import kotlinx.coroutines.launch 31 | import kotlinx.coroutines.withContext 32 | 33 | /** 34 | * Concrete implementation to load tasks from the data sources into a cache. 35 | */ 36 | class DefaultTasksRepository private constructor(application: Application) { 37 | 38 | private val tasksRemoteDataSource: TasksDataSource 39 | private val tasksLocalDataSource: TasksDataSource 40 | private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO 41 | 42 | companion object { 43 | @Volatile 44 | private var INSTANCE: DefaultTasksRepository? = null 45 | 46 | fun getRepository(app: Application): DefaultTasksRepository { 47 | return INSTANCE ?: synchronized(this) { 48 | DefaultTasksRepository(app).also { 49 | INSTANCE = it 50 | } 51 | } 52 | } 53 | } 54 | 55 | init { 56 | val database = Room.databaseBuilder(application.applicationContext, 57 | ToDoDatabase::class.java, "Tasks.db") 58 | .build() 59 | 60 | tasksRemoteDataSource = TasksRemoteDataSource 61 | tasksLocalDataSource = TasksLocalDataSource(database.taskDao()) 62 | } 63 | 64 | suspend fun getTasks(forceUpdate: Boolean = false): Result> { 65 | if (forceUpdate) { 66 | try { 67 | updateTasksFromRemoteDataSource() 68 | } catch (ex: Exception) { 69 | return Result.Error(ex) 70 | } 71 | } 72 | return tasksLocalDataSource.getTasks() 73 | } 74 | 75 | suspend fun refreshTasks() { 76 | updateTasksFromRemoteDataSource() 77 | } 78 | 79 | fun observeTasks(): LiveData>> { 80 | return tasksLocalDataSource.observeTasks() 81 | } 82 | 83 | suspend fun refreshTask(taskId: String) { 84 | updateTaskFromRemoteDataSource(taskId) 85 | } 86 | 87 | private suspend fun updateTasksFromRemoteDataSource() { 88 | val remoteTasks = tasksRemoteDataSource.getTasks() 89 | 90 | if (remoteTasks is Success) { 91 | // Real apps might want to do a proper sync. 92 | tasksLocalDataSource.deleteAllTasks() 93 | remoteTasks.data.forEach { task -> 94 | tasksLocalDataSource.saveTask(task) 95 | } 96 | } else if (remoteTasks is Result.Error) { 97 | throw remoteTasks.exception 98 | } 99 | } 100 | 101 | fun observeTask(taskId: String): LiveData> { 102 | return tasksLocalDataSource.observeTask(taskId) 103 | } 104 | 105 | private suspend fun updateTaskFromRemoteDataSource(taskId: String) { 106 | val remoteTask = tasksRemoteDataSource.getTask(taskId) 107 | 108 | if (remoteTask is Success) { 109 | tasksLocalDataSource.saveTask(remoteTask.data) 110 | } 111 | } 112 | 113 | /** 114 | * Relies on [getTasks] to fetch data and picks the task with the same ID. 115 | */ 116 | suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result { 117 | if (forceUpdate) { 118 | updateTaskFromRemoteDataSource(taskId) 119 | } 120 | return tasksLocalDataSource.getTask(taskId) 121 | } 122 | 123 | suspend fun saveTask(task: Task) { 124 | coroutineScope { 125 | launch { tasksRemoteDataSource.saveTask(task) } 126 | launch { tasksLocalDataSource.saveTask(task) } 127 | } 128 | } 129 | 130 | suspend fun completeTask(task: Task) { 131 | coroutineScope { 132 | launch { tasksRemoteDataSource.completeTask(task) } 133 | launch { tasksLocalDataSource.completeTask(task) } 134 | } 135 | } 136 | 137 | suspend fun completeTask(taskId: String) { 138 | withContext(ioDispatcher) { 139 | (getTaskWithId(taskId) as? Success)?.let { it -> 140 | completeTask(it.data) 141 | } 142 | } 143 | } 144 | 145 | suspend fun activateTask(task: Task) = withContext(ioDispatcher) { 146 | coroutineScope { 147 | launch { tasksRemoteDataSource.activateTask(task) } 148 | launch { tasksLocalDataSource.activateTask(task) } 149 | } 150 | } 151 | 152 | suspend fun activateTask(taskId: String) { 153 | withContext(ioDispatcher) { 154 | (getTaskWithId(taskId) as? Success)?.let { it -> 155 | activateTask(it.data) 156 | } 157 | } 158 | } 159 | 160 | suspend fun clearCompletedTasks() { 161 | coroutineScope { 162 | launch { tasksRemoteDataSource.clearCompletedTasks() } 163 | launch { tasksLocalDataSource.clearCompletedTasks() } 164 | } 165 | } 166 | 167 | suspend fun deleteAllTasks() { 168 | withContext(ioDispatcher) { 169 | coroutineScope { 170 | launch { tasksRemoteDataSource.deleteAllTasks() } 171 | launch { tasksLocalDataSource.deleteAllTasks() } 172 | } 173 | } 174 | } 175 | 176 | suspend fun deleteTask(taskId: String) { 177 | coroutineScope { 178 | launch { tasksRemoteDataSource.deleteTask(taskId) } 179 | launch { tasksLocalDataSource.deleteTask(taskId) } 180 | } 181 | } 182 | 183 | private suspend fun getTaskWithId(id: String): Result { 184 | return tasksLocalDataSource.getTask(id) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.data.source 17 | 18 | import androidx.lifecycle.LiveData 19 | import com.example.android.architecture.blueprints.todoapp.data.Result 20 | import com.example.android.architecture.blueprints.todoapp.data.Task 21 | 22 | /** 23 | * Main entry point for accessing tasks data. 24 | */ 25 | interface TasksDataSource { 26 | 27 | 28 | fun observeTasks(): LiveData>> 29 | 30 | suspend fun getTasks(): Result> 31 | 32 | suspend fun refreshTasks() 33 | 34 | fun observeTask(taskId: String): LiveData> 35 | 36 | suspend fun getTask(taskId: String): Result 37 | 38 | suspend fun refreshTask(taskId: String) 39 | 40 | suspend fun saveTask(task: Task) 41 | 42 | suspend fun completeTask(task: Task) 43 | 44 | suspend fun completeTask(taskId: String) 45 | 46 | suspend fun activateTask(task: Task) 47 | 48 | suspend fun activateTask(taskId: String) 49 | 50 | suspend fun clearCompletedTasks() 51 | 52 | suspend fun deleteAllTasks() 53 | 54 | suspend fun deleteTask(taskId: String) 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.data.source.local 18 | 19 | import androidx.lifecycle.LiveData 20 | import androidx.room.Dao 21 | import androidx.room.Insert 22 | import androidx.room.OnConflictStrategy 23 | import androidx.room.Query 24 | import androidx.room.Update 25 | import com.example.android.architecture.blueprints.todoapp.data.Task 26 | 27 | /** 28 | * Data Access Object for the tasks table. 29 | */ 30 | @Dao 31 | interface TasksDao { 32 | 33 | /** 34 | * Observes list of tasks. 35 | * 36 | * @return all tasks. 37 | */ 38 | @Query("SELECT * FROM Tasks") 39 | fun observeTasks(): LiveData> 40 | 41 | /** 42 | * Observes a single task. 43 | * 44 | * @param taskId the task id. 45 | * @return the task with taskId. 46 | */ 47 | @Query("SELECT * FROM Tasks WHERE entryid = :taskId") 48 | fun observeTaskById(taskId: String): LiveData 49 | 50 | /** 51 | * Select all tasks from the tasks table. 52 | * 53 | * @return all tasks. 54 | */ 55 | @Query("SELECT * FROM Tasks") 56 | suspend fun getTasks(): List 57 | 58 | /** 59 | * Select a task by id. 60 | * 61 | * @param taskId the task id. 62 | * @return the task with taskId. 63 | */ 64 | @Query("SELECT * FROM Tasks WHERE entryid = :taskId") 65 | suspend fun getTaskById(taskId: String): Task? 66 | 67 | /** 68 | * Insert a task in the database. If the task already exists, replace it. 69 | * 70 | * @param task the task to be inserted. 71 | */ 72 | @Insert(onConflict = OnConflictStrategy.REPLACE) 73 | suspend fun insertTask(task: Task) 74 | 75 | /** 76 | * Update a task. 77 | * 78 | * @param task task to be updated 79 | * @return the number of tasks updated. This should always be 1. 80 | */ 81 | @Update 82 | suspend fun updateTask(task: Task): Int 83 | 84 | /** 85 | * Update the complete status of a task 86 | * 87 | * @param taskId id of the task 88 | * @param completed status to be updated 89 | */ 90 | @Query("UPDATE tasks SET completed = :completed WHERE entryid = :taskId") 91 | suspend fun updateCompleted(taskId: String, completed: Boolean) 92 | 93 | /** 94 | * Delete a task by id. 95 | * 96 | * @return the number of tasks deleted. This should always be 1. 97 | */ 98 | @Query("DELETE FROM Tasks WHERE entryid = :taskId") 99 | suspend fun deleteTaskById(taskId: String): Int 100 | 101 | /** 102 | * Delete all tasks. 103 | */ 104 | @Query("DELETE FROM Tasks") 105 | suspend fun deleteTasks() 106 | 107 | /** 108 | * Delete all completed tasks from the table. 109 | * 110 | * @return the number of tasks deleted. 111 | */ 112 | @Query("DELETE FROM Tasks WHERE completed = 1") 113 | suspend fun deleteCompletedTasks(): Int 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.data.source.local 17 | 18 | import androidx.lifecycle.LiveData 19 | import androidx.lifecycle.map 20 | import com.example.android.architecture.blueprints.todoapp.data.Result 21 | import com.example.android.architecture.blueprints.todoapp.data.Result.Error 22 | import com.example.android.architecture.blueprints.todoapp.data.Result.Success 23 | import com.example.android.architecture.blueprints.todoapp.data.Task 24 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource 25 | import kotlinx.coroutines.CoroutineDispatcher 26 | import kotlinx.coroutines.Dispatchers 27 | import kotlinx.coroutines.withContext 28 | 29 | /** 30 | * Concrete implementation of a data source as a db. 31 | */ 32 | class TasksLocalDataSource internal constructor( 33 | private val tasksDao: TasksDao, 34 | private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO 35 | ) : TasksDataSource { 36 | 37 | override fun observeTasks(): LiveData>> { 38 | return tasksDao.observeTasks().map { 39 | Success(it) 40 | } 41 | } 42 | 43 | override fun observeTask(taskId: String): LiveData> { 44 | return tasksDao.observeTaskById(taskId).map { 45 | Success(it) 46 | } 47 | } 48 | 49 | override suspend fun refreshTask(taskId: String) { 50 | //NO-OP 51 | } 52 | 53 | override suspend fun refreshTasks() { 54 | //NO-OP 55 | } 56 | 57 | override suspend fun getTasks(): Result> = withContext(ioDispatcher) { 58 | return@withContext try { 59 | Success(tasksDao.getTasks()) 60 | } catch (e: Exception) { 61 | Error(e) 62 | } 63 | } 64 | 65 | override suspend fun getTask(taskId: String): Result = withContext(ioDispatcher) { 66 | try { 67 | val task = tasksDao.getTaskById(taskId) 68 | if (task != null) { 69 | return@withContext Success(task) 70 | } else { 71 | return@withContext Error(Exception("Task not found!")) 72 | } 73 | } catch (e: Exception) { 74 | return@withContext Error(e) 75 | } 76 | } 77 | 78 | override suspend fun saveTask(task: Task) = withContext(ioDispatcher) { 79 | tasksDao.insertTask(task) 80 | } 81 | 82 | override suspend fun completeTask(task: Task) = withContext(ioDispatcher) { 83 | tasksDao.updateCompleted(task.id, true) 84 | } 85 | 86 | override suspend fun completeTask(taskId: String) { 87 | tasksDao.updateCompleted(taskId, true) 88 | } 89 | 90 | override suspend fun activateTask(task: Task) = withContext(ioDispatcher) { 91 | tasksDao.updateCompleted(task.id, false) 92 | } 93 | 94 | override suspend fun activateTask(taskId: String) { 95 | tasksDao.updateCompleted(taskId, false) 96 | } 97 | 98 | override suspend fun clearCompletedTasks() = withContext(ioDispatcher) { 99 | tasksDao.deleteCompletedTasks() 100 | } 101 | 102 | override suspend fun deleteAllTasks() = withContext(ioDispatcher) { 103 | tasksDao.deleteTasks() 104 | } 105 | 106 | override suspend fun deleteTask(taskId: String) = withContext(ioDispatcher) { 107 | tasksDao.deleteTaskById(taskId) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.data.source.local 18 | 19 | import androidx.room.Database 20 | import androidx.room.RoomDatabase 21 | import com.example.android.architecture.blueprints.todoapp.data.Task 22 | 23 | /** 24 | * The Room Database that contains the Task table. 25 | * 26 | * Note that exportSchema should be true in production databases. 27 | */ 28 | @Database(entities = [Task::class], version = 1, exportSchema = false) 29 | abstract class ToDoDatabase : RoomDatabase() { 30 | 31 | abstract fun taskDao(): TasksDao 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/TasksRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.data.source.remote 17 | 18 | import android.annotation.SuppressLint 19 | import androidx.lifecycle.LiveData 20 | import androidx.lifecycle.MutableLiveData 21 | import androidx.lifecycle.map 22 | import com.example.android.architecture.blueprints.todoapp.data.Result 23 | import com.example.android.architecture.blueprints.todoapp.data.Result.Error 24 | import com.example.android.architecture.blueprints.todoapp.data.Result.Success 25 | import com.example.android.architecture.blueprints.todoapp.data.Task 26 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource 27 | import kotlinx.coroutines.delay 28 | 29 | /** 30 | * Implementation of the data source that adds a latency simulating network. 31 | */ 32 | object TasksRemoteDataSource : TasksDataSource { 33 | 34 | private const val SERVICE_LATENCY_IN_MILLIS = 2000L 35 | 36 | private var TASKS_SERVICE_DATA = LinkedHashMap(2) 37 | 38 | init { 39 | addTask("Build tower in Pisa", "Ground looks good, no foundation work required.") 40 | addTask("Finish bridge in Tacoma", "Found awesome girders at half the cost!") 41 | } 42 | 43 | private val observableTasks = MutableLiveData>>() 44 | 45 | @SuppressLint("NullSafeMutableLiveData") 46 | override suspend fun refreshTasks() { 47 | observableTasks.value = getTasks() 48 | } 49 | 50 | override suspend fun refreshTask(taskId: String) { 51 | refreshTasks() 52 | } 53 | 54 | override fun observeTasks(): LiveData>> { 55 | return observableTasks 56 | } 57 | 58 | override fun observeTask(taskId: String): LiveData> { 59 | return observableTasks.map { tasks -> 60 | when (tasks) { 61 | is Result.Loading -> Result.Loading 62 | is Error -> Error(tasks.exception) 63 | is Success -> { 64 | val task = tasks.data.firstOrNull() { it.id == taskId } 65 | ?: return@map Error(Exception("Not found")) 66 | Success(task) 67 | } 68 | } 69 | } 70 | } 71 | 72 | override suspend fun getTasks(): Result> { 73 | // Simulate network by delaying the execution. 74 | val tasks = TASKS_SERVICE_DATA.values.toList() 75 | delay(SERVICE_LATENCY_IN_MILLIS) 76 | return Success(tasks) 77 | } 78 | 79 | override suspend fun getTask(taskId: String): Result { 80 | // Simulate network by delaying the execution. 81 | delay(SERVICE_LATENCY_IN_MILLIS) 82 | TASKS_SERVICE_DATA[taskId]?.let { 83 | return Success(it) 84 | } 85 | return Error(Exception("Task not found")) 86 | } 87 | 88 | private fun addTask(title: String, description: String) { 89 | val newTask = Task(title, description) 90 | TASKS_SERVICE_DATA[newTask.id] = newTask 91 | } 92 | 93 | override suspend fun saveTask(task: Task) { 94 | TASKS_SERVICE_DATA[task.id] = task 95 | } 96 | 97 | override suspend fun completeTask(task: Task) { 98 | val completedTask = Task(task.title, task.description, true, task.id) 99 | TASKS_SERVICE_DATA[task.id] = completedTask 100 | } 101 | 102 | override suspend fun completeTask(taskId: String) { 103 | // Not required for the remote data source 104 | } 105 | 106 | override suspend fun activateTask(task: Task) { 107 | val activeTask = Task(task.title, task.description, false, task.id) 108 | TASKS_SERVICE_DATA[task.id] = activeTask 109 | } 110 | 111 | override suspend fun activateTask(taskId: String) { 112 | // Not required for the remote data source 113 | } 114 | 115 | override suspend fun clearCompletedTasks() { 116 | TASKS_SERVICE_DATA = TASKS_SERVICE_DATA.filterValues { 117 | !it.isCompleted 118 | } as LinkedHashMap 119 | } 120 | 121 | override suspend fun deleteAllTasks() { 122 | TASKS_SERVICE_DATA.clear() 123 | } 124 | 125 | override suspend fun deleteTask(taskId: String) { 126 | TASKS_SERVICE_DATA.remove(taskId) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.statistics 17 | 18 | import android.os.Bundle 19 | import android.view.LayoutInflater 20 | import android.view.View 21 | import android.view.ViewGroup 22 | import androidx.databinding.DataBindingUtil 23 | import androidx.fragment.app.Fragment 24 | import androidx.fragment.app.viewModels 25 | import com.example.android.architecture.blueprints.todoapp.R 26 | import com.example.android.architecture.blueprints.todoapp.databinding.StatisticsFragBinding 27 | import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout 28 | 29 | /** 30 | * Main UI for the statistics screen. 31 | */ 32 | class StatisticsFragment : Fragment() { 33 | 34 | private lateinit var viewDataBinding: StatisticsFragBinding 35 | 36 | private val viewModel by viewModels() 37 | 38 | override fun onCreateView( 39 | inflater: LayoutInflater, container: ViewGroup?, 40 | savedInstanceState: Bundle? 41 | ): View? { 42 | viewDataBinding = DataBindingUtil.inflate( 43 | inflater, R.layout.statistics_frag, container, 44 | false 45 | ) 46 | return viewDataBinding.root 47 | } 48 | 49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 50 | viewDataBinding.viewmodel = viewModel 51 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 52 | this.setupRefreshLayout(viewDataBinding.refreshLayout) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.statistics 18 | 19 | import com.example.android.architecture.blueprints.todoapp.data.Task 20 | 21 | /** 22 | * Function that does some trivial computation. Used to showcase unit tests. 23 | */ 24 | internal fun getActiveAndCompletedStats(tasks: List?): StatsResult { 25 | val totalTasks = tasks!!.size 26 | val numberOfActiveTasks = tasks.count { it.isActive } 27 | return StatsResult( 28 | activeTasksPercent = 100f * numberOfActiveTasks / tasks.size, 29 | completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size 30 | ) 31 | } 32 | 33 | data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float) 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.statistics 18 | 19 | import android.app.Application 20 | import androidx.lifecycle.* 21 | import com.example.android.architecture.blueprints.todoapp.data.Result 22 | import com.example.android.architecture.blueprints.todoapp.data.Result.Error 23 | import com.example.android.architecture.blueprints.todoapp.data.Result.Success 24 | import com.example.android.architecture.blueprints.todoapp.data.Task 25 | import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository 26 | import kotlinx.coroutines.launch 27 | 28 | /** 29 | * ViewModel for the statistics screen. 30 | */ 31 | class StatisticsViewModel(application: Application) : AndroidViewModel(application) { 32 | 33 | // Note, for testing and architecture purposes, it's bad practice to construct the repository 34 | // here. We'll show you how to fix this during the codelab 35 | private val tasksRepository = DefaultTasksRepository.getRepository(application) 36 | 37 | private val tasks: LiveData>> = tasksRepository.observeTasks() 38 | private val _dataLoading = MutableLiveData(false) 39 | private val stats: LiveData = tasks.map { 40 | if (it is Success) { 41 | getActiveAndCompletedStats(it.data) 42 | } else { 43 | null 44 | } 45 | } 46 | 47 | val activeTasksPercent = stats.map { 48 | it?.activeTasksPercent ?: 0f } 49 | val completedTasksPercent: LiveData = stats.map { it?.completedTasksPercent ?: 0f } 50 | val dataLoading: LiveData = _dataLoading 51 | val error: LiveData = tasks.map { it is Error } 52 | val empty: LiveData = tasks.map { (it as? Success)?.data.isNullOrEmpty() } 53 | 54 | fun refresh() { 55 | _dataLoading.value = true 56 | viewModelScope.launch { 57 | tasksRepository.refreshTasks() 58 | _dataLoading.value = false 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.taskdetail 17 | 18 | import android.os.Bundle 19 | import android.view.LayoutInflater 20 | import android.view.Menu 21 | import android.view.MenuInflater 22 | import android.view.MenuItem 23 | import android.view.View 24 | import android.view.ViewGroup 25 | import androidx.fragment.app.Fragment 26 | import androidx.fragment.app.viewModels 27 | import androidx.navigation.fragment.findNavController 28 | import androidx.navigation.fragment.navArgs 29 | import com.example.android.architecture.blueprints.todoapp.EventObserver 30 | import com.example.android.architecture.blueprints.todoapp.R 31 | import com.example.android.architecture.blueprints.todoapp.databinding.TaskdetailFragBinding 32 | import com.example.android.architecture.blueprints.todoapp.tasks.DELETE_RESULT_OK 33 | import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout 34 | import com.example.android.architecture.blueprints.todoapp.util.setupSnackbar 35 | import com.google.android.material.snackbar.Snackbar 36 | 37 | /** 38 | * Main UI for the task detail screen. 39 | */ 40 | class TaskDetailFragment : Fragment() { 41 | private lateinit var viewDataBinding: TaskdetailFragBinding 42 | 43 | private val args: TaskDetailFragmentArgs by navArgs() 44 | 45 | private val viewModel by viewModels() 46 | 47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 48 | setupFab() 49 | view.setupSnackbar(this, viewModel.snackbarText, Snackbar.LENGTH_SHORT) 50 | setupNavigation() 51 | this.setupRefreshLayout(viewDataBinding.refreshLayout) 52 | } 53 | 54 | private fun setupNavigation() { 55 | viewModel.deleteTaskEvent.observe(viewLifecycleOwner, EventObserver { 56 | val action = TaskDetailFragmentDirections 57 | .actionTaskDetailFragmentToTasksFragment(DELETE_RESULT_OK) 58 | findNavController().navigate(action) 59 | }) 60 | viewModel.editTaskEvent.observe(viewLifecycleOwner, EventObserver { 61 | val action = TaskDetailFragmentDirections 62 | .actionTaskDetailFragmentToAddEditTaskFragment( 63 | args.taskId, 64 | resources.getString(R.string.edit_task) 65 | ) 66 | findNavController().navigate(action) 67 | }) 68 | } 69 | 70 | private fun setupFab() { 71 | activity?.findViewById(R.id.edit_task_fab)?.setOnClickListener { 72 | viewModel.editTask() 73 | } 74 | } 75 | 76 | override fun onCreateView( 77 | inflater: LayoutInflater, 78 | container: ViewGroup?, 79 | savedInstanceState: Bundle? 80 | ): View? { 81 | val view = inflater.inflate(R.layout.taskdetail_frag, container, false) 82 | viewDataBinding = TaskdetailFragBinding.bind(view).apply { 83 | viewmodel = viewModel 84 | } 85 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 86 | 87 | viewModel.start(args.taskId) 88 | 89 | setHasOptionsMenu(true) 90 | return view 91 | } 92 | 93 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 94 | return when (item.itemId) { 95 | R.id.menu_delete -> { 96 | viewModel.deleteTask() 97 | true 98 | } 99 | else -> false 100 | } 101 | } 102 | 103 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 104 | inflater.inflate(R.menu.taskdetail_fragment_menu, menu) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.taskdetail 17 | 18 | import android.app.Application 19 | import androidx.annotation.StringRes 20 | import androidx.lifecycle.* 21 | import com.example.android.architecture.blueprints.todoapp.Event 22 | import com.example.android.architecture.blueprints.todoapp.R 23 | import com.example.android.architecture.blueprints.todoapp.data.Result 24 | import com.example.android.architecture.blueprints.todoapp.data.Result.Success 25 | import com.example.android.architecture.blueprints.todoapp.data.Task 26 | import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository 27 | import kotlinx.coroutines.launch 28 | 29 | /** 30 | * ViewModel for the Details screen. 31 | */ 32 | class TaskDetailViewModel(application: Application) : AndroidViewModel(application) { 33 | 34 | // Note, for testing and architecture purposes, it's bad practice to construct the repository 35 | // here. We'll show you how to fix this during the codelab 36 | private val tasksRepository = DefaultTasksRepository.getRepository(application) 37 | 38 | private val _taskId = MutableLiveData() 39 | 40 | private val _task = _taskId.switchMap { taskId -> 41 | tasksRepository.observeTask(taskId).map { computeResult(it) } 42 | } 43 | val task: LiveData = _task 44 | 45 | val isDataAvailable: LiveData = _task.map { it != null } 46 | 47 | private val _dataLoading = MutableLiveData() 48 | val dataLoading: LiveData = _dataLoading 49 | 50 | private val _editTaskEvent = MutableLiveData>() 51 | val editTaskEvent: LiveData> = _editTaskEvent 52 | 53 | private val _deleteTaskEvent = MutableLiveData>() 54 | val deleteTaskEvent: LiveData> = _deleteTaskEvent 55 | 56 | private val _snackbarText = MutableLiveData>() 57 | val snackbarText: LiveData> = _snackbarText 58 | 59 | // This LiveData depends on another so we can use a transformation. 60 | val completed: LiveData = _task.map { input: Task? -> 61 | input?.isCompleted ?: false 62 | } 63 | 64 | fun deleteTask() = viewModelScope.launch { 65 | _taskId.value?.let { 66 | tasksRepository.deleteTask(it) 67 | _deleteTaskEvent.value = Event(Unit) 68 | } 69 | } 70 | 71 | fun editTask() { 72 | _editTaskEvent.value = Event(Unit) 73 | } 74 | 75 | fun setCompleted(completed: Boolean) = viewModelScope.launch { 76 | val task = _task.value ?: return@launch 77 | if (completed) { 78 | tasksRepository.completeTask(task) 79 | showSnackbarMessage(R.string.task_marked_complete) 80 | } else { 81 | tasksRepository.activateTask(task) 82 | showSnackbarMessage(R.string.task_marked_active) 83 | } 84 | } 85 | 86 | fun start(taskId: String) { 87 | // If we're already loading or already loaded, return (might be a config change) 88 | if (_dataLoading.value == true || taskId == _taskId.value) { 89 | return 90 | } 91 | // Trigger the load 92 | _taskId.value = taskId 93 | } 94 | 95 | private fun computeResult(taskResult: Result): Task? { 96 | return if (taskResult is Success) { 97 | taskResult.data 98 | } else { 99 | showSnackbarMessage(R.string.loading_tasks_error) 100 | null 101 | } 102 | } 103 | 104 | 105 | fun refresh() { 106 | // Refresh the repository and the task will be updated automatically. 107 | _task.value?.let { 108 | _dataLoading.value = true 109 | viewModelScope.launch { 110 | tasksRepository.refreshTask(it.id) 111 | _dataLoading.value = false 112 | } 113 | } 114 | } 115 | 116 | private fun showSnackbarMessage(@StringRes message: Int) { 117 | _snackbarText.value = Event(message) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.tasks 17 | 18 | import android.app.Activity 19 | import android.os.Bundle 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.drawerlayout.widget.DrawerLayout 22 | import androidx.navigation.NavController 23 | import androidx.navigation.findNavController 24 | import androidx.navigation.ui.AppBarConfiguration 25 | import androidx.navigation.ui.navigateUp 26 | import androidx.navigation.ui.setupActionBarWithNavController 27 | import androidx.navigation.ui.setupWithNavController 28 | import com.example.android.architecture.blueprints.todoapp.R 29 | import com.google.android.material.navigation.NavigationView 30 | 31 | /** 32 | * Main activity for the todoapp. Holds the Navigation Host Fragment and the Drawer, Toolbar, etc. 33 | */ 34 | class TasksActivity : AppCompatActivity() { 35 | 36 | private lateinit var drawerLayout: DrawerLayout 37 | private lateinit var appBarConfiguration: AppBarConfiguration 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | setContentView(R.layout.tasks_act) 42 | setupNavigationDrawer() 43 | setSupportActionBar(findViewById(R.id.toolbar)) 44 | 45 | val navController: NavController = findNavController(R.id.nav_host_fragment) 46 | appBarConfiguration = 47 | AppBarConfiguration.Builder(R.id.tasks_fragment_dest, R.id.statistics_fragment_dest) 48 | .setOpenableLayout(drawerLayout) 49 | .build() 50 | setupActionBarWithNavController(navController, appBarConfiguration) 51 | findViewById(R.id.nav_view) 52 | .setupWithNavController(navController) 53 | } 54 | 55 | override fun onSupportNavigateUp(): Boolean { 56 | return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration) 57 | || super.onSupportNavigateUp() 58 | } 59 | 60 | private fun setupNavigationDrawer() { 61 | drawerLayout = (findViewById(R.id.drawer_layout)) 62 | .apply { 63 | setStatusBarBackground(R.color.colorPrimaryDark) 64 | } 65 | } 66 | } 67 | 68 | // Keys for navigation 69 | const val ADD_EDIT_RESULT_OK = Activity.RESULT_FIRST_USER + 1 70 | const val DELETE_RESULT_OK = Activity.RESULT_FIRST_USER + 2 71 | const val EDIT_RESULT_OK = Activity.RESULT_FIRST_USER + 3 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.tasks 17 | 18 | import android.view.LayoutInflater 19 | import android.view.ViewGroup 20 | import androidx.recyclerview.widget.DiffUtil 21 | import androidx.recyclerview.widget.ListAdapter 22 | import androidx.recyclerview.widget.RecyclerView 23 | import com.example.android.architecture.blueprints.todoapp.data.Task 24 | import com.example.android.architecture.blueprints.todoapp.databinding.TaskItemBinding 25 | import com.example.android.architecture.blueprints.todoapp.tasks.TasksAdapter.ViewHolder 26 | 27 | /** 28 | * Adapter for the task list. Has a reference to the [TasksViewModel] to send actions back to it. 29 | */ 30 | class TasksAdapter(private val viewModel: TasksViewModel) : 31 | ListAdapter(TaskDiffCallback()) { 32 | 33 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 34 | val item = getItem(position) 35 | 36 | holder.bind(viewModel, item) 37 | } 38 | 39 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 40 | return ViewHolder.from(parent) 41 | } 42 | 43 | class ViewHolder private constructor(val binding: TaskItemBinding) : 44 | RecyclerView.ViewHolder(binding.root) { 45 | 46 | fun bind(viewModel: TasksViewModel, item: Task) { 47 | 48 | binding.viewmodel = viewModel 49 | binding.task = item 50 | binding.executePendingBindings() 51 | } 52 | 53 | companion object { 54 | fun from(parent: ViewGroup): ViewHolder { 55 | val layoutInflater = LayoutInflater.from(parent.context) 56 | val binding = TaskItemBinding.inflate(layoutInflater, parent, false) 57 | 58 | return ViewHolder(binding) 59 | } 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Callback for calculating the diff between two non-null items in a list. 66 | * 67 | * Used by ListAdapter to calculate the minimum number of changes between and old list and a new 68 | * list that's been passed to `submitList`. 69 | */ 70 | class TaskDiffCallback : DiffUtil.ItemCallback() { 71 | override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean { 72 | return oldItem.id == newItem.id 73 | } 74 | 75 | override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean { 76 | return oldItem == newItem 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFilterType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.tasks 17 | 18 | /** 19 | * Used with the filter spinner in the tasks list. 20 | */ 21 | enum class TasksFilterType { 22 | /** 23 | * Do not filter tasks. 24 | */ 25 | ALL_TASKS, 26 | 27 | /** 28 | * Filters only the active (not completed yet) tasks. 29 | */ 30 | ACTIVE_TASKS, 31 | 32 | /** 33 | * Filters only the completed tasks. 34 | */ 35 | COMPLETED_TASKS 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.tasks 18 | 19 | import android.os.Bundle 20 | import android.view.LayoutInflater 21 | import android.view.Menu 22 | import android.view.MenuInflater 23 | import android.view.MenuItem 24 | import android.view.View 25 | import android.view.ViewGroup 26 | import androidx.appcompat.widget.PopupMenu 27 | import androidx.fragment.app.Fragment 28 | import androidx.fragment.app.viewModels 29 | import androidx.navigation.fragment.findNavController 30 | import androidx.navigation.fragment.navArgs 31 | import com.example.android.architecture.blueprints.todoapp.EventObserver 32 | import com.example.android.architecture.blueprints.todoapp.R 33 | import com.example.android.architecture.blueprints.todoapp.data.Task 34 | import com.example.android.architecture.blueprints.todoapp.databinding.TasksFragBinding 35 | import com.example.android.architecture.blueprints.todoapp.util.setupRefreshLayout 36 | import com.example.android.architecture.blueprints.todoapp.util.setupSnackbar 37 | import com.google.android.material.floatingactionbutton.FloatingActionButton 38 | import com.google.android.material.snackbar.Snackbar 39 | import timber.log.Timber 40 | 41 | /** 42 | * Display a grid of [Task]s. User can choose to view all, active or completed tasks. 43 | */ 44 | class TasksFragment : Fragment() { 45 | 46 | private val viewModel by viewModels() 47 | 48 | private val args: TasksFragmentArgs by navArgs() 49 | 50 | private lateinit var viewDataBinding: TasksFragBinding 51 | 52 | private lateinit var listAdapter: TasksAdapter 53 | 54 | override fun onCreateView( 55 | inflater: LayoutInflater, container: ViewGroup?, 56 | savedInstanceState: Bundle? 57 | ): View { 58 | viewDataBinding = TasksFragBinding.inflate(inflater, container, false).apply { 59 | viewmodel = viewModel 60 | } 61 | setHasOptionsMenu(true) 62 | return viewDataBinding.root 63 | } 64 | 65 | override fun onOptionsItemSelected(item: MenuItem) = 66 | when (item.itemId) { 67 | R.id.menu_clear -> { 68 | viewModel.clearCompletedTasks() 69 | true 70 | } 71 | R.id.menu_filter -> { 72 | showFilteringPopUpMenu() 73 | true 74 | } 75 | R.id.menu_refresh -> { 76 | viewModel.loadTasks(true) 77 | true 78 | } 79 | else -> false 80 | } 81 | 82 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 83 | inflater.inflate(R.menu.tasks_fragment_menu, menu) 84 | } 85 | 86 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 87 | // Set the lifecycle owner to the lifecycle of the view 88 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 89 | setupSnackbar() 90 | setupListAdapter() 91 | setupRefreshLayout(viewDataBinding.refreshLayout, viewDataBinding.tasksList) 92 | setupNavigation() 93 | setupFab() 94 | } 95 | 96 | private fun setupNavigation() { 97 | viewModel.openTaskEvent.observe(viewLifecycleOwner, EventObserver { 98 | openTaskDetails(it) 99 | }) 100 | viewModel.newTaskEvent.observe(viewLifecycleOwner, EventObserver { 101 | navigateToAddNewTask() 102 | }) 103 | } 104 | 105 | private fun setupSnackbar() { 106 | view?.setupSnackbar(this, viewModel.snackbarText, Snackbar.LENGTH_SHORT) 107 | arguments?.let { 108 | viewModel.showEditResultMessage(args.userMessage) 109 | } 110 | } 111 | 112 | private fun showFilteringPopUpMenu() { 113 | val view = activity?.findViewById(R.id.menu_filter) ?: return 114 | PopupMenu(requireContext(), view).run { 115 | menuInflater.inflate(R.menu.filter_tasks, menu) 116 | 117 | setOnMenuItemClickListener { 118 | viewModel.setFiltering( 119 | when (it.itemId) { 120 | R.id.active -> TasksFilterType.ACTIVE_TASKS 121 | R.id.completed -> TasksFilterType.COMPLETED_TASKS 122 | else -> TasksFilterType.ALL_TASKS 123 | } 124 | ) 125 | true 126 | } 127 | show() 128 | } 129 | } 130 | 131 | private fun setupFab() { 132 | viewDataBinding.addTaskFab.setOnClickListener { 133 | navigateToAddNewTask() 134 | } 135 | } 136 | 137 | private fun navigateToAddNewTask() { 138 | val action = TasksFragmentDirections 139 | .actionTasksFragmentToAddEditTaskFragment( 140 | null, 141 | resources.getString(R.string.add_task) 142 | ) 143 | findNavController().navigate(action) 144 | } 145 | 146 | private fun openTaskDetails(taskId: String) { 147 | val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId) 148 | findNavController().navigate(action) 149 | } 150 | 151 | private fun setupListAdapter() { 152 | val viewModel = viewDataBinding.viewmodel 153 | if (viewModel != null) { 154 | listAdapter = TasksAdapter(viewModel) 155 | viewDataBinding.tasksList.adapter = listAdapter 156 | } else { 157 | Timber.w("ViewModel not initialized when attempting to set up adapter.") 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksListBindings.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.tasks 17 | 18 | import android.graphics.Paint 19 | import android.widget.TextView 20 | import androidx.databinding.BindingAdapter 21 | import androidx.recyclerview.widget.RecyclerView 22 | import com.example.android.architecture.blueprints.todoapp.data.Task 23 | 24 | 25 | 26 | /** 27 | * [BindingAdapter]s for the [Task]s list. 28 | */ 29 | @BindingAdapter("app:items") 30 | fun setItems(listView: RecyclerView, items: List?) { 31 | items?.let { 32 | (listView.adapter as TasksAdapter).submitList(items) 33 | } 34 | } 35 | 36 | @BindingAdapter("app:completedTask") 37 | fun setStyle(textView: TextView, enabled: Boolean) { 38 | if (enabled) { 39 | textView.paintFlags = textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG 40 | } else { 41 | textView.paintFlags = textView.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.tasks 17 | 18 | import android.app.Application 19 | import androidx.annotation.DrawableRes 20 | import androidx.annotation.StringRes 21 | import androidx.lifecycle.* 22 | import com.example.android.architecture.blueprints.todoapp.Event 23 | import com.example.android.architecture.blueprints.todoapp.R 24 | import com.example.android.architecture.blueprints.todoapp.data.Result 25 | import com.example.android.architecture.blueprints.todoapp.data.Result.Success 26 | import com.example.android.architecture.blueprints.todoapp.data.Task 27 | import com.example.android.architecture.blueprints.todoapp.data.source.DefaultTasksRepository 28 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource 29 | import kotlinx.coroutines.launch 30 | 31 | /** 32 | * ViewModel for the task list screen. 33 | */ 34 | class TasksViewModel(application: Application) : AndroidViewModel(application) { 35 | 36 | // Note, for testing and architecture purposes, it's bad practice to construct the repository 37 | // here. We'll show you how to fix this during the codelab 38 | private val tasksRepository = DefaultTasksRepository.getRepository(application) 39 | 40 | private val _forceUpdate = MutableLiveData(false) 41 | 42 | private val _items: LiveData> = _forceUpdate.switchMap { forceUpdate -> 43 | if (forceUpdate) { 44 | _dataLoading.value = true 45 | viewModelScope.launch { 46 | tasksRepository.refreshTasks() 47 | _dataLoading.value = false 48 | } 49 | } 50 | tasksRepository.observeTasks().switchMap { filterTasks(it) } 51 | 52 | } 53 | 54 | val items: LiveData> = _items 55 | 56 | private val _dataLoading = MutableLiveData() 57 | val dataLoading: LiveData = _dataLoading 58 | 59 | private val _currentFilteringLabel = MutableLiveData() 60 | val currentFilteringLabel: LiveData = _currentFilteringLabel 61 | 62 | private val _noTasksLabel = MutableLiveData() 63 | val noTasksLabel: LiveData = _noTasksLabel 64 | 65 | private val _noTaskIconRes = MutableLiveData() 66 | val noTaskIconRes: LiveData = _noTaskIconRes 67 | 68 | private val _tasksAddViewVisible = MutableLiveData() 69 | val tasksAddViewVisible: LiveData = _tasksAddViewVisible 70 | 71 | private val _snackbarText = MutableLiveData>() 72 | val snackbarText: LiveData> = _snackbarText 73 | 74 | private var currentFiltering = TasksFilterType.ALL_TASKS 75 | 76 | // Not used at the moment 77 | private val isDataLoadingError = MutableLiveData() 78 | 79 | private val _openTaskEvent = MutableLiveData>() 80 | val openTaskEvent: LiveData> = _openTaskEvent 81 | 82 | private val _newTaskEvent = MutableLiveData>() 83 | val newTaskEvent: LiveData> = _newTaskEvent 84 | 85 | private var resultMessageShown: Boolean = false 86 | 87 | // This LiveData depends on another so we can use a transformation. 88 | val empty: LiveData = Transformations.map(_items) { 89 | it.isEmpty() 90 | } 91 | 92 | init { 93 | // Set initial state 94 | setFiltering(TasksFilterType.ALL_TASKS) 95 | loadTasks(true) 96 | } 97 | 98 | /** 99 | * Sets the current task filtering type. 100 | * 101 | * @param requestType Can be [TasksFilterType.ALL_TASKS], 102 | * [TasksFilterType.COMPLETED_TASKS], or 103 | * [TasksFilterType.ACTIVE_TASKS] 104 | */ 105 | fun setFiltering(requestType: TasksFilterType) { 106 | currentFiltering = requestType 107 | 108 | // Depending on the filter type, set the filtering label, icon drawables, etc. 109 | when (requestType) { 110 | TasksFilterType.ALL_TASKS -> { 111 | setFilter( 112 | R.string.label_all, R.string.no_tasks_all, 113 | R.drawable.logo_no_fill, true 114 | ) 115 | } 116 | TasksFilterType.ACTIVE_TASKS -> { 117 | setFilter( 118 | R.string.label_active, R.string.no_tasks_active, 119 | R.drawable.ic_check_circle_96dp, false 120 | ) 121 | } 122 | TasksFilterType.COMPLETED_TASKS -> { 123 | setFilter( 124 | R.string.label_completed, R.string.no_tasks_completed, 125 | R.drawable.ic_verified_user_96dp, false 126 | ) 127 | } 128 | } 129 | // Refresh list 130 | loadTasks(false) 131 | } 132 | 133 | private fun setFilter( 134 | @StringRes filteringLabelString: Int, @StringRes noTasksLabelString: Int, 135 | @DrawableRes noTaskIconDrawable: Int, tasksAddVisible: Boolean 136 | ) { 137 | _currentFilteringLabel.value = filteringLabelString 138 | _noTasksLabel.value = noTasksLabelString 139 | _noTaskIconRes.value = noTaskIconDrawable 140 | _tasksAddViewVisible.value = tasksAddVisible 141 | } 142 | 143 | fun clearCompletedTasks() { 144 | viewModelScope.launch { 145 | tasksRepository.clearCompletedTasks() 146 | showSnackbarMessage(R.string.completed_tasks_cleared) 147 | } 148 | } 149 | 150 | fun completeTask(task: Task, completed: Boolean) = viewModelScope.launch { 151 | if (completed) { 152 | tasksRepository.completeTask(task) 153 | showSnackbarMessage(R.string.task_marked_complete) 154 | } else { 155 | tasksRepository.activateTask(task) 156 | showSnackbarMessage(R.string.task_marked_active) 157 | } 158 | } 159 | 160 | /** 161 | * Called by the Data Binding library and the FAB's click listener. 162 | */ 163 | fun addNewTask() { 164 | _newTaskEvent.value = Event(Unit) 165 | } 166 | 167 | /** 168 | * Called by Data Binding. 169 | */ 170 | fun openTask(taskId: String) { 171 | _openTaskEvent.value = Event(taskId) 172 | } 173 | 174 | fun showEditResultMessage(result: Int) { 175 | if (resultMessageShown) return 176 | when (result) { 177 | EDIT_RESULT_OK -> showSnackbarMessage(R.string.successfully_saved_task_message) 178 | ADD_EDIT_RESULT_OK -> showSnackbarMessage(R.string.successfully_added_task_message) 179 | DELETE_RESULT_OK -> showSnackbarMessage(R.string.successfully_deleted_task_message) 180 | } 181 | resultMessageShown = true 182 | } 183 | 184 | private fun showSnackbarMessage(message: Int) { 185 | _snackbarText.value = Event(message) 186 | } 187 | 188 | private fun filterTasks(tasksResult: Result>): LiveData> { 189 | // TODO: This is a good case for liveData builder. Replace when stable. 190 | val result = MutableLiveData>() 191 | 192 | if (tasksResult is Success) { 193 | isDataLoadingError.value = false 194 | viewModelScope.launch { 195 | result.value = filterItems(tasksResult.data, currentFiltering) 196 | } 197 | } else { 198 | result.value = emptyList() 199 | showSnackbarMessage(R.string.loading_tasks_error) 200 | isDataLoadingError.value = true 201 | } 202 | 203 | return result 204 | } 205 | 206 | /** 207 | * @param forceUpdate Pass in true to refresh the data in the [TasksDataSource] 208 | */ 209 | fun loadTasks(forceUpdate: Boolean) { 210 | _forceUpdate.value = forceUpdate 211 | } 212 | 213 | private fun filterItems(tasks: List, filteringType: TasksFilterType): List { 214 | val tasksToShow = ArrayList() 215 | // We filter the tasks based on the requestType 216 | for (task in tasks) { 217 | when (filteringType) { 218 | TasksFilterType.ALL_TASKS -> tasksToShow.add(task) 219 | TasksFilterType.ACTIVE_TASKS -> if (task.isActive) { 220 | tasksToShow.add(task) 221 | } 222 | TasksFilterType.COMPLETED_TASKS -> if (task.isCompleted) { 223 | tasksToShow.add(task) 224 | } 225 | } 226 | } 227 | return tasksToShow 228 | } 229 | 230 | fun refresh() { 231 | _forceUpdate.value = true 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ViewExt.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.android.architecture.blueprints.todoapp.util 17 | 18 | /** 19 | * Extension functions and Binding Adapters. 20 | */ 21 | 22 | import android.view.View 23 | import androidx.core.content.ContextCompat 24 | import androidx.fragment.app.Fragment 25 | import androidx.lifecycle.LifecycleOwner 26 | import androidx.lifecycle.LiveData 27 | import androidx.lifecycle.Observer 28 | import com.example.android.architecture.blueprints.todoapp.Event 29 | import com.example.android.architecture.blueprints.todoapp.R 30 | import com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout 31 | import com.google.android.material.snackbar.Snackbar 32 | 33 | /** 34 | * Transforms static java function Snackbar.make() to an extension function on View. 35 | */ 36 | fun View.showSnackbar(snackbarText: String, timeLength: Int) { 37 | Snackbar.make(this, snackbarText, timeLength).run { 38 | show() 39 | } 40 | } 41 | 42 | /** 43 | * Triggers a snackbar message when the value contained by snackbarTaskMessageLiveEvent is modified. 44 | */ 45 | fun View.setupSnackbar( 46 | lifecycleOwner: LifecycleOwner, 47 | snackbarEvent: LiveData>, 48 | timeLength: Int 49 | ) { 50 | 51 | snackbarEvent.observe(lifecycleOwner, Observer { event -> 52 | event.getContentIfNotHandled()?.let { 53 | showSnackbar(context.getString(it), timeLength) 54 | } 55 | }) 56 | } 57 | 58 | fun Fragment.setupRefreshLayout( 59 | refreshLayout: ScrollChildSwipeRefreshLayout, 60 | scrollUpChild: View? = null 61 | ) { 62 | refreshLayout.setColorSchemeColors( 63 | ContextCompat.getColor(requireActivity(), R.color.colorPrimary), 64 | ContextCompat.getColor(requireActivity(), R.color.colorAccent), 65 | ContextCompat.getColor(requireActivity(), R.color.colorPrimaryDark) 66 | ) 67 | // Set the scrolling view in the custom SwipeRefreshLayout. 68 | scrollUpChild?.let { 69 | refreshLayout.scrollUpChild = it 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/drawer_item_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_assignment_turned_in_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_circle_96dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter_list.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_statistics.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_statistics_100dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_statistics_24dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_verified_user_96dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_completed_touch_feedback.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo_no_fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/advanced-android-testing/c46787c415420c1fed44a3e876be40d5b0279f9b/app/src/main/res/drawable/logo_no_fill.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/touch_feedback.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/trash_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/advanced-android-testing/c46787c415420c1fed44a3e876be40d5b0279f9b/app/src/main/res/drawable/trash_icon.png -------------------------------------------------------------------------------- /app/src/main/res/font/opensans_font.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/font/opensans_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/advanced-android-testing/c46787c415420c1fed44a3e876be40d5b0279f9b/app/src/main/res/font/opensans_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/opensans_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/advanced-android-testing/c46787c415420c1fed44a3e876be40d5b0279f9b/app/src/main/res/font/opensans_semibold.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/addtask_frag.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 33 | 34 | 40 | 41 | 44 | 45 | 54 | 55 | 66 | 67 | 76 | 77 | 78 | 79 | 80 | 81 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_header.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 33 | 34 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/statistics_frag.xml: -------------------------------------------------------------------------------- 1 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 34 | 35 | 43 | 44 | 50 | 51 | 57 | 58 | 66 | 67 | 68 | 69 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/src/main/res/layout/task_item.xml: -------------------------------------------------------------------------------- 1 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 30 | 31 | 32 | 41 | 42 | 49 | 50 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/layout/taskdetail_frag.xml: -------------------------------------------------------------------------------- 1 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 34 | 35 | 36 | 42 | 43 | 51 | 52 | 61 | 62 | 68 | 69 | 70 | 79 | 80 | 81 | 82 | 91 | 92 | 100 | 101 | 110 | 111 | 112 | 113 | 114 | 115 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /app/src/main/res/layout/tasks_act.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 31 | 32 | 35 | 36 | 43 | 44 | 45 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/layout/tasks_frag.xml: -------------------------------------------------------------------------------- 1 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 44 | 45 | 51 | 52 | 58 | 59 | 70 | 71 | 77 | 78 | 79 | 86 | 87 | 94 | 95 | 102 | 103 | 104 | 105 | 106 | 107 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /app/src/main/res/menu/drawer_actions.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/menu/filter_tasks.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/menu/taskdetail_fragment_menu.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/menu/tasks_fragment_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 25 | 29 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/advanced-android-testing/c46787c415420c1fed44a3e876be40d5b0279f9b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/advanced-android-testing/c46787c415420c1fed44a3e876be40d5b0279f9b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/advanced-android-testing/c46787c415420c1fed44a3e876be40d5b0279f9b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/advanced-android-testing/c46787c415420c1fed44a3e876be40d5b0279f9b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/advanced-android-testing/c46787c415420c1fed44a3e876be40d5b0279f9b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 27 | 30 | 33 | 36 | 37 | 41 | 45 | 46 | 50 | 53 | 56 | 59 | 63 | 64 | 68 | 72 | 76 | 80 | 81 | 84 | 85 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 21 | 64dp 22 | 23 | 24dp 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | #FFFFF0 19 | #263238 20 | #2E7D32 21 | #000000 22 | #757575 23 | 24 | #CCCCCC 25 | 26 | #CFD8DC 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 16dp 20 | 16dp 21 | 22 | 16dp 23 | 24 | 8dp 25 | 192dp 26 | 16dp 27 | 100dp 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Todo 19 | New Task 20 | Edit Task 21 | Task marked complete 22 | Task marked active 23 | Error while loading tasks 24 | Completed tasks cleared 25 | Filter 26 | Clear completed 27 | Delete task 28 | Todo 29 | Title 30 | Enter your task here. 31 | Tasks cannot be empty 32 | Task saved 33 | Task List 34 | Statistics 35 | You have no tasks. 36 | Active tasks: %.1f%% 37 | Completed tasks: %.1f%% 38 | Error loading statistics. 39 | No data 40 | LOADING 41 | 42 | 43 | @string/nav_all 44 | @string/nav_active 45 | @string/nav_completed 46 | 47 | All 48 | Active 49 | Completed 50 | All Tasks 51 | Active Tasks 52 | Completed Tasks 53 | You have no tasks! 54 | You have no active tasks! 55 | You have no completed tasks! 56 | Refresh 57 | Task was deleted 58 | Task added 59 | 60 | 61 | 62 | Tasks header image 63 | No tasks image 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 33 | 34 | 35 | 38 | 39 |