diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties
new file mode 100644
index 000000000..2962a6399
--- /dev/null
+++ b/.github/ci-gradle.properties
@@ -0,0 +1,23 @@
+#
+# Copyright 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.gradle.daemon=false
+org.gradle.parallel=false
+org.gradle.jvmargs=-Xmx5120m
+org.gradle.workers.max=2
+
+kotlin.incremental=false
+kotlin.compiler.execution.strategy=in-process
\ No newline at end of file
diff --git a/.github/workflows/build_test.yaml b/.github/workflows/build_test.yaml
new file mode 100644
index 000000000..8e52b225a
--- /dev/null
+++ b/.github/workflows/build_test.yaml
@@ -0,0 +1,60 @@
+name: build_test
+
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    strategy:
+      matrix:
+        api-level: [29]
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Enable KVM group perms
+        run: |
+          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+          sudo udevadm control --reload-rules
+          sudo udevadm trigger --name-match=kvm
+          ls /dev/kvm
+          
+      - name: Copy CI gradle.properties
+        run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+      - name: Set Up JDK
+        uses: actions/setup-java@v4
+        with:
+          distribution: 'zulu' # See 'Supported distributions' for available options
+          java-version: '17'
+          cache: 'gradle'
+
+      - name: Setup Gradle
+        uses: gradle/actions/setup-gradle@v4
+        
+      - name: Setup Android SDK
+        uses: android-actions/setup-android@v3
+        
+      - name: Run instrumentation tests
+        uses: reactivecircus/android-emulator-runner@v2
+        with:
+          api-level: ${{ matrix.api-level }}
+          arch: x86
+          disable-animations: true
+          script: ./gradlew connectedCheck --stacktrace
+
+      - name: Upload test reports
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: test-reports-${{ matrix.api-level }}
+          path: ./app/build/reports/androidTests
+
+
diff --git a/.github/workflows/copy-branch.yml b/.github/workflows/copy-branch.yml
new file mode 100644
index 000000000..f8f8572d9
--- /dev/null
+++ b/.github/workflows/copy-branch.yml
@@ -0,0 +1,31 @@
+# Duplicates default main branch to the old master branch
+
+name: Duplicates main to old master branch
+
+# Controls when the action will run. Triggers the workflow on push or pull request
+# events but only for the main branch
+on:
+  push:
+    branches: [ main ]
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+  # This workflow contains a single job called "copy-branch"
+  copy-branch:
+    # The type of runner that the job will run on
+    runs-on: ubuntu-latest
+
+    # Steps represent a sequence of tasks that will be executed as part of the job
+    steps:
+    # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it,
+    # but specifies master branch (old default).
+    - uses: actions/checkout@v2
+      with:
+        fetch-depth: 0
+        ref: master
+        
+    - run: |
+        git config user.name github-actions
+        git config user.email github-actions@github.com
+        git merge origin/main
+        git push
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..4d789636a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+*.iml
+.gradle
+local.properties
+.idea
+.DS_Store
+build
+captures
+.externalNativeBuild
diff --git a/.google/packaging.yaml b/.google/packaging.yaml
new file mode 100644
index 000000000..4ce97a7d8
--- /dev/null
+++ b/.google/packaging.yaml
@@ -0,0 +1,43 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# GOOGLE SAMPLE PACKAGING DATA
+#
+# This file is used by Google as part of our samples packaging process.
+# End users may safely ignore this file. It has no relevance to other systems.
+---
+status:       PUBLISHED
+technologies: [Android, JetpackCompose, Coroutines]
+categories:
+  - AndroidTesting
+  - AndroidArchitecture
+  - AndroidArchitectureUILayer
+  - AndroidArchitectureDataLayer
+  - AndroidArchitectureStateProduction
+  - AndroidArchitectureStateHolder
+  - AndroidArchitectureUIEvents
+  - JetpackComposeTesting
+  - JetpackComposeArchitectureAndState
+  - JetpackComposeNavigation
+languages:    [Kotlin]
+solutions:
+  - Mobile
+  - Flow
+  - JetpackHilt
+  - JetpackRoom
+  - JetpackNavigation
+  - JetpackLifecycle
+github:       android/architecture-samples
+level:        INTERMEDIATE
+license: apache2
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d3543fd27..262d7276c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,42 +1,30 @@
 # How to become a contributor and submit your own code
 
-To contribute with a small fix, simply create a pull request against the appropriate branch. This branch is usually [todo-mvp](https://github.com/googlesamples/android-architecture/tree/todo-mvp) unless the change is related to a variant's implementation. See [Development branches](https://github.com/googlesamples/android-architecture/wiki/Development-branches) for more information or create an [issue](https://github.com/googlesamples/android-architecture/issues) if you're not sure what branch to target.
-
-Before starting work on new sample intended for submission, please open an issue to discuss it with the team. This will allow us to review the architecture and frameworks used to determine if a spec-compatible app is likely to be accepted.
-
-Please note that this project is released with a [Contributor Code of Conduct](https://github.com/googlesamples/android-architecture/blob/master/code-of-conduct.md). By participating in this project you agree to abide by its terms.
-
-## Code style and structure
-
-Please check out the [Code Style for Contributors](https://source.android.com/source/code-style.html) section in AOSP. Also, check out the rest of the samples and maintain as much consistency with them as possible.
-
 ## Contributor License Agreements
 
-We'd love to accept your sample apps and patches! Before we can take them, we
+We'd love to accept your patches! Before we can take them, we 
 have to jump a couple of legal hurdles.
 
-Please fill out either the individual or corporate Contributor License Agreement (CLA).
-
-  * If you are an individual writing original source code and you're sure you
-    own the intellectual property, then you'll need to sign an [individual CLA](https://cla.developers.google.com).
-  * If you work for a company that wants to allow you to contribute your work,
-    then you'll need to sign a [corporate CLA](https://cla.developers.google.com).
-  * Please make sure you sign both, Android and Google CLA
-
-Follow either of the two links above to access the appropriate CLA and
-instructions for how to sign and return it. Once we receive it, we'll be able to
-accept your pull requests.
-
-## Contributing A Patch
-
-1. Submit an issue describing your proposed change to the repo in question.
-1. The repo owner will respond to your issue promptly.
-1. If your proposed change is accepted, and you haven't already done so, sign a
-   Contributor License Agreement (see details above).
-1. Fork the desired repo, develop and test your code changes.
-1. Ensure that your code adheres to the existing style in the sample to which
-   you are contributing. Refer to the
-   [Android Code Style Guide](https://source.android.com/source/code-style.html) for the
-   recommended coding standards for this organization.
-1. Ensure that your code has an appropriate set of tests which all pass.
-1. Submit a pull request.
+### Before you contribute
+Before we can use your code, you must sign the
+[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
+(CLA), which you can do online. The CLA is necessary mainly because you own the
+copyright to your changes, even after your contribution becomes part of our
+codebase, so we need your permission to use and distribute your code. We also
+need to be sure of various other things—for instance that you'll tell us if you
+know that your code infringes on other people's patents. You don't have to sign
+the CLA until after you've submitted your code for review and a member has
+approved it, but you must do it before we can put your code into our codebase.
+Before you start working on a larger contribution, you should get in touch with
+us first through the issue tracker with your idea so that we can help out and
+possibly guide you. Coordinating up front makes it much easier to avoid
+frustration later on.
+
+### Code reviews
+All submissions, including submissions by project members, require review. We
+use Github pull requests for this purpose.
+
+### The small print
+Contributions made by corporations are covered by a different agreement than
+the one above, the
+[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
deleted file mode 100644
index 25401ed4c..000000000
--- a/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,4 +0,0 @@
-/* Remove this comment
-Please add the branch/sample ("all", "todo-mvp", "todo-mvp-dagger", etc.) 
-in the title if it applies.
-*/
diff --git a/LICENSE b/LICENSE
index b79cfb703..1af981f55 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright 2018 The Android Open Source Project
+   Copyright 2014 The Android Open Source Project
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index ca65112b1..fbc2603f5 100644
--- a/README.md
+++ b/README.md
@@ -1,140 +1,88 @@
-# Android Architecture Blueprints
+# Android Architecture Samples
 
-<img src="https://github.com/googlesamples/android-architecture/wiki/images/aab-logo.png" alt="Android Architecture Blueprints"/>
+These samples showcase different architectural approaches to developing Android apps. In its different branches you'll find the same app (a TODO app) implemented with small differences.
 
-The Android framework provides a lot of flexibility in deciding how to organize and <em>architect</em> an Android app. While this freedom is very valuable, it can also lead to apps with large classes, inconsistent naming schemes, as well as mismatching or missing architectures. These types of issues can make testing, maintaining and extending your apps difficult.
+In this branch you'll find:
+*   User Interface built with **[Jetpack Compose](https://developer.android.com/jetpack/compose)** 
+*   A single-activity architecture, using **[Navigation Compose](https://developer.android.com/jetpack/compose/navigation)**.
+*   A presentation layer that contains a Compose screen (View) and a **ViewModel** per screen (or feature).
+*   Reactive UIs using **[Flow](https://developer.android.com/kotlin/flow)** and **[coroutines](https://kotlinlang.org/docs/coroutines-overview.html)** for asynchronous operations.
+*   A **data layer** with a repository and two data sources (local using Room and a fake remote).
+*   Two **product flavors**, `mock` and `prod`, [to ease development and testing](https://android-developers.googleblog.com/2015/12/leveraging-product-flavors-in-android.html).
+*   A collection of unit, integration and e2e **tests**, including "shared" tests that can be run on emulator/device.
+*   Dependency injection using [Hilt](https://developer.android.com/training/dependency-injection/hilt-android).
 
-The Android Architecture Blueprints project demonstrates strategies to help solve or avoid these common problems. This project implements the same app using different architectural concepts and tools.
-
-You can use the samples in this project as a learning reference, or as a starting point for creating your own apps. The focus of this project is on demonstrating how to structure your code, design your architecture, and the eventual impact of adopting these patterns on testing and maintaining your app. You can use the techniques demonstrated here in many different ways to build apps. Your own particular priorities will impact how you implement the concepts in these projects, so you should not consider these samples to be canonical examples. To ensure the focus is kept on the aims described above, the app uses a simple UI.
-
-## Explore the samples
+## Variations
 
 This project hosts each sample app in separate repository branches. For more information, see the `README.md` file in each branch.
 
-### Stable samples - Java
-| Sample | Description |
-| ------------- | ------------- |
-| [todo‑mvp](https://github.com/googlesamples/android-architecture/tree/todo-mvp/) | Demonstrates a basic [Model‑View‑Presenter](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) (MVP) architecture and provides a foundation on which the other samples are built. This sample also acts as a reference point for comparing and contrasting the other samples in this project. |
-| [todo‑mvp‑clean](https://github.com/googlesamples/android-architecture/tree/todo-mvp-clean/) | Uses concepts from [Clean Architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html). |
-| [todo‑mvp‑dagger](https://github.com/googlesamples/android-architecture/tree/todo-mvp-dagger/) | Uses [Dagger 2](https://google.github.io/dagger/) to add support for [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). |
-| [todo‑mvp‑rxjava](https://github.com/googlesamples/android-architecture/tree/todo-mvp-rxjava/) | Uses [RxJava 2](https://github.com/ReactiveX/RxJava) to implement concurrency, and abstract the data layer. |
-| [todo‑mvvm‑databinding](https://github.com/googlesamples/android-architecture/tree/todo-mvvm-databinding/) | Based on the todo-databinding sample, this version incorporates the [Model‑View‑ViewModel](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) pattern.|
-| [todo‑mvvm‑live](https://github.com/googlesamples/android-architecture/tree/todo-mvvm-live/) | Uses ViewModels and LiveData from [Architecture Components](http://developer.android.com/arch) and the Data Binding library with an MVVM architecture. |
-
 ### Stable samples - Kotlin
-| Sample | Description |
-| ------------- | ------------- |
-| [todo-mvp-kotlin](https://github.com/googlesamples/android-architecture/tree/todo-mvp-kotlin/) | Conversion of todo-mvp to Kotlin. |
-| [todo-mvvm-live-kotlin](https://github.com/googlesamples/android-architecture/tree/todo-mvvm-live-kotlin/) | Conversion of todo-mvvm-live to Kotlin. |
-
-### External samples
-[External samples](https://github.com/googlesamples/android-architecture/wiki/External-samples) are variants that may not be in sync with the rest of the branches in this repository.
-
-| Sample | Description |
-| ------------- | ------------- |
-| [todo‑mvp‑fragmentless](https://github.com/Syhids/android-architecture/tree/todo-mvp-fragmentless) | Uses [View](https://developer.android.com/reference/android/view/View.html) objects instead of [Fragment](https://developer.android.com/reference/android/app/Fragment.html) objects.|
-| [todo‑mvp‑conductor](https://github.com/grepx/android-architecture/tree/todo-mvp-conductor) | Uses the [Conductor](https://github.com/bluelinelabs/Conductor) framework to refactor the app to use a single Activity architecture. |
-| [todo‑mvi-rxjava](https://github.com/oldergod/android-architecture/tree/todo-mvi-rxjava) | Adapts the [Model-View-Intent](https://cycle.js.org/model-view-intent.html) pattern to Android to create a fully reactive architecture. |
-| [todo‑mvp-kotlin-coroutines](https://github.com/dmytrodanylyk/android-architecture/tree/todo-mvp-kotlin-coroutines) | Replaces the asynchronous operations with [Kotlin's coroutines](https://github.com/Kotlin/kotlinx.coroutines/blob/master/README.md#documentation). |
-
-
-### Deprecated samples
-
-These samples are no longer being maintained, but their implementation is still valid.
-
-| Sample | Description |
+|     Sample     | Description |
 | ------------- | ------------- |
-| [todo‑mvp‑loaders](https://github.com/googlesamples/android-architecture/tree/deprecated-todo-mvp-loaders/) | Fetches data using the [Loaders API](https://developer.android.com/guide/components/loaders.html). |
-| [todo‑databinding](https://github.com/googlesamples/android-architecture/tree/deprecated-todo-databinding/) | Replaced by [todo‑mvvm‑databinding](https://github.com/googlesamples/android-architecture/tree/todo-mvvm-databinding/) |
-[todo‑mvp‑contentproviders](https://github.com/googlesamples/android-architecture/tree/deprecated-todo-mvp-contentproviders/) | Based on the todo-mvp-loaders sample, this version fetches data using the Loaders API, and also makes use of [content providers](https://developer.android.com/guide/topics/providers/content-providers.html). |
+| [main](https://github.com/googlesamples/android-architecture/tree/main) | This branch |
+| [service-locator](https://github.com/googlesamples/android-architecture/tree/service-locator) | A simple setup that removes Hilt in favor of a service locator |
+| [livedata](https://github.com/googlesamples/android-architecture/tree/livedata) | Uses LiveData instead of StateFlow as the data stream solution |
+| [usecases](https://github.com/googlesamples/android-architecture/tree/usecases) | Adds a new domain layer that uses UseCases for business logic (not using Compose yet) |
+| [views](https://github.com/googlesamples/android-architecture/tree/views) | Uses Views instead of Jetpack Compose to render UI elements on the screen |
+| [views-hilt](https://github.com/googlesamples/android-architecture/tree/views-hilt) | Uses Views and Hilt instead together |
 
-### Samples in progress
 
-| Sample | Description |
-| ------------- | ------------- |
-| [dev‑todo‑mvvm‑rxjava](https://github.com/googlesamples/android-architecture/tree/dev-todo-mvvm-rxjava/) | Based on the todo-rxjava sample, this version incorporates the [Model‑View‑ViewModel](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) pattern.|
+## Screenshots
 
-For information about planned samples, see ["New sample" issues](https://github.com/googlesamples/android-architecture/issues?q=is%3Aissue+is%3Aopen+label%3A%22New+sample%22).
+<img src="screenshots/screenshots.png" alt="Screenshot">
 
 ## Why a to-do app?
 
 The app in this project aims to be simple enough that you can understand it quickly, but complex enough to showcase difficult design decisions and testing scenarios. For more information, see the [app's specification](https://github.com/googlesamples/android-architecture/wiki/To-do-app-specification).
 
-The following screenshot illustrates the UI of the app:
-
-<img src="https://github.com/googlesamples/android-architecture/wiki/images/tasks2.png" alt="A screenshot illustratrating the UI of the app" width="160" style="display: inline; float: right"/>
+## What is it not?
+*   A template. Check out the [Architecture Templates](https://github.com/android/architecture-templates) instead.
+*   A UI/Material Design sample. The interface of the app is deliberately kept simple to focus on architecture. Check out the [Compose Samples](https://github.com/android/compose-samples) instead.
+*   A complete Jetpack sample covering all libraries. Check out [Now in Android](https://github.com/android/nowinandroid) or the advanced [GitHub Browser Sample](https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample) instead.
+*   A real production app with network access, user authentication, etc. Check out the [Now in Android app](https://github.com/android/nowinandroid) instead.
 
-## Choose a sample for your app
+## Who is it for?
 
-Each sample includes a dedicated `README.md` file where you can find related metrics, as well as subjective assessments and observations by contributors. The following factors are worth considering when selecting a particular sample for your app:
+*   Intermediate developers and beginners looking for a way to structure their app in a testable and maintainable way.
+*   Advanced developers looking for quick reference.
 
-* The size of the app you are developing
-* The size and experience of your team
-* The amount of maintenance that you are expecting to have to do
-* Whether you need a tablet layout
-* Whether you need to support multiple platforms
-* Your preference for the compactness of your codebase
+## Opening a sample in Android Studio
 
-For more information on choosing and comparing samples, see the following pages:
-* [Samples at a glance](https://github.com/googlesamples/android-architecture/wiki/Samples-at-a-glance)
-* [How to compare samples](https://github.com/googlesamples/android-architecture/wiki/How-to-compare-samples)
-
-## Open a sample in Android Studio
-
-To open one of the samples in Android Studio, begin by checking out one of the sample branches, and then open the `todoapp/` directory in Android Studio. The following series of steps illustrate how to open the [todo‑mvp](https://github.com/googlesamples/android-architecture/tree/todo-mvp) sample.
-
-**Note:** The master branch does not compile.
+To open one of the samples in Android Studio, begin by checking out one of the sample branches, and then open the root directory in Android Studio. The following series of steps illustrate how to open the [usecases](tree/usecases/) sample.
 
 Clone the repository:
 
 ```
-git clone git@github.com:googlesamples/android-architecture.git
+git clone git@github.com:android/architecture-samples.git
 ```
+This step checks out the master branch. If you want to change to a different sample: 
 
-Checkout the todo-mvp sample:
 ```
-git checkout todo-mvp
+git checkout usecases
 ```
 
-**Note:** To review a different sample, replace `todo-mvp` with the name of sample you want to check out.
-
-Finally open the `todoapp/` directory in Android Studio.
+**Note:** To review a different sample, replace `usecases` with the name of sample you want to check out.
 
-## Contributors
+Finally open the `architecture-samples/` directory in Android Studio.
 
-This project is **built by the community**, and curated by Google as well as other core maintainers.
+### License
 
-### External contributors
 
-[David González](http://github.com/malmstein) - Core developer (MVP Content Providers sample)
-
-[Karumi](http://github.com/Karumi) - Developers (MVP Clean Architecture sample)
-
-[Natalie Masse](http://github.com/freewheelnat) - Core developer
-
-[Erik Hellman](https://github.com/ErikHellman) - Developer (MVP RxJava sample)
-
-[Saúl Molinero](https://github.com/saulmm) - Developer (MVP Dagger sample)
-
-[Mike Nakhimovich](https://github.com/digitalbuddha) - Developer (MVP Dagger sample)
-
-[Voicu Klein](https://github.com/kleinsenberg) - Developer (MVP RxJava sample)
-
-### Googlers
-
-[Jose Alcérreca](http://github.com/JoseAlcerreca) - Lead/Core developer
-
-[Mustafa Kurtuldu](https://github.com/mustafa-x) - UX/design
-
-[Stephan Linzner](http://github.com/slinzner) - Core developer
-
-[Florina Muntenescu](https://github.com/florina-muntenescu) - Core developer
-
-[Sharif Salah](https://github.com/sharifsalah) - Technical Writer
-
-[Doug Sigelbaum](https://github.com/DougSig) - Kotlin conversion
-
-[Ben Weiss](https://github.com/keyboardsurfer) - Kotlin conversion
-
-For more information on joining the project, see [how to become a contributor](https://github.com/googlesamples/android-architecture/blob/master/CONTRIBUTING.md) and the [contributor's guide](https://github.com/googlesamples/android-architecture/wiki/Contributions)
+```
+Copyright 2022 Google, Inc.
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor
+license agreements. See the NOTICE file distributed with this work for
+additional information regarding copyright ownership. The ASF licenses this
+file to you under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+```
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 000000000..457602507
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    alias(libs.plugins.android.application)
+    alias(libs.plugins.kotlin.android)
+    alias(libs.plugins.kapt)
+    alias(libs.plugins.ksp)
+    alias(libs.plugins.hilt)
+}
+
+android {
+    compileSdk = libs.versions.compileSdk.get().toInt()
+
+    defaultConfig {
+        applicationId = "com.example.android.architecture.blueprints.main"
+        minSdk = libs.versions.minSdk.get().toInt()
+        targetSdk = libs.versions.targetSdk.get().toInt()
+        versionCode = 1
+        versionName = "1.0"
+
+        testInstrumentationRunner = "com.example.android.architecture.blueprints.todoapp.CustomTestRunner"
+
+        javaCompileOptions {
+            annotationProcessorOptions {
+                arguments += "room.incremental" to "true"
+            }
+        }
+    }
+
+    buildTypes {
+        getByName("debug") {
+            isMinifyEnabled = false
+            isTestCoverageEnabled = true
+            proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+            testProguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguardTest-rules.pro")
+        }
+
+        getByName("release") {
+            isMinifyEnabled = true
+            isShrinkResources = true
+            proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+            testProguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguardTest-rules.pro")
+        }
+    }
+
+    // Always show the result of every unit test, even if it passes.
+    testOptions.unitTests {
+        isIncludeAndroidResources = true
+
+        all { test ->
+            with(test) {
+                testLogging {
+                    events = setOf(
+                        org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
+                        org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED,
+                        org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
+                        org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_OUT,
+                        org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR,
+                    )
+                }
+            }
+        }
+    }
+
+    buildFeatures {
+        compose = true
+    }
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_1_8
+        targetCompatibility = JavaVersion.VERSION_1_8
+    }
+
+    kotlinOptions {
+        jvmTarget = "1.8"
+    }
+
+    packagingOptions {
+        excludes += "META-INF/AL2.0"
+        excludes += "META-INF/LGPL2.1"
+    }
+
+    composeOptions {
+        kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get()
+    }
+
+    tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
+        kotlinOptions {
+            freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
+            freeCompilerArgs += "-opt-in=kotlin.Experimental"
+        }
+    }
+}
+
+/*
+ Dependency versions are defined in the top level build.gradle file. This helps keeping track of
+ all versions in a single place. This improves readability and helps managing project complexity.
+ */
+dependencies {
+
+    // App dependencies
+    implementation(libs.androidx.annotation)
+    implementation(libs.kotlinx.coroutines.android)
+    implementation(libs.timber)
+    implementation(libs.androidx.test.espresso.idling.resources)
+
+    // Architecture Components
+    implementation(libs.room.runtime)
+    implementation(libs.room.ktx)
+    ksp(libs.room.compiler)
+    implementation(libs.androidx.lifecycle.runtimeCompose)
+    implementation(libs.androidx.lifecycle.viewModelCompose)
+
+    // Hilt
+    implementation(libs.hilt.android.core)
+    implementation(libs.androidx.hilt.navigation.compose)
+    kapt(libs.hilt.compiler)
+
+    // Jetpack Compose
+    val composeBom = platform(libs.androidx.compose.bom)
+
+    implementation(libs.androidx.activity.compose)
+    implementation(libs.androidx.compose.compiler)
+    implementation(composeBom)
+    implementation(libs.androidx.compose.foundation.core)
+    implementation(libs.androidx.compose.foundation.layout)
+    implementation(libs.androidx.compose.animation)
+    implementation(libs.androidx.compose.material.core)
+    implementation(libs.androidx.compose.ui.tooling.preview)
+    implementation(libs.androidx.navigation.compose)
+    implementation(libs.androidx.lifecycle.runtimeCompose)
+    implementation(libs.androidx.lifecycle.viewModelCompose)
+    implementation(libs.accompanist.appcompat.theme)
+    implementation(libs.accompanist.swiperefresh)
+
+    debugImplementation(composeBom)
+    debugImplementation(libs.androidx.compose.ui.tooling.core)
+    debugImplementation(libs.androidx.compose.ui.test.manifest)
+
+    // Dependencies for local unit tests
+    testImplementation(composeBom)
+    testImplementation(libs.junit4)
+    testImplementation(libs.androidx.archcore.testing)
+    testImplementation(libs.kotlinx.coroutines.android)
+    testImplementation(libs.kotlinx.coroutines.test)
+    testImplementation(libs.androidx.navigation.testing)
+    testImplementation(libs.androidx.test.espresso.core)
+    testImplementation(libs.androidx.test.espresso.contrib)
+    testImplementation(libs.androidx.test.espresso.intents)
+    testImplementation(libs.google.truth)
+    testImplementation(libs.androidx.compose.ui.test.junit)
+
+    // JVM tests - Hilt
+    testImplementation(libs.hilt.android.testing)
+    kaptTest(libs.hilt.compiler)
+
+    // Dependencies for Android unit tests
+    androidTestImplementation(composeBom)
+    androidTestImplementation(libs.junit4)
+    androidTestImplementation(libs.kotlinx.coroutines.test)
+    androidTestImplementation(libs.androidx.compose.ui.test.junit)
+
+    // AndroidX Test - JVM testing
+    testImplementation(libs.androidx.test.core.ktx)
+    testImplementation(libs.androidx.test.ext)
+    testImplementation(libs.androidx.test.rules)
+    testImplementation(project(":shared-test"))
+
+    // AndroidX Test - Instrumented testing
+    androidTestImplementation(libs.androidx.test.core.ktx)
+    androidTestImplementation(libs.androidx.test.ext)
+    androidTestImplementation(libs.androidx.test.rules)
+    androidTestImplementation(libs.room.testing)
+    androidTestImplementation(libs.androidx.archcore.testing)
+    androidTestImplementation(libs.androidx.navigation.testing)
+    androidTestImplementation(libs.androidx.test.espresso.core)
+    androidTestImplementation(libs.androidx.test.espresso.contrib)
+    androidTestImplementation(libs.androidx.test.espresso.intents)
+    androidTestImplementation(libs.androidx.test.espresso.idling.resources)
+    androidTestImplementation(libs.androidx.test.espresso.idling.concurrent)
+    androidTestImplementation(project(":shared-test"))
+
+    // AndroidX Test - Hilt testing
+    androidTestImplementation(libs.hilt.android.testing)
+    kaptAndroidTest(libs.hilt.compiler)
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 000000000..5b42c227e
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,31 @@
+-dontoptimize
+
+# Some methods are only called from tests, so make sure the shrinker keeps them.
+-keep class com.example.android.architecture.blueprints.** { *; }
+
+-keep class androidx.drawerlayout.widget.DrawerLayout { *; }
+-keep class androidx.test.espresso.**
+# keep the class and specified members from being removed or renamed
+-keep class androidx.test.espresso.IdlingRegistry { *; }
+-keep class androidx.test.espresso.IdlingResource { *; }
+
+-keep class com.google.common.base.Preconditions { *; }
+
+-keep class androidx.room.RoomDataBase { *; }
+-keep class androidx.room.Room { *; }
+-keep class android.arch.** { *; }
+
+# Proguard rules that are applied to your test apk/code.
+-ignorewarnings
+
+-keepattributes *Annotation*
+
+-dontnote junit.framework.**
+-dontnote junit.runner.**
+
+-dontwarn androidx.test.**
+-dontwarn org.junit.**
+-dontwarn org.hamcrest.**
+-dontwarn com.squareup.javawriter.JavaWriter
+# Uncomment this if you use Mockito
+-dontwarn org.mockito.**
diff --git a/app/proguardTest-rules.pro b/app/proguardTest-rules.pro
new file mode 100644
index 000000000..b0558e9bf
--- /dev/null
+++ b/app/proguardTest-rules.pro
@@ -0,0 +1,18 @@
+# Proguard rules that are applied to your test apk/code.
+-ignorewarnings
+-dontoptimize
+
+-keepattributes *Annotation*
+
+-keep class androidx.test.espresso.**
+# keep the class and specified members from being removed or renamed
+-keep class androidx.test.espresso.IdlingRegistry { *; }
+-keep class androidx.test.espresso.IdlingResource { *; }
+
+-dontnote junit.framework.**
+-dontnote junit.runner.**
+
+-dontwarn androidx.test.**
+-dontwarn org.junit.**
+-dontwarn org.hamcrest.**
+-dontwarn com.squareup.javawriter.JavaWriter
diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt
new file mode 100644
index 000000000..f7b68a521
--- /dev/null
+++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.addedittask
+
+import androidx.compose.material.Surface
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasSetTextAction
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextClearance
+import androidx.compose.ui.test.performTextInput
+import androidx.lifecycle.SavedStateHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.example.android.architecture.blueprints.todoapp.HiltTestActivity
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.google.accompanist.appcompattheme.AppCompatTheme
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Integration test for the Add Task screen.
+ */
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+@HiltAndroidTest
+@ExperimentalCoroutinesApi
+class AddEditTaskScreenTest {
+
+    @get:Rule(order = 0)
+    var hiltRule = HiltAndroidRule(this)
+
+    @get:Rule(order = 1)
+    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
+    private val activity get() = composeTestRule.activity
+
+    @Inject
+    lateinit var repository: TaskRepository
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+
+        // GIVEN - On the "Add Task" screen.
+        composeTestRule.setContent {
+            AppCompatTheme {
+                Surface {
+                    AddEditTaskScreen(
+                        viewModel = AddEditTaskViewModel(repository, SavedStateHandle()),
+                        topBarTitle = R.string.add_task,
+                        onTaskUpdate = { },
+                        onBack = { },
+                    )
+                }
+            }
+        }
+    }
+
+    @Test
+    fun emptyTask_isNotSaved() {
+        // WHEN - Enter invalid title and description combination and click save
+        findTextField(R.string.title_hint).performTextClearance()
+        findTextField(R.string.description_hint).performTextClearance()
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
+            .performClick()
+
+        // THEN - Entered Task is still displayed (a correct task would close it).
+        composeTestRule
+            .onNodeWithText(activity.getString(R.string.empty_task_message))
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun validTask_isSaved() = runTest {
+        // WHEN - Valid title and description combination and click save
+        findTextField(R.string.title_hint).performTextInput("title")
+        findTextField(R.string.description_hint).performTextInput("description")
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
+            .performClick()
+
+        // THEN - Verify that the repository saved the task
+        val tasks = repository.getTasks(true)
+        assertEquals(1, tasks.size)
+        assertEquals("title", tasks[0].title)
+        assertEquals("description", tasks[0].description)
+    }
+
+    private fun findTextField(text: Int): SemanticsNodeInteraction {
+        return composeTestRule.onNode(
+            hasSetTextAction() and hasText(activity.getString(text))
+        )
+    }
+}
diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt
new file mode 100644
index 000000000..4575af69e
--- /dev/null
+++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data.source.local
+
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNotNull
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TaskDaoTest {
+
+    // using an in-memory database because the information stored here disappears when the
+    // process is killed
+    private lateinit var database: ToDoDatabase
+
+    // Ensure that we use a new database for each test.
+    @Before
+    fun initDb() {
+        database = Room.inMemoryDatabaseBuilder(
+            getApplicationContext(),
+            ToDoDatabase::class.java
+        ).allowMainThreadQueries().build()
+    }
+    @Test
+    fun insertTaskAndGetById() = runTest {
+        // GIVEN - insert a task
+        val task = LocalTask(
+            title = "title",
+            description = "description",
+            id = "id",
+            isCompleted = false,
+        )
+        database.taskDao().upsert(task)
+
+        // WHEN - Get the task by id from the database
+        val loaded = database.taskDao().getById(task.id)
+
+        // THEN - The loaded data contains the expected values
+        assertNotNull(loaded as LocalTask)
+        assertEquals(task.id, loaded.id)
+        assertEquals(task.title, loaded.title)
+        assertEquals(task.description, loaded.description)
+        assertEquals(task.isCompleted, loaded.isCompleted)
+    }
+
+    @Test
+    fun insertTaskReplacesOnConflict() = runTest {
+        // Given that a task is inserted
+        val task = LocalTask(
+            title = "title",
+            description = "description",
+            id = "id",
+            isCompleted = false,
+        )
+        database.taskDao().upsert(task)
+
+        // When a task with the same id is inserted
+        val newTask = LocalTask(
+            title = "title2",
+            description = "description2",
+            isCompleted = true,
+            id = task.id
+        )
+        database.taskDao().upsert(newTask)
+
+        // THEN - The loaded data contains the expected values
+        val loaded = database.taskDao().getById(task.id)
+        assertEquals(task.id, loaded?.id)
+        assertEquals("title2", loaded?.title)
+        assertEquals("description2", loaded?.description)
+        assertEquals(true, loaded?.isCompleted)
+    }
+
+    @Test
+    fun insertTaskAndGetTasks() = runTest {
+        // GIVEN - insert a task
+        val task = LocalTask(
+            title = "title",
+            description = "description",
+            id = "id",
+            isCompleted = false,
+        )
+        database.taskDao().upsert(task)
+
+        // WHEN - Get tasks from the database
+        val tasks = database.taskDao().getAll()
+
+        // THEN - There is only 1 task in the database, and contains the expected values
+        assertEquals(1, tasks.size)
+        assertEquals(tasks[0].id, task.id)
+        assertEquals(tasks[0].title, task.title)
+        assertEquals(tasks[0].description, task.description)
+        assertEquals(tasks[0].isCompleted, task.isCompleted)
+    }
+
+    @Test
+    fun updateTaskAndGetById() = runTest {
+        // When inserting a task
+        val originalTask = LocalTask(
+            title = "title",
+            description = "description",
+            id = "id",
+            isCompleted = false,
+        )
+
+        database.taskDao().upsert(originalTask)
+
+        // When the task is updated
+        val updatedTask = LocalTask(
+            title = "new title",
+            description = "new description",
+            isCompleted = true,
+            id = originalTask.id
+        )
+        database.taskDao().upsert(updatedTask)
+
+        // THEN - The loaded data contains the expected values
+        val loaded = database.taskDao().getById(originalTask.id)
+        assertEquals(originalTask.id, loaded?.id)
+        assertEquals("new title", loaded?.title)
+        assertEquals("new description", loaded?.description)
+        assertEquals(true, loaded?.isCompleted)
+    }
+
+    @Test
+    fun updateCompletedAndGetById() = runTest {
+        // When inserting a task
+        val task = LocalTask(
+            title = "title",
+            description = "description",
+            id = "id",
+            isCompleted = true
+        )
+        database.taskDao().upsert(task)
+
+        // When the task is updated
+        database.taskDao().updateCompleted(task.id, false)
+
+        // THEN - The loaded data contains the expected values
+        val loaded = database.taskDao().getById(task.id)
+        assertEquals(task.id, loaded?.id)
+        assertEquals(task.title, loaded?.title)
+        assertEquals(task.description, loaded?.description)
+        assertEquals(false, loaded?.isCompleted)
+    }
+
+    @Test
+    fun deleteTaskByIdAndGettingTasks() = runTest {
+        // Given a task inserted
+        val task = LocalTask(
+            title = "title",
+            description = "description",
+            id = "id",
+            isCompleted = false,
+        )
+        database.taskDao().upsert(task)
+
+        // When deleting a task by id
+        database.taskDao().deleteById(task.id)
+
+        // THEN - The list is empty
+        val tasks = database.taskDao().getAll()
+        assertEquals(true, tasks.isEmpty())
+    }
+
+    @Test
+    fun deleteTasksAndGettingTasks() = runTest {
+        // Given a task inserted
+        database.taskDao().upsert(
+            LocalTask(
+                title = "title",
+                description = "description",
+                id = "id",
+                isCompleted = false,
+            )
+        )
+
+        // When deleting all tasks
+        database.taskDao().deleteAll()
+
+        // THEN - The list is empty
+        val tasks = database.taskDao().getAll()
+        assertEquals(true, tasks.isEmpty())
+    }
+
+    @Test
+    fun deleteCompletedTasksAndGettingTasks() = runTest {
+        // Given a completed task inserted
+        database.taskDao().upsert(
+            LocalTask(title = "completed", description = "task", id = "id", isCompleted = true)
+        )
+
+        // When deleting completed tasks
+        database.taskDao().deleteCompleted()
+
+        // THEN - The list is empty
+        val tasks = database.taskDao().getAll()
+        assertEquals(true, tasks.isEmpty())
+    }
+}
diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt
new file mode 100644
index 000000000..ac28599c4
--- /dev/null
+++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.statistics
+
+import androidx.compose.material.Surface
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.example.android.architecture.blueprints.todoapp.HiltTestActivity
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.google.accompanist.appcompattheme.AppCompatTheme
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Integration test for the statistics screen.
+ */
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+@HiltAndroidTest
+@ExperimentalCoroutinesApi
+class StatisticsScreenTest {
+
+    @get:Rule(order = 0)
+    var hiltRule = HiltAndroidRule(this)
+
+    @get:Rule(order = 1)
+    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
+    private val activity get() = composeTestRule.activity
+
+    @Inject
+    lateinit var repository: TaskRepository
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+    }
+
+    @Test
+    fun tasks_showsNonEmptyMessage() = runTest {
+        // Given some tasks
+        repository.apply {
+            createTask("Title1", "Description1")
+            createTask("Title2", "Description2").also {
+                completeTask(it)
+            }
+        }
+
+        composeTestRule.setContent {
+            AppCompatTheme {
+                Surface {
+                    StatisticsScreen(
+                        openDrawer = { },
+                        viewModel = StatisticsViewModel(repository)
+                    )
+                }
+            }
+        }
+
+        val expectedActiveTaskText = activity.getString(R.string.statistics_active_tasks, 50.0f)
+        val expectedCompletedTaskText = activity
+            .getString(R.string.statistics_completed_tasks, 50.0f)
+
+        // check that both info boxes are displayed and contain the correct info
+        composeTestRule.onNodeWithText(expectedActiveTaskText).assertIsDisplayed()
+        composeTestRule.onNodeWithText(expectedCompletedTaskText).assertIsDisplayed()
+    }
+}
diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt
new file mode 100644
index 000000000..666f76747
--- /dev/null
+++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.taskdetail
+
+import androidx.compose.material.Surface
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.isToggleable
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.lifecycle.SavedStateHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.example.android.architecture.blueprints.todoapp.HiltTestActivity
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.google.accompanist.appcompattheme.AppCompatTheme
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Integration test for the Task Details screen.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@HiltAndroidTest
+@ExperimentalCoroutinesApi
+class TaskDetailScreenTest {
+
+    @get:Rule(order = 0)
+    var hiltRule = HiltAndroidRule(this)
+
+    @get:Rule(order = 1)
+    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
+
+    @Inject
+    lateinit var repository: TaskRepository
+
+    @Before
+    fun setup() {
+        hiltRule.inject()
+    }
+
+    @Test
+    fun activeTaskDetails_DisplayedInUi() = runTest {
+        // GIVEN - Add active (incomplete) task to the DB
+        val activeTaskId = repository.createTask(
+            title = "Active Task",
+            description = "AndroidX Rocks"
+        )
+
+        // WHEN - Details screen is opened
+        setContent(activeTaskId)
+
+        // THEN - Task details are displayed on the screen
+        // make sure that the title/description are both shown and correct
+        composeTestRule.onNodeWithText("Active Task").assertIsDisplayed()
+        composeTestRule.onNodeWithText("AndroidX Rocks").assertIsDisplayed()
+        // and make sure the "active" checkbox is shown unchecked
+        composeTestRule.onNode(isToggleable()).assertIsOff()
+    }
+
+    @Test
+    fun completedTaskDetails_DisplayedInUi() = runTest {
+        // GIVEN - Add completed task to the DB
+        val completedTaskId = repository.createTask("Completed Task", "AndroidX Rocks")
+        repository.completeTask(completedTaskId)
+
+        // WHEN - Details screen is opened
+        setContent(completedTaskId)
+
+        // THEN - Task details are displayed on the screen
+        // make sure that the title/description are both shown and correct
+        composeTestRule.onNodeWithText("Completed Task").assertIsDisplayed()
+        composeTestRule.onNodeWithText("AndroidX Rocks").assertIsDisplayed()
+        // and make sure the "active" checkbox is shown unchecked
+        composeTestRule.onNode(isToggleable()).assertIsOn()
+    }
+
+    private fun setContent(activeTaskId: String) {
+        composeTestRule.setContent {
+            AppCompatTheme {
+                Surface {
+                    TaskDetailScreen(
+                        viewModel = TaskDetailViewModel(
+                            repository,
+                            SavedStateHandle(mapOf("taskId" to activeTaskId))
+                        ),
+                        onEditTask = { /*TODO*/ },
+                        onBack = { },
+                        onDeleteTask = { },
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt
new file mode 100644
index 000000000..2d9014eb9
--- /dev/null
+++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.tasks
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.example.android.architecture.blueprints.todoapp.HiltTestActivity
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.TodoNavGraph
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.google.accompanist.appcompattheme.AppCompatTheme
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for scenarios that requires navigating within the app.
+ */
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+@HiltAndroidTest
+class AppNavigationTest {
+
+    @get:Rule(order = 0)
+    var hiltRule = HiltAndroidRule(this)
+
+    // Executes tasks in the Architecture Components in the same thread
+    @get:Rule(order = 1)
+    var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+    @get:Rule(order = 2)
+    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
+    private val activity get() = composeTestRule.activity
+
+    @Inject
+    lateinit var taskRepository: TaskRepository
+
+    @Before
+    fun init() {
+        hiltRule.inject()
+    }
+
+    @Test
+    fun drawerNavigationFromTasksToStatistics() {
+        setContent()
+
+        openDrawer()
+        // Start statistics screen.
+        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title)).performClick()
+        // Check that statistics screen was opened.
+        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_no_tasks))
+            .assertIsDisplayed()
+
+        openDrawer()
+        // Start tasks screen.
+        composeTestRule.onNodeWithText(activity.getString(R.string.list_title)).performClick()
+        // Check that tasks screen was opened.
+        composeTestRule.onNodeWithText(activity.getString(R.string.no_tasks_all))
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun tasksScreen_clickOnAndroidHomeIcon_OpensNavigation() {
+        setContent()
+
+        // Check that left drawer is closed at startup
+        composeTestRule.onNodeWithText(activity.getString(R.string.list_title))
+            .assertIsNotDisplayed()
+        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title))
+            .assertIsNotDisplayed()
+
+        openDrawer()
+
+        // Check if drawer is open
+        composeTestRule.onNodeWithText(activity.getString(R.string.list_title)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title))
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun statsScreen_clickOnAndroidHomeIcon_OpensNavigation() {
+        setContent()
+
+        // When the user navigates to the stats screen
+        openDrawer()
+        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title)).performClick()
+
+        composeTestRule.onNodeWithText(activity.getString(R.string.list_title))
+            .assertIsNotDisplayed()
+
+        openDrawer()
+
+        // Check if drawer is open
+        composeTestRule.onNodeWithText(activity.getString(R.string.list_title)).assertIsDisplayed()
+        assertTrue(
+            composeTestRule.onAllNodesWithText(activity.getString(R.string.statistics_title))
+                .fetchSemanticsNodes().isNotEmpty()
+        )
+    }
+
+    @Test
+    fun taskDetailScreen_doubleUIBackButton() = runTest {
+        val taskName = "UI <- button"
+        taskRepository.createTask(taskName, "Description")
+
+        setContent()
+
+        // Click on the task on the list
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskName).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskName).performClick()
+
+        // Click on the edit task button
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))
+            .assertIsDisplayed()
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))
+            .performClick()
+
+        // Confirm that if we click "<-" once, we end up back at the task details page
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
+            .performClick()
+        composeTestRule.onNodeWithText(taskName).assertIsDisplayed()
+
+        // Confirm that if we click "<-" a second time, we end up back at the home screen
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
+            .performClick()
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+    }
+
+    @Test
+    fun taskDetailScreen_doubleBackButton() = runTest {
+        val taskName = "Back button"
+        taskRepository.createTask(taskName, "Description")
+
+        setContent()
+
+        // Click on the task on the list
+        composeTestRule.onNodeWithText(taskName).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskName).performClick()
+        // Click on the edit task button
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))
+            .performClick()
+
+        // Confirm that if we click back once, we end up back at the task details page
+        pressBack()
+        composeTestRule.onNodeWithText(taskName).assertIsDisplayed()
+
+        // Confirm that if we click back a second time, we end up back at the home screen
+        pressBack()
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+    }
+
+    private fun setContent() {
+        composeTestRule.setContent {
+            AppCompatTheme {
+                TodoNavGraph()
+            }
+        }
+    }
+
+    private fun openDrawer() {
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.open_drawer))
+            .performClick()
+    }
+}
diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt
new file mode 100644
index 000000000..a91ad574c
--- /dev/null
+++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.tasks
+
+import androidx.annotation.StringRes
+import androidx.compose.material.Surface
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.isToggleable
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.lifecycle.SavedStateHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.example.android.architecture.blueprints.todoapp.HiltTestActivity
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.google.accompanist.appcompattheme.AppCompatTheme
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Integration test for the Task List screen.
+ */
+// TODO - Move to the sharedTest folder when https://issuetracker.google.com/224974381 is fixed
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+// @LooperMode(LooperMode.Mode.PAUSED)
+// @TextLayoutMode(TextLayoutMode.Mode.REALISTIC)
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class TasksScreenTest {
+
+    @get:Rule(order = 0)
+    var hiltRule = HiltAndroidRule(this)
+
+    @get:Rule(order = 1)
+    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
+    private val activity get() = composeTestRule.activity
+
+    @Inject
+    lateinit var repository: TaskRepository
+
+    @Before
+    fun init() {
+        hiltRule.inject()
+    }
+
+    @Test
+    fun displayTask_whenRepositoryHasData() = runTest {
+        // GIVEN - One task already in the repository
+        repository.createTask("TITLE1", "DESCRIPTION1")
+
+        // WHEN - On startup
+        setContent()
+
+        // THEN - Verify task is displayed on screen
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+    }
+
+    @Test
+    fun displayActiveTask() = runTest {
+        repository.createTask("TITLE1", "DESCRIPTION1")
+
+        setContent()
+
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+
+        openFilterAndSelectOption(R.string.nav_active)
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+
+        openFilterAndSelectOption(R.string.nav_completed)
+
+        composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
+    }
+
+    @Test
+    fun displayCompletedTask() = runTest {
+        repository.apply {
+            createTask("TITLE1", "DESCRIPTION1").also { completeTask(it) }
+        }
+
+        setContent()
+
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+
+        openFilterAndSelectOption(R.string.nav_active)
+        composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
+
+        openFilterAndSelectOption(R.string.nav_completed)
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+    }
+
+    @Test
+    fun markTaskAsComplete() = runTest {
+        repository.createTask("TITLE1", "DESCRIPTION1")
+
+        setContent()
+
+        // Mark the task as complete
+        composeTestRule.onNode(isToggleable()).performClick()
+
+        // Verify task is shown as complete
+        openFilterAndSelectOption(R.string.nav_all)
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+        openFilterAndSelectOption(R.string.nav_active)
+        composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
+        openFilterAndSelectOption(R.string.nav_completed)
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+    }
+
+    @Test
+    fun markTaskAsActive() = runTest {
+        repository.apply {
+            createTask("TITLE1", "DESCRIPTION1").also { completeTask(it) }
+        }
+
+        setContent()
+
+        // Mark the task as active
+        composeTestRule.onNode(isToggleable()).performClick()
+
+        // Verify task is shown as active
+        openFilterAndSelectOption(R.string.nav_all)
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+        openFilterAndSelectOption(R.string.nav_active)
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+        openFilterAndSelectOption(R.string.nav_completed)
+        composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
+    }
+
+    @Test
+    fun showAllTasks() = runTest {
+        // Add one active task and one completed task
+        repository.apply {
+            createTask("TITLE1", "DESCRIPTION1")
+            createTask("TITLE2", "DESCRIPTION2").also { completeTask(it) }
+        }
+
+        setContent()
+
+        // Verify that both of our tasks are shown
+        openFilterAndSelectOption(R.string.nav_all)
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+        composeTestRule.onNodeWithText("TITLE2").assertIsDisplayed()
+    }
+
+    @Test
+    fun showActiveTasks() = runTest {
+        // Add 2 active tasks and one completed task
+        repository.apply {
+            createTask("TITLE1", "DESCRIPTION1")
+            createTask("TITLE2", "DESCRIPTION2")
+            createTask("TITLE3", "DESCRIPTION3").also { completeTask(it) }
+        }
+
+        setContent()
+
+        // Verify that the active tasks (but not the completed task) are shown
+        openFilterAndSelectOption(R.string.nav_active)
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+        composeTestRule.onNodeWithText("TITLE2").assertIsDisplayed()
+        composeTestRule.onNodeWithText("TITLE3").assertDoesNotExist()
+    }
+
+    @Test
+    fun showCompletedTasks() = runTest {
+        // Add one active task and 2 completed tasks
+        repository.apply {
+            createTask("TITLE1", "DESCRIPTION1")
+            createTask("TITLE2", "DESCRIPTION2").also { completeTask(it) }
+            createTask("TITLE3", "DESCRIPTION3").also { completeTask(it) }
+        }
+
+        setContent()
+
+        // Verify that the completed tasks (but not the active task) are shown
+        openFilterAndSelectOption(R.string.nav_completed)
+        composeTestRule.onNodeWithText("TITLE1").assertDoesNotExist()
+        composeTestRule.onNodeWithText("TITLE2").assertIsDisplayed()
+        composeTestRule.onNodeWithText("TITLE3").assertIsDisplayed()
+    }
+
+    @Test
+    fun clearCompletedTasks() = runTest {
+        // Add one active task and one completed task
+        repository.apply {
+            createTask("TITLE1", "DESCRIPTION1")
+            createTask("TITLE2", "DESCRIPTION2").also { completeTask(it) }
+        }
+
+        setContent()
+
+        // Click clear completed in menu
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_more))
+            .performClick()
+        composeTestRule.onNodeWithText(activity.getString(R.string.menu_clear)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(activity.getString(R.string.menu_clear)).performClick()
+
+        openFilterAndSelectOption(R.string.nav_all)
+        // Verify that only the active task is shown
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+        composeTestRule.onNodeWithText("TITLE2").assertDoesNotExist()
+    }
+
+    @Test
+    fun noTasks_AllTasksFilter_AddTaskViewVisible() {
+        setContent()
+
+        openFilterAndSelectOption(R.string.nav_all)
+
+        // Verify the "You have no tasks!" text is shown
+        composeTestRule.onNodeWithText("You have no tasks!").assertIsDisplayed()
+    }
+
+    @Test
+    fun noTasks_CompletedTasksFilter_AddTaskViewNotVisible() {
+        setContent()
+
+        openFilterAndSelectOption(R.string.nav_completed)
+        // Verify the "You have no completed tasks!" text is shown
+        composeTestRule.onNodeWithText("You have no completed tasks!").assertIsDisplayed()
+    }
+
+    @Test
+    fun noTasks_ActiveTasksFilter_AddTaskViewNotVisible() {
+        setContent()
+
+        openFilterAndSelectOption(R.string.nav_active)
+        // Verify the "You have no active tasks!" text is shown
+        composeTestRule.onNodeWithText("You have no active tasks!").assertIsDisplayed()
+    }
+
+    private fun setContent() {
+        composeTestRule.setContent {
+            AppCompatTheme {
+                Surface {
+                    TasksScreen(
+                        viewModel = TasksViewModel(repository, SavedStateHandle()),
+                        userMessage = R.string.successfully_added_task_message,
+                        onUserMessageDisplayed = { },
+                        onAddTask = { },
+                        onTaskClick = { },
+                        openDrawer = { }
+                    )
+                }
+            }
+        }
+    }
+
+    private fun openFilterAndSelectOption(@StringRes option: Int) {
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_filter))
+            .performClick()
+        composeTestRule.onNodeWithText(activity.getString(option)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(activity.getString(option)).performClick()
+    }
+}
diff --git a/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt
new file mode 100644
index 000000000..8d3097e87
--- /dev/null
+++ b/app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt
@@ -0,0 +1,311 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.tasks
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.hasSetTextAction
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.isToggleable
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import androidx.compose.ui.test.performTextReplacement
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.example.android.architecture.blueprints.todoapp.HiltTestActivity
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.TodoNavGraph
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.google.accompanist.appcompattheme.AppCompatTheme
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Large End-to-End test for the tasks module.
+ */
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class TasksTest {
+
+    @get:Rule(order = 0)
+    var hiltRule = HiltAndroidRule(this)
+
+    // Executes tasks in the Architecture Components in the same thread
+    @get:Rule(order = 1)
+    var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+    @get:Rule(order = 2)
+    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
+    private val activity get() = composeTestRule.activity
+
+    @Inject
+    lateinit var repository: TaskRepository
+
+    @Before
+    fun init() {
+        hiltRule.inject()
+    }
+
+    @Test
+    fun editTask() = runTest {
+        val originalTaskTitle = "TITLE1"
+        repository.createTask(originalTaskTitle, "DESCRIPTION")
+
+        setContent()
+
+        // Click on the task on the list and verify that all the data is correct
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(originalTaskTitle).assertIsDisplayed()
+        composeTestRule.onNodeWithText(originalTaskTitle).performClick()
+
+        // Task detail screen
+        composeTestRule.onNodeWithText(activity.getString(R.string.task_details))
+            .assertIsDisplayed()
+        composeTestRule.onNodeWithText(originalTaskTitle).assertIsDisplayed()
+        composeTestRule.onNodeWithText("DESCRIPTION").assertIsDisplayed()
+        composeTestRule.onNode(isToggleable()).assertIsOff()
+
+        // Click on the edit button, edit, and save
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))
+            .performClick()
+        composeTestRule.onNodeWithText(activity.getString(R.string.edit_task)).assertIsDisplayed()
+        findTextField(originalTaskTitle).performTextReplacement("NEW TITLE")
+        findTextField("DESCRIPTION").performTextReplacement("NEW DESCRIPTION")
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
+            .performClick()
+
+        // Verify task is displayed on screen in the task list.
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText("NEW TITLE").assertIsDisplayed()
+        // Verify previous task is not displayed
+        composeTestRule.onNodeWithText(originalTaskTitle).assertDoesNotExist()
+    }
+
+    @Test
+    fun createOneTask_deleteTask() {
+        setContent()
+
+        val taskTitle = "TITLE1"
+        // Add active task
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.add_task))
+            .performClick()
+        findTextField(R.string.title_hint).performTextInput(taskTitle)
+        findTextField(R.string.description_hint).performTextInput("DESCRIPTION")
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
+            .performClick()
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
+
+        // Open the task detail screen
+        composeTestRule.onNodeWithText(taskTitle).performClick()
+        composeTestRule.onNodeWithText(activity.getString(R.string.task_details))
+            .assertIsDisplayed()
+        // Click delete task in menu
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_delete_task))
+            .performClick()
+
+        // Verify it was deleted
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_filter))
+            .performClick()
+        composeTestRule.onNodeWithText(activity.getString(R.string.nav_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).assertDoesNotExist()
+    }
+
+    @Test
+    fun createTwoTasks_deleteOneTask() = runTest {
+        repository.apply {
+            createTask("TITLE1", "DESCRIPTION")
+            createTask("TITLE2", "DESCRIPTION")
+        }
+
+        setContent()
+
+        // Open the second task in details view
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText("TITLE2").assertIsDisplayed()
+        composeTestRule.onNodeWithText("TITLE2").performClick()
+        // Click delete task in menu
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_delete_task))
+            .performClick()
+
+        // Verify only one task was deleted
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_filter))
+            .performClick()
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).performClick()
+        composeTestRule.onNodeWithText("TITLE1").assertIsDisplayed()
+        composeTestRule.onNodeWithText("TITLE2").assertDoesNotExist()
+    }
+
+    @Test
+    fun markTaskAsCompleteOnDetailScreen_taskIsCompleteInList() = runTest {
+        // Add 1 active task
+        val taskTitle = "COMPLETED"
+        repository.createTask(taskTitle, "DESCRIPTION")
+
+        setContent()
+
+        // Click on the task on the list
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).performClick()
+
+        // Click on the checkbox in task details screen
+        composeTestRule.onNode(isToggleable()).performClick()
+
+        // Click on the navigation up button to go back to the list
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
+            .performClick()
+
+        // Check that the task is marked as completed
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNode(isToggleable()).assertIsOn()
+    }
+
+    @Test
+    fun markTaskAsActiveOnDetailScreen_taskIsActiveInList() = runTest {
+        // Add 1 completed task
+        val taskTitle = "ACTIVE"
+        repository.apply {
+            createTask(taskTitle, "DESCRIPTION").also { completeTask(it) }
+        }
+
+        setContent()
+
+        // Click on the task on the list
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).performClick()
+
+        // Click on the checkbox in task details screen
+        composeTestRule.onNode(isToggleable()).performClick()
+
+        // Click on the navigation up button to go back to the list
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
+            .performClick()
+
+        // Check that the task is marked as active
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNode(isToggleable()).assertIsOff()
+    }
+
+    @Test
+    fun markTaskAsCompleteAndActiveOnDetailScreen_taskIsActiveInList() = runTest {
+        // Add 1 active task
+        val taskTitle = "ACT-COMP"
+        repository.createTask(taskTitle, "DESCRIPTION")
+
+        setContent()
+
+        // Click on the task on the list
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).performClick()
+
+        // Click on the checkbox in task details screen
+        composeTestRule.onNode(isToggleable()).performClick()
+        // Click again to restore it to original state
+        composeTestRule.onNode(isToggleable()).performClick()
+
+        // Click on the navigation up button to go back to the list
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
+            .performClick()
+
+        // Check that the task is marked as active
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNode(isToggleable()).assertIsOff()
+    }
+
+    @Test
+    fun markTaskAsActiveAndCompleteOnDetailScreen_taskIsCompleteInList() = runTest {
+        // Add 1 completed task
+        val taskTitle = "COMP-ACT"
+        repository.apply {
+            createTask(taskTitle, "DESCRIPTION").also { completeTask(it) }
+        }
+
+        setContent()
+
+        // Click on the task on the list
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()
+        composeTestRule.onNodeWithText(taskTitle).performClick()
+        // Click on the checkbox in task details screen
+        composeTestRule.onNode(isToggleable()).performClick()
+        // Click again to restore it to original state
+        composeTestRule.onNode(isToggleable()).performClick()
+
+        // Click on the navigation up button to go back to the list
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))
+            .performClick()
+
+        // Check that the task is marked as active
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNode(isToggleable()).assertIsOn()
+    }
+
+    @Test
+    fun createTask() {
+        setContent()
+
+        // Click on the "+" button, add details, and save
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.add_task))
+            .performClick()
+        findTextField(R.string.title_hint).performTextInput("title")
+        findTextField(R.string.description_hint).performTextInput("description")
+        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))
+            .performClick()
+
+        // Then verify task is displayed on screen
+        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()
+        composeTestRule.onNodeWithText("title").assertIsDisplayed()
+    }
+
+    private fun setContent() {
+        composeTestRule.setContent {
+            AppCompatTheme {
+                TodoNavGraph()
+            }
+        }
+    }
+
+    private fun findTextField(textId: Int): SemanticsNodeInteraction {
+        return composeTestRule.onNode(
+            hasSetTextAction() and hasText(activity.getString(textId))
+        )
+    }
+
+    private fun findTextField(text: String): SemanticsNodeInteraction {
+        return composeTestRule.onNode(
+            hasSetTextAction() and hasText(text)
+        )
+    }
+}
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..dbf09ddfc
--- /dev/null
+++ b/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android.architecture.blueprints.todoapp">
+
+    <application>
+        <activity
+            android:name=".HiltTestActivity"
+            android:exported="false"  />
+    </application>
+
+</manifest>
diff --git a/app/src/debug/java/com/example/android/architecture/blueprints/todoapp/HiltTestActivity.kt b/app/src/debug/java/com/example/android/architecture/blueprints/todoapp/HiltTestActivity.kt
new file mode 100644
index 000000000..27f031018
--- /dev/null
+++ b/app/src/debug/java/com/example/android/architecture/blueprints/todoapp/HiltTestActivity.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp
+
+import androidx.activity.ComponentActivity
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class HiltTestActivity : ComponentActivity()
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..79bb88f91
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android.architecture.blueprints.todoapp">
+
+    <application
+        android:allowBackup="false"
+        android:name=".TodoApplication"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity
+            android:name="com.example.android.architecture.blueprints.todoapp.TodoActivity"
+            android:windowSoftInputMode="adjustResize"
+            android:exported="true"
+            android:theme="@style/AppTheme.OverlapSystemBar">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoActivity.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoActivity.kt
new file mode 100644
index 000000000..90ad33d03
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoActivity.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.google.accompanist.appcompattheme.AppCompatTheme
+import dagger.hilt.android.AndroidEntryPoint
+
+/**
+ * Main activity for the todoapp
+ */
+@AndroidEntryPoint
+class TodoActivity : ComponentActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContent {
+            AppCompatTheme {
+                TodoNavGraph()
+            }
+        }
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt
new file mode 100644
index 000000000..c8381d922
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+import timber.log.Timber
+import timber.log.Timber.DebugTree
+
+/**
+ * Application that sets up Timber in the DEBUG BuildConfig.
+ * Read Timber's documentation for production setups.
+ */
+@HiltAndroidApp
+class TodoApplication : Application() {
+
+    override fun onCreate() {
+        super.onCreate()
+        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavGraph.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavGraph.kt
new file mode 100644
index 000000000..c488d87d5
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavGraph.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp
+
+import android.app.Activity
+import androidx.compose.material.DrawerState
+import androidx.compose.material.DrawerValue
+import androidx.compose.material.rememberDrawerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navArgument
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TASK_ID_ARG
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TITLE_ARG
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.USER_MESSAGE_ARG
+import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskScreen
+import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsScreen
+import com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailScreen
+import com.example.android.architecture.blueprints.todoapp.tasks.TasksScreen
+import com.example.android.architecture.blueprints.todoapp.util.AppModalDrawer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun TodoNavGraph(
+    modifier: Modifier = Modifier,
+    navController: NavHostController = rememberNavController(),
+    coroutineScope: CoroutineScope = rememberCoroutineScope(),
+    drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed),
+    startDestination: String = TodoDestinations.TASKS_ROUTE,
+    navActions: TodoNavigationActions = remember(navController) {
+        TodoNavigationActions(navController)
+    }
+) {
+    val currentNavBackStackEntry by navController.currentBackStackEntryAsState()
+    val currentRoute = currentNavBackStackEntry?.destination?.route ?: startDestination
+
+    NavHost(
+        navController = navController,
+        startDestination = startDestination,
+        modifier = modifier
+    ) {
+        composable(
+            TodoDestinations.TASKS_ROUTE,
+            arguments = listOf(
+                navArgument(USER_MESSAGE_ARG) { type = NavType.IntType; defaultValue = 0 }
+            )
+        ) { entry ->
+            AppModalDrawer(drawerState, currentRoute, navActions) {
+                TasksScreen(
+                    userMessage = entry.arguments?.getInt(USER_MESSAGE_ARG)!!,
+                    onUserMessageDisplayed = { entry.arguments?.putInt(USER_MESSAGE_ARG, 0) },
+                    onAddTask = { navActions.navigateToAddEditTask(R.string.add_task, null) },
+                    onTaskClick = { task -> navActions.navigateToTaskDetail(task.id) },
+                    openDrawer = { coroutineScope.launch { drawerState.open() } }
+                )
+            }
+        }
+        composable(TodoDestinations.STATISTICS_ROUTE) {
+            AppModalDrawer(drawerState, currentRoute, navActions) {
+                StatisticsScreen(openDrawer = { coroutineScope.launch { drawerState.open() } })
+            }
+        }
+        composable(
+            TodoDestinations.ADD_EDIT_TASK_ROUTE,
+            arguments = listOf(
+                navArgument(TITLE_ARG) { type = NavType.IntType },
+                navArgument(TASK_ID_ARG) { type = NavType.StringType; nullable = true },
+            )
+        ) { entry ->
+            val taskId = entry.arguments?.getString(TASK_ID_ARG)
+            AddEditTaskScreen(
+                topBarTitle = entry.arguments?.getInt(TITLE_ARG)!!,
+                onTaskUpdate = {
+                    navActions.navigateToTasks(
+                        if (taskId == null) ADD_EDIT_RESULT_OK else EDIT_RESULT_OK
+                    )
+                },
+                onBack = { navController.popBackStack() }
+            )
+        }
+        composable(TodoDestinations.TASK_DETAIL_ROUTE) {
+            TaskDetailScreen(
+                onEditTask = { taskId ->
+                    navActions.navigateToAddEditTask(R.string.edit_task, taskId)
+                },
+                onBack = { navController.popBackStack() },
+                onDeleteTask = { navActions.navigateToTasks(DELETE_RESULT_OK) }
+            )
+        }
+    }
+}
+
+// Keys for navigation
+const val ADD_EDIT_RESULT_OK = Activity.RESULT_FIRST_USER + 1
+const val DELETE_RESULT_OK = Activity.RESULT_FIRST_USER + 2
+const val EDIT_RESULT_OK = Activity.RESULT_FIRST_USER + 3
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavigation.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavigation.kt
new file mode 100644
index 000000000..ea0bc4a66
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavigation.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp
+
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavHostController
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TASK_ID_ARG
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TITLE_ARG
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.USER_MESSAGE_ARG
+import com.example.android.architecture.blueprints.todoapp.TodoScreens.ADD_EDIT_TASK_SCREEN
+import com.example.android.architecture.blueprints.todoapp.TodoScreens.STATISTICS_SCREEN
+import com.example.android.architecture.blueprints.todoapp.TodoScreens.TASKS_SCREEN
+import com.example.android.architecture.blueprints.todoapp.TodoScreens.TASK_DETAIL_SCREEN
+
+/**
+ * Screens used in [TodoDestinations]
+ */
+private object TodoScreens {
+    const val TASKS_SCREEN = "tasks"
+    const val STATISTICS_SCREEN = "statistics"
+    const val TASK_DETAIL_SCREEN = "task"
+    const val ADD_EDIT_TASK_SCREEN = "addEditTask"
+}
+
+/**
+ * Arguments used in [TodoDestinations] routes
+ */
+object TodoDestinationsArgs {
+    const val USER_MESSAGE_ARG = "userMessage"
+    const val TASK_ID_ARG = "taskId"
+    const val TITLE_ARG = "title"
+}
+
+/**
+ * Destinations used in the [TodoActivity]
+ */
+object TodoDestinations {
+    const val TASKS_ROUTE = "$TASKS_SCREEN?$USER_MESSAGE_ARG={$USER_MESSAGE_ARG}"
+    const val STATISTICS_ROUTE = STATISTICS_SCREEN
+    const val TASK_DETAIL_ROUTE = "$TASK_DETAIL_SCREEN/{$TASK_ID_ARG}"
+    const val ADD_EDIT_TASK_ROUTE = "$ADD_EDIT_TASK_SCREEN/{$TITLE_ARG}?$TASK_ID_ARG={$TASK_ID_ARG}"
+}
+
+/**
+ * Models the navigation actions in the app.
+ */
+class TodoNavigationActions(private val navController: NavHostController) {
+
+    fun navigateToTasks(userMessage: Int = 0) {
+        val navigatesFromDrawer = userMessage == 0
+        navController.navigate(
+            TASKS_SCREEN.let {
+                if (userMessage != 0) "$it?$USER_MESSAGE_ARG=$userMessage" else it
+            }
+        ) {
+            popUpTo(navController.graph.findStartDestination().id) {
+                inclusive = !navigatesFromDrawer
+                saveState = navigatesFromDrawer
+            }
+            launchSingleTop = true
+            restoreState = navigatesFromDrawer
+        }
+    }
+
+    fun navigateToStatistics() {
+        navController.navigate(TodoDestinations.STATISTICS_ROUTE) {
+            // Pop up to the start destination of the graph to
+            // avoid building up a large stack of destinations
+            // on the back stack as users select items
+            popUpTo(navController.graph.findStartDestination().id) {
+                saveState = true
+            }
+            // Avoid multiple copies of the same destination when
+            // reselecting the same item
+            launchSingleTop = true
+            // Restore state when reselecting a previously selected item
+            restoreState = true
+        }
+    }
+
+    fun navigateToTaskDetail(taskId: String) {
+        navController.navigate("$TASK_DETAIL_SCREEN/$taskId")
+    }
+
+    fun navigateToAddEditTask(title: Int, taskId: String?) {
+        navController.navigate(
+            "$ADD_EDIT_TASK_SCREEN/$title".let {
+                if (taskId != null) "$it?$TASK_ID_ARG=$taskId" else it
+            }
+        )
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt
new file mode 100644
index 000000000..26e381546
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.addedittask
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Scaffold
+import androidx.compose.material.ScaffoldState
+import androidx.compose.material.Text
+import androidx.compose.material.TextFieldDefaults
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material.rememberScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.util.AddEditTaskTopAppBar
+import com.google.accompanist.swiperefresh.SwipeRefresh
+import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
+
+@OptIn(ExperimentalLifecycleComposeApi::class)
+@Composable
+fun AddEditTaskScreen(
+    @StringRes topBarTitle: Int,
+    onTaskUpdate: () -> Unit,
+    onBack: () -> Unit,
+    modifier: Modifier = Modifier,
+    scaffoldState: ScaffoldState = rememberScaffoldState(),
+    viewModel: AddEditTaskViewModel = hiltViewModel()
+) {
+    Scaffold(
+        modifier = modifier.fillMaxSize(),
+        scaffoldState = scaffoldState,
+        topBar = { AddEditTaskTopAppBar(topBarTitle, onBack) },
+        floatingActionButton = {
+            FloatingActionButton(onClick = viewModel::saveTask) {
+                Icon(Icons.Filled.Done, stringResource(id = R.string.cd_save_task))
+            }
+        }
+    ) { paddingValues ->
+        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+        AddEditTaskContent(
+            loading = uiState.isLoading,
+            title = uiState.title,
+            description = uiState.description,
+            onTitleChanged = viewModel::updateTitle,
+            onDescriptionChanged = viewModel::updateDescription,
+            modifier = Modifier.padding(paddingValues)
+        )
+
+        // Check if the task is saved and call onTaskUpdate event
+        LaunchedEffect(uiState.isTaskSaved) {
+            if (uiState.isTaskSaved) {
+                onTaskUpdate()
+            }
+        }
+
+        // Check for user messages to display on the screen
+        uiState.userMessage?.let { userMessage ->
+            val snackbarText = stringResource(userMessage)
+            LaunchedEffect(scaffoldState, viewModel, userMessage, snackbarText) {
+                scaffoldState.snackbarHostState.showSnackbar(snackbarText)
+                viewModel.snackbarMessageShown()
+            }
+        }
+    }
+}
+
+@Composable
+private fun AddEditTaskContent(
+    loading: Boolean,
+    title: String,
+    description: String,
+    onTitleChanged: (String) -> Unit,
+    onDescriptionChanged: (String) -> Unit,
+    modifier: Modifier = Modifier
+) {
+    if (loading) {
+        SwipeRefresh(
+            // Show the loading spinner—`loading` is `true` in this code path
+            state = rememberSwipeRefreshState(true),
+            onRefresh = { /* DO NOTHING */ },
+            content = { },
+        )
+    } else {
+        Column(
+            modifier
+                .fillMaxWidth()
+                .padding(all = dimensionResource(id = R.dimen.horizontal_margin))
+                .verticalScroll(rememberScrollState())
+        ) {
+            val textFieldColors = TextFieldDefaults.outlinedTextFieldColors(
+                focusedBorderColor = Color.Transparent,
+                unfocusedBorderColor = Color.Transparent,
+                cursorColor = MaterialTheme.colors.secondary.copy(alpha = ContentAlpha.high)
+            )
+            OutlinedTextField(
+                value = title,
+                modifier = Modifier.fillMaxWidth(),
+                onValueChange = onTitleChanged,
+                placeholder = {
+                    Text(
+                        text = stringResource(id = R.string.title_hint),
+                        style = MaterialTheme.typography.h6
+                    )
+                },
+                textStyle = MaterialTheme.typography.h6.copy(fontWeight = FontWeight.Bold),
+                maxLines = 1,
+                colors = textFieldColors
+            )
+            OutlinedTextField(
+                value = description,
+                onValueChange = onDescriptionChanged,
+                placeholder = { Text(stringResource(id = R.string.description_hint)) },
+                modifier = Modifier
+                    .height(350.dp)
+                    .fillMaxWidth(),
+                colors = textFieldColors
+            )
+        }
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt
new file mode 100644
index 000000000..247573042
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.addedittask
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+/**
+ * UiState for the Add/Edit screen
+ */
+data class AddEditTaskUiState(
+    val title: String = "",
+    val description: String = "",
+    val isTaskCompleted: Boolean = false,
+    val isLoading: Boolean = false,
+    val userMessage: Int? = null,
+    val isTaskSaved: Boolean = false
+)
+
+/**
+ * ViewModel for the Add/Edit screen.
+ */
+@HiltViewModel
+class AddEditTaskViewModel @Inject constructor(
+    private val taskRepository: TaskRepository,
+    savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+    private val taskId: String? = savedStateHandle[TodoDestinationsArgs.TASK_ID_ARG]
+
+    // A MutableStateFlow needs to be created in this ViewModel. The source of truth of the current
+    // editable Task is the ViewModel, we need to mutate the UI state directly in methods such as
+    // `updateTitle` or `updateDescription`
+    private val _uiState = MutableStateFlow(AddEditTaskUiState())
+    val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
+
+    init {
+        if (taskId != null) {
+            loadTask(taskId)
+        }
+    }
+
+    // Called when clicking on fab.
+    fun saveTask() {
+        if (uiState.value.title.isEmpty() || uiState.value.description.isEmpty()) {
+            _uiState.update {
+                it.copy(userMessage = R.string.empty_task_message)
+            }
+            return
+        }
+
+        if (taskId == null) {
+            createNewTask()
+        } else {
+            updateTask()
+        }
+    }
+
+    fun snackbarMessageShown() {
+        _uiState.update {
+            it.copy(userMessage = null)
+        }
+    }
+
+    fun updateTitle(newTitle: String) {
+        _uiState.update {
+            it.copy(title = newTitle)
+        }
+    }
+
+    fun updateDescription(newDescription: String) {
+        _uiState.update {
+            it.copy(description = newDescription)
+        }
+    }
+
+    private fun createNewTask() = viewModelScope.launch {
+        taskRepository.createTask(uiState.value.title, uiState.value.description)
+        _uiState.update {
+            it.copy(isTaskSaved = true)
+        }
+    }
+
+    private fun updateTask() {
+        if (taskId == null) {
+            throw RuntimeException("updateTask() was called but task is new.")
+        }
+        viewModelScope.launch {
+            taskRepository.updateTask(
+                taskId,
+                title = uiState.value.title,
+                description = uiState.value.description,
+            )
+            _uiState.update {
+                it.copy(isTaskSaved = true)
+            }
+        }
+    }
+
+    private fun loadTask(taskId: String) {
+        _uiState.update {
+            it.copy(isLoading = true)
+        }
+        viewModelScope.launch {
+            taskRepository.getTask(taskId).let { task ->
+                if (task != null) {
+                    _uiState.update {
+                        it.copy(
+                            title = task.title,
+                            description = task.description,
+                            isTaskCompleted = task.isCompleted,
+                            isLoading = false
+                        )
+                    }
+                } else {
+                    _uiState.update {
+                        it.copy(isLoading = false)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt
new file mode 100644
index 000000000..9fa32de87
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data
+
+import com.example.android.architecture.blueprints.todoapp.data.source.local.TaskDao
+import com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkDataSource
+import com.example.android.architecture.blueprints.todoapp.di.ApplicationScope
+import com.example.android.architecture.blueprints.todoapp.di.DefaultDispatcher
+import java.util.UUID
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Default implementation of [TaskRepository]. Single entry point for managing tasks' data.
+ *
+ * @param networkDataSource - The network data source
+ * @param localDataSource - The local data source
+ * @param dispatcher - The dispatcher to be used for long running or complex operations, such as ID
+ * generation or mapping many models.
+ * @param scope - The coroutine scope used for deferred jobs where the result isn't important, such
+ * as sending data to the network.
+ */
+@Singleton
+class DefaultTaskRepository @Inject constructor(
+    private val networkDataSource: NetworkDataSource,
+    private val localDataSource: TaskDao,
+    @DefaultDispatcher private val dispatcher: CoroutineDispatcher,
+    @ApplicationScope private val scope: CoroutineScope,
+) : TaskRepository {
+
+    override suspend fun createTask(title: String, description: String): String {
+        // ID creation might be a complex operation so it's executed using the supplied
+        // coroutine dispatcher
+        val taskId = withContext(dispatcher) {
+            UUID.randomUUID().toString()
+        }
+        val task = Task(
+            title = title,
+            description = description,
+            id = taskId,
+        )
+        localDataSource.upsert(task.toLocal())
+        saveTasksToNetwork()
+        return taskId
+    }
+
+    override suspend fun updateTask(taskId: String, title: String, description: String) {
+        val task = getTask(taskId)?.copy(
+            title = title,
+            description = description
+        ) ?: throw Exception("Task (id $taskId) not found")
+
+        localDataSource.upsert(task.toLocal())
+        saveTasksToNetwork()
+    }
+
+    override suspend fun getTasks(forceUpdate: Boolean): List<Task> {
+        if (forceUpdate) {
+            refresh()
+        }
+        return withContext(dispatcher) {
+            localDataSource.getAll().toExternal()
+        }
+    }
+
+    override fun getTasksStream(): Flow<List<Task>> {
+        return localDataSource.observeAll().map { tasks ->
+            withContext(dispatcher) {
+                tasks.toExternal()
+            }
+        }
+    }
+
+    override suspend fun refreshTask(taskId: String) {
+        refresh()
+    }
+
+    override fun getTaskStream(taskId: String): Flow<Task?> {
+        return localDataSource.observeById(taskId).map { it.toExternal() }
+    }
+
+    /**
+     * Get a Task with the given ID. Will return null if the task cannot be found.
+     *
+     * @param taskId - The ID of the task
+     * @param forceUpdate - true if the task should be updated from the network data source first.
+     */
+    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Task? {
+        if (forceUpdate) {
+            refresh()
+        }
+        return localDataSource.getById(taskId)?.toExternal()
+    }
+
+    override suspend fun completeTask(taskId: String) {
+        localDataSource.updateCompleted(taskId = taskId, completed = true)
+        saveTasksToNetwork()
+    }
+
+    override suspend fun activateTask(taskId: String) {
+        localDataSource.updateCompleted(taskId = taskId, completed = false)
+        saveTasksToNetwork()
+    }
+
+    override suspend fun clearCompletedTasks() {
+        localDataSource.deleteCompleted()
+        saveTasksToNetwork()
+    }
+
+    override suspend fun deleteAllTasks() {
+        localDataSource.deleteAll()
+        saveTasksToNetwork()
+    }
+
+    override suspend fun deleteTask(taskId: String) {
+        localDataSource.deleteById(taskId)
+        saveTasksToNetwork()
+    }
+
+    /**
+     * The following methods load tasks from (refresh), and save tasks to, the network.
+     *
+     * Real apps may want to do a proper sync, rather than the "one-way sync everything" approach
+     * below. See https://developer.android.com/topic/architecture/data-layer/offline-first
+     * for more efficient and robust synchronisation strategies.
+     *
+     * Note that the refresh operation is a suspend function (forces callers to wait) and the save
+     * operation is not. It returns immediately so callers don't have to wait.
+     */
+
+    /**
+     * Delete everything in the local data source and replace it with everything from the network
+     * data source.
+     *
+     * `withContext` is used here in case the bulk `toLocal` mapping operation is complex.
+     */
+    override suspend fun refresh() {
+        withContext(dispatcher) {
+            val remoteTasks = networkDataSource.loadTasks()
+            localDataSource.deleteAll()
+            localDataSource.upsertAll(remoteTasks.toLocal())
+        }
+    }
+
+    /**
+     * Send the tasks from the local data source to the network data source
+     *
+     * Returns immediately after launching the job. Real apps may want to suspend here until the
+     * operation is complete or (better) use WorkManager to schedule this work. Both approaches
+     * should provide a mechanism for failures to be communicated back to the user so that
+     * they are aware that their data isn't being backed up.
+     */
+    private fun saveTasksToNetwork() {
+        scope.launch {
+            try {
+                val localTasks = localDataSource.getAll()
+                val networkTasks = withContext(dispatcher) {
+                    localTasks.toNetwork()
+                }
+                networkDataSource.saveTasks(networkTasks)
+            } catch (e: Exception) {
+                // In a real app you'd handle the exception e.g. by exposing a `networkStatus` flow
+                // to an app level UI state holder which could then display a Toast message.
+            }
+        }
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/ModelMappingExt.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/ModelMappingExt.kt
new file mode 100644
index 000000000..38b14282f
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/ModelMappingExt.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data
+
+import com.example.android.architecture.blueprints.todoapp.data.source.local.LocalTask
+import com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkTask
+import com.example.android.architecture.blueprints.todoapp.data.source.network.TaskStatus
+
+/**
+ * Data model mapping extension functions. There are three model types:
+ *
+ * - Task: External model exposed to other layers in the architecture.
+ * Obtained using `toExternal`.
+ *
+ * - NetworkTask: Internal model used to represent a task from the network. Obtained using
+ * `toNetwork`.
+ *
+ * - LocalTask: Internal model used to represent a task stored locally in a database. Obtained
+ * using `toLocal`.
+ *
+ */
+
+// External to local
+fun Task.toLocal() = LocalTask(
+    id = id,
+    title = title,
+    description = description,
+    isCompleted = isCompleted,
+)
+
+fun List<Task>.toLocal() = map(Task::toLocal)
+
+// Local to External
+fun LocalTask.toExternal() = Task(
+    id = id,
+    title = title,
+    description = description,
+    isCompleted = isCompleted,
+)
+
+// Note: JvmName is used to provide a unique name for each extension function with the same name.
+// Without this, type erasure will cause compiler errors because these methods will have the same
+// signature on the JVM.
+@JvmName("localToExternal")
+fun List<LocalTask>.toExternal() = map(LocalTask::toExternal)
+
+// Network to Local
+fun NetworkTask.toLocal() = LocalTask(
+    id = id,
+    title = title,
+    description = shortDescription,
+    isCompleted = (status == TaskStatus.COMPLETE),
+)
+
+@JvmName("networkToLocal")
+fun List<NetworkTask>.toLocal() = map(NetworkTask::toLocal)
+
+// Local to Network
+fun LocalTask.toNetwork() = NetworkTask(
+    id = id,
+    title = title,
+    shortDescription = description,
+    status = if (isCompleted) { TaskStatus.COMPLETE } else { TaskStatus.ACTIVE }
+)
+
+fun List<LocalTask>.toNetwork() = map(LocalTask::toNetwork)
+
+// External to Network
+fun Task.toNetwork() = toLocal().toNetwork()
+
+@JvmName("externalToNetwork")
+fun List<Task>.toNetwork() = map(Task::toNetwork)
+
+// Network to External
+fun NetworkTask.toExternal() = toLocal().toExternal()
+
+@JvmName("networkToExternal")
+fun List<NetworkTask>.toExternal() = map(NetworkTask::toExternal)
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt
new file mode 100644
index 000000000..91377071c
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data
+
+/**
+ * Immutable model class for a Task.
+ *
+ * @param title title of the task
+ * @param description description of the task
+ * @param isCompleted whether or not this task is completed
+ * @param id id of the task
+ *
+ * TODO: The constructor of this class should be `internal` but it is used in previews and tests
+ *  so that's not possible until those previews/tests are refactored.
+ */
+data class Task(
+    val title: String = "",
+    val description: String = "",
+    val isCompleted: Boolean = false,
+    val id: String,
+) {
+
+    val titleForList: String
+        get() = if (title.isNotEmpty()) title else description
+
+    val isActive
+        get() = !isCompleted
+
+    val isEmpty
+        get() = title.isEmpty() || description.isEmpty()
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/TaskRepository.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/TaskRepository.kt
new file mode 100644
index 000000000..6f0ee0bb5
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/TaskRepository.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Interface to the data layer.
+ */
+interface TaskRepository {
+
+    fun getTasksStream(): Flow<List<Task>>
+
+    suspend fun getTasks(forceUpdate: Boolean = false): List<Task>
+
+    suspend fun refresh()
+
+    fun getTaskStream(taskId: String): Flow<Task?>
+
+    suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Task?
+
+    suspend fun refreshTask(taskId: String)
+
+    suspend fun createTask(title: String, description: String): String
+
+    suspend fun updateTask(taskId: String, title: String, description: String)
+
+    suspend fun completeTask(taskId: String)
+
+    suspend fun activateTask(taskId: String)
+
+    suspend fun clearCompletedTasks()
+
+    suspend fun deleteAllTasks()
+
+    suspend fun deleteTask(taskId: String)
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/LocalTask.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/LocalTask.kt
new file mode 100644
index 000000000..9d9b084e4
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/LocalTask.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data.source.local
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/**
+ * Internal model used to represent a task stored locally in a Room database. This is used inside
+ * the data layer only.
+ *
+ * See ModelMappingExt.kt for mapping functions used to convert this model to other
+ * models.
+ */
+@Entity(
+    tableName = "task"
+)
+data class LocalTask(
+    @PrimaryKey val id: String,
+    var title: String,
+    var description: String,
+    var isCompleted: Boolean,
+)
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDao.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDao.kt
new file mode 100644
index 000000000..23251efe8
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDao.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data.source.local
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Upsert
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Data Access Object for the task table.
+ */
+@Dao
+interface TaskDao {
+
+    /**
+     * Observes list of tasks.
+     *
+     * @return all tasks.
+     */
+    @Query("SELECT * FROM task")
+    fun observeAll(): Flow<List<LocalTask>>
+
+    /**
+     * Observes a single task.
+     *
+     * @param taskId the task id.
+     * @return the task with taskId.
+     */
+    @Query("SELECT * FROM task WHERE id = :taskId")
+    fun observeById(taskId: String): Flow<LocalTask>
+
+    /**
+     * Select all tasks from the tasks table.
+     *
+     * @return all tasks.
+     */
+    @Query("SELECT * FROM task")
+    suspend fun getAll(): List<LocalTask>
+
+    /**
+     * Select a task by id.
+     *
+     * @param taskId the task id.
+     * @return the task with taskId.
+     */
+    @Query("SELECT * FROM task WHERE id = :taskId")
+    suspend fun getById(taskId: String): LocalTask?
+
+    /**
+     * Insert or update a task in the database. If a task already exists, replace it.
+     *
+     * @param task the task to be inserted or updated.
+     */
+    @Upsert
+    suspend fun upsert(task: LocalTask)
+
+    /**
+     * Insert or update tasks in the database. If a task already exists, replace it.
+     *
+     * @param tasks the tasks to be inserted or updated.
+     */
+    @Upsert
+    suspend fun upsertAll(tasks: List<LocalTask>)
+
+    /**
+     * Update the complete status of a task
+     *
+     * @param taskId id of the task
+     * @param completed status to be updated
+     */
+    @Query("UPDATE task SET isCompleted = :completed WHERE id = :taskId")
+    suspend fun updateCompleted(taskId: String, completed: Boolean)
+
+    /**
+     * Delete a task by id.
+     *
+     * @return the number of tasks deleted. This should always be 1.
+     */
+    @Query("DELETE FROM task WHERE id = :taskId")
+    suspend fun deleteById(taskId: String): Int
+
+    /**
+     * Delete all tasks.
+     */
+    @Query("DELETE FROM task")
+    suspend fun deleteAll()
+
+    /**
+     * Delete all completed tasks from the table.
+     *
+     * @return the number of tasks deleted.
+     */
+    @Query("DELETE FROM task WHERE isCompleted = 1")
+    suspend fun deleteCompleted(): Int
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt
new file mode 100644
index 000000000..4f4386af2
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data.source.local
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+
+/**
+ * The Room Database that contains the Task table.
+ *
+ * Note that exportSchema should be true in production databases.
+ */
+@Database(entities = [LocalTask::class], version = 1, exportSchema = false)
+abstract class ToDoDatabase : RoomDatabase() {
+
+    abstract fun taskDao(): TaskDao
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkDataSource.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkDataSource.kt
new file mode 100644
index 000000000..f6e298771
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkDataSource.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data.source.network
+
+/**
+ * Main entry point for accessing tasks data from the network.
+ *
+ */
+interface NetworkDataSource {
+
+    suspend fun loadTasks(): List<NetworkTask>
+
+    suspend fun saveTasks(tasks: List<NetworkTask>)
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkTask.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkTask.kt
new file mode 100644
index 000000000..e7168a7ec
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkTask.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data.source.network
+
+/**
+ * Internal model used to represent a task obtained from the network. This is used inside the data
+ * layer only.
+ *
+ * See ModelMappingExt.kt for mapping functions used to convert this model to other
+ * models.
+ */
+data class NetworkTask(
+    val id: String,
+    val title: String,
+    val shortDescription: String,
+    val priority: Int? = null,
+    val status: TaskStatus = TaskStatus.ACTIVE
+)
+
+enum class TaskStatus {
+    ACTIVE,
+    COMPLETE
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/TaskNetworkDataSource.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/TaskNetworkDataSource.kt
new file mode 100644
index 000000000..3f248fc3a
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/TaskNetworkDataSource.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data.source.network
+
+import javax.inject.Inject
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+class TaskNetworkDataSource @Inject constructor() : NetworkDataSource {
+
+    // A mutex is used to ensure that reads and writes are thread-safe.
+    private val accessMutex = Mutex()
+    private var tasks = listOf(
+        NetworkTask(
+            id = "PISA",
+            title = "Build tower in Pisa",
+            shortDescription = "Ground looks good, no foundation work required."
+        ),
+        NetworkTask(
+            id = "TACOMA",
+            title = "Finish bridge in Tacoma",
+            shortDescription = "Found awesome girders at half the cost!"
+        )
+    )
+
+    override suspend fun loadTasks(): List<NetworkTask> = accessMutex.withLock {
+        delay(SERVICE_LATENCY_IN_MILLIS)
+        return tasks
+    }
+
+    override suspend fun saveTasks(newTasks: List<NetworkTask>) = accessMutex.withLock {
+        delay(SERVICE_LATENCY_IN_MILLIS)
+        tasks = newTasks
+    }
+}
+
+private const val SERVICE_LATENCY_IN_MILLIS = 2000L
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt
new file mode 100644
index 000000000..262d52e39
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Qualifier
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+annotation class IoDispatcher
+
+@Retention(AnnotationRetention.RUNTIME)
+@Qualifier
+annotation class DefaultDispatcher
+
+@Retention(AnnotationRetention.RUNTIME)
+@Qualifier
+annotation class ApplicationScope
+
+@Module
+@InstallIn(SingletonComponent::class)
+object CoroutinesModule {
+
+    @Provides
+    @IoDispatcher
+    fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
+
+    @Provides
+    @DefaultDispatcher
+    fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
+
+    @Provides
+    @Singleton
+    @ApplicationScope
+    fun providesCoroutineScope(
+        @DefaultDispatcher dispatcher: CoroutineDispatcher
+    ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt
new file mode 100644
index 000000000..aab699ccd
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.di
+
+import android.content.Context
+import androidx.room.Room
+import com.example.android.architecture.blueprints.todoapp.data.DefaultTaskRepository
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.example.android.architecture.blueprints.todoapp.data.source.local.TaskDao
+import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase
+import com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkDataSource
+import com.example.android.architecture.blueprints.todoapp.data.source.network.TaskNetworkDataSource
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class RepositoryModule {
+
+    @Singleton
+    @Binds
+    abstract fun bindTaskRepository(repository: DefaultTaskRepository): TaskRepository
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class DataSourceModule {
+
+    @Singleton
+    @Binds
+    abstract fun bindNetworkDataSource(dataSource: TaskNetworkDataSource): NetworkDataSource
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DatabaseModule {
+
+    @Singleton
+    @Provides
+    fun provideDataBase(@ApplicationContext context: Context): ToDoDatabase {
+        return Room.databaseBuilder(
+            context.applicationContext,
+            ToDoDatabase::class.java,
+            "Tasks.db"
+        ).build()
+    }
+
+    @Provides
+    fun provideTaskDao(database: ToDoDatabase): TaskDao = database.taskDao()
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt
new file mode 100644
index 000000000..cd5b24d71
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.statistics
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Scaffold
+import androidx.compose.material.ScaffoldState
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.rememberScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.util.LoadingContent
+import com.example.android.architecture.blueprints.todoapp.util.StatisticsTopAppBar
+import com.google.accompanist.appcompattheme.AppCompatTheme
+
+@OptIn(ExperimentalLifecycleComposeApi::class)
+@Composable
+fun StatisticsScreen(
+    openDrawer: () -> Unit,
+    modifier: Modifier = Modifier,
+    viewModel: StatisticsViewModel = hiltViewModel(),
+    scaffoldState: ScaffoldState = rememberScaffoldState()
+) {
+    Scaffold(
+        scaffoldState = scaffoldState,
+        topBar = { StatisticsTopAppBar(openDrawer) }
+    ) { paddingValues ->
+        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+        StatisticsContent(
+            loading = uiState.isLoading,
+            empty = uiState.isEmpty,
+            activeTasksPercent = uiState.activeTasksPercent,
+            completedTasksPercent = uiState.completedTasksPercent,
+            onRefresh = { viewModel.refresh() },
+            modifier = modifier.padding(paddingValues)
+        )
+    }
+}
+
+@Composable
+private fun StatisticsContent(
+    loading: Boolean,
+    empty: Boolean,
+    activeTasksPercent: Float,
+    completedTasksPercent: Float,
+    onRefresh: () -> Unit,
+    modifier: Modifier = Modifier
+) {
+    val commonModifier = modifier
+        .fillMaxWidth()
+        .padding(all = dimensionResource(id = R.dimen.horizontal_margin))
+
+    LoadingContent(
+        loading = loading,
+        empty = empty,
+        onRefresh = onRefresh,
+        modifier = modifier,
+        emptyContent = {
+            Text(
+                text = stringResource(id = R.string.statistics_no_tasks),
+                modifier = commonModifier
+            )
+        }
+    ) {
+        Column(
+            commonModifier
+                .fillMaxSize()
+                .verticalScroll(rememberScrollState())
+        ) {
+            if (!loading) {
+                Text(stringResource(id = R.string.statistics_active_tasks, activeTasksPercent))
+                Text(
+                    stringResource(
+                        id = R.string.statistics_completed_tasks,
+                        completedTasksPercent
+                    )
+                )
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+fun StatisticsContentPreview() {
+    AppCompatTheme {
+        Surface {
+            StatisticsContent(
+                loading = false,
+                empty = false,
+                activeTasksPercent = 80f,
+                completedTasksPercent = 20f,
+                onRefresh = { }
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+fun StatisticsContentEmptyPreview() {
+    AppCompatTheme {
+        Surface {
+            StatisticsContent(
+                loading = false,
+                empty = true,
+                activeTasksPercent = 0f,
+                completedTasksPercent = 0f,
+                onRefresh = { }
+            )
+        }
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt
new file mode 100644
index 000000000..99942a94c
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.statistics
+
+import com.example.android.architecture.blueprints.todoapp.data.Task
+
+/**
+ * Function that does some trivial computation. Used to showcase unit tests.
+ */
+internal fun getActiveAndCompletedStats(tasks: List<Task>): StatsResult {
+
+    return if (tasks.isEmpty()) {
+        StatsResult(0f, 0f)
+    } else {
+        val totalTasks = tasks.size
+        val numberOfActiveTasks = tasks.count { it.isActive }
+        StatsResult(
+            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
+            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
+        )
+    }
+}
+
+data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt
new file mode 100644
index 000000000..4db5c1f22
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.statistics
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.example.android.architecture.blueprints.todoapp.util.Async
+import com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/**
+ * UiState for the statistics screen.
+ */
+data class StatisticsUiState(
+    val isEmpty: Boolean = false,
+    val isLoading: Boolean = false,
+    val activeTasksPercent: Float = 0f,
+    val completedTasksPercent: Float = 0f
+)
+
+/**
+ * ViewModel for the statistics screen.
+ */
+@HiltViewModel
+class StatisticsViewModel @Inject constructor(
+    private val taskRepository: TaskRepository
+) : ViewModel() {
+
+    val uiState: StateFlow<StatisticsUiState> =
+        taskRepository.getTasksStream()
+            .map { Async.Success(it) }
+            .catch<Async<List<Task>>> { emit(Async.Error(R.string.loading_tasks_error)) }
+            .map { taskAsync -> produceStatisticsUiState(taskAsync) }
+            .stateIn(
+                scope = viewModelScope,
+                started = WhileUiSubscribed,
+                initialValue = StatisticsUiState(isLoading = true)
+            )
+
+    fun refresh() {
+        viewModelScope.launch {
+            taskRepository.refresh()
+        }
+    }
+
+    private fun produceStatisticsUiState(taskLoad: Async<List<Task>>) =
+        when (taskLoad) {
+            Async.Loading -> {
+                StatisticsUiState(isLoading = true, isEmpty = true)
+            }
+            is Async.Error -> {
+                // TODO: Show error message?
+                StatisticsUiState(isEmpty = true, isLoading = false)
+            }
+            is Async.Success -> {
+                val stats = getActiveAndCompletedStats(taskLoad.data)
+                StatisticsUiState(
+                    isEmpty = taskLoad.data.isEmpty(),
+                    activeTasksPercent = stats.activeTasksPercent,
+                    completedTasksPercent = stats.completedTasksPercent,
+                    isLoading = false
+                )
+            }
+        }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt
new file mode 100644
index 000000000..814377da3
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.taskdetail
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Checkbox
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.ScaffoldState
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.rememberScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import com.example.android.architecture.blueprints.todoapp.util.LoadingContent
+import com.example.android.architecture.blueprints.todoapp.util.TaskDetailTopAppBar
+import com.google.accompanist.appcompattheme.AppCompatTheme
+
+@OptIn(ExperimentalLifecycleComposeApi::class)
+@Composable
+fun TaskDetailScreen(
+    onEditTask: (String) -> Unit,
+    onBack: () -> Unit,
+    onDeleteTask: () -> Unit,
+    modifier: Modifier = Modifier,
+    viewModel: TaskDetailViewModel = hiltViewModel(),
+    scaffoldState: ScaffoldState = rememberScaffoldState()
+) {
+    Scaffold(
+        scaffoldState = scaffoldState,
+        modifier = modifier.fillMaxSize(),
+        topBar = {
+            TaskDetailTopAppBar(onBack = onBack, onDelete = viewModel::deleteTask)
+        },
+        floatingActionButton = {
+            FloatingActionButton(onClick = { onEditTask(viewModel.taskId) }) {
+                Icon(Icons.Filled.Edit, stringResource(id = R.string.edit_task))
+            }
+        }
+    ) { paddingValues ->
+        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+        EditTaskContent(
+            loading = uiState.isLoading,
+            empty = uiState.task == null && !uiState.isLoading,
+            task = uiState.task,
+            onRefresh = viewModel::refresh,
+            onTaskCheck = viewModel::setCompleted,
+            modifier = Modifier.padding(paddingValues)
+        )
+
+        // Check for user messages to display on the screen
+        uiState.userMessage?.let { userMessage ->
+            val snackbarText = stringResource(userMessage)
+            LaunchedEffect(scaffoldState, viewModel, userMessage, snackbarText) {
+                scaffoldState.snackbarHostState.showSnackbar(snackbarText)
+                viewModel.snackbarMessageShown()
+            }
+        }
+
+        // Check if the task is deleted and call onDeleteTask
+        LaunchedEffect(uiState.isTaskDeleted) {
+            if (uiState.isTaskDeleted) {
+                onDeleteTask()
+            }
+        }
+    }
+}
+
+@Composable
+private fun EditTaskContent(
+    loading: Boolean,
+    empty: Boolean,
+    task: Task?,
+    onTaskCheck: (Boolean) -> Unit,
+    onRefresh: () -> Unit,
+    modifier: Modifier = Modifier
+) {
+    val screenPadding = Modifier.padding(
+        horizontal = dimensionResource(id = R.dimen.horizontal_margin),
+        vertical = dimensionResource(id = R.dimen.vertical_margin),
+    )
+    val commonModifier = modifier
+        .fillMaxWidth()
+        .then(screenPadding)
+
+    LoadingContent(
+        loading = loading,
+        empty = empty,
+        emptyContent = {
+            Text(
+                text = stringResource(id = R.string.no_data),
+                modifier = commonModifier
+            )
+        },
+        onRefresh = onRefresh
+    ) {
+        Column(commonModifier.verticalScroll(rememberScrollState())) {
+            Row(
+                Modifier
+                    .fillMaxWidth()
+                    .then(screenPadding),
+
+            ) {
+                if (task != null) {
+                    Checkbox(task.isCompleted, onTaskCheck)
+                    Column {
+                        Text(text = task.title, style = MaterialTheme.typography.h6)
+                        Text(text = task.description, style = MaterialTheme.typography.body1)
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun EditTaskContentPreview() {
+    AppCompatTheme {
+        Surface {
+            EditTaskContent(
+                loading = false,
+                empty = false,
+                Task(
+                    title = "Title",
+                    description = "Description",
+                    isCompleted = false,
+                    id = "ID"
+                ),
+                onTaskCheck = { },
+                onRefresh = { }
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun EditTaskContentTaskCompletedPreview() {
+    AppCompatTheme {
+        Surface {
+            EditTaskContent(
+                loading = false,
+                empty = false,
+                Task(
+                    title = "Title",
+                    description = "Description",
+                    isCompleted = false,
+                    id = "ID"
+                ),
+                onTaskCheck = { },
+                onRefresh = { }
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun EditTaskContentEmptyPreview() {
+    AppCompatTheme {
+        Surface {
+            EditTaskContent(
+                loading = false,
+                empty = true,
+                Task(
+                    title = "Title",
+                    description = "Description",
+                    isCompleted = false,
+                    id = "ID"
+                ),
+                onTaskCheck = { },
+                onRefresh = { }
+            )
+        }
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt
new file mode 100644
index 000000000..080b22c04
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.taskdetail
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.example.android.architecture.blueprints.todoapp.util.Async
+import com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/**
+ * UiState for the Details screen.
+ */
+data class TaskDetailUiState(
+    val task: Task? = null,
+    val isLoading: Boolean = false,
+    val userMessage: Int? = null,
+    val isTaskDeleted: Boolean = false
+)
+
+/**
+ * ViewModel for the Details screen.
+ */
+@HiltViewModel
+class TaskDetailViewModel @Inject constructor(
+    private val taskRepository: TaskRepository,
+    savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+    val taskId: String = savedStateHandle[TodoDestinationsArgs.TASK_ID_ARG]!!
+
+    private val _userMessage: MutableStateFlow<Int?> = MutableStateFlow(null)
+    private val _isLoading = MutableStateFlow(false)
+    private val _isTaskDeleted = MutableStateFlow(false)
+    private val _taskAsync = taskRepository.getTaskStream(taskId)
+        .map { handleTask(it) }
+        .catch { emit(Async.Error(R.string.loading_task_error)) }
+
+    val uiState: StateFlow<TaskDetailUiState> = combine(
+        _userMessage, _isLoading, _isTaskDeleted, _taskAsync
+    ) { userMessage, isLoading, isTaskDeleted, taskAsync ->
+        when (taskAsync) {
+            Async.Loading -> {
+                TaskDetailUiState(isLoading = true)
+            }
+            is Async.Error -> {
+                TaskDetailUiState(
+                    userMessage = taskAsync.errorMessage,
+                    isTaskDeleted = isTaskDeleted
+                )
+            }
+            is Async.Success -> {
+                TaskDetailUiState(
+                    task = taskAsync.data,
+                    isLoading = isLoading,
+                    userMessage = userMessage,
+                    isTaskDeleted = isTaskDeleted
+                )
+            }
+        }
+    }
+        .stateIn(
+            scope = viewModelScope,
+            started = WhileUiSubscribed,
+            initialValue = TaskDetailUiState(isLoading = true)
+        )
+
+    fun deleteTask() = viewModelScope.launch {
+        taskRepository.deleteTask(taskId)
+        _isTaskDeleted.value = true
+    }
+
+    fun setCompleted(completed: Boolean) = viewModelScope.launch {
+        val task = uiState.value.task ?: return@launch
+        if (completed) {
+            taskRepository.completeTask(task.id)
+            showSnackbarMessage(R.string.task_marked_complete)
+        } else {
+            taskRepository.activateTask(task.id)
+            showSnackbarMessage(R.string.task_marked_active)
+        }
+    }
+
+    fun refresh() {
+        _isLoading.value = true
+        viewModelScope.launch {
+            taskRepository.refreshTask(taskId)
+            _isLoading.value = false
+        }
+    }
+
+    fun snackbarMessageShown() {
+        _userMessage.value = null
+    }
+
+    private fun showSnackbarMessage(message: Int) {
+        _userMessage.value = message
+    }
+
+    private fun handleTask(task: Task?): Async<Task?> {
+        if (task == null) {
+            return Async.Error(R.string.task_not_found)
+        }
+        return Async.Success(task)
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFilterType.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFilterType.kt
new file mode 100644
index 000000000..4de088319
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFilterType.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.tasks
+
+/**
+ * Used with the filter spinner in the tasks list.
+ */
+enum class TasksFilterType {
+    /**
+     * Do not filter tasks.
+     */
+    ALL_TASKS,
+
+    /**
+     * Filters only the active (not completed yet) tasks.
+     */
+    ACTIVE_TASKS,
+
+    /**
+     * Filters only the completed tasks.
+     */
+    COMPLETED_TASKS
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt
new file mode 100644
index 000000000..16cf81e86
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.tasks
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Checkbox
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.ScaffoldState
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.rememberScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ACTIVE_TASKS
+import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ALL_TASKS
+import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.COMPLETED_TASKS
+import com.example.android.architecture.blueprints.todoapp.util.LoadingContent
+import com.example.android.architecture.blueprints.todoapp.util.TasksTopAppBar
+import com.google.accompanist.appcompattheme.AppCompatTheme
+
+@OptIn(ExperimentalLifecycleComposeApi::class)
+@Composable
+fun TasksScreen(
+    @StringRes userMessage: Int,
+    onAddTask: () -> Unit,
+    onTaskClick: (Task) -> Unit,
+    onUserMessageDisplayed: () -> Unit,
+    openDrawer: () -> Unit,
+    modifier: Modifier = Modifier,
+    viewModel: TasksViewModel = hiltViewModel(),
+    scaffoldState: ScaffoldState = rememberScaffoldState()
+) {
+    Scaffold(
+        scaffoldState = scaffoldState,
+        topBar = {
+            TasksTopAppBar(
+                openDrawer = openDrawer,
+                onFilterAllTasks = { viewModel.setFiltering(ALL_TASKS) },
+                onFilterActiveTasks = { viewModel.setFiltering(ACTIVE_TASKS) },
+                onFilterCompletedTasks = { viewModel.setFiltering(COMPLETED_TASKS) },
+                onClearCompletedTasks = { viewModel.clearCompletedTasks() },
+                onRefresh = { viewModel.refresh() }
+            )
+        },
+        modifier = modifier.fillMaxSize(),
+        floatingActionButton = {
+            FloatingActionButton(onClick = onAddTask) {
+                Icon(Icons.Filled.Add, stringResource(id = R.string.add_task))
+            }
+        }
+    ) { paddingValues ->
+        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+        TasksContent(
+            loading = uiState.isLoading,
+            tasks = uiState.items,
+            currentFilteringLabel = uiState.filteringUiInfo.currentFilteringLabel,
+            noTasksLabel = uiState.filteringUiInfo.noTasksLabel,
+            noTasksIconRes = uiState.filteringUiInfo.noTaskIconRes,
+            onRefresh = viewModel::refresh,
+            onTaskClick = onTaskClick,
+            onTaskCheckedChange = viewModel::completeTask,
+            modifier = Modifier.padding(paddingValues)
+        )
+
+        // Check for user messages to display on the screen
+        uiState.userMessage?.let { message ->
+            val snackbarText = stringResource(message)
+            LaunchedEffect(scaffoldState, viewModel, message, snackbarText) {
+                scaffoldState.snackbarHostState.showSnackbar(snackbarText)
+                viewModel.snackbarMessageShown()
+            }
+        }
+
+        // Check if there's a userMessage to show to the user
+        val currentOnUserMessageDisplayed by rememberUpdatedState(onUserMessageDisplayed)
+        LaunchedEffect(userMessage) {
+            if (userMessage != 0) {
+                viewModel.showEditResultMessage(userMessage)
+                currentOnUserMessageDisplayed()
+            }
+        }
+    }
+}
+
+@Composable
+private fun TasksContent(
+    loading: Boolean,
+    tasks: List<Task>,
+    @StringRes currentFilteringLabel: Int,
+    @StringRes noTasksLabel: Int,
+    @DrawableRes noTasksIconRes: Int,
+    onRefresh: () -> Unit,
+    onTaskClick: (Task) -> Unit,
+    onTaskCheckedChange: (Task, Boolean) -> Unit,
+    modifier: Modifier = Modifier
+) {
+    LoadingContent(
+        loading = loading,
+        empty = tasks.isEmpty() && !loading,
+        emptyContent = { TasksEmptyContent(noTasksLabel, noTasksIconRes, modifier) },
+        onRefresh = onRefresh
+    ) {
+        Column(
+            modifier = modifier
+                .fillMaxSize()
+                .padding(horizontal = dimensionResource(id = R.dimen.horizontal_margin))
+        ) {
+            Text(
+                text = stringResource(currentFilteringLabel),
+                modifier = Modifier.padding(
+                    horizontal = dimensionResource(id = R.dimen.list_item_padding),
+                    vertical = dimensionResource(id = R.dimen.vertical_margin)
+                ),
+                style = MaterialTheme.typography.h6
+            )
+            LazyColumn {
+                items(tasks) { task ->
+                    TaskItem(
+                        task = task,
+                        onTaskClick = onTaskClick,
+                        onCheckedChange = { onTaskCheckedChange(task, it) }
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun TaskItem(
+    task: Task,
+    onCheckedChange: (Boolean) -> Unit,
+    onTaskClick: (Task) -> Unit
+) {
+    Row(
+        verticalAlignment = Alignment.CenterVertically,
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(
+                horizontal = dimensionResource(id = R.dimen.horizontal_margin),
+                vertical = dimensionResource(id = R.dimen.list_item_padding),
+            )
+            .clickable { onTaskClick(task) }
+    ) {
+        Checkbox(
+            checked = task.isCompleted,
+            onCheckedChange = onCheckedChange
+        )
+        Text(
+            text = task.titleForList,
+            style = MaterialTheme.typography.h6,
+            modifier = Modifier.padding(
+                start = dimensionResource(id = R.dimen.horizontal_margin)
+            ),
+            textDecoration = if (task.isCompleted) {
+                TextDecoration.LineThrough
+            } else {
+                null
+            }
+        )
+    }
+}
+
+@Composable
+private fun TasksEmptyContent(
+    @StringRes noTasksLabel: Int,
+    @DrawableRes noTasksIconRes: Int,
+    modifier: Modifier = Modifier
+) {
+    Column(
+        modifier = modifier.fillMaxSize(),
+        verticalArrangement = Arrangement.Center,
+        horizontalAlignment = Alignment.CenterHorizontally
+    ) {
+        Image(
+            painter = painterResource(id = noTasksIconRes),
+            contentDescription = stringResource(R.string.no_tasks_image_content_description),
+            modifier = Modifier.size(96.dp)
+        )
+        Text(stringResource(id = noTasksLabel))
+    }
+}
+
+@Preview
+@Composable
+private fun TasksContentPreview() {
+    AppCompatTheme {
+        Surface {
+            TasksContent(
+                loading = false,
+                tasks = listOf(
+                    Task(
+                        title = "Title 1",
+                        description = "Description 1",
+                        isCompleted = false,
+                        id = "ID 1"
+                    ),
+                    Task(
+                        title = "Title 2",
+                        description = "Description 2",
+                        isCompleted = true,
+                        id = "ID 2"
+                    ),
+                    Task(
+                        title = "Title 3",
+                        description = "Description 3",
+                        isCompleted = true,
+                        id = "ID 3"
+                    ),
+                    Task(
+                        title = "Title 4",
+                        description = "Description 4",
+                        isCompleted = false,
+                        id = "ID 4"
+                    ),
+                    Task(
+                        title = "Title 5",
+                        description = "Description 5",
+                        isCompleted = true,
+                        id = "ID 5"
+                    ),
+                ),
+                currentFilteringLabel = R.string.label_all,
+                noTasksLabel = R.string.no_tasks_all,
+                noTasksIconRes = R.drawable.logo_no_fill,
+                onRefresh = { },
+                onTaskClick = { },
+                onTaskCheckedChange = { _, _ -> },
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun TasksContentEmptyPreview() {
+    AppCompatTheme {
+        Surface {
+            TasksContent(
+                loading = false,
+                tasks = emptyList(),
+                currentFilteringLabel = R.string.label_all,
+                noTasksLabel = R.string.no_tasks_all,
+                noTasksIconRes = R.drawable.logo_no_fill,
+                onRefresh = { },
+                onTaskClick = { },
+                onTaskCheckedChange = { _, _ -> },
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun TasksEmptyContentPreview() {
+    AppCompatTheme {
+        Surface {
+            TasksEmptyContent(
+                noTasksLabel = R.string.no_tasks_all,
+                noTasksIconRes = R.drawable.logo_no_fill
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun TaskItemPreview() {
+    AppCompatTheme {
+        Surface {
+            TaskItem(
+                task = Task(
+                    title = "Title",
+                    description = "Description",
+                    id = "ID"
+                ),
+                onTaskClick = { },
+                onCheckedChange = { }
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun TaskItemCompletedPreview() {
+    AppCompatTheme {
+        Surface {
+            TaskItem(
+                task = Task(
+                    title = "Title",
+                    description = "Description",
+                    isCompleted = true,
+                    id = "ID"
+                ),
+                onTaskClick = { },
+                onCheckedChange = { }
+            )
+        }
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt
new file mode 100644
index 000000000..9868f80a7
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.tasks
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.android.architecture.blueprints.todoapp.ADD_EDIT_RESULT_OK
+import com.example.android.architecture.blueprints.todoapp.DELETE_RESULT_OK
+import com.example.android.architecture.blueprints.todoapp.EDIT_RESULT_OK
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ACTIVE_TASKS
+import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ALL_TASKS
+import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.COMPLETED_TASKS
+import com.example.android.architecture.blueprints.todoapp.util.Async
+import com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/**
+ * UiState for the task list screen.
+ */
+data class TasksUiState(
+    val items: List<Task> = emptyList(),
+    val isLoading: Boolean = false,
+    val filteringUiInfo: FilteringUiInfo = FilteringUiInfo(),
+    val userMessage: Int? = null
+)
+
+/**
+ * ViewModel for the task list screen.
+ */
+@HiltViewModel
+class TasksViewModel @Inject constructor(
+    private val taskRepository: TaskRepository,
+    private val savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+    private val _savedFilterType =
+        savedStateHandle.getStateFlow(TASKS_FILTER_SAVED_STATE_KEY, ALL_TASKS)
+
+    private val _filterUiInfo = _savedFilterType.map { getFilterUiInfo(it) }.distinctUntilChanged()
+    private val _userMessage: MutableStateFlow<Int?> = MutableStateFlow(null)
+    private val _isLoading = MutableStateFlow(false)
+    private val _filteredTasksAsync =
+        combine(taskRepository.getTasksStream(), _savedFilterType) { tasks, type ->
+            filterTasks(tasks, type)
+        }
+            .map { Async.Success(it) }
+            .catch<Async<List<Task>>> { emit(Async.Error(R.string.loading_tasks_error)) }
+
+    val uiState: StateFlow<TasksUiState> = combine(
+        _filterUiInfo, _isLoading, _userMessage, _filteredTasksAsync
+    ) { filterUiInfo, isLoading, userMessage, tasksAsync ->
+        when (tasksAsync) {
+            Async.Loading -> {
+                TasksUiState(isLoading = true)
+            }
+            is Async.Error -> {
+                TasksUiState(userMessage = tasksAsync.errorMessage)
+            }
+            is Async.Success -> {
+                TasksUiState(
+                    items = tasksAsync.data,
+                    filteringUiInfo = filterUiInfo,
+                    isLoading = isLoading,
+                    userMessage = userMessage
+                )
+            }
+        }
+    }
+        .stateIn(
+            scope = viewModelScope,
+            started = WhileUiSubscribed,
+            initialValue = TasksUiState(isLoading = true)
+        )
+
+    fun setFiltering(requestType: TasksFilterType) {
+        savedStateHandle[TASKS_FILTER_SAVED_STATE_KEY] = requestType
+    }
+
+    fun clearCompletedTasks() {
+        viewModelScope.launch {
+            taskRepository.clearCompletedTasks()
+            showSnackbarMessage(R.string.completed_tasks_cleared)
+            refresh()
+        }
+    }
+
+    fun completeTask(task: Task, completed: Boolean) = viewModelScope.launch {
+        if (completed) {
+            taskRepository.completeTask(task.id)
+            showSnackbarMessage(R.string.task_marked_complete)
+        } else {
+            taskRepository.activateTask(task.id)
+            showSnackbarMessage(R.string.task_marked_active)
+        }
+    }
+
+    fun showEditResultMessage(result: Int) {
+        when (result) {
+            EDIT_RESULT_OK -> showSnackbarMessage(R.string.successfully_saved_task_message)
+            ADD_EDIT_RESULT_OK -> showSnackbarMessage(R.string.successfully_added_task_message)
+            DELETE_RESULT_OK -> showSnackbarMessage(R.string.successfully_deleted_task_message)
+        }
+    }
+
+    fun snackbarMessageShown() {
+        _userMessage.value = null
+    }
+
+    private fun showSnackbarMessage(message: Int) {
+        _userMessage.value = message
+    }
+
+    fun refresh() {
+        _isLoading.value = true
+        viewModelScope.launch {
+            taskRepository.refresh()
+            _isLoading.value = false
+        }
+    }
+
+    private fun filterTasks(tasks: List<Task>, filteringType: TasksFilterType): List<Task> {
+        val tasksToShow = ArrayList<Task>()
+        // We filter the tasks based on the requestType
+        for (task in tasks) {
+            when (filteringType) {
+                ALL_TASKS -> tasksToShow.add(task)
+                ACTIVE_TASKS -> if (task.isActive) {
+                    tasksToShow.add(task)
+                }
+                COMPLETED_TASKS -> if (task.isCompleted) {
+                    tasksToShow.add(task)
+                }
+            }
+        }
+        return tasksToShow
+    }
+
+    private fun getFilterUiInfo(requestType: TasksFilterType): FilteringUiInfo =
+        when (requestType) {
+            ALL_TASKS -> {
+                FilteringUiInfo(
+                    R.string.label_all, R.string.no_tasks_all,
+                    R.drawable.logo_no_fill
+                )
+            }
+            ACTIVE_TASKS -> {
+                FilteringUiInfo(
+                    R.string.label_active, R.string.no_tasks_active,
+                    R.drawable.ic_check_circle_96dp
+                )
+            }
+            COMPLETED_TASKS -> {
+                FilteringUiInfo(
+                    R.string.label_completed, R.string.no_tasks_completed,
+                    R.drawable.ic_verified_user_96dp
+                )
+            }
+        }
+}
+
+// Used to save the current filtering in SavedStateHandle.
+const val TASKS_FILTER_SAVED_STATE_KEY = "TASKS_FILTER_SAVED_STATE_KEY"
+
+data class FilteringUiInfo(
+    val currentFilteringLabel: Int = R.string.label_all,
+    val noTasksLabel: Int = R.string.no_tasks_all,
+    val noTaskIconRes: Int = R.drawable.logo_no_fill,
+)
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/Async.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/Async.kt
new file mode 100644
index 000000000..004da7e4a
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/Async.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.util
+
+/**
+ * A generic class that holds a loading signal or the result of an async operation.
+ */
+sealed class Async<out T> {
+    object Loading : Async<Nothing>()
+
+    data class Error(val errorMessage: Int) : Async<Nothing>()
+
+    data class Success<out T>(val data: T) : Async<T>()
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt
new file mode 100644
index 000000000..84d641712
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.util
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.google.accompanist.swiperefresh.SwipeRefresh
+import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
+
+val primaryDarkColor: Color = Color(0xFF263238)
+
+/**
+ * Display an initial empty state or swipe to refresh content.
+ *
+ * @param loading (state) when true, display a loading spinner over [content]
+ * @param empty (state) when true, display [emptyContent]
+ * @param emptyContent (slot) the content to display for the empty state
+ * @param onRefresh (event) event to request refresh
+ * @param modifier the modifier to apply to this layout.
+ * @param content (slot) the main content to show
+ */
+@Composable
+fun LoadingContent(
+    loading: Boolean,
+    empty: Boolean,
+    emptyContent: @Composable () -> Unit,
+    onRefresh: () -> Unit,
+    modifier: Modifier = Modifier,
+    content: @Composable () -> Unit
+) {
+    if (empty) {
+        emptyContent()
+    } else {
+        SwipeRefresh(
+            state = rememberSwipeRefreshState(loading),
+            onRefresh = onRefresh,
+            modifier = modifier,
+            content = content,
+        )
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt
new file mode 100644
index 000000000..318b2c90e
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.util
+
+import kotlinx.coroutines.flow.SharingStarted
+
+private const val StopTimeoutMillis: Long = 5000
+
+/**
+ * A [SharingStarted] meant to be used with a [StateFlow] to expose data to the UI.
+ *
+ * When the UI stops observing, upstream flows stay active for some time to allow the system to
+ * come back from a short-lived configuration change (such as rotations). If the UI stops
+ * observing for longer, the cache is kept but the upstream flows are stopped. When the UI comes
+ * back, the latest value is replayed and the upstream flows are executed again. This is done to
+ * save resources when the app is in the background but let users switch between apps quickly.
+ */
+val WhileUiSubscribed: SharingStarted = SharingStarted.WhileSubscribed(StopTimeoutMillis)
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/SimpleCountingIdlingResource.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/SimpleCountingIdlingResource.kt
new file mode 100644
index 000000000..706756359
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/SimpleCountingIdlingResource.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.util
+
+import androidx.test.espresso.IdlingResource
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * An simple counter implementation of [IdlingResource] that determines idleness by
+ * maintaining an internal counter. When the counter is 0 - it is considered to be idle, when it is
+ * non-zero it is not idle. This is very similar to the way a [java.util.concurrent.Semaphore]
+ * behaves.
+ *
+ *
+ * This class can then be used to wrap up operations that while in progress should block tests from
+ * accessing the UI.
+ */
+class SimpleCountingIdlingResource(private val resourceName: String) : IdlingResource {
+
+    private val counter = AtomicInteger(0)
+
+    // written from main thread, read from any thread.
+    @Volatile
+    private var resourceCallback: IdlingResource.ResourceCallback? = null
+
+    override fun getName() = resourceName
+
+    override fun isIdleNow() = counter.get() == 0
+
+    override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) {
+        this.resourceCallback = resourceCallback
+    }
+
+    /**
+     * Increments the count of in-flight transactions to the resource being monitored.
+     */
+    fun increment() {
+        counter.getAndIncrement()
+    }
+
+    /**
+     * Decrements the count of in-flight transactions to the resource being monitored.
+     * If this operation results in the counter falling below 0 - an exception is raised.
+     *
+     * @throws IllegalStateException if the counter is below 0.
+     */
+    fun decrement() {
+        val counterVal = counter.decrementAndGet()
+        if (counterVal == 0) {
+            // we've gone from non-zero to zero. That means we're idle now! Tell espresso.
+            resourceCallback?.onTransitionToIdle()
+        } else if (counterVal < 0) {
+            throw IllegalStateException("Counter has been corrupted!")
+        }
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt
new file mode 100644
index 000000000..8e5ed06af
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.util
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.DrawerState
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.ModalDrawer
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.TodoDestinations
+import com.example.android.architecture.blueprints.todoapp.TodoNavigationActions
+import com.google.accompanist.appcompattheme.AppCompatTheme
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun AppModalDrawer(
+    drawerState: DrawerState,
+    currentRoute: String,
+    navigationActions: TodoNavigationActions,
+    coroutineScope: CoroutineScope = rememberCoroutineScope(),
+    content: @Composable () -> Unit
+) {
+    ModalDrawer(
+        drawerState = drawerState,
+        drawerContent = {
+            AppDrawer(
+                currentRoute = currentRoute,
+                navigateToTasks = { navigationActions.navigateToTasks() },
+                navigateToStatistics = { navigationActions.navigateToStatistics() },
+                closeDrawer = { coroutineScope.launch { drawerState.close() } }
+            )
+        }
+    ) {
+        content()
+    }
+}
+
+@Composable
+private fun AppDrawer(
+    currentRoute: String,
+    navigateToTasks: () -> Unit,
+    navigateToStatistics: () -> Unit,
+    closeDrawer: () -> Unit,
+    modifier: Modifier = Modifier
+) {
+    Column(modifier = modifier.fillMaxSize()) {
+        DrawerHeader()
+        DrawerButton(
+            painter = painterResource(id = R.drawable.ic_list),
+            label = stringResource(id = R.string.list_title),
+            isSelected = currentRoute == TodoDestinations.TASKS_ROUTE,
+            action = {
+                navigateToTasks()
+                closeDrawer()
+            }
+        )
+        DrawerButton(
+            painter = painterResource(id = R.drawable.ic_statistics),
+            label = stringResource(id = R.string.statistics_title),
+            isSelected = currentRoute == TodoDestinations.STATISTICS_ROUTE,
+            action = {
+                navigateToStatistics()
+                closeDrawer()
+            }
+        )
+    }
+}
+
+@Composable
+private fun DrawerHeader(
+    modifier: Modifier = Modifier
+) {
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.Center,
+        modifier = modifier
+            .fillMaxWidth()
+            .background(primaryDarkColor)
+            .height(dimensionResource(id = R.dimen.header_height))
+            .padding(dimensionResource(id = R.dimen.header_padding))
+    ) {
+        Image(
+            painter = painterResource(id = R.drawable.logo_no_fill),
+            contentDescription =
+            stringResource(id = R.string.tasks_header_image_content_description),
+            modifier = Modifier.width(dimensionResource(id = R.dimen.header_image_width))
+        )
+        Text(
+            text = stringResource(id = R.string.navigation_view_header_title),
+            color = MaterialTheme.colors.surface
+        )
+    }
+}
+
+@Composable
+private fun DrawerButton(
+    painter: Painter,
+    label: String,
+    isSelected: Boolean,
+    action: () -> Unit,
+    modifier: Modifier = Modifier
+) {
+    val tintColor = if (isSelected) {
+        MaterialTheme.colors.secondary
+    } else {
+        MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
+    }
+
+    TextButton(
+        onClick = action,
+        modifier = modifier
+            .fillMaxWidth()
+            .padding(horizontal = dimensionResource(id = R.dimen.horizontal_margin))
+    ) {
+        Row(
+            horizontalArrangement = Arrangement.Start,
+            verticalAlignment = Alignment.CenterVertically,
+            modifier = Modifier.fillMaxWidth()
+        ) {
+            Icon(
+                painter = painter,
+                contentDescription = null, // decorative
+                tint = tintColor
+            )
+            Spacer(Modifier.width(16.dp))
+            Text(
+                text = label,
+                style = MaterialTheme.typography.body2,
+                color = tintColor
+            )
+        }
+    }
+}
+
+@Preview("Drawer contents")
+@Composable
+fun PreviewAppDrawer() {
+    AppCompatTheme {
+        Surface {
+            AppDrawer(
+                currentRoute = TodoDestinations.TASKS_ROUTE,
+                navigateToTasks = {},
+                navigateToStatistics = {},
+                closeDrawer = {}
+            )
+        }
+    }
+}
diff --git a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt
new file mode 100644
index 000000000..abf26cc2e
--- /dev/null
+++ b/app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.util
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.android.architecture.blueprints.todoapp.R
+import com.google.accompanist.appcompattheme.AppCompatTheme
+
+@Composable
+fun TasksTopAppBar(
+    openDrawer: () -> Unit,
+    onFilterAllTasks: () -> Unit,
+    onFilterActiveTasks: () -> Unit,
+    onFilterCompletedTasks: () -> Unit,
+    onClearCompletedTasks: () -> Unit,
+    onRefresh: () -> Unit
+) {
+    TopAppBar(
+        title = { Text(text = stringResource(id = R.string.app_name)) },
+        navigationIcon = {
+            IconButton(onClick = openDrawer) {
+                Icon(Icons.Filled.Menu, stringResource(id = R.string.open_drawer))
+            }
+        },
+        actions = {
+            FilterTasksMenu(onFilterAllTasks, onFilterActiveTasks, onFilterCompletedTasks)
+            MoreTasksMenu(onClearCompletedTasks, onRefresh)
+        },
+        modifier = Modifier.fillMaxWidth()
+    )
+}
+
+@Composable
+private fun FilterTasksMenu(
+    onFilterAllTasks: () -> Unit,
+    onFilterActiveTasks: () -> Unit,
+    onFilterCompletedTasks: () -> Unit
+) {
+    TopAppBarDropdownMenu(
+        iconContent = {
+            Icon(
+                painterResource(id = R.drawable.ic_filter_list),
+                stringResource(id = R.string.menu_filter)
+            )
+        }
+    ) { closeMenu ->
+        DropdownMenuItem(onClick = { onFilterAllTasks(); closeMenu() }) {
+            Text(text = stringResource(id = R.string.nav_all))
+        }
+        DropdownMenuItem(onClick = { onFilterActiveTasks(); closeMenu() }) {
+            Text(text = stringResource(id = R.string.nav_active))
+        }
+        DropdownMenuItem(onClick = { onFilterCompletedTasks(); closeMenu() }) {
+            Text(text = stringResource(id = R.string.nav_completed))
+        }
+    }
+}
+
+@Composable
+private fun MoreTasksMenu(
+    onClearCompletedTasks: () -> Unit,
+    onRefresh: () -> Unit
+) {
+    TopAppBarDropdownMenu(
+        iconContent = {
+            Icon(Icons.Filled.MoreVert, stringResource(id = R.string.menu_more))
+        }
+    ) { closeMenu ->
+        DropdownMenuItem(onClick = { onClearCompletedTasks(); closeMenu() }) {
+            Text(text = stringResource(id = R.string.menu_clear))
+        }
+        DropdownMenuItem(onClick = { onRefresh(); closeMenu() }) {
+            Text(text = stringResource(id = R.string.refresh))
+        }
+    }
+}
+
+@Composable
+private fun TopAppBarDropdownMenu(
+    iconContent: @Composable () -> Unit,
+    content: @Composable ColumnScope.(() -> Unit) -> Unit
+) {
+    var expanded by remember { mutableStateOf(false) }
+
+    Box(modifier = Modifier.wrapContentSize(Alignment.TopEnd)) {
+        IconButton(onClick = { expanded = !expanded }) {
+            iconContent()
+        }
+        DropdownMenu(
+            expanded = expanded,
+            onDismissRequest = { expanded = false },
+            modifier = Modifier.wrapContentSize(Alignment.TopEnd)
+        ) {
+            content { expanded = !expanded }
+        }
+    }
+}
+
+@Composable
+fun StatisticsTopAppBar(openDrawer: () -> Unit) {
+    TopAppBar(
+        title = { Text(text = stringResource(id = R.string.statistics_title)) },
+        navigationIcon = {
+            IconButton(onClick = openDrawer) {
+                Icon(Icons.Filled.Menu, stringResource(id = R.string.open_drawer))
+            }
+        },
+        modifier = Modifier.fillMaxWidth()
+    )
+}
+
+@Composable
+fun TaskDetailTopAppBar(onBack: () -> Unit, onDelete: () -> Unit) {
+    TopAppBar(
+        title = {
+            Text(text = stringResource(id = R.string.task_details))
+        },
+        navigationIcon = {
+            IconButton(onClick = onBack) {
+                Icon(Icons.Filled.ArrowBack, stringResource(id = R.string.menu_back))
+            }
+        },
+        actions = {
+            IconButton(onClick = onDelete) {
+                Icon(Icons.Filled.Delete, stringResource(id = R.string.menu_delete_task))
+            }
+        },
+        modifier = Modifier.fillMaxWidth()
+    )
+}
+
+@Composable
+fun AddEditTaskTopAppBar(@StringRes title: Int, onBack: () -> Unit) {
+    TopAppBar(
+        title = { Text(text = stringResource(title)) },
+        navigationIcon = {
+            IconButton(onClick = onBack) {
+                Icon(Icons.Filled.ArrowBack, stringResource(id = R.string.menu_back))
+            }
+        },
+        modifier = Modifier.fillMaxWidth()
+    )
+}
+
+@Preview
+@Composable
+private fun TasksTopAppBarPreview() {
+    AppCompatTheme {
+        Surface {
+            TasksTopAppBar({}, {}, {}, {}, {}, {})
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun StatisticsTopAppBarPreview() {
+    AppCompatTheme {
+        Surface {
+            StatisticsTopAppBar { }
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun TaskDetailTopAppBarPreview() {
+    AppCompatTheme {
+        Surface {
+            TaskDetailTopAppBar({ }, { })
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun AddEditTaskTopAppBarPreview() {
+    AppCompatTheme {
+        Surface {
+            AddEditTaskTopAppBar(R.string.add_task) { }
+        }
+    }
+}
diff --git a/app/src/main/res/drawable/drawer_item_color.xml b/app/src/main/res/drawable/drawer_item_color.xml
new file mode 100644
index 000000000..5ad440684
--- /dev/null
+++ b/app/src/main/res/drawable/drawer_item_color.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/colorAccent" android:state_checked="true" />
+    <item android:color="@color/colorGrey" android:state_checked="false" />
+</selector>
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 000000000..4d3e17f4d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_assignment_turned_in_24dp.xml b/app/src/main/res/drawable/ic_assignment_turned_in_24dp.xml
new file mode 100644
index 000000000..efe9b1e51
--- /dev/null
+++ b/app/src/main/res/drawable/ic_assignment_turned_in_24dp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector android:alpha="0.49"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zm-7,0c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zm-2,14l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_check_circle_96dp.xml b/app/src/main/res/drawable/ic_check_circle_96dp.xml
new file mode 100644
index 000000000..5b193c864
--- /dev/null
+++ b/app/src/main/res/drawable/ic_check_circle_96dp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:alpha="0.49"
+    android:width="96dp"
+    android:height="96dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#2E7D32"
+        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm-2,15l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_done.xml b/app/src/main/res/drawable/ic_done.xml
new file mode 100644
index 000000000..caf1ec700
--- /dev/null
+++ b/app/src/main/res/drawable/ic_done.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml
new file mode 100644
index 000000000..536f23785
--- /dev/null
+++ b/app/src/main/res/drawable/ic_edit.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_filter_list.xml b/app/src/main/res/drawable/ic_filter_list.xml
new file mode 100644
index 000000000..e582f3efc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_filter_list.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#2E7D32"
+        android:pathData="M10,18h4v-2h-4v2zM3,6v2h18V6H3zm3,7h12v-2H6v2z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml
new file mode 100644
index 000000000..f96b69203
--- /dev/null
+++ b/app/src/main/res/drawable/ic_list.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#2E7D32"
+        android:pathData="M3,13h2v-2H3v2zm0,4h2v-2H3v2zm0,-8h2V7H3v2zm4,4h14v-2H7v2zm0,4h14v-2H7v2zM7,7v2h14V7H7z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_menu.xml
new file mode 100644
index 000000000..b3badec7f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_menu.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M3,18h18v-2H3v2zm0,-5h18v-2H3v2zm0,-7v2h18V6H3z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_statistics.xml b/app/src/main/res/drawable/ic_statistics.xml
new file mode 100644
index 000000000..4b8843749
--- /dev/null
+++ b/app/src/main/res/drawable/ic_statistics.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zm4,0h-2V7h2v10zm4,0h-2v-4h2v4z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_statistics_100dp.xml b/app/src/main/res/drawable/ic_statistics_100dp.xml
new file mode 100644
index 000000000..58ee450c8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_statistics_100dp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector android:alpha="0.80"
+    android:height="100dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    android:width="100dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zm4,0h-2V7h2v10zm4,0h-2v-4h2v4z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_statistics_24dp.xml b/app/src/main/res/drawable/ic_statistics_24dp.xml
new file mode 100644
index 000000000..4b8843749
--- /dev/null
+++ b/app/src/main/res/drawable/ic_statistics_24dp.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zm4,0h-2V7h2v10zm4,0h-2v-4h2v4z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_verified_user_96dp.xml b/app/src/main/res/drawable/ic_verified_user_96dp.xml
new file mode 100644
index 000000000..02e9dae9e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_verified_user_96dp.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector
+    android:alpha="0.49"
+    android:height="96dp"
+    android:width="96dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="#2E7D32"
+        android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4zm-2,16l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z" />
+</vector>
diff --git a/app/src/main/res/drawable/list_completed_touch_feedback.xml b/app/src/main/res/drawable/list_completed_touch_feedback.xml
new file mode 100644
index 000000000..9a7304cd6
--- /dev/null
+++ b/app/src/main/res/drawable/list_completed_touch_feedback.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/touchFeedback" />
+
+    <item android:drawable="@drawable/completedTaskBackground" />
+</selector>
diff --git a/app/src/main/res/drawable/logo_no_fill.png b/app/src/main/res/drawable/logo_no_fill.png
new file mode 100644
index 000000000..b1bac3114
Binary files /dev/null and b/app/src/main/res/drawable/logo_no_fill.png differ
diff --git a/app/src/main/res/drawable/touch_feedback.xml b/app/src/main/res/drawable/touch_feedback.xml
new file mode 100644
index 000000000..68c9b1cc1
--- /dev/null
+++ b/app/src/main/res/drawable/touch_feedback.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/touchFeedback" />
+</selector>
diff --git a/app/src/main/res/drawable/trash_icon.png b/app/src/main/res/drawable/trash_icon.png
new file mode 100644
index 000000000..4e466e560
Binary files /dev/null and b/app/src/main/res/drawable/trash_icon.png differ
diff --git a/app/src/main/res/font/opensans_font.xml b/app/src/main/res/font/opensans_font.xml
new file mode 100644
index 000000000..1cdc8aeb4
--- /dev/null
+++ b/app/src/main/res/font/opensans_font.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
+    <font
+        app:fontStyle="normal"
+        app:fontWeight="400"
+        app:font="@font/opensans_regular" />
+    <font
+        app:fontStyle="normal"
+        app:fontWeight="700"
+        app:font="@font/opensans_semibold" />
+</font-family>
diff --git a/app/src/main/res/font/opensans_regular.ttf b/app/src/main/res/font/opensans_regular.ttf
new file mode 100644
index 000000000..2d4da3a6e
Binary files /dev/null and b/app/src/main/res/font/opensans_regular.ttf differ
diff --git a/app/src/main/res/font/opensans_semibold.ttf b/app/src/main/res/font/opensans_semibold.ttf
new file mode 100644
index 000000000..fd71fe9da
Binary files /dev/null and b/app/src/main/res/font/opensans_semibold.ttf differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..ec70e05ca
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..7b8f7fd34
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..fe342ca68
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..9e8e5da08
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..a176762df
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml
new file mode 100644
index 000000000..e42c06cbf
--- /dev/null
+++ b/app/src/main/res/values-v21/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+
+    <style name="AppTheme" parent="Base.AppTheme" />
+
+    <style name="AppTheme.OverlapSystemBar" parent="Base.AppTheme">
+        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:windowTranslucentStatus">true</item>
+        <item name="android:fitsSystemWindows">true</item>
+    </style>
+
+</resources>
diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 000000000..bfde1cfa8
--- /dev/null
+++ b/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+         (such as screen margins) for screens with more than 820dp of available width. This
+         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+    <dimen name="horizontal_margin">64dp</dimen>
+
+    <dimen name="fab_margin">24dp</dimen>
+
+</resources>
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..824764515
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <declare-styleable name="ScrollChildSwipeRefreshLayout">
+        <attr name="refreshing" format="boolean" />
+    </declare-styleable>
+</resources>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..288283dd8
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <color name="colorPrimary">#FFFFF0</color>
+    <color name="colorPrimaryDark">#263238</color>
+    <color name="colorAccent">#2E7D32</color>
+    <color name="colorTextPrimary">#000000</color>
+    <color name="colorGrey">#757575</color>
+
+    <drawable name="completedTaskBackground">#CCCCCC</drawable>
+
+    <drawable name="touchFeedback">#CFD8DC</drawable>
+
+</resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..59cf881d5
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="horizontal_margin">16dp</dimen>
+    <dimen name="vertical_margin">16dp</dimen>
+
+    <dimen name="fab_margin">16dp</dimen>
+
+    <dimen name="list_item_padding">8dp</dimen>
+    <dimen name="header_height">192dp</dimen>
+    <dimen name="header_padding">16dp</dimen>
+    <dimen name="header_image_width">100dp</dimen>
+
+</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..644ec9d09
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <string name="app_name">Todo</string>
+    <string name="add_task">New Task</string>
+    <string name="edit_task">Edit Task</string>
+    <string name="task_details">Task Details</string>
+    <string name="task_marked_complete">Task marked complete</string>
+    <string name="task_marked_active">Task marked active</string>
+    <string name="loading_tasks_error">Error while loading tasks</string>
+    <string name="loading_task_error">Error while loading task</string>
+    <string name="task_not_found">Task not found</string>
+    <string name="completed_tasks_cleared">Completed tasks cleared</string>
+    <string name="menu_filter">Filter</string>
+    <string name="open_drawer">Open Drawer</string>
+    <string name="menu_back">Back</string>
+    <string name="menu_clear">Clear completed</string>
+    <string name="menu_delete_task">Delete task</string>
+    <string name="menu_more">More</string>
+    <string name="navigation_view_header_title">Todo</string>
+    <string name="title_hint">Title</string>
+    <string name="description_hint">Enter your task here.</string>
+    <string name="cd_save_task">Save task</string>
+    <string name="empty_task_message">Tasks cannot be empty</string>
+    <string name="successfully_saved_task_message">Task saved</string>
+    <string name="list_title">Task List</string>
+    <string name="statistics_title">Statistics</string>
+    <string name="statistics_no_tasks">You have no tasks.</string>
+    <string name="statistics_active_tasks">Active tasks: %.1f%%</string>
+    <string name="statistics_completed_tasks">Completed tasks: %.1f%%</string>
+    <string name="statistics_error">Error loading statistics.</string>
+    <string name="no_data">No data</string>
+    <string name="loading">LOADING</string>
+
+    <string-array name="list_tasks_array">
+        <item>@string/nav_all</item>
+        <item>@string/nav_active</item>
+        <item>@string/nav_completed</item>
+    </string-array>
+    <string name="nav_all">All</string>
+    <string name="nav_active">Active</string>
+    <string name="nav_completed">Completed</string>
+    <string name="label_all">All Tasks</string>
+    <string name="label_active">Active Tasks</string>
+    <string name="label_completed">Completed Tasks</string>
+    <string name="no_tasks_all">You have no tasks!</string>
+    <string name="no_tasks_active">You have no active tasks!</string>
+    <string name="no_tasks_completed">You have no completed tasks!</string>
+    <string name="refresh">Refresh</string>
+    <string name="successfully_deleted_task_message">Task was deleted</string>
+    <string name="successfully_added_task_message">Task added</string>
+    <string name="no_data_description" />
+
+    <!-- Content Descriptions -->
+    <string name="tasks_header_image_content_description">Tasks header image</string>
+    <string name="no_tasks_image_content_description">No tasks image</string>
+
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..cfa6d2bfb
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+    <!-- Base application theme. --><style name="AppTheme" parent="Base.AppTheme" />
+
+    <style name="AppTheme.OverlapSystemBar" parent="Base.AppTheme" />
+
+    <style name="Base.AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+        <!-- For Android sdk versions 26+. -->
+        <item name="android:fontFamily" tools:targetApi="jelly_bean">@font/opensans_font</item>
+        <!-- Target Android sdk versions < 26 and > 14. -->
+        <item name="fontFamily">@font/opensans_font</item>
+        <!-- sets the style for the overflow button -->
+        <item name="actionOverflowButtonStyle">@style/actionOverflowButtonStyle</item>
+    </style>
+
+    <!-- This style defines the tint color of the overflow menu button -->
+    <style name="actionOverflowButtonStyle" parent="@style/Widget.AppCompat.ActionButton.Overflow">
+        <item name="android:tint">@color/colorAccent</item>
+    </style>
+
+    <style name="Toolbar" parent="ThemeOverlay.AppCompat.Light" />
+</resources>
diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModelTest.kt
new file mode 100644
index 000000000..1a65c10af
--- /dev/null
+++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModelTest.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.addedittask
+
+import androidx.lifecycle.SavedStateHandle
+import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
+import com.example.android.architecture.blueprints.todoapp.R.string
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs
+import com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests for the implementation of [AddEditTaskViewModel].
+ */
+@ExperimentalCoroutinesApi
+class AddEditTaskViewModelTest {
+
+    // Subject under test
+    private lateinit var addEditTaskViewModel: AddEditTaskViewModel
+
+    // Use a fake repository to be injected into the viewmodel
+    private lateinit var tasksRepository: FakeTaskRepository
+    private val task = Task(title = "Title1", description = "Description1", id = "0")
+
+    // Set the main coroutines dispatcher for unit testing.
+    @ExperimentalCoroutinesApi
+    @get:Rule
+    val mainCoroutineRule = MainCoroutineRule()
+
+    @Before
+    fun setupViewModel() {
+        // We initialise the repository with no tasks
+        tasksRepository = FakeTaskRepository().apply {
+            addTasks(task)
+        }
+    }
+
+    @Test
+    fun saveNewTaskToRepository_showsSuccessMessageUi() {
+        addEditTaskViewModel = AddEditTaskViewModel(
+            tasksRepository,
+            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to "0"))
+        )
+
+        val newTitle = "New Task Title"
+        val newDescription = "Some Task Description"
+        addEditTaskViewModel.apply {
+            updateTitle(newTitle)
+            updateDescription(newDescription)
+        }
+        addEditTaskViewModel.saveTask()
+
+        val newTask = tasksRepository.savedTasks.value.values.first()
+
+        // Then a task is saved in the repository and the view updated
+        assertThat(newTask.title).isEqualTo(newTitle)
+        assertThat(newTask.description).isEqualTo(newDescription)
+    }
+
+    @Test
+    fun loadTasks_loading() = runTest {
+        // Set Main dispatcher to not run coroutines eagerly, for just this one test
+        Dispatchers.setMain(StandardTestDispatcher())
+
+        addEditTaskViewModel = AddEditTaskViewModel(
+            tasksRepository,
+            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to "0"))
+        )
+
+        // Then progress indicator is shown
+        assertThat(addEditTaskViewModel.uiState.value.isLoading).isTrue()
+
+        // Execute pending coroutines actions
+        advanceUntilIdle()
+
+        // Then progress indicator is hidden
+        assertThat(addEditTaskViewModel.uiState.value.isLoading).isFalse()
+    }
+
+    @Test
+    fun loadTasks_taskShown() {
+        addEditTaskViewModel = AddEditTaskViewModel(
+            tasksRepository,
+            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to "0"))
+        )
+
+        // Add task to repository
+        tasksRepository.addTasks(task)
+
+        // Verify a task is loaded
+        val uiState = addEditTaskViewModel.uiState.value
+        assertThat(uiState.title).isEqualTo(task.title)
+        assertThat(uiState.description).isEqualTo(task.description)
+        assertThat(uiState.isLoading).isFalse()
+    }
+
+    @Test
+    fun saveNewTaskToRepository_emptyTitle_error() {
+        addEditTaskViewModel = AddEditTaskViewModel(
+            tasksRepository,
+            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to "0"))
+        )
+
+        saveTaskAndAssertUserMessage("", "Some Task Description")
+    }
+
+    @Test
+    fun saveNewTaskToRepository_emptyDescription_error() {
+        addEditTaskViewModel = AddEditTaskViewModel(
+            tasksRepository,
+            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to "0"))
+        )
+
+        saveTaskAndAssertUserMessage("Title", "")
+    }
+
+    @Test
+    fun saveNewTaskToRepository_emptyDescriptionEmptyTitle_error() {
+        addEditTaskViewModel = AddEditTaskViewModel(
+            tasksRepository,
+            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to "0"))
+        )
+
+        saveTaskAndAssertUserMessage("", "")
+    }
+
+    private fun saveTaskAndAssertUserMessage(title: String, description: String) {
+        addEditTaskViewModel.apply {
+            updateTitle(title)
+            updateDescription(description)
+        }
+
+        // When saving an incomplete task
+        addEditTaskViewModel.saveTask()
+
+        assertThat(
+            addEditTaskViewModel.uiState.value.userMessage
+        ).isEqualTo(string.empty_task_message)
+    }
+}
diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt
new file mode 100644
index 000000000..1c41c8654
--- /dev/null
+++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data
+
+import com.example.android.architecture.blueprints.todoapp.data.source.local.FakeTaskDao
+import com.example.android.architecture.blueprints.todoapp.data.source.network.FakeNetworkDataSource
+import com.google.common.truth.Truth.assertThat
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit tests for the implementation of the in-memory repository with cache.
+ */
+@ExperimentalCoroutinesApi
+class DefaultTaskRepositoryTest {
+
+    private val task1 = Task(id = "1", title = "Title1", description = "Description1")
+    private val task2 = Task(id = "2", title = "Title2", description = "Description2")
+    private val task3 = Task(id = "3", title = "Title3", description = "Description3")
+
+    private val newTaskTitle = "Title new"
+    private val newTaskDescription = "Description new"
+    private val newTask = Task(id = "new", title = newTaskTitle, description = newTaskDescription)
+    private val newTasks = listOf(newTask)
+
+    private val networkTasks = listOf(task1, task2).toNetwork()
+    private val localTasks = listOf(task3.toLocal())
+
+    // Test dependencies
+    private lateinit var networkDataSource: FakeNetworkDataSource
+    private lateinit var localDataSource: FakeTaskDao
+
+    private var testDispatcher = UnconfinedTestDispatcher()
+    private var testScope = TestScope(testDispatcher)
+
+    // Class under test
+    private lateinit var taskRepository: DefaultTaskRepository
+
+    @ExperimentalCoroutinesApi
+    @Before
+    fun createRepository() {
+        networkDataSource = FakeNetworkDataSource(networkTasks.toMutableList())
+        localDataSource = FakeTaskDao(localTasks)
+        // Get a reference to the class under test
+        taskRepository = DefaultTaskRepository(
+            networkDataSource = networkDataSource,
+            localDataSource = localDataSource,
+            dispatcher = testDispatcher,
+            scope = testScope
+        )
+    }
+
+    @ExperimentalCoroutinesApi
+    @Test
+    fun getTasks_emptyRepositoryAndUninitializedCache() = testScope.runTest {
+        networkDataSource.tasks?.clear()
+        localDataSource.deleteAll()
+
+        assertThat(taskRepository.getTasks().size).isEqualTo(0)
+    }
+
+    @Test
+    fun getTasks_repositoryCachesAfterFirstApiCall() = testScope.runTest {
+        // Trigger the repository to load tasks from the remote data source
+        val initial = taskRepository.getTasks(forceUpdate = true)
+
+        // Change the remote data source
+        networkDataSource.tasks = newTasks.toNetwork().toMutableList()
+
+        // Load the tasks again without forcing a refresh
+        val second = taskRepository.getTasks()
+
+        // Initial and second should match because we didn't force a refresh (no tasks were loaded
+        // from the remote data source)
+        assertThat(second).isEqualTo(initial)
+    }
+
+    @Test
+    fun getTasks_requestsAllTasksFromRemoteDataSource() = testScope.runTest {
+        // When tasks are requested from the tasks repository
+        val tasks = taskRepository.getTasks(true)
+
+        // Then tasks are loaded from the remote data source
+        assertThat(tasks).isEqualTo(networkTasks.toExternal())
+    }
+
+    @Test
+    fun saveTask_savesToLocalAndRemote() = testScope.runTest {
+        // When a task is saved to the tasks repository
+        val newTaskId = taskRepository.createTask(newTask.title, newTask.description)
+
+        // Then the remote and local sources contain the new task
+        assertThat(networkDataSource.tasks?.map { it.id }?.contains(newTaskId))
+        assertThat(localDataSource.tasks?.map { it.id }?.contains(newTaskId))
+    }
+
+    @Test
+    fun getTasks_WithDirtyCache_tasksAreRetrievedFromRemote() = testScope.runTest {
+        // First call returns from REMOTE
+        val tasks = taskRepository.getTasks()
+
+        // Set a different list of tasks in REMOTE
+        networkDataSource.tasks = newTasks.toNetwork().toMutableList()
+
+        // But if tasks are cached, subsequent calls load from cache
+        val cachedTasks = taskRepository.getTasks()
+        assertThat(cachedTasks).isEqualTo(tasks)
+
+        // Now force remote loading
+        val refreshedTasks = taskRepository.getTasks(true)
+
+        // Tasks must be the recently updated in REMOTE
+        assertThat(refreshedTasks).isEqualTo(newTasks)
+    }
+
+    @Test(expected = Exception::class)
+    fun getTasks_WithDirtyCache_remoteUnavailable_throwsException() = testScope.runTest {
+        // Make remote data source unavailable
+        networkDataSource.tasks = null
+
+        // Load tasks forcing remote load
+        taskRepository.getTasks(true)
+
+        // Exception should be thrown
+    }
+
+    @Test
+    fun getTasks_WithRemoteDataSourceUnavailable_tasksAreRetrievedFromLocal() =
+        testScope.runTest {
+            // When the remote data source is unavailable
+            networkDataSource.tasks = null
+
+            // The repository fetches from the local source
+            assertThat(taskRepository.getTasks()).isEqualTo(localTasks.toExternal())
+        }
+
+    @Test(expected = Exception::class)
+    fun getTasks_WithBothDataSourcesUnavailable_throwsError() = testScope.runTest {
+        // When both sources are unavailable
+        networkDataSource.tasks = null
+        localDataSource.tasks = null
+
+        // The repository throws an error
+        taskRepository.getTasks()
+    }
+
+    @Test
+    fun getTasks_refreshesLocalDataSource() = testScope.runTest {
+        // Forcing an update will fetch tasks from remote
+        val expectedTasks = networkTasks.toExternal()
+
+        val newTasks = taskRepository.getTasks(true)
+
+        assertEquals(expectedTasks, newTasks)
+        assertEquals(expectedTasks, localDataSource.tasks?.toExternal())
+    }
+
+    @Test
+    fun completeTask_completesTaskToServiceAPIUpdatesCache() = testScope.runTest {
+        // Save a task
+        val newTaskId = taskRepository.createTask(newTask.title, newTask.description)
+
+        // Make sure it's active
+        assertThat(taskRepository.getTask(newTaskId)?.isCompleted).isFalse()
+
+        // Mark is as complete
+        taskRepository.completeTask(newTaskId)
+
+        // Verify it's now completed
+        assertThat(taskRepository.getTask(newTaskId)?.isCompleted).isTrue()
+    }
+
+    @Test
+    fun completeTask_activeTaskToServiceAPIUpdatesCache() = testScope.runTest {
+        // Save a task
+        val newTaskId = taskRepository.createTask(newTask.title, newTask.description)
+        taskRepository.completeTask(newTaskId)
+
+        // Make sure it's completed
+        assertThat(taskRepository.getTask(newTaskId)?.isActive).isFalse()
+
+        // Mark is as active
+        taskRepository.activateTask(newTaskId)
+
+        // Verify it's now activated
+        assertThat(taskRepository.getTask(newTaskId)?.isActive).isTrue()
+    }
+
+    @Test
+    fun getTask_repositoryCachesAfterFirstApiCall() = testScope.runTest {
+        // Obtain a task from the local data source
+        localDataSource = FakeTaskDao(mutableListOf(task1.toLocal()))
+        val initial = taskRepository.getTask(task1.id)
+
+        // Change the tasks on the remote
+        networkDataSource.tasks = newTasks.toNetwork().toMutableList()
+
+        // Obtain the same task again
+        val second = taskRepository.getTask(task1.id)
+
+        // Initial and second tasks should match because we didn't force a refresh
+        assertThat(second).isEqualTo(initial)
+    }
+
+    @Test
+    fun getTask_forceRefresh() = testScope.runTest {
+        // Trigger the repository to load data, which loads from remote and caches
+        networkDataSource.tasks = mutableListOf(task1.toNetwork())
+        val task1FirstTime = taskRepository.getTask(task1.id, forceUpdate = true)
+        assertThat(task1FirstTime?.id).isEqualTo(task1.id)
+
+        // Configure the remote data source to return a different task
+        networkDataSource.tasks = mutableListOf(task2.toNetwork())
+
+        // Force refresh
+        val task1SecondTime = taskRepository.getTask(task1.id, true)
+        val task2SecondTime = taskRepository.getTask(task2.id, true)
+
+        // Only task2 works because task1 does not exist on the remote
+        assertThat(task1SecondTime).isNull()
+        assertThat(task2SecondTime?.id).isEqualTo(task2.id)
+    }
+
+    @Test
+    fun clearCompletedTasks() = testScope.runTest {
+        val completedTask = task1.copy(isCompleted = true)
+        localDataSource.tasks = listOf(completedTask.toLocal(), task2.toLocal())
+        taskRepository.clearCompletedTasks()
+
+        val tasks = taskRepository.getTasks(true)
+
+        assertThat(tasks).hasSize(1)
+        assertThat(tasks).contains(task2)
+        assertThat(tasks).doesNotContain(completedTask)
+    }
+
+    @Test
+    fun deleteAllTasks() = testScope.runTest {
+        val initialTasks = taskRepository.getTasks()
+
+        // Verify tasks are returned
+        assertThat(initialTasks.size).isEqualTo(1)
+
+        // Delete all tasks
+        taskRepository.deleteAllTasks()
+
+        // Verify tasks are empty now
+        val afterDeleteTasks = taskRepository.getTasks()
+        assertThat(afterDeleteTasks).isEmpty()
+    }
+
+    @Test
+    fun deleteSingleTask() = testScope.runTest {
+        val initialTasksSize = taskRepository.getTasks(true).size
+
+        // Delete first task
+        taskRepository.deleteTask(task1.id)
+
+        // Fetch data again
+        val afterDeleteTasks = taskRepository.getTasks(true)
+
+        // Verify only one task was deleted
+        assertThat(afterDeleteTasks.size).isEqualTo(initialTasksSize - 1)
+        assertThat(afterDeleteTasks).doesNotContain(task1)
+    }
+}
diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt
new file mode 100644
index 000000000..d35aab169
--- /dev/null
+++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.statistics
+
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import org.hamcrest.core.Is.`is`
+import org.junit.Assert.assertThat
+import org.junit.Test
+
+/**
+ * Unit tests for [getActiveAndCompletedStats].
+ */
+class StatisticsUtilsTest {
+
+    @Test
+    fun getActiveAndCompletedStats_noCompleted() {
+        val tasks = listOf(
+            Task(
+                id = "id",
+                title = "title",
+                description = "desc",
+                isCompleted = false,
+            )
+        )
+        // When the list of tasks is computed with an active task
+        val result = getActiveAndCompletedStats(tasks)
+
+        // Then the percentages are 100 and 0
+        assertThat(result.activeTasksPercent, `is`(100f))
+        assertThat(result.completedTasksPercent, `is`(0f))
+    }
+
+    @Test
+    fun getActiveAndCompletedStats_noActive() {
+        val tasks = listOf(
+            Task(
+                id = "id",
+                title = "title",
+                description = "desc",
+                isCompleted = true,
+            )
+        )
+        // When the list of tasks is computed with a completed task
+        val result = getActiveAndCompletedStats(tasks)
+
+        // Then the percentages are 0 and 100
+        assertThat(result.activeTasksPercent, `is`(0f))
+        assertThat(result.completedTasksPercent, `is`(100f))
+    }
+
+    @Test
+    fun getActiveAndCompletedStats_both() {
+        // Given 3 completed tasks and 2 active tasks
+        val tasks = listOf(
+            Task(id = "1", title = "title", description = "desc", isCompleted = true),
+            Task(id = "2", title = "title", description = "desc", isCompleted = true),
+            Task(id = "3", title = "title", description = "desc", isCompleted = true),
+            Task(id = "4", title = "title", description = "desc", isCompleted = false),
+            Task(id = "5", title = "title", description = "desc", isCompleted = false),
+        )
+        // When the list of tasks is computed
+        val result = getActiveAndCompletedStats(tasks)
+
+        // Then the result is 40-60
+        assertThat(result.activeTasksPercent, `is`(40f))
+        assertThat(result.completedTasksPercent, `is`(60f))
+    }
+
+    @Test
+    fun getActiveAndCompletedStats_empty() {
+        // When there are no tasks
+        val result = getActiveAndCompletedStats(emptyList())
+
+        // Both active and completed tasks are 0
+        assertThat(result.activeTasksPercent, `is`(0f))
+        assertThat(result.completedTasksPercent, `is`(0f))
+    }
+}
diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModelTest.kt
new file mode 100644
index 000000000..532c70301
--- /dev/null
+++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModelTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.statistics
+
+import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
+import com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests for the implementation of [StatisticsViewModel]
+ */
+@ExperimentalCoroutinesApi
+class StatisticsViewModelTest {
+
+    // Subject under test
+    private lateinit var statisticsViewModel: StatisticsViewModel
+
+    // Use a fake repository to be injected into the viewmodel
+    private lateinit var tasksRepository: FakeTaskRepository
+
+    // Set the main coroutines dispatcher for unit testing.
+    @ExperimentalCoroutinesApi
+    @get:Rule
+    val mainCoroutineRule = MainCoroutineRule()
+
+    @Before
+    fun setupStatisticsViewModel() {
+        tasksRepository = FakeTaskRepository()
+        statisticsViewModel = StatisticsViewModel(tasksRepository)
+    }
+
+    @Test
+    fun loadEmptyTasksFromRepository_EmptyResults() = runTest {
+        // Given an initialized StatisticsViewModel with no tasks
+
+        // Then the results are empty
+        val uiState = statisticsViewModel.uiState.first()
+        assertThat(uiState.isEmpty).isTrue()
+    }
+
+    @Test
+    fun loadNonEmptyTasksFromRepository_NonEmptyResults() = runTest {
+        // We initialise the tasks to 3, with one active and two completed
+        val task1 = Task(id = "1", title = "Title1", description = "Desc1")
+        val task2 = Task(id = "2", title = "Title2", description = "Desc2", isCompleted = true)
+        val task3 = Task(id = "3", title = "Title3", description = "Desc3", isCompleted = true)
+        val task4 = Task(id = "4", title = "Title4", description = "Desc4", isCompleted = true)
+        tasksRepository.addTasks(task1, task2, task3, task4)
+
+        // Then the results are not empty
+        val uiState = statisticsViewModel.uiState.first()
+        assertThat(uiState.isEmpty).isFalse()
+        assertThat(uiState.activeTasksPercent).isEqualTo(25f)
+        assertThat(uiState.completedTasksPercent).isEqualTo(75f)
+        assertThat(uiState.isLoading).isEqualTo(false)
+    }
+
+    @Test
+    fun loadTasks_loading() = runTest {
+        // Set Main dispatcher to not run coroutines eagerly, for just this one test
+        Dispatchers.setMain(StandardTestDispatcher())
+
+        var isLoading: Boolean? = true
+        val job = launch {
+            statisticsViewModel.uiState.collect {
+                isLoading = it.isLoading
+            }
+        }
+
+        // Then progress indicator is shown
+        assertThat(isLoading).isTrue()
+
+        // Execute pending coroutines actions
+        advanceUntilIdle()
+
+        // Then progress indicator is hidden
+        assertThat(isLoading).isFalse()
+        job.cancel()
+    }
+}
diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt
new file mode 100644
index 000000000..c9a2c7618
--- /dev/null
+++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.taskdetail
+
+import androidx.lifecycle.SavedStateHandle
+import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs
+import com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests for the implementation of [TaskDetailViewModel]
+ */
+@ExperimentalCoroutinesApi
+class TaskDetailViewModelTest {
+
+    // Set the main coroutines dispatcher for unit testing.
+    @ExperimentalCoroutinesApi
+    @get:Rule
+    val mainCoroutineRule = MainCoroutineRule()
+
+    // Subject under test
+    private lateinit var taskDetailViewModel: TaskDetailViewModel
+
+    // Use a fake repository to be injected into the viewmodel
+    private lateinit var tasksRepository: FakeTaskRepository
+    private val task = Task(title = "Title1", description = "Description1", id = "0")
+
+    @Before
+    fun setupViewModel() {
+        tasksRepository = FakeTaskRepository()
+        tasksRepository.addTasks(task)
+
+        taskDetailViewModel = TaskDetailViewModel(
+            tasksRepository,
+            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to "0"))
+        )
+    }
+
+    @Test
+    fun getActiveTaskFromRepositoryAndLoadIntoView() = runTest {
+        val uiState = taskDetailViewModel.uiState.first()
+        // Then verify that the view was notified
+        assertThat(uiState.task?.title).isEqualTo(task.title)
+        assertThat(uiState.task?.description).isEqualTo(task.description)
+    }
+
+    @Test
+    fun completeTask() = runTest {
+        // Verify that the task was active initially
+        assertThat(tasksRepository.savedTasks.value[task.id]?.isCompleted).isFalse()
+
+        // When the ViewModel is asked to complete the task
+        assertThat(taskDetailViewModel.uiState.first().task?.id).isEqualTo("0")
+        taskDetailViewModel.setCompleted(true)
+
+        // Then the task is completed and the snackbar shows the correct message
+        assertThat(tasksRepository.savedTasks.value[task.id]?.isCompleted).isTrue()
+        assertThat(taskDetailViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.task_marked_complete)
+    }
+
+    @Test
+    fun activateTask() = runTest {
+        tasksRepository.deleteAllTasks()
+        tasksRepository.addTasks(task.copy(isCompleted = true))
+
+        // Verify that the task was completed initially
+        assertThat(tasksRepository.savedTasks.value[task.id]?.isCompleted).isTrue()
+
+        // When the ViewModel is asked to complete the task
+        assertThat(taskDetailViewModel.uiState.first().task?.id).isEqualTo("0")
+        taskDetailViewModel.setCompleted(false)
+
+        // Then the task is not completed and the snackbar shows the correct message
+        val newTask = tasksRepository.getTask(task.id)
+        assertTrue((newTask?.isActive) ?: false)
+        assertThat(taskDetailViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.task_marked_active)
+    }
+
+    @Test
+    fun taskDetailViewModel_repositoryError() = runTest {
+        // Given a repository that throws errors
+        tasksRepository.setShouldThrowError(true)
+
+        // Then the task is null and the snackbar shows a loading error message
+        assertThat(taskDetailViewModel.uiState.value.task).isNull()
+        assertThat(taskDetailViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.loading_task_error)
+    }
+
+    @Test
+    fun taskDetailViewModel_taskNotFound() = runTest {
+        // Given an ID for a non existent task
+        taskDetailViewModel = TaskDetailViewModel(
+            tasksRepository,
+            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to "nonexistent_id"))
+        )
+
+        // The task is null and the snackbar shows a "not found" error message
+        assertThat(taskDetailViewModel.uiState.value.task).isNull()
+        assertThat(taskDetailViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.task_not_found)
+    }
+
+    @Test
+    fun deleteTask() = runTest {
+        assertThat(tasksRepository.savedTasks.value.containsValue(task)).isTrue()
+
+        // When the deletion of a task is requested
+        taskDetailViewModel.deleteTask()
+
+        assertThat(tasksRepository.savedTasks.value.containsValue(task)).isFalse()
+    }
+
+    @Test
+    fun loadTask_loading() = runTest {
+        // Set Main dispatcher to not run coroutines eagerly, for just this one test
+        Dispatchers.setMain(StandardTestDispatcher())
+
+        var isLoading: Boolean? = true
+        val job = launch {
+            taskDetailViewModel.uiState.collect {
+                isLoading = it.isLoading
+            }
+        }
+
+        // Then progress indicator is shown
+        assertThat(isLoading).isTrue()
+
+        // Execute pending coroutines actions
+        advanceUntilIdle()
+
+        // Then progress indicator is hidden
+        assertThat(isLoading).isFalse()
+        job.cancel()
+    }
+}
diff --git a/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt
new file mode 100644
index 000000000..97cc75c08
--- /dev/null
+++ b/app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.tasks
+
+import androidx.lifecycle.SavedStateHandle
+import com.example.android.architecture.blueprints.todoapp.ADD_EDIT_RESULT_OK
+import com.example.android.architecture.blueprints.todoapp.DELETE_RESULT_OK
+import com.example.android.architecture.blueprints.todoapp.EDIT_RESULT_OK
+import com.example.android.architecture.blueprints.todoapp.MainCoroutineRule
+import com.example.android.architecture.blueprints.todoapp.R
+import com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository
+import com.example.android.architecture.blueprints.todoapp.data.Task
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Unit tests for the implementation of [TasksViewModel]
+ */
+@ExperimentalCoroutinesApi
+class TasksViewModelTest {
+
+    // Subject under test
+    private lateinit var tasksViewModel: TasksViewModel
+
+    // Use a fake repository to be injected into the viewmodel
+    private lateinit var tasksRepository: FakeTaskRepository
+
+    // Set the main coroutines dispatcher for unit testing.
+    @ExperimentalCoroutinesApi
+    @get:Rule
+    val mainCoroutineRule = MainCoroutineRule()
+
+    @Before
+    fun setupViewModel() {
+        // We initialise the tasks to 3, with one active and two completed
+        tasksRepository = FakeTaskRepository()
+        val task1 = Task(id = "1", title = "Title1", description = "Desc1")
+        val task2 = Task(id = "2", title = "Title2", description = "Desc2", isCompleted = true)
+        val task3 = Task(id = "3", title = "Title3", description = "Desc3", isCompleted = true)
+        tasksRepository.addTasks(task1, task2, task3)
+
+        tasksViewModel = TasksViewModel(tasksRepository, SavedStateHandle())
+    }
+
+    @Test
+    fun loadAllTasksFromRepository_loadingTogglesAndDataLoaded() = runTest {
+        // Set Main dispatcher to not run coroutines eagerly, for just this one test
+        Dispatchers.setMain(StandardTestDispatcher())
+
+        // Given an initialized TasksViewModel with initialized tasks
+        // When loading of Tasks is requested
+        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
+
+        // Trigger loading of tasks
+        tasksViewModel.refresh()
+
+        // Then progress indicator is shown
+        assertThat(tasksViewModel.uiState.first().isLoading).isTrue()
+
+        // Execute pending coroutines actions
+        advanceUntilIdle()
+
+        // Then progress indicator is hidden
+        assertThat(tasksViewModel.uiState.first().isLoading).isFalse()
+
+        // And data correctly loaded
+        assertThat(tasksViewModel.uiState.first().items).hasSize(3)
+    }
+
+    @Test
+    fun loadActiveTasksFromRepositoryAndLoadIntoView() = runTest {
+        // Given an initialized TasksViewModel with initialized tasks
+        // When loading of Tasks is requested
+        tasksViewModel.setFiltering(TasksFilterType.ACTIVE_TASKS)
+
+        // Load tasks
+        tasksViewModel.refresh()
+
+        // Then progress indicator is hidden
+        assertThat(tasksViewModel.uiState.first().isLoading).isFalse()
+
+        // And data correctly loaded
+        assertThat(tasksViewModel.uiState.first().items).hasSize(1)
+    }
+
+    @Test
+    fun loadCompletedTasksFromRepositoryAndLoadIntoView() = runTest {
+        // Given an initialized TasksViewModel with initialized tasks
+        // When loading of Tasks is requested
+        tasksViewModel.setFiltering(TasksFilterType.COMPLETED_TASKS)
+
+        // Load tasks
+        tasksViewModel.refresh()
+
+        // Then progress indicator is hidden
+        assertThat(tasksViewModel.uiState.first().isLoading).isFalse()
+
+        // And data correctly loaded
+        assertThat(tasksViewModel.uiState.first().items).hasSize(2)
+    }
+
+    @Test
+    fun loadTasks_error() = runTest {
+        // Make the repository throw errors
+        tasksRepository.setShouldThrowError(true)
+
+        // Load tasks
+        tasksViewModel.refresh()
+
+        // Then progress indicator is hidden
+        assertThat(tasksViewModel.uiState.first().isLoading).isFalse()
+
+        // And the list of items is empty
+        assertThat(tasksViewModel.uiState.first().items).isEmpty()
+        assertThat(tasksViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.loading_tasks_error)
+    }
+
+    @Test
+    fun clearCompletedTasks_clearsTasks() = runTest {
+        // When completed tasks are cleared
+        tasksViewModel.clearCompletedTasks()
+
+        // Fetch tasks
+        tasksViewModel.refresh()
+
+        // Fetch tasks
+        val allTasks = tasksViewModel.uiState.first().items
+        val completedTasks = allTasks?.filter { it.isCompleted }
+
+        // Verify there are no completed tasks left
+        assertThat(completedTasks).isEmpty()
+
+        // Verify active task is not cleared
+        assertThat(allTasks).hasSize(1)
+
+        // Verify snackbar is updated
+        assertThat(tasksViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.completed_tasks_cleared)
+    }
+
+    @Test
+    fun showEditResultMessages_editOk_snackbarUpdated() = runTest {
+        // When the viewmodel receives a result from another destination
+        tasksViewModel.showEditResultMessage(EDIT_RESULT_OK)
+
+        // The snackbar is updated
+        assertThat(tasksViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.successfully_saved_task_message)
+    }
+
+    @Test
+    fun showEditResultMessages_addOk_snackbarUpdated() = runTest {
+        // When the viewmodel receives a result from another destination
+        tasksViewModel.showEditResultMessage(ADD_EDIT_RESULT_OK)
+
+        // The snackbar is updated
+        assertThat(tasksViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.successfully_added_task_message)
+    }
+
+    @Test
+    fun showEditResultMessages_deleteOk_snackbarUpdated() = runTest {
+        // When the viewmodel receives a result from another destination
+        tasksViewModel.showEditResultMessage(DELETE_RESULT_OK)
+
+        // The snackbar is updated
+        assertThat(tasksViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.successfully_deleted_task_message)
+    }
+
+    @Test
+    fun completeTask_dataAndSnackbarUpdated() = runTest {
+        // With a repository that has an active task
+        val task = Task(id = "id", title = "Title", description = "Description")
+        tasksRepository.addTasks(task)
+
+        // Complete task
+        tasksViewModel.completeTask(task, true)
+
+        // Verify the task is completed
+        assertThat(tasksRepository.savedTasks.value[task.id]?.isCompleted).isTrue()
+
+        // The snackbar is updated
+        assertThat(tasksViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.task_marked_complete)
+    }
+
+    @Test
+    fun activateTask_dataAndSnackbarUpdated() = runTest {
+        // With a repository that has a completed task
+        val task = Task(id = "id", title = "Title", description = "Description", isCompleted = true)
+        tasksRepository.addTasks(task)
+
+        // Activate task
+        tasksViewModel.completeTask(task, false)
+
+        // Verify the task is active
+        assertThat(tasksRepository.savedTasks.value[task.id]?.isActive).isTrue()
+
+        // The snackbar is updated
+        assertThat(tasksViewModel.uiState.first().userMessage)
+            .isEqualTo(R.string.task_marked_active)
+    }
+}
diff --git a/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 000000000..1f0955d45
--- /dev/null
+++ b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 000000000..79946bd70
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+plugins {
+    alias(libs.plugins.android.application) apply false
+    alias(libs.plugins.android.library) apply false
+    alias(libs.plugins.kotlin.android) apply false
+    alias(libs.plugins.kapt) apply false
+    alias(libs.plugins.ksp) apply false
+    alias(libs.plugins.hilt) apply false
+}
diff --git a/code-of-conduct.md b/code-of-conduct.md
deleted file mode 100644
index a55ece4cc..000000000
--- a/code-of-conduct.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# Contributor Code of Conduct
-
-## Our Pledge
-
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to making participation in our project and
-our community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, gender identity and expression, level of experience,
-nationality, personal appearance, race, religion, or sexual identity and
-orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment
-include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or
-advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
-  address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
-  professional setting
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies both within project spaces and in public spaces
-when an individual is representing the project or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an appointed
-representative at an online or offline event. Representation of a project may be
-further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team. All
-complaints will be reviewed and investigated and will result in a response that
-is deemed necessary and appropriate to the circumstances. The project team is
-obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at [http://contributor-covenant.org/version/1/4][version]
-
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..504538451
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+android.enableJetifier=true
+android.useAndroidX=true
+kapt.incremental.apt=true
+ksp.incremental.apt=true
+org.gradle.unsafe.configuration-cache=true
\ No newline at end of file
diff --git a/gradle/init.gradle.kts b/gradle/init.gradle.kts
new file mode 100644
index 000000000..82b61b7e4
--- /dev/null
+++ b/gradle/init.gradle.kts
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+// The init script is used to run Spotless in a gradle configuration cache compliant manner as
+// Spotless itself is not gradle configuration cache compliant.
+// Note that the init script needs to be run with the configuration cache turned off.
+
+val ktlintVersion = "0.44.0"
+
+initscript {
+    val spotlessVersion = "6.13.0"
+
+    repositories {
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion")
+    }
+}
+
+rootProject {
+    subprojects {
+        apply<com.diffplug.gradle.spotless.SpotlessPlugin>()
+        extensions.configure<com.diffplug.gradle.spotless.SpotlessExtension> {
+            kotlin {
+                target("**/*.kt")
+                targetExclude("**/build/**/*.kt")
+                ktlint(ktlintVersion).userData(mapOf("android" to "true"))
+                licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
+            }
+            format("kts") {
+                target("**/*.kts")
+                targetExclude("**/build/**/*.kts")
+                // Look for the first line that doesn't have a block comment (assumed to be the license)
+                licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
+            }
+            format("xml") {
+                target("**/*.xml")
+                targetExclude("**/build/**/*.xml")
+                // Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml)
+                licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[^!?])")
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000..c7415dcf8
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,154 @@
+[versions]
+accompanist = "0.28.0"
+annotation = "1.5.0"
+androidDesugarJdkLibs = "1.2.2"
+androidGradlePlugin = "7.4.0"
+androidxActivity = "1.6.1"
+androidxAppCompat = "1.5.1"
+androidxArchCore = "2.1.0"
+androidxBrowser = "1.4.0"
+androidxComposeBom = "2023.01.00"
+androidxComposeCompiler = "1.4.3"
+androidxCompose = "1.2.0"
+androidxComposeRuntimeTracing = "1.0.0-alpha01"
+androidxCore = "1.9.0"
+androidxCoreSplashscreen = "1.0.0"
+androidxDataStore = "1.0.0"
+androidxEspresso = "3.5.0"
+androidxHiltNavigationCompose = "1.0.0"
+androidxLifecycle = "2.6.0-alpha03"
+androidxMacroBenchmark = "1.1.1"
+androidxMetrics = "1.0.0-alpha03"
+androidxNavigation = "2.5.3"
+androidxProfileinstaller = "1.2.1"
+androidxStartup = "1.1.1"
+androidxTestCore = "1.5.0"
+androidxTestExt = "1.1.4"
+androidxTestRules = "1.5.0"
+androidxTestRunner = "1.5.1"
+androidxTracing = "1.1.0"
+androidxUiAutomator = "2.2.0"
+androidxWindowManager = "1.0.0"
+androidxWork = "2.7.1"
+coil = "2.2.2"
+# @keep
+compileSdk = "33"
+hamcrest = "1.3"
+hilt = "2.44.2"
+hiltExt = "1.0.0"
+jacoco = "0.8.7"
+junit4 = "4.13.2"
+kotlin = "1.8.10"
+kotlinxCoroutines = "1.6.4"
+kotlinxDatetime = "0.4.0"
+kotlinxSerializationJson = "1.5.0"
+ksp = "1.8.10-1.0.9"
+lint = "30.3.1"
+# @keep
+minSdk = "21"
+okhttp = "4.10.0"
+protobuf = "3.21.12"
+protobufPlugin = "0.8.19"
+retrofit = "2.9.0"
+retrofitKotlinxSerializationJson = "0.8.0"
+room = "2.5.0-rc01"
+spotless = "5.12.5"
+timber = "5.0.1"
+# @keep
+targetSdk = "33"
+truth = "1.1.2"
+turbine = "0.12.1"
+
+[libraries]
+accompanist-appcompat-theme = { group = "com.google.accompanist", name = "accompanist-appcompat-theme", version.ref = "accompanist" }
+accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" }
+accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version.ref = "accompanist" }
+accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" }
+accompanist-testharness = { group = "com.google.accompanist", name = "accompanist-testharness", version.ref = "accompanist" }
+android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
+androidx-archcore-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidxArchCore" }
+androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
+androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
+androidx-compose-compiler = { group = "androidx.compose.compiler", name = "compiler", version.ref = "androidxComposeCompiler" }
+androidx-compose-foundation-core = { group = "androidx.compose.foundation", name = "foundation" }
+androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" }
+androidx-compose-material-core = { group = "androidx.compose.material", name = "material" }
+androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class" }
+androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
+androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
+androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidxComposeRuntimeTracing" }
+androidx-compose-ui-test-junit = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-compose-ui-tooling-core = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" }
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
+androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" }
+androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" }
+androidx-dataStore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDataStore" }
+androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }
+androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidxLifecycle" }
+androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
+androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
+androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
+androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" }
+androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" }
+androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidxStartup" }
+androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "androidxTestCore" }
+androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" }
+androidx-test-espresso-contrib = { group = "androidx.test.espresso", name = "espresso-contrib", version.ref = "androidxEspresso" }
+androidx-test-espresso-intents = { group = "androidx.test.espresso", name = "espresso-intents", version.ref = "androidxEspresso" }
+androidx-test-espresso-idling-resources = { group = "androidx.test.espresso", name = "espresso-idling-resource", version.ref = "androidxEspresso" }
+androidx-test-espresso-idling-concurrent = { group = "androidx.test.espresso.idling", name = "idling-concurrent", version.ref = "androidxEspresso" }
+androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExt" }
+androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
+androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" }
+androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" }
+androidx-tracing-ktx = { group = "androidx.tracing", name="tracing-ktx", version.ref = "androidxTracing" }
+androidx-window-manager = { module = "androidx.window:window", version.ref = "androidxWindowManager" }
+androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" }
+androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" }
+coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
+coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
+coil-kt-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
+google-truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
+hamcrest = { group = "org.hamcrest", name = "hamcrest-all", version.ref = "hamcrest" }
+hilt-android-core = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
+hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
+hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
+hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" }
+hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltExt" }
+junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
+kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" }
+kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
+kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
+kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
+kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
+lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" }
+room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
+room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
+room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
+room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
+timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
+
+# Dependencies of the included build-logic
+android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
+kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
+android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
+android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
+hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..943f0cbfa
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..8049c684f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 000000000..65dcd68d6
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,244 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045 
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045 
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..6689b85be
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 000000000..5024bfc04
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,33 @@
+{
+  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+  "extends": [
+    "config:base",
+    "group:all",
+    ":dependencyDashboard",
+    "schedule:daily"
+  ],
+  "baseBranches": [
+    "main"
+  ],
+  "commitMessageExtra": "{{{currentValue}}} to {{#if isPinDigest}}{{{newDigestShort}}}{{else}}{{#if isMajor}}{{prettyNewMajor}}{{else}}{{#if isSingleVersion}}{{prettyNewVersion}}{{else}}{{#if newValue}}{{{newValue}}}{{else}}{{{newDigestShort}}}{{/if}}{{/if}}{{/if}}{{/if}}",
+  "packageRules": [
+    {
+      "matchPackagePatterns": [
+        "androidx.compose.compiler:compiler"
+      ],
+      "groupName": "kotlin"
+    },
+    {
+      "matchPackagePatterns": [
+        "org.jetbrains.kotlin.*"
+      ],
+      "groupName": "kotlin"
+    },
+    {
+      "matchPackagePatterns": [
+        "com.google.devtools.ksp"
+      ],
+      "groupName": "kotlin"
+    }
+  ]
+}
diff --git a/screenshots/screenshots.png b/screenshots/screenshots.png
new file mode 100644
index 000000000..3eb72ad9b
Binary files /dev/null and b/screenshots/screenshots.png differ
diff --git a/screenshots/todoapp.gif b/screenshots/todoapp.gif
new file mode 100644
index 000000000..20e15ff81
Binary files /dev/null and b/screenshots/todoapp.gif differ
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 000000000..24d1e04d2
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+pluginManagement {
+    repositories {
+        google()
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+include(":app")
+include(":shared-test")
diff --git a/shared-test/.gitignore b/shared-test/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/shared-test/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/shared-test/build.gradle.kts b/shared-test/build.gradle.kts
new file mode 100644
index 000000000..2060fc01b
--- /dev/null
+++ b/shared-test/build.gradle.kts
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+plugins {
+    alias(libs.plugins.android.library)
+    alias(libs.plugins.kotlin.android)
+    alias(libs.plugins.kapt)
+    alias(libs.plugins.ksp)
+    alias(libs.plugins.hilt)
+}
+
+android {
+    namespace = "com.example.android.architecture.blueprints.todoapp.shared.test"
+    compileSdk = libs.versions.compileSdk.get().toInt()
+    defaultConfig {
+        minSdk = libs.versions.minSdk.get().toInt()
+    }
+}
+
+dependencies {
+    implementation(project(":app"))
+    implementation(libs.kotlinx.coroutines.android)
+    implementation(libs.kotlinx.coroutines.test)
+    implementation(libs.junit4)
+    implementation(libs.androidx.test.core.ktx)
+    implementation(libs.androidx.test.ext)
+    implementation(libs.androidx.test.rules)
+    implementation(libs.hilt.android.core)
+    implementation(libs.hilt.android.testing)
+    kapt(libs.hilt.compiler)
+
+    // Room
+    implementation(libs.room.runtime)
+    implementation(libs.room.ktx)
+    ksp(libs.room.compiler)
+}
\ No newline at end of file
diff --git a/shared-test/src/main/AndroidManifest.xml b/shared-test/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..312b22fd3
--- /dev/null
+++ b/shared-test/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest />
diff --git a/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/CustomTestRunner.kt b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/CustomTestRunner.kt
new file mode 100644
index 000000000..26222729b
--- /dev/null
+++ b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/CustomTestRunner.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+import dagger.hilt.android.testing.HiltTestApplication
+
+class CustomTestRunner : AndroidJUnitRunner() {
+    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
+        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
+    }
+}
diff --git a/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/MainCoroutineRule.kt b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/MainCoroutineRule.kt
new file mode 100644
index 000000000..d76b3c4e4
--- /dev/null
+++ b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/MainCoroutineRule.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Sets the main coroutines dispatcher to a [TestDispatcher] for unit testing.
+ *
+ * Declare it as a JUnit Rule:
+ *
+ * ```
+ * @get:Rule
+ * val mainCoroutineRule = MainCoroutineRule()
+ * ```
+ *
+ * Then, use `runTest` to execute your tests.
+ */
+@ExperimentalCoroutinesApi
+class MainCoroutineRule(
+    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
+) : TestWatcher() {
+
+    override fun starting(description: Description?) {
+        super.starting(description)
+        Dispatchers.setMain(testDispatcher)
+    }
+
+    override fun finished(description: Description?) {
+        super.finished(description)
+        Dispatchers.resetMain()
+    }
+}
diff --git a/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/FakeTaskRepository.kt b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/FakeTaskRepository.kt
new file mode 100644
index 000000000..20056ff7d
--- /dev/null
+++ b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/FakeTaskRepository.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data
+
+import androidx.annotation.VisibleForTesting
+import java.util.UUID
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+
+/**
+ * Implementation of a tasks repository with static access to the data for easy testing.
+ */
+class FakeTaskRepository : TaskRepository {
+
+    private var shouldThrowError = false
+
+    private val _savedTasks = MutableStateFlow(LinkedHashMap<String, Task>())
+    val savedTasks: StateFlow<LinkedHashMap<String, Task>> = _savedTasks.asStateFlow()
+
+    private val observableTasks: Flow<List<Task>> = savedTasks.map {
+        if (shouldThrowError) {
+            throw Exception("Test exception")
+        } else {
+            it.values.toList()
+        }
+    }
+
+    fun setShouldThrowError(value: Boolean) {
+        shouldThrowError = value
+    }
+
+    override suspend fun refresh() {
+        // Tasks already refreshed
+    }
+
+    override suspend fun refreshTask(taskId: String) {
+        refresh()
+    }
+
+    override suspend fun createTask(title: String, description: String): String {
+        val taskId = generateTaskId()
+        Task(title = title, description = description, id = taskId).also {
+            saveTask(it)
+        }
+        return taskId
+    }
+
+    override fun getTasksStream(): Flow<List<Task>> = observableTasks
+
+    override fun getTaskStream(taskId: String): Flow<Task?> {
+        return observableTasks.map { tasks ->
+            return@map tasks.firstOrNull { it.id == taskId }
+        }
+    }
+
+    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Task? {
+        if (shouldThrowError) {
+            throw Exception("Test exception")
+        }
+        return savedTasks.value[taskId]
+    }
+
+    override suspend fun getTasks(forceUpdate: Boolean): List<Task> {
+        if (shouldThrowError) {
+            throw Exception("Test exception")
+        }
+        return observableTasks.first()
+    }
+
+    override suspend fun updateTask(taskId: String, title: String, description: String) {
+        val updatedTask = _savedTasks.value[taskId]?.copy(
+            title = title,
+            description = description
+        ) ?: throw Exception("Task (id $taskId) not found")
+
+        saveTask(updatedTask)
+    }
+
+    private fun saveTask(task: Task) {
+        _savedTasks.update { tasks ->
+            val newTasks = LinkedHashMap<String, Task>(tasks)
+            newTasks[task.id] = task
+            newTasks
+        }
+    }
+
+    override suspend fun completeTask(taskId: String) {
+        _savedTasks.value[taskId]?.let {
+            saveTask(it.copy(isCompleted = true))
+        }
+    }
+
+    override suspend fun activateTask(taskId: String) {
+        _savedTasks.value[taskId]?.let {
+            saveTask(it.copy(isCompleted = false))
+        }
+    }
+
+    override suspend fun clearCompletedTasks() {
+        _savedTasks.update { tasks ->
+            tasks.filterValues {
+                !it.isCompleted
+            } as LinkedHashMap<String, Task>
+        }
+    }
+
+    override suspend fun deleteTask(taskId: String) {
+        _savedTasks.update { tasks ->
+            val newTasks = LinkedHashMap<String, Task>(tasks)
+            newTasks.remove(taskId)
+            newTasks
+        }
+    }
+
+    override suspend fun deleteAllTasks() {
+        _savedTasks.update {
+            LinkedHashMap()
+        }
+    }
+
+    private fun generateTaskId() = UUID.randomUUID().toString()
+
+    @VisibleForTesting
+    fun addTasks(vararg tasks: Task) {
+        _savedTasks.update { oldTasks ->
+            val newTasks = LinkedHashMap<String, Task>(oldTasks)
+            for (task in tasks) {
+                newTasks[task.id] = task
+            }
+            newTasks
+        }
+    }
+}
diff --git a/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/FakeTaskDao.kt b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/FakeTaskDao.kt
new file mode 100644
index 000000000..a916afd88
--- /dev/null
+++ b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/FakeTaskDao.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data.source.local
+
+import kotlinx.coroutines.flow.Flow
+
+class FakeTaskDao(initialTasks: List<LocalTask>? = emptyList()) : TaskDao {
+
+    private var _tasks: MutableMap<String, LocalTask>? = null
+
+    var tasks: List<LocalTask>?
+        get() = _tasks?.values?.toList()
+        set(newTasks) {
+            _tasks = newTasks?.associateBy { it.id }?.toMutableMap()
+        }
+
+    init {
+        tasks = initialTasks
+    }
+
+    override suspend fun getAll() = tasks ?: throw Exception("Task list is null")
+
+    override suspend fun getById(taskId: String): LocalTask? = _tasks?.get(taskId)
+
+    override suspend fun upsertAll(tasks: List<LocalTask>) {
+        _tasks?.putAll(tasks.associateBy { it.id })
+    }
+
+    override suspend fun upsert(task: LocalTask) {
+        _tasks?.put(task.id, task)
+    }
+
+    override suspend fun updateCompleted(taskId: String, completed: Boolean) {
+        _tasks?.get(taskId)?.let { it.isCompleted = completed }
+    }
+
+    override suspend fun deleteAll() {
+        _tasks?.clear()
+    }
+
+    override suspend fun deleteById(taskId: String): Int {
+        return if (_tasks?.remove(taskId) == null) {
+            0
+        } else {
+            1
+        }
+    }
+
+    override suspend fun deleteCompleted(): Int {
+        _tasks?.apply {
+            val originalSize = size
+            entries.removeIf { it.value.isCompleted }
+            return originalSize - size
+        }
+        return 0
+    }
+
+    override fun observeAll(): Flow<List<LocalTask>> {
+        TODO("Not implemented")
+    }
+
+    override fun observeById(taskId: String): Flow<LocalTask> {
+        TODO("Not implemented")
+    }
+}
diff --git a/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/FakeNetworkDataSource.kt b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/FakeNetworkDataSource.kt
new file mode 100644
index 000000000..eb007ecd3
--- /dev/null
+++ b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/FakeNetworkDataSource.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.data.source.network
+
+class FakeNetworkDataSource(
+    var tasks: MutableList<NetworkTask>? = mutableListOf()
+) : NetworkDataSource {
+    override suspend fun loadTasks() = tasks ?: throw Exception("Task list is null")
+
+    override suspend fun saveTasks(tasks: List<NetworkTask>) {
+        this.tasks = tasks.toMutableList()
+    }
+}
diff --git a/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DatabaseTestModule.kt b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DatabaseTestModule.kt
new file mode 100644
index 000000000..b9d4f04d2
--- /dev/null
+++ b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DatabaseTestModule.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.di
+
+import android.content.Context
+import androidx.room.Room
+import com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import dagger.hilt.testing.TestInstallIn
+import javax.inject.Singleton
+
+@Module
+@TestInstallIn(
+    components = [SingletonComponent::class],
+    replaces = [DatabaseModule::class]
+)
+object DatabaseTestModule {
+
+    @Singleton
+    @Provides
+    fun provideDataBase(@ApplicationContext context: Context): ToDoDatabase {
+        return Room
+            .inMemoryDatabaseBuilder(context.applicationContext, ToDoDatabase::class.java)
+            .allowMainThreadQueries()
+            .build()
+    }
+}
diff --git a/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/RepositoryTestModule.kt b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/RepositoryTestModule.kt
new file mode 100644
index 000000000..3d1448380
--- /dev/null
+++ b/shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/RepositoryTestModule.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.architecture.blueprints.todoapp.di
+
+import com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository
+import com.example.android.architecture.blueprints.todoapp.data.TaskRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.components.SingletonComponent
+import dagger.hilt.testing.TestInstallIn
+import javax.inject.Singleton
+
+@Module
+@TestInstallIn(
+    components = [SingletonComponent::class],
+    replaces = [RepositoryModule::class]
+)
+object RepositoryTestModule {
+
+    @Singleton
+    @Provides
+    fun provideTasksRepository(): TaskRepository {
+        return FakeTaskRepository()
+    }
+}
diff --git a/spotless/copyright.kt b/spotless/copyright.kt
new file mode 100644
index 000000000..806db0fb5
--- /dev/null
+++ b/spotless/copyright.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright $YEAR The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
diff --git a/spotless/copyright.kts b/spotless/copyright.kts
new file mode 100644
index 000000000..8c5d92cc5
--- /dev/null
+++ b/spotless/copyright.kts
@@ -0,0 +1,15 @@
+/*
+ * Copyright $YEAR The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
diff --git a/spotless/copyright.xml b/spotless/copyright.xml
new file mode 100644
index 000000000..3085c43e7
--- /dev/null
+++ b/spotless/copyright.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright $YEAR The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
