├── .circleci └── config.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── proguardTest-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── android │ │ └── architecture │ │ └── blueprints │ │ └── todoapp │ │ ├── TestUtils.kt │ │ ├── custom │ │ └── action │ │ │ └── NavigationViewActions.kt │ │ ├── data │ │ └── TasksLocalDataSourceTest.kt │ │ └── tasks │ │ ├── AppNavigationTest.kt │ │ └── TasksScreenTest.kt │ ├── androidTestMock │ └── java │ │ └── com │ │ └── example │ │ └── android │ │ └── architecture │ │ └── blueprints │ │ └── todoapp │ │ ├── addedittask │ │ └── AddEditTaskScreenTest.kt │ │ ├── statistics │ │ └── StatisticsScreenTest.kt │ │ └── taskdetail │ │ └── TaskDetailScreenTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── architecture │ │ │ └── blueprints │ │ │ └── todoapp │ │ │ ├── addedittask │ │ │ ├── AddEditTaskAction.kt │ │ │ ├── AddEditTaskActionProcessorHolder.kt │ │ │ ├── AddEditTaskActivity.kt │ │ │ ├── AddEditTaskFragment.kt │ │ │ ├── AddEditTaskIntent.kt │ │ │ ├── AddEditTaskResult.kt │ │ │ ├── AddEditTaskViewModel.kt │ │ │ └── AddEditTaskViewState.kt │ │ │ ├── data │ │ │ ├── Task.kt │ │ │ └── source │ │ │ │ ├── TasksDataSource.kt │ │ │ │ ├── TasksRepository.kt │ │ │ │ ├── local │ │ │ │ ├── TasksDbHelper.kt │ │ │ │ ├── TasksLocalDataSource.kt │ │ │ │ └── TasksPersistenceContract.kt │ │ │ │ └── remote │ │ │ │ └── TasksRemoteDataSource.kt │ │ │ ├── mvibase │ │ │ ├── MviAction.kt │ │ │ ├── MviIntent.kt │ │ │ ├── MviResult.kt │ │ │ ├── MviView.kt │ │ │ ├── MviViewModel.kt │ │ │ └── MviViewState.kt │ │ │ ├── statistics │ │ │ ├── StatisticsAction.kt │ │ │ ├── StatisticsActionProcessorHolder.kt │ │ │ ├── StatisticsActivity.kt │ │ │ ├── StatisticsFragment.kt │ │ │ ├── StatisticsIntent.kt │ │ │ ├── StatisticsResult.kt │ │ │ ├── StatisticsViewModel.kt │ │ │ └── StatisticsViewState.kt │ │ │ ├── taskdetail │ │ │ ├── TaskDetailAction.kt │ │ │ ├── TaskDetailActionProcessorHolder.kt │ │ │ ├── TaskDetailActivity.kt │ │ │ ├── TaskDetailFragment.kt │ │ │ ├── TaskDetailIntent.kt │ │ │ ├── TaskDetailResult.kt │ │ │ ├── TaskDetailViewModel.kt │ │ │ └── TaskDetailViewState.kt │ │ │ ├── tasks │ │ │ ├── ScrollChildSwipeRefreshLayout.kt │ │ │ ├── TasksAction.kt │ │ │ ├── TasksActionProcessorHolder.kt │ │ │ ├── TasksActivity.kt │ │ │ ├── TasksAdapter.kt │ │ │ ├── TasksFilterType.kt │ │ │ ├── TasksFragment.kt │ │ │ ├── TasksIntent.kt │ │ │ ├── TasksResult.kt │ │ │ ├── TasksViewModel.kt │ │ │ └── TasksViewState.kt │ │ │ └── util │ │ │ ├── ActivityUtils.kt │ │ │ ├── ObservableUtils.kt │ │ │ ├── RxExt.kt │ │ │ ├── SingletonHolderDoubleArg.kt │ │ │ ├── SingletonHolderSingleArg.kt │ │ │ ├── StringExt.kt │ │ │ ├── ToDoViewModelFactory.kt │ │ │ └── schedulers │ │ │ ├── BaseSchedulerProvider.kt │ │ │ ├── ImmediateSchedulerProvider.kt │ │ │ └── SchedulerProvider.kt │ └── res │ │ ├── drawable-hdpi │ │ └── logo.png │ │ ├── drawable-mdpi │ │ └── logo.png │ │ ├── drawable-xhdpi │ │ └── logo.png │ │ ├── drawable-xxhdpi │ │ └── logo.png │ │ ├── drawable-xxxhdpi │ │ └── logo.png │ │ ├── drawable │ │ ├── ic_add.xml │ │ ├── ic_assignment_turned_in_24dp.xml │ │ ├── ic_check_circle_24dp.xml │ │ ├── ic_done.xml │ │ ├── ic_edit.xml │ │ ├── ic_filter_list.xml │ │ ├── ic_list.xml │ │ ├── ic_menu.xml │ │ ├── ic_statistics.xml │ │ ├── ic_statistics_100dp.xml │ │ ├── ic_statistics_24dp.xml │ │ ├── ic_verified_user_24dp.xml │ │ ├── list_completed_touch_feedback.xml │ │ └── touch_feedback.xml │ │ ├── layout │ │ ├── addtask_act.xml │ │ ├── addtask_frag.xml │ │ ├── nav_header.xml │ │ ├── statistics_act.xml │ │ ├── statistics_frag.xml │ │ ├── task_item.xml │ │ ├── taskdetail_act.xml │ │ ├── taskdetail_frag.xml │ │ ├── tasks_act.xml │ │ └── tasks_frag.xml │ │ ├── menu │ │ ├── drawer_actions.xml │ │ ├── filter_tasks.xml │ │ ├── taskdetail_fragment_menu.xml │ │ └── tasks_fragment_menu.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── mock │ └── java │ │ └── com │ │ └── example │ │ └── android │ │ └── architecture │ │ └── blueprints │ │ └── todoapp │ │ ├── Injection.kt │ │ └── data │ │ └── FakeTasksRemoteDataSource.kt │ ├── prod │ └── java │ │ └── com │ │ └── example │ │ └── android │ │ └── architecture │ │ └── blueprints │ │ └── todoapp │ │ └── Injection.kt │ └── test │ └── java │ └── com │ └── example │ └── android │ └── architecture │ └── blueprints │ └── todoapp │ ├── addedittask │ └── AddEditTaskViewModelTest.kt │ ├── data │ └── source │ │ └── TasksRepositoryTest.kt │ ├── statistics │ └── StatisticsViewModelTest.kt │ ├── taskdetail │ └── TaskDetailViewModelTest.kt │ └── tasks │ └── TasksViewModelTest.kt ├── art ├── MVI_detail.png └── MVI_global.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── signing └── firebase_service-account.aes /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | docker: 5 | - image: circleci/android:api-26-alpha 6 | working_directory: ~/todoapp 7 | environment: 8 | GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx3200m -XX:+HeapDumpOnOutOfMemoryError"' 9 | resource_class: large 10 | 11 | jobs: 12 | build: 13 | <<: *defaults 14 | steps: 15 | - checkout 16 | - restore_cache: 17 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 18 | - run: 19 | name: Download Dependencies 20 | command: ./gradlew dependencies 21 | - run: 22 | name: Check APKs 23 | command: ./gradlew clean check 24 | - save_cache: 25 | paths: 26 | - ~/.gradle 27 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 28 | - store_artifacts: 29 | path: app/build/reports 30 | destination: reports 31 | - store_test_results: 32 | path: app/build/test-results 33 | - persist_to_workspace: 34 | root: . 35 | paths: 36 | - .gradle 37 | - build 38 | - app/build 39 | firebase_test_lab: 40 | <<: *defaults 41 | steps: 42 | - checkout 43 | - restore_cache: 44 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 45 | - attach_workspace: 46 | at: ~/todoapp 47 | - run: 48 | name: Decrypt Firebase's Service Account 49 | command: openssl enc -aes-256-cbc -d -in signing/firebase_service-account.aes -out signing/firebase_service-account.json -k $ENCRYPT_KEY 50 | - run: 51 | name: Assemble APKs for tests 52 | command: ./gradlew :app:assembleDebug :app:assembleAndroidTest -PdisablePreDex 53 | - run: 54 | name: Set GCloud 55 | command: | 56 | gcloud config set project android-architecture-mvi 57 | gcloud auth activate-service-account ci-service-account@android-architecture-mvi.iam.gserviceaccount.com \ 58 | --key-file ./signing/firebase_service-account.json 59 | - run: 60 | name: Tests on Firebase Test Lab 61 | command: | 62 | echo y | sudo pip uninstall crcmod 63 | sudo pip install -U crcmod 64 | export _GCLOUD_LOG=_gcloud.log 65 | export _DEVICES=${TESTLAB_DEVICES:-Nexus5X} 66 | export _APIS=${TESTLAB_APIS:-26} 67 | export _LOCALES=${TESTLAB_LOCALES:-fr} 68 | if gcloud firebase test android run \ 69 | --type instrumentation \ 70 | --app app/build/outputs/apk/mock/debug/app-mock-debug.apk \ 71 | --test app/build/outputs/apk/androidTest/mock/debug/app-mock-debug-androidTest.apk \ 72 | --device-ids $_DEVICES \ 73 | --os-version-ids $_APIS \ 74 | --locales $_LOCALES \ 75 | --orientations portrait \ 76 | 2>&1 | tee $_GCLOUD_LOG 77 | then 78 | echo "Test matrix successfully finished" 79 | FIREBASE_SUCCESS=true 80 | else 81 | FIREBASE_SUCCESS=$? 82 | echo "Test matrix exited abnormally with non-zero exit code: " $FIREBASE_SUCCESS 83 | fi 84 | export _BUCKET_ID=`cat $_GCLOUD_LOG | sed -n -E 's#^.+test-lab-(.+)/.+#\1#p'` 85 | echo "Bucket ID: " $_BUCKET_ID 86 | mkdir -p firebaseTestLab 87 | gsutil -m cp -r gs://test-lab-$_BUCKET_ID firebaseTestLab/ 88 | test $FIREBASE_SUCCESS = true || exit $FIREBASE_SUCCESS 89 | - store_artifacts: 90 | path: firebaseTestLab 91 | destination: firebaseTestLab 92 | - store_artifacts: 93 | path: app/build/outputs/apk 94 | destination: apks 95 | 96 | workflows: 97 | version: 2 98 | build_and_test: 99 | jobs: 100 | - build 101 | - firebase_test_lab: 102 | requires: 103 | - build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | local.properties 4 | build 5 | .gradle 6 | # Eclipse project files 7 | .project 8 | .settings/ 9 | .classpath 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | 8 | ### Before you contribute 9 | Before we can use your code, you must sign the 10 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 11 | (CLA), which you can do online. The CLA is necessary mainly because you own the 12 | copyright to your changes, even after your contribution becomes part of our 13 | codebase, so we need your permission to use and distribute your code. We also 14 | need to be sure of various other things—for instance that you'll tell us if you 15 | know that your code infringes on other people's patents. You don't have to sign 16 | the CLA until after you've submitted your code for review and a member has 17 | approved it, but you must do it before we can put your code into our codebase. 18 | Before you start working on a larger contribution, you should get in touch with 19 | us first through the issue tracker with your idea so that we can help out and 20 | possibly guide you. Coordinating up front makes it much easier to avoid 21 | frustration later on. 22 | 23 | ### Code reviews 24 | All submissions, including submissions by project members, require review. We 25 | use Github pull requests for this purpose. 26 | 27 | ### The small print 28 | Contributions made by corporations are covered by a different agreement than 29 | the one above, the 30 | [Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). 31 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android-extensions' 3 | apply plugin: 'org.jetbrains.kotlin.android' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion rootProject.ext.compileSdkVersion 8 | buildToolsVersion rootProject.ext.buildToolsVersion 9 | 10 | defaultConfig { 11 | applicationId "com.example.android.architecture.blueprints.todomvp" 12 | minSdkVersion rootProject.ext.minSdkVersion 13 | targetSdkVersion rootProject.ext.targetSdkVersion 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' 18 | } 19 | 20 | compileOptions { 21 | sourceCompatibility JavaVersion.VERSION_1_8 22 | targetCompatibility JavaVersion.VERSION_1_8 23 | } 24 | 25 | buildTypes { 26 | debug { 27 | // Minifying the variant used for tests is not supported when using Jack. 28 | minifyEnabled false 29 | // Uses new built-in shrinker http://tools.android.com/tech-docs/new-build-system/built-in-shrinker 30 | useProguard false 31 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 32 | testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguardTest-rules.pro' 33 | } 34 | 35 | release { 36 | minifyEnabled true 37 | useProguard true 38 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 39 | testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguardTest-rules.pro' 40 | } 41 | } 42 | 43 | flavorDimensions "default" 44 | 45 | // If you need to add more flavors, consider using flavor dimensions. 46 | productFlavors { 47 | mock { 48 | applicationIdSuffix = ".mock" 49 | } 50 | prod {} 51 | } 52 | 53 | // Remove mockRelease as it's not needed. 54 | android.variantFilter { variant -> 55 | if (variant.buildType.name == 'release' && variant.getFlavors().get(0).name == 'mock') { 56 | variant.setIgnore(true) 57 | } 58 | } 59 | 60 | lintOptions { 61 | warning 'InvalidPackage' // prevent error from references of non-Android package 62 | } 63 | 64 | // Always show the result of every unit test, even if it passes. 65 | testOptions.unitTests.all { 66 | testLogging { 67 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 68 | } 69 | } 70 | } 71 | 72 | /* 73 | Dependency versions are defined in the top level build.gradle file. This helps keeping track of 74 | all versions in a single place. This improves readability and helps managing project complexity. 75 | */ 76 | dependencies { 77 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 78 | // App's dependencies, including test 79 | implementation "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion" 80 | implementation "com.android.support:cardview-v7:$rootProject.supportLibraryVersion" 81 | implementation "com.android.support:design:$rootProject.supportLibraryVersion" 82 | implementation "com.android.support:recyclerview-v7:$rootProject.supportLibraryVersion" 83 | implementation "com.android.support:support-v4:$rootProject.supportLibraryVersion" 84 | implementation "com.android.support.test.espresso:espresso-idling-resource:$rootProject.espressoVersion" 85 | implementation "io.reactivex.rxjava2:rxjava:$rootProject.rxjavaVersion" 86 | implementation "io.reactivex.rxjava2:rxandroid:$rootProject.rxandroidVersion" 87 | implementation "com.squareup.sqlbrite2:sqlbrite:$rootProject.sqlbriteVersion" 88 | implementation "com.jakewharton.rxbinding2:rxbinding-support-v4:$rootProject.rxBindingVersion" 89 | implementation "android.arch.lifecycle:runtime:$rootProject.archComponentsVersion" 90 | implementation "android.arch.lifecycle:extensions:$rootProject.archComponentsVersion" 91 | implementation "io.reactivex.rxjava2:rxkotlin:$rootProject.rxKotlinVersion" 92 | kapt "android.arch.lifecycle:compiler:$rootProject.archComponentsVersion" 93 | 94 | // Dependencies for local unit tests 95 | testImplementation "com.nhaarman:mockito-kotlin:$rootProject.mockitoKotlinVersion" 96 | testImplementation "junit:junit:$rootProject.junitVersion" 97 | testImplementation "org.mockito:mockito-all:$rootProject.mockitoVersion" 98 | testImplementation "org.hamcrest:hamcrest-all:$rootProject.hamcrestVersion" 99 | 100 | // Android Testing Support Library's runner and rules 101 | androidTestImplementation "com.android.support.test:runner:$rootProject.runnerVersion" 102 | androidTestImplementation "com.android.support.test:rules:$rootProject.runnerVersion" 103 | 104 | // Dependencies for Android unit tests 105 | androidTestImplementation "junit:junit:$rootProject.junitVersion" 106 | androidTestImplementation "org.mockito:mockito-core:$rootProject.mockitoVersion" 107 | androidTestImplementation 'com.google.dexmaker:dexmaker:1.2' 108 | androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2' 109 | 110 | // Espresso UI Testing 111 | androidTestImplementation "com.android.support.test.espresso:espresso-core:$rootProject.espressoVersion" 112 | androidTestImplementation "com.android.support.test.espresso:espresso-contrib:$rootProject.espressoVersion" 113 | androidTestImplementation "com.android.support.test.espresso:espresso-intents:$rootProject.espressoVersion" 114 | 115 | // Resolve conflicts between main and test APK: 116 | androidTestImplementation "com.android.support:support-annotations:$rootProject.supportLibraryVersion" 117 | androidTestImplementation "com.android.support:support-v4:$rootProject.supportLibraryVersion" 118 | androidTestImplementation "com.android.support:recyclerview-v7:$rootProject.supportLibraryVersion" 119 | androidTestImplementation "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion" 120 | androidTestImplementation "com.android.support:design:$rootProject.supportLibraryVersion" 121 | } 122 | 123 | /* 124 | Resolves dependency versions across test and production APKs, specifically, transitive 125 | dependencies. This is required since Espresso internally has a dependency on support-annotations. 126 | */ 127 | configurations.all { 128 | // resolutionStrategy.force "com.android.support:support-annotations:$rootProject.ext.lib.supportLibraryVersion" 129 | resolutionStrategy.force "com.google.code.findbugs:jsr305:$rootProject.jsr305Version" 130 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Some methods are only called from tests, so make sure the shrinker keeps them. 2 | -keep class com.example.android.architecture.blueprints.** { *; } 3 | 4 | -keep class android.support.v4.widget.DrawerLayout { *; } 5 | -keep class android.support.test.espresso.IdlingResource { *; } 6 | -keep class com.google.common.base.Preconditions { *; } 7 | 8 | # For Guava: 9 | -dontwarn javax.annotation.** 10 | -dontwarn javax.inject.** 11 | -dontwarn sun.misc.Unsafe 12 | 13 | # Proguard rules that are applied to your test apk/code. 14 | -ignorewarnings 15 | 16 | -keepattributes *Annotation* 17 | 18 | -dontnote junit.framework.** 19 | -dontnote junit.runner.** 20 | 21 | -dontwarn android.test.** 22 | -dontwarn android.support.test.** 23 | -dontwarn org.junit.** 24 | -dontwarn org.hamcrest.** 25 | -dontwarn com.squareup.javawriter.JavaWriter 26 | # Uncomment this if you use Mockito 27 | -dontwarn org.mockito.** 28 | 29 | # rxjava 30 | -keep class rx.observers.TestSubscriber { 31 | public ; 32 | } -------------------------------------------------------------------------------- /app/proguardTest-rules.pro: -------------------------------------------------------------------------------- 1 | # Proguard rules that are applied to your test apk/code. 2 | -ignorewarnings 3 | 4 | -keepattributes *Annotation* 5 | 6 | -dontnote junit.framework.** 7 | -dontnote junit.runner.** 8 | 9 | -dontwarn android.test.** 10 | -dontwarn android.support.test.** 11 | -dontwarn org.junit.** 12 | -dontwarn org.hamcrest.** 13 | -dontwarn com.squareup.javawriter.JavaWriter 14 | # Uncomment this if you use Mockito 15 | -dontwarn org.mockito.** -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp 18 | 19 | import android.app.Activity 20 | import android.content.pm.ActivityInfo 21 | import android.content.res.Configuration 22 | import android.support.annotation.IdRes 23 | import android.support.test.InstrumentationRegistry.getInstrumentation 24 | import android.support.test.runner.lifecycle.ActivityLifecycleMonitor 25 | import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry 26 | import android.support.test.runner.lifecycle.Stage.RESUMED 27 | import android.support.v7.widget.Toolbar 28 | 29 | /** 30 | * Useful test methods common to all activities 31 | */ 32 | 33 | /** 34 | * Gets an Activity in the RESUMED stage. 35 | * 36 | * 37 | * This method should never be called from the Main thread. In certain situations there might 38 | * be more than one Activities in RESUMED stage, but only one is returned. 39 | * See [ActivityLifecycleMonitor]. 40 | */ 41 | // The array is just to wrap the Activity and be able to access it from the Runnable. 42 | fun getCurrentActivity(): Activity { 43 | val resumedActivity = arrayOfNulls(1) 44 | 45 | getInstrumentation().runOnMainSync { 46 | val resumedActivities = 47 | ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(RESUMED) 48 | 49 | if (resumedActivities.iterator().hasNext()) { 50 | resumedActivity[0] = resumedActivities.iterator().next() as Activity 51 | } else { 52 | throw IllegalStateException("No Activity in stage RESUMED") 53 | } 54 | } 55 | return resumedActivity[0]!! 56 | } 57 | 58 | private fun rotateToLandscape(activity: Activity) { 59 | activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE 60 | } 61 | 62 | private fun rotateToPortrait(activity: Activity) { 63 | activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 64 | } 65 | 66 | fun rotateOrientation(activity: Activity) { 67 | val currentOrientation = activity.resources.configuration.orientation 68 | 69 | when (currentOrientation) { 70 | Configuration.ORIENTATION_LANDSCAPE -> rotateToPortrait(activity) 71 | Configuration.ORIENTATION_PORTRAIT -> rotateToLandscape(activity) 72 | else -> rotateToLandscape(activity) 73 | } 74 | } 75 | 76 | /** 77 | * Returns the content description for the navigation button view in the toolbar. 78 | */ 79 | fun getToolbarNavigationContentDescription( 80 | activity: Activity, 81 | @IdRes toolbar1: Int 82 | ): String { 83 | return activity.findViewById(toolbar1).navigationContentDescription!!.toString() 84 | } 85 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/custom/action/NavigationViewActions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.custom.action 18 | 19 | import android.content.res.Resources.NotFoundException 20 | import android.support.design.widget.NavigationView 21 | import android.support.test.espresso.PerformException 22 | import android.support.test.espresso.UiController 23 | import android.support.test.espresso.ViewAction 24 | import android.support.test.espresso.matcher.ViewMatchers 25 | import android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom 26 | import android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast 27 | import android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility 28 | import android.support.test.espresso.util.HumanReadables 29 | import android.support.v4.widget.DrawerLayout 30 | import android.view.Menu 31 | import android.view.View 32 | import org.hamcrest.Matcher 33 | import org.hamcrest.Matchers.allOf 34 | 35 | /** 36 | * View actions for interacting with [NavigationView] 37 | */ 38 | object NavigationViewActions { 39 | /** 40 | * Returns a [ViewAction] that navigates to a menu item in [NavigationView] using a 41 | * menu item resource id. 42 | * 43 | * 44 | * 45 | * 46 | * View constraints: 47 | * 48 | * * View must be a child of a [DrawerLayout] 49 | * * View must be of type [NavigationView] 50 | * * View must be visible on screen 51 | * * View must be displayed on screen 52 | * 53 | * 54 | * @param menuItemId the resource id of the menu item 55 | * @return a [ViewAction] that navigates on a menu item 56 | */ 57 | fun navigateTo(menuItemId: Int): ViewAction { 58 | return object : ViewAction { 59 | override fun perform(uiController: UiController, view: View) { 60 | val navigationView = view as NavigationView 61 | val menu = navigationView.menu 62 | if (null == menu.findItem(menuItemId)) { 63 | throw PerformException.Builder() 64 | .withActionDescription(this.description) 65 | .withViewDescription(HumanReadables.describe(view)) 66 | .withCause(RuntimeException(getErrorMessage(menu, view))) 67 | .build() 68 | } 69 | menu.performIdentifierAction(menuItemId, 0) 70 | uiController.loopMainThreadUntilIdle() 71 | } 72 | 73 | private fun getErrorMessage(menu: Menu, view: View): String { 74 | val NEW_LINE = System.getProperty("line.separator") 75 | val errorMessage = StringBuilder("Menu item was not found, " + "available menu items:") 76 | .append(NEW_LINE) 77 | for (position in 0 until menu.size()) { 78 | errorMessage.append("[MenuItem] position=").append(position) 79 | val menuItem = menu.getItem(position) 80 | if (menuItem != null) { 81 | val itemTitle = menuItem.title 82 | if (itemTitle != null) { 83 | errorMessage.append(", title=").append(itemTitle) 84 | } 85 | if (view.resources != null) { 86 | val itemId = menuItem.itemId 87 | try { 88 | errorMessage.append(", id=") 89 | val menuItemResourceName = view.resources.getResourceName(itemId) 90 | errorMessage.append(menuItemResourceName) 91 | } catch (nfe: NotFoundException) { 92 | errorMessage.append("not found") 93 | } 94 | 95 | } 96 | errorMessage.append(NEW_LINE) 97 | } 98 | } 99 | return errorMessage.toString() 100 | } 101 | 102 | override fun getDescription(): String { 103 | return "click on menu item with id" 104 | } 105 | 106 | override fun getConstraints(): Matcher { 107 | return allOf(isAssignableFrom(NavigationView::class.java), 108 | withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), isDisplayingAtLeast(90)) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.tasks 18 | 19 | import android.support.test.espresso.Espresso.onView 20 | import android.support.test.espresso.Espresso.pressBack 21 | import android.support.test.espresso.NoActivityResumedException 22 | import android.support.test.espresso.action.ViewActions.click 23 | import android.support.test.espresso.assertion.ViewAssertions.matches 24 | import android.support.test.espresso.contrib.DrawerActions.open 25 | import android.support.test.espresso.contrib.DrawerMatchers.isClosed 26 | import android.support.test.espresso.contrib.DrawerMatchers.isOpen 27 | import android.support.test.espresso.matcher.ViewMatchers.isDisplayed 28 | import android.support.test.espresso.matcher.ViewMatchers.withContentDescription 29 | import android.support.test.espresso.matcher.ViewMatchers.withId 30 | import android.support.test.filters.LargeTest 31 | import android.support.test.rule.ActivityTestRule 32 | import android.support.test.runner.AndroidJUnit4 33 | import android.support.v4.widget.DrawerLayout 34 | import android.view.Gravity 35 | import com.example.android.architecture.blueprints.todoapp.R 36 | import com.example.android.architecture.blueprints.todoapp.custom.action.NavigationViewActions.navigateTo 37 | import com.example.android.architecture.blueprints.todoapp.getToolbarNavigationContentDescription 38 | import junit.framework.Assert.fail 39 | import org.junit.Rule 40 | import org.junit.Test 41 | import org.junit.runner.RunWith 42 | 43 | /** 44 | * Tests for the [DrawerLayout] layout component in [TasksActivity] which manages 45 | * navigation within the app. 46 | */ 47 | @RunWith(AndroidJUnit4::class) 48 | @LargeTest 49 | class AppNavigationTest { 50 | 51 | /** 52 | * [ActivityTestRule] is a JUnit [@Rule][Rule] to launch your activity under test. 53 | * 54 | * 55 | * 56 | * 57 | * Rules are interceptors which are executed for each test method and are important building 58 | * blocks of Junit tests. 59 | */ 60 | @get:Rule 61 | var activityTestRule = ActivityTestRule(TasksActivity::class.java) 62 | 63 | @Test 64 | fun clickOnStatisticsNavigationItem_ShowsStatisticsScreen() { 65 | openStatisticsScreen() 66 | 67 | // Check that statistics Activity was opened. 68 | onView(withId(R.id.statistics)).check(matches(isDisplayed())) 69 | } 70 | 71 | @Test 72 | fun clickOnListNavigationItem_ShowsListScreen() { 73 | openStatisticsScreen() 74 | 75 | openTasksScreen() 76 | 77 | // Check that Tasks Activity was opened. 78 | onView(withId(R.id.tasksContainer)).check(matches(isDisplayed())) 79 | } 80 | 81 | @Test 82 | fun clickOnAndroidHomeIcon_OpensNavigation() { 83 | // Check that left drawer is closed at startup 84 | onView(withId(R.id.drawer_layout)) 85 | // Left Drawer should be closed. 86 | .check(matches(isClosed(Gravity.LEFT))) 87 | 88 | // Open Drawer 89 | onView( 90 | withContentDescription( 91 | getToolbarNavigationContentDescription(activityTestRule.activity, R.id.toolbar)) 92 | ).perform(click()) 93 | 94 | // Check if drawer is open 95 | onView(withId(R.id.drawer_layout)) 96 | // Left drawer is open. 97 | .check(matches(isOpen(Gravity.LEFT))) 98 | } 99 | 100 | 101 | @Test 102 | fun Statistics_backNavigatesToTasks() { 103 | openStatisticsScreen() 104 | 105 | // Press back to go back to the tasks list 106 | pressBack() 107 | 108 | // Check that Tasks Activity was restored. 109 | onView(withId(R.id.tasksContainer)).check(matches(isDisplayed())) 110 | } 111 | 112 | @Test 113 | fun backFromTasksScreen_ExitsApp() { 114 | // From the tasks screen, press back should exit the app. 115 | assertPressingBackExitsApp() 116 | } 117 | 118 | @Test 119 | fun backFromTasksScreenAfterStats_ExitsApp() { 120 | // This test checks that TasksActivity is a parent of StatisticsActivity 121 | 122 | // Open the stats screen 123 | openStatisticsScreen() 124 | 125 | // Open the tasks screen to restore the task 126 | openTasksScreen() 127 | 128 | // Pressing back should exit app 129 | assertPressingBackExitsApp() 130 | } 131 | 132 | private fun assertPressingBackExitsApp() { 133 | try { 134 | pressBack() 135 | fail("Should kill the app and throw an exception") 136 | } catch (e: NoActivityResumedException) { 137 | // Test OK 138 | } 139 | 140 | } 141 | 142 | private fun openTasksScreen() { 143 | // Open Drawer to click on navigation item. 144 | onView(withId(R.id.drawer_layout)) 145 | // Left Drawer should be closed. 146 | .check(matches(isClosed(Gravity.LEFT))) 147 | // Open Drawer 148 | .perform(open()) 149 | 150 | // Start tasks list screen. 151 | onView(withId(R.id.nav_view)) 152 | .perform(navigateTo(R.id.list_navigation_menu_item)) 153 | } 154 | 155 | private fun openStatisticsScreen() { 156 | // Open Drawer to click on navigation item. 157 | onView(withId(R.id.drawer_layout)) 158 | // Left Drawer should be closed. 159 | .check(matches(isClosed(Gravity.LEFT))) 160 | // Open Drawer 161 | .perform(open()) 162 | 163 | // Start statistics screen. 164 | onView(withId(R.id.nav_view)) 165 | .perform(navigateTo(R.id.statistics_navigation_menu_item)) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/androidTestMock/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.addedittask 18 | 19 | import android.content.Intent 20 | import android.content.res.Resources 21 | import android.support.test.InstrumentationRegistry 22 | import android.support.test.espresso.Espresso.onView 23 | import android.support.test.espresso.action.ViewActions.clearText 24 | import android.support.test.espresso.action.ViewActions.click 25 | import android.support.test.espresso.assertion.ViewAssertions.matches 26 | import android.support.test.espresso.intent.rule.IntentsTestRule 27 | import android.support.test.espresso.matcher.BoundedMatcher 28 | import android.support.test.espresso.matcher.ViewMatchers.isDisplayed 29 | import android.support.test.espresso.matcher.ViewMatchers.withId 30 | import android.support.test.filters.LargeTest 31 | import android.support.test.rule.ActivityTestRule 32 | import android.support.test.runner.AndroidJUnit4 33 | import android.support.v7.widget.Toolbar 34 | import android.view.View 35 | import com.example.android.architecture.blueprints.todoapp.R 36 | import com.example.android.architecture.blueprints.todoapp.R.id.toolbar 37 | import com.example.android.architecture.blueprints.todoapp.data.FakeTasksRemoteDataSource 38 | import com.example.android.architecture.blueprints.todoapp.data.Task 39 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository 40 | import com.example.android.architecture.blueprints.todoapp.rotateOrientation 41 | import org.hamcrest.Description 42 | import org.hamcrest.Matcher 43 | import org.junit.Rule 44 | import org.junit.Test 45 | import org.junit.runner.RunWith 46 | 47 | /** 48 | * Tests for the add task screen. 49 | */ 50 | @RunWith(AndroidJUnit4::class) 51 | @LargeTest 52 | class AddEditTaskScreenTest { 53 | 54 | /** 55 | * [IntentsTestRule] is an [ActivityTestRule] which inits and releases Espresso 56 | * Intents before and after each test run. 57 | * 58 | * 59 | * 60 | * 61 | * Rules are interceptors which are executed for each test method and are important building 62 | * blocks of Junit tests. 63 | */ 64 | @get:Rule 65 | var mActivityTestRule = ActivityTestRule(AddEditTaskActivity::class.java, false, false) 66 | 67 | @Test 68 | fun emptyTask_isNotSaved() { 69 | // Launch activity to add a new task 70 | launchNewTaskActivity(null) 71 | 72 | // Add invalid title and description combination 73 | onView(withId(R.id.add_task_title)).perform(clearText()) 74 | onView(withId(R.id.add_task_description)).perform(clearText()) 75 | // Try to save the task 76 | onView(withId(R.id.fab_edit_task_done)).perform(click()) 77 | 78 | // Verify that the activity is still displayed (a correct task would close it). 79 | onView(withId(R.id.add_task_title)).check(matches(isDisplayed())) 80 | } 81 | 82 | @Test 83 | fun toolbarTitle_newTask_persistsRotation() { 84 | // Launch activity to add a new task 85 | launchNewTaskActivity(null) 86 | 87 | // Check that the toolbar shows the correct title 88 | onView(withId(toolbar)).check(matches(withToolbarTitle(R.string.add_task))) 89 | 90 | // Rotate activity 91 | rotateOrientation(mActivityTestRule.activity) 92 | 93 | // Check that the toolbar title is persisted 94 | onView(withId(toolbar)).check(matches(withToolbarTitle(R.string.add_task))) 95 | } 96 | 97 | @Test 98 | fun toolbarTitle_editTask_persistsRotation() { 99 | // Put a task in the repository and start the activity to edit it 100 | TasksRepository.clearInstance() 101 | FakeTasksRemoteDataSource.addTasks( 102 | Task(id = TASK_ID, title = "Title1", description = "", completed = false) 103 | ) 104 | launchNewTaskActivity(TASK_ID) 105 | 106 | // Check that the toolbar shows the correct title 107 | onView(withId(toolbar)).check(matches(withToolbarTitle(R.string.edit_task))) 108 | 109 | // Rotate activity 110 | rotateOrientation(mActivityTestRule.activity) 111 | 112 | // check that the toolbar title is persisted 113 | onView(withId(toolbar)).check(matches(withToolbarTitle(R.string.edit_task))) 114 | } 115 | 116 | /** 117 | * @param taskId is null if used to add a new task, otherwise it edits the task. 118 | */ 119 | private fun launchNewTaskActivity(taskId: String?) { 120 | val intent = Intent( 121 | InstrumentationRegistry.getInstrumentation().targetContext, 122 | AddEditTaskActivity::class.java) 123 | 124 | intent.putExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId) 125 | mActivityTestRule.launchActivity(intent) 126 | } 127 | 128 | companion object { 129 | private val TASK_ID = "1" 130 | 131 | /** 132 | * Matches the toolbar title with a specific string resource. 133 | * 134 | * @param resourceId the ID of the string resource to match 135 | */ 136 | fun withToolbarTitle(resourceId: Int): Matcher { 137 | return object : BoundedMatcher(Toolbar::class.java) { 138 | 139 | override fun describeTo(description: Description) { 140 | description.appendText("with toolbar title from resource id: ") 141 | description.appendValue(resourceId) 142 | } 143 | 144 | override fun matchesSafely(toolbar: Toolbar): Boolean { 145 | var expectedText: CharSequence = "" 146 | try { 147 | expectedText = toolbar.resources.getString(resourceId) 148 | } catch (ignored: Resources.NotFoundException) { 149 | /* view could be from a context unaware of the resource id. */ 150 | } 151 | 152 | val actualText = toolbar.title 153 | return expectedText == actualText 154 | } 155 | } 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /app/src/androidTestMock/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.statistics 18 | 19 | import android.content.Intent 20 | import android.support.test.InstrumentationRegistry 21 | import android.support.test.espresso.Espresso.onView 22 | import android.support.test.espresso.assertion.ViewAssertions.matches 23 | import android.support.test.espresso.matcher.ViewMatchers.isDisplayed 24 | import android.support.test.espresso.matcher.ViewMatchers.withText 25 | import android.support.test.filters.LargeTest 26 | import android.support.test.rule.ActivityTestRule 27 | import android.support.test.runner.AndroidJUnit4 28 | import com.example.android.architecture.blueprints.todoapp.Injection 29 | import com.example.android.architecture.blueprints.todoapp.R 30 | import com.example.android.architecture.blueprints.todoapp.data.Task 31 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository 32 | import com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailActivity 33 | import org.hamcrest.Matchers.containsString 34 | import org.junit.Before 35 | import org.junit.Rule 36 | import org.junit.Test 37 | import org.junit.runner.RunWith 38 | 39 | /** 40 | * Tests for the statistics screen. 41 | */ 42 | @RunWith(AndroidJUnit4::class) 43 | @LargeTest 44 | class StatisticsScreenTest { 45 | 46 | /** 47 | * [ActivityTestRule] is a JUnit [@Rule][Rule] to launch your activity under test. 48 | * 49 | * Rules are interceptors which are executed for each test method and are important building 50 | * blocks of Junit tests. 51 | */ 52 | @Suppress("MemberVisibilityCanPrivate") 53 | @get:Rule 54 | val statisticsActivityTestRule = 55 | ActivityTestRule(StatisticsActivity::class.java, true, false) 56 | 57 | /** 58 | * Setup your test fixture with a fake task id. The [TaskDetailActivity] is started with 59 | * a particular task id, which is then loaded from the service API. 60 | * 61 | * Note that this test runs hermetically and is fully isolated using a fake implementation of 62 | * the service API. This is a great way to make your tests more reliable and faster at the same 63 | * time, since they are isolated from any outside dependencies. 64 | */ 65 | @Before 66 | fun intentWithStubbedTaskId() { 67 | // Given some tasks 68 | TasksRepository.clearInstance() 69 | val repository = Injection.provideTasksRepository(InstrumentationRegistry.getTargetContext()) 70 | repository.saveTask(Task(title = "Title1", description = "", completed = false)) 71 | repository.saveTask(Task(title = "Title2", description = "", completed = true)) 72 | 73 | // Lazily start the Activity from the ActivityTestRule 74 | val startIntent = Intent() 75 | statisticsActivityTestRule.launchActivity(startIntent) 76 | } 77 | 78 | @Test 79 | @Throws(Exception::class) 80 | fun Tasks_ShowsNonEmptyMessage() { 81 | // Check that the active and completed tasks text is displayed 82 | val expectedActiveTaskText = InstrumentationRegistry.getTargetContext().getString( 83 | R.string.statistics_active_tasks) 84 | onView(withText(containsString(expectedActiveTaskText))).check(matches(isDisplayed())) 85 | 86 | val expectedCompletedTaskText = InstrumentationRegistry.getTargetContext().getString( 87 | R.string.statistics_completed_tasks) 88 | onView(withText(containsString(expectedCompletedTaskText))).check(matches(isDisplayed())) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/androidTestMock/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.taskdetail 18 | 19 | import android.app.Activity 20 | import android.content.Intent 21 | import android.support.test.InstrumentationRegistry 22 | import android.support.test.espresso.Espresso.onView 23 | import android.support.test.espresso.assertion.ViewAssertions.matches 24 | import android.support.test.espresso.matcher.ViewMatchers.isChecked 25 | import android.support.test.espresso.matcher.ViewMatchers.isDisplayed 26 | import android.support.test.espresso.matcher.ViewMatchers.withId 27 | import android.support.test.espresso.matcher.ViewMatchers.withText 28 | import android.support.test.filters.LargeTest 29 | import android.support.test.rule.ActivityTestRule 30 | import android.support.test.runner.AndroidJUnit4 31 | import com.example.android.architecture.blueprints.todoapp.Injection 32 | import com.example.android.architecture.blueprints.todoapp.R 33 | import com.example.android.architecture.blueprints.todoapp.data.Task 34 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository 35 | import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksLocalDataSource 36 | import com.example.android.architecture.blueprints.todoapp.rotateOrientation 37 | import org.hamcrest.core.IsNot.not 38 | import org.junit.Rule 39 | import org.junit.Test 40 | import org.junit.runner.RunWith 41 | 42 | /** 43 | * Tests for the tasks screen, the main screen which contains a list of all tasks. 44 | */ 45 | @RunWith(AndroidJUnit4::class) 46 | @LargeTest 47 | class TaskDetailScreenTest { 48 | 49 | /** 50 | * [ActivityTestRule] is a JUnit [Rule] to launch your activity under test. 51 | * 52 | * 53 | * 54 | * 55 | * Rules are interceptors which are executed for each test method and are important building 56 | * blocks of Junit tests. 57 | * 58 | * 59 | * 60 | * 61 | * Sometimes an [Activity] requires a custom start [Intent] to receive data 62 | * from the source Activity. ActivityTestRule has a feature which let's you lazily start the 63 | * Activity under test, so you can control the Intent that is used to start the target Activity. 64 | */ 65 | @get:Rule 66 | var taskDetailActivityTestRule = ActivityTestRule(TaskDetailActivity::class.java, true, false) 67 | 68 | private fun loadActiveTask() { 69 | startActivityWithWithStubbedTask(ACTIVE_TASK) 70 | } 71 | 72 | private fun loadCompletedTask() { 73 | startActivityWithWithStubbedTask(COMPLETED_TASK) 74 | } 75 | 76 | /** 77 | * Setup your test fixture with a fake task id. The [TaskDetailActivity] is started with 78 | * a particular task id, which is then loaded from the service API. 79 | * 80 | * 81 | * 82 | * 83 | * Note that this test runs hermetically and is fully isolated using a fake implementation of 84 | * the service API. This is a great way to make your tests more reliable and faster at the same 85 | * time, since they are isolated from any outside dependencies. 86 | */ 87 | private fun startActivityWithWithStubbedTask(task: Task) { 88 | // Add a task stub to the fake service api layer. 89 | TasksRepository.clearInstance() 90 | TasksLocalDataSource.clearInstance() 91 | Injection.provideTasksRepository(InstrumentationRegistry.getTargetContext()).saveTask(task) 92 | 93 | // Lazily start the Activity from the ActivityTestRule this time to inject the start Intent 94 | val startIntent = Intent().apply { putExtra(TaskDetailActivity.EXTRA_TASK_ID, task.id) } 95 | taskDetailActivityTestRule.launchActivity(startIntent) 96 | } 97 | 98 | @Test 99 | @Throws(Exception::class) 100 | fun activeTaskDetails_DisplayedInUi() { 101 | loadActiveTask() 102 | 103 | // Check that the task title and description are displayed 104 | onView(withId(R.id.task_detail_title)).check(matches(withText(TASK_TITLE))) 105 | onView(withId(R.id.task_detail_description)).check(matches(withText(TASK_DESCRIPTION))) 106 | onView(withId(R.id.task_detail_complete)).check(matches(not(isChecked()))) 107 | } 108 | 109 | @Test 110 | @Throws(Exception::class) 111 | fun completedTaskDetails_DisplayedInUi() { 112 | loadCompletedTask() 113 | 114 | // Check that the task title and description are displayed 115 | onView(withId(R.id.task_detail_title)).check(matches(withText(TASK_TITLE))) 116 | onView(withId(R.id.task_detail_description)).check(matches(withText(TASK_DESCRIPTION))) 117 | onView(withId(R.id.task_detail_complete)).check(matches(isChecked())) 118 | } 119 | 120 | @Test 121 | fun orientationChange_menuAndTaskPersist() { 122 | loadActiveTask() 123 | 124 | // Check that the task is shown 125 | onView(withId(R.id.task_detail_title)).check(matches(withText(TASK_TITLE))) 126 | onView(withId(R.id.task_detail_description)).check(matches(withText(TASK_DESCRIPTION))) 127 | 128 | // Check delete menu item is displayed and is unique 129 | onView(withId(R.id.menu_delete)).check(matches(isDisplayed())) 130 | 131 | rotateOrientation(taskDetailActivityTestRule.activity) 132 | 133 | // Check that the task is shown 134 | onView(withId(R.id.task_detail_title)).check(matches(withText(TASK_TITLE))) 135 | onView(withId(R.id.task_detail_description)).check(matches(withText(TASK_DESCRIPTION))) 136 | 137 | // Check delete menu item is displayed and is unique 138 | onView(withId(R.id.menu_delete)).check(matches(isDisplayed())) 139 | } 140 | 141 | companion object { 142 | private val TASK_TITLE = "ATSL" 143 | private val TASK_DESCRIPTION = "Rocks" 144 | /** 145 | * [Task] stub that is added to the fake service API layer. 146 | */ 147 | private val ACTIVE_TASK = Task( 148 | title = TASK_TITLE, 149 | description = TASK_DESCRIPTION, 150 | completed = false) 151 | /** 152 | * [Task] stub that is added to the fake service API layer. 153 | */ 154 | private val COMPLETED_TASK = Task( 155 | title = TASK_TITLE, 156 | description = TASK_DESCRIPTION, 157 | completed = true) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.addedittask 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviAction 4 | 5 | sealed class AddEditTaskAction : MviAction { 6 | data class PopulateTaskAction(val taskId: String) : AddEditTaskAction() 7 | 8 | data class CreateTaskAction(val title: String, val description: String) : AddEditTaskAction() 9 | 10 | data class UpdateTaskAction( 11 | val taskId: String, 12 | val title: String, 13 | val description: String 14 | ) : AddEditTaskAction() 15 | 16 | object SkipMe : AddEditTaskAction() 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskActionProcessorHolder.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.addedittask 2 | 3 | import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskAction.CreateTaskAction 4 | import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskAction.PopulateTaskAction 5 | import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskAction.UpdateTaskAction 6 | import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskResult.CreateTaskResult 7 | import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskResult.PopulateTaskResult 8 | import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskResult.UpdateTaskResult 9 | import com.example.android.architecture.blueprints.todoapp.data.Task 10 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository 11 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviAction 12 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviResult 13 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewModel 14 | import com.example.android.architecture.blueprints.todoapp.util.schedulers.BaseSchedulerProvider 15 | import io.reactivex.Observable 16 | import io.reactivex.ObservableTransformer 17 | 18 | 19 | /** 20 | * Contains and executes the business logic for all emitted [MviAction] 21 | * and returns one unique [Observable] of [MviResult]. 22 | * 23 | * 24 | * This could have been included inside the [MviViewModel] 25 | * but was separated to ease maintenance, as the [MviViewModel] was getting too big. 26 | */ 27 | class AddEditTaskActionProcessorHolder( 28 | private val tasksRepository: TasksRepository, 29 | private val schedulerProvider: BaseSchedulerProvider 30 | ) { 31 | private val populateTaskProcessor = 32 | ObservableTransformer { actions -> 33 | actions.flatMap { action -> 34 | tasksRepository.getTask(action.taskId) 35 | // Transform the Single to an Observable to allow emission of multiple 36 | // events down the stream (e.g. the InFlight event) 37 | .toObservable() 38 | // Wrap returned data into an immutable object 39 | .map(PopulateTaskResult::Success) 40 | .cast(PopulateTaskResult::class.java) 41 | // Wrap any error into an immutable object and pass it down the stream 42 | // without crashing. 43 | // Because errors are data and hence, should just be part of the stream. 44 | .onErrorReturn(PopulateTaskResult::Failure) 45 | .subscribeOn(schedulerProvider.io()) 46 | .observeOn(schedulerProvider.ui()) 47 | // Emit an InFlight event to notify the subscribers (e.g. the UI) we are 48 | // doing work and waiting on a response. 49 | // We emit it after observing on the UI thread to allow the event to be emitted 50 | // on the current frame and avoid jank. 51 | .startWith(PopulateTaskResult.InFlight) 52 | } 53 | } 54 | 55 | private val createTaskProcessor = 56 | ObservableTransformer { actions -> 57 | actions 58 | .map { action -> Task(title = action.title, description = action.description) } 59 | .publish { task -> 60 | Observable.merge( 61 | task.filter(Task::empty).map { CreateTaskResult.Empty }, 62 | task.filter { !it.empty }.flatMap { 63 | tasksRepository.saveTask(it).andThen(Observable.just(CreateTaskResult.Success)) 64 | } 65 | ) 66 | } 67 | } 68 | 69 | private val updateTaskProcessor = 70 | ObservableTransformer { actions -> 71 | actions.flatMap { action -> 72 | tasksRepository.saveTask( 73 | Task(title = action.title, description = action.description, id = action.taskId) 74 | ).andThen(Observable.just(UpdateTaskResult)) 75 | } 76 | } 77 | 78 | /** 79 | * Splits the [Observable] to match each type of [MviAction] to 80 | * its corresponding business logic processor. Each processor takes a defined [MviAction], 81 | * returns a defined [MviResult] 82 | * The global actionProcessor then merges all [Observable] back to 83 | * one unique [Observable]. 84 | * 85 | * 86 | * The splitting is done using [Observable.publish] which allows almost anything 87 | * on the passed [Observable] as long as one and only one [Observable] is returned. 88 | * 89 | * 90 | * An security layer is also added for unhandled [MviAction] to allow early crash 91 | * at runtime to easy the maintenance. 92 | */ 93 | internal var actionProcessor = 94 | ObservableTransformer { actions -> 95 | actions.publish { shared -> 96 | Observable.merge( 97 | // Match PopulateTasks to populateTaskProcessor 98 | shared.ofType(AddEditTaskAction.PopulateTaskAction::class.java) 99 | .compose(populateTaskProcessor), 100 | // Match CreateTasks to createTaskProcessor 101 | shared.ofType(AddEditTaskAction.CreateTaskAction::class.java) 102 | .compose(createTaskProcessor), 103 | // Match UpdateTasks to updateTaskProcessor 104 | shared.ofType(AddEditTaskAction.UpdateTaskAction::class.java) 105 | .compose(updateTaskProcessor)) 106 | .mergeWith( 107 | // Error for not implemented actions 108 | shared.filter { v -> 109 | v !is AddEditTaskAction.PopulateTaskAction && 110 | v !is AddEditTaskAction.CreateTaskAction && 111 | v !is AddEditTaskAction.UpdateTaskAction 112 | } 113 | .flatMap { w -> 114 | Observable.error( 115 | IllegalArgumentException("Unknown Action type: " + w)) 116 | }) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.addedittask 18 | 19 | import android.os.Bundle 20 | import android.support.v7.app.ActionBar 21 | import android.support.v7.app.AppCompatActivity 22 | import com.example.android.architecture.blueprints.todoapp.R 23 | import com.example.android.architecture.blueprints.todoapp.util.addFragmentToActivity 24 | 25 | /** 26 | * Displays an add or edit task screen. 27 | */ 28 | class AddEditTaskActivity : AppCompatActivity() { 29 | 30 | private lateinit var actionBar: ActionBar 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | setContentView(R.layout.addtask_act) 35 | 36 | // Set up the toolbar. 37 | setSupportActionBar(findViewById(R.id.toolbar)) 38 | supportActionBar?.run { 39 | actionBar = this 40 | setDisplayHomeAsUpEnabled(true) 41 | setDisplayShowHomeEnabled(true) 42 | } 43 | 44 | val taskId = intent.getStringExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID) 45 | setToolbarTitle(taskId) 46 | 47 | if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { 48 | val addEditTaskFragment = AddEditTaskFragment.invoke() 49 | 50 | if (taskId != null) { 51 | val args = Bundle() 52 | args.putString(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId) 53 | addEditTaskFragment.arguments = args 54 | } 55 | 56 | addFragmentToActivity(supportFragmentManager, addEditTaskFragment, R.id.contentFrame) 57 | } 58 | } 59 | 60 | private fun setToolbarTitle(taskId: String?) { 61 | actionBar.setTitle(if (taskId == null) R.string.add_task else R.string.edit_task) 62 | } 63 | 64 | override fun onSupportNavigateUp(): Boolean { 65 | onBackPressed() 66 | return true 67 | } 68 | 69 | companion object { 70 | const val REQUEST_ADD_TASK = 1 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.addedittask 18 | 19 | import android.app.Activity 20 | import android.arch.lifecycle.ViewModelProviders 21 | import android.os.Bundle 22 | import android.support.design.widget.FloatingActionButton 23 | import android.support.design.widget.Snackbar 24 | import android.support.v4.app.Fragment 25 | import android.view.LayoutInflater 26 | import android.view.View 27 | import android.view.ViewGroup 28 | import android.widget.TextView 29 | import com.example.android.architecture.blueprints.todoapp.R 30 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviIntent 31 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviView 32 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewModel 33 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewState 34 | import com.example.android.architecture.blueprints.todoapp.util.ToDoViewModelFactory 35 | import com.jakewharton.rxbinding2.view.RxView 36 | import io.reactivex.Observable 37 | import io.reactivex.disposables.CompositeDisposable 38 | import kotlin.LazyThreadSafetyMode.NONE 39 | 40 | /** 41 | * Main UI for the add task screen. Users can enter a task title and description. 42 | */ 43 | class AddEditTaskFragment : Fragment(), MviView { 44 | private lateinit var title: TextView 45 | private lateinit var description: TextView 46 | private lateinit var fab: FloatingActionButton 47 | // Used to manage the data flow lifecycle and avoid memory leak. 48 | private val disposables = CompositeDisposable() 49 | private val viewModel: AddEditTaskViewModel by lazy(NONE) { 50 | ViewModelProviders 51 | .of(this, ToDoViewModelFactory.getInstance(context!!)) 52 | .get(AddEditTaskViewModel::class.java) 53 | } 54 | 55 | private val argumentTaskId: String? 56 | get() = arguments?.getString(ARGUMENT_EDIT_TASK_ID) 57 | 58 | override fun onDestroy() { 59 | super.onDestroy() 60 | disposables.dispose() 61 | } 62 | 63 | override fun onCreateView( 64 | inflater: LayoutInflater, 65 | container: ViewGroup?, 66 | savedInstanceState: Bundle? 67 | ): View? { 68 | return inflater.inflate(R.layout.addtask_frag, container, false) 69 | .also { 70 | title = it.findViewById(R.id.add_task_title) 71 | description = it.findViewById(R.id.add_task_description) 72 | setHasOptionsMenu(true) 73 | } 74 | } 75 | 76 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 77 | super.onViewCreated(view, savedInstanceState) 78 | fab = activity!!.findViewById(R.id.fab_edit_task_done) 79 | fab.setImageResource(R.drawable.ic_done) 80 | 81 | bind() 82 | } 83 | 84 | /** 85 | * Connect the [MviView] with the [MviViewModel] 86 | * We subscribe to the [MviViewModel] before passing it the [MviView]'s [MviIntent]s. 87 | * If we were to pass [MviIntent]s to the [MviViewModel] before listening to it, 88 | * emitted [MviViewState]s could be lost 89 | */ 90 | private fun bind() { 91 | // Subscribe to the ViewModel and call render for every emitted state 92 | disposables.add(viewModel.states().subscribe(this::render)) 93 | // Pass the UI's intents to the ViewModel 94 | viewModel.processIntents(intents()) 95 | } 96 | 97 | override fun intents(): Observable { 98 | return Observable.merge(initialIntent(), saveTaskIntent()) 99 | } 100 | 101 | /** 102 | * The initial Intent the [MviView] emit to convey to the [MviViewModel] 103 | * that it is ready to receive data. 104 | * This initial Intent is also used to pass any parameters the [MviViewModel] might need 105 | * to render the initial [MviViewState] (e.g. the task id to load). 106 | */ 107 | private fun initialIntent(): Observable { 108 | return Observable.just(AddEditTaskIntent.InitialIntent(argumentTaskId)) 109 | } 110 | 111 | private fun saveTaskIntent(): Observable { 112 | // Wrap the FAB click events into a SaveTaskIntent and set required information 113 | return RxView.clicks(fab).map { 114 | AddEditTaskIntent.SaveTask(argumentTaskId, title.text.toString(), description.text.toString()) 115 | } 116 | } 117 | 118 | override fun render(state: AddEditTaskViewState) { 119 | if (state.isSaved) { 120 | showTasksList() 121 | return 122 | } 123 | if (state.isEmpty) { 124 | showEmptyTaskError() 125 | } 126 | if (state.title.isNotEmpty()) { 127 | setTitle(state.title) 128 | } 129 | if (state.description.isNotEmpty()) { 130 | setDescription(state.description) 131 | } 132 | } 133 | 134 | private fun showEmptyTaskError() { 135 | Snackbar.make(title, getString(R.string.empty_task_message), Snackbar.LENGTH_LONG).show() 136 | } 137 | 138 | private fun showTasksList() { 139 | activity!!.setResult(Activity.RESULT_OK) 140 | activity!!.finish() 141 | } 142 | 143 | private fun setTitle(title: String) { 144 | this.title.text = title 145 | } 146 | 147 | private fun setDescription(description: String) { 148 | this.description.text = description 149 | } 150 | 151 | companion object { 152 | const val ARGUMENT_EDIT_TASK_ID = "EDIT_TASK_ID" 153 | 154 | operator fun invoke(): AddEditTaskFragment = AddEditTaskFragment() 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskIntent.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.addedittask 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviIntent 4 | 5 | sealed class AddEditTaskIntent : MviIntent { 6 | data class InitialIntent(val taskId: String?) : AddEditTaskIntent() 7 | 8 | data class SaveTask( 9 | val taskId: String?, 10 | val title: String, 11 | val description: String 12 | ) : AddEditTaskIntent() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.addedittask 2 | 3 | import com.example.android.architecture.blueprints.todoapp.data.Task 4 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviResult 5 | 6 | 7 | sealed class AddEditTaskResult : MviResult { 8 | sealed class PopulateTaskResult : AddEditTaskResult() { 9 | data class Success(val task: Task) : PopulateTaskResult() 10 | data class Failure(val error: Throwable) : PopulateTaskResult() 11 | object InFlight : PopulateTaskResult() 12 | } 13 | 14 | sealed class CreateTaskResult : AddEditTaskResult() { 15 | object Success : CreateTaskResult() 16 | object Empty : CreateTaskResult() 17 | } 18 | 19 | object UpdateTaskResult : AddEditTaskResult() 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewState.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.addedittask 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewState 4 | 5 | data class AddEditTaskViewState( 6 | val isEmpty: Boolean, 7 | val isSaved: Boolean, 8 | val title: String, 9 | val description: String, 10 | val error: Throwable? 11 | ) : MviViewState { 12 | companion object { 13 | fun idle(): AddEditTaskViewState { 14 | return AddEditTaskViewState( 15 | title = "", 16 | description = "", 17 | error = null, 18 | isEmpty = false, 19 | isSaved = false 20 | ) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.data 2 | 3 | import com.example.android.architecture.blueprints.todoapp.util.isNotNullNorEmpty 4 | import java.util.UUID 5 | 6 | data class Task( 7 | val id: String = UUID.randomUUID().toString(), 8 | val title: String?, 9 | val description: String?, 10 | val completed: Boolean = false 11 | ) { 12 | val titleForList = 13 | if (title.isNotNullNorEmpty()) { 14 | title 15 | } else { 16 | description 17 | } 18 | 19 | val active = !completed 20 | 21 | val empty = title.isNullOrEmpty() && description.isNullOrEmpty() 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/TasksDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.data.source 18 | 19 | import com.example.android.architecture.blueprints.todoapp.data.Task 20 | 21 | import io.reactivex.Completable 22 | import io.reactivex.Single 23 | 24 | /** 25 | * Main entry point for accessing tasks data. 26 | * 27 | * 28 | */ 29 | interface TasksDataSource { 30 | fun getTasks(forceUpdate: Boolean): Single> { 31 | if (forceUpdate) refreshTasks() 32 | return getTasks() 33 | } 34 | 35 | fun getTasks(): Single> 36 | 37 | fun getTask(taskId: String): Single 38 | 39 | fun saveTask(task: Task): Completable 40 | 41 | fun completeTask(task: Task): Completable 42 | 43 | fun completeTask(taskId: String): Completable 44 | 45 | fun activateTask(task: Task): Completable 46 | 47 | fun activateTask(taskId: String): Completable 48 | 49 | fun clearCompletedTasks(): Completable 50 | 51 | fun refreshTasks() 52 | 53 | fun deleteAllTasks() 54 | 55 | fun deleteTask(taskId: String): Completable 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksDbHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.data.source.local 18 | 19 | import android.content.Context 20 | import android.database.sqlite.SQLiteDatabase 21 | import android.database.sqlite.SQLiteOpenHelper 22 | import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksPersistenceContract.TaskEntry.COLUMN_NAME_COMPLETED 23 | import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksPersistenceContract.TaskEntry.COLUMN_NAME_DESCRIPTION 24 | import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksPersistenceContract.TaskEntry.COLUMN_NAME_ENTRY_ID 25 | import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksPersistenceContract.TaskEntry.COLUMN_NAME_TITLE 26 | import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksPersistenceContract.TaskEntry.TABLE_NAME 27 | 28 | class TasksDbHelper( 29 | context: Context 30 | ) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { 31 | 32 | override fun onCreate(db: SQLiteDatabase) { 33 | db.execSQL(SQL_CREATE_ENTRIES) 34 | } 35 | 36 | override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 37 | // Not required as at version 1 38 | } 39 | 40 | override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 41 | // Not required as at version 1 42 | } 43 | 44 | companion object { 45 | val DATABASE_VERSION = 1 46 | val DATABASE_NAME = "Tasks.db" 47 | private val TEXT_TYPE = " TEXT" 48 | private val BOOLEAN_TYPE = " INTEGER" 49 | private val COMMA_SEP = "," 50 | private val SQL_CREATE_ENTRIES = "CREATE TABLE " + TABLE_NAME + " (" + 51 | COLUMN_NAME_ENTRY_ID + TEXT_TYPE + " PRIMARY KEY," + 52 | COLUMN_NAME_TITLE + TEXT_TYPE + COMMA_SEP + 53 | COLUMN_NAME_DESCRIPTION + TEXT_TYPE + COMMA_SEP + 54 | COLUMN_NAME_COMPLETED + BOOLEAN_TYPE + 55 | " )" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.data.source.local 18 | 19 | import android.content.ContentValues 20 | import android.content.Context 21 | import android.database.Cursor 22 | import android.database.sqlite.SQLiteDatabase 23 | import android.text.TextUtils 24 | import com.example.android.architecture.blueprints.todoapp.data.Task 25 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource 26 | import com.example.android.architecture.blueprints.todoapp.data.source.local.TasksPersistenceContract.TaskEntry 27 | import com.example.android.architecture.blueprints.todoapp.util.SingletonHolderDoubleArg 28 | import com.example.android.architecture.blueprints.todoapp.util.schedulers.BaseSchedulerProvider 29 | import com.squareup.sqlbrite2.BriteDatabase 30 | import com.squareup.sqlbrite2.SqlBrite 31 | import io.reactivex.Completable 32 | import io.reactivex.Single 33 | import io.reactivex.functions.Function 34 | 35 | /** 36 | * Concrete implementation of a data source as a db. 37 | */ 38 | class TasksLocalDataSource private constructor( 39 | context: Context, 40 | schedulerProvider: BaseSchedulerProvider 41 | ) : TasksDataSource { 42 | 43 | private val databaseHelper: BriteDatabase 44 | private val taskMapperFunction: Function 45 | 46 | init { 47 | val dbHelper = TasksDbHelper(context) 48 | val sqlBrite = SqlBrite.Builder().build() 49 | databaseHelper = sqlBrite.wrapDatabaseHelper(dbHelper, schedulerProvider.io()) 50 | taskMapperFunction = Function { this.getTask(it) } 51 | } 52 | 53 | private fun getTask(c: Cursor): Task { 54 | val itemId = c.getString(c.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_ENTRY_ID)) 55 | val title = c.getString(c.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_TITLE)) 56 | val description = c.getString(c.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_DESCRIPTION)) 57 | val completed = c.getInt(c.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_COMPLETED)) == 1 58 | return Task( 59 | title = title, 60 | description = description, 61 | id = itemId, 62 | completed = completed) 63 | } 64 | 65 | override fun getTasks(): Single> { 66 | val projection = arrayOf( 67 | TaskEntry.COLUMN_NAME_ENTRY_ID, TaskEntry.COLUMN_NAME_TITLE, 68 | TaskEntry.COLUMN_NAME_DESCRIPTION, TaskEntry.COLUMN_NAME_COMPLETED) 69 | 70 | val sql = String.format("SELECT %s FROM %s", 71 | TextUtils.join(",", projection), 72 | TaskEntry.TABLE_NAME) 73 | 74 | return databaseHelper 75 | .createQuery(TaskEntry.TABLE_NAME, sql) 76 | .mapToList(taskMapperFunction) 77 | .firstOrError() 78 | } 79 | 80 | override fun getTask(taskId: String): Single { 81 | val projection = arrayOf( 82 | TaskEntry.COLUMN_NAME_ENTRY_ID, TaskEntry.COLUMN_NAME_TITLE, 83 | TaskEntry.COLUMN_NAME_DESCRIPTION, TaskEntry.COLUMN_NAME_COMPLETED) 84 | 85 | val sql = String.format("SELECT %s FROM %s WHERE %s LIKE ?", 86 | TextUtils.join(",", projection), 87 | TaskEntry.TABLE_NAME, TaskEntry.COLUMN_NAME_ENTRY_ID) 88 | 89 | return databaseHelper 90 | .createQuery(TaskEntry.TABLE_NAME, sql, taskId) 91 | .mapToOne(taskMapperFunction) 92 | .firstOrError() 93 | } 94 | 95 | override fun saveTask(task: Task): Completable { 96 | val values = ContentValues() 97 | values.put(TaskEntry.COLUMN_NAME_ENTRY_ID, task.id) 98 | values.put(TaskEntry.COLUMN_NAME_TITLE, task.title) 99 | values.put(TaskEntry.COLUMN_NAME_DESCRIPTION, task.description) 100 | values.put(TaskEntry.COLUMN_NAME_COMPLETED, task.completed) 101 | databaseHelper.insert(TaskEntry.TABLE_NAME, values, SQLiteDatabase.CONFLICT_REPLACE) 102 | return Completable.complete() 103 | } 104 | 105 | override fun completeTask(task: Task): Completable { 106 | completeTask(task.id) 107 | return Completable.complete() 108 | } 109 | 110 | override fun completeTask(taskId: String): Completable { 111 | val values = ContentValues() 112 | values.put(TaskEntry.COLUMN_NAME_COMPLETED, true) 113 | 114 | val selection = TaskEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?" 115 | val selectionArgs = arrayOf(taskId) 116 | databaseHelper.update(TaskEntry.TABLE_NAME, values, selection, *selectionArgs) 117 | return Completable.complete() 118 | } 119 | 120 | override fun activateTask(task: Task): Completable { 121 | activateTask(task.id) 122 | return Completable.complete() 123 | } 124 | 125 | override fun activateTask(taskId: String): Completable { 126 | val values = ContentValues() 127 | values.put(TaskEntry.COLUMN_NAME_COMPLETED, false) 128 | 129 | val selection = TaskEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?" 130 | val selectionArgs = arrayOf(taskId) 131 | databaseHelper.update(TaskEntry.TABLE_NAME, values, selection, *selectionArgs) 132 | return Completable.complete() 133 | } 134 | 135 | override fun clearCompletedTasks(): Completable { 136 | val selection = TaskEntry.COLUMN_NAME_COMPLETED + " LIKE ?" 137 | val selectionArgs = arrayOf("1") 138 | databaseHelper.delete(TaskEntry.TABLE_NAME, selection, *selectionArgs) 139 | return Completable.complete() 140 | } 141 | 142 | override fun refreshTasks() { 143 | // Not required because the [TasksRepository] handles the logic of refreshing the 144 | // tasks from all the available data sources. 145 | } 146 | 147 | override fun deleteAllTasks() { 148 | databaseHelper.delete(TaskEntry.TABLE_NAME, null) 149 | } 150 | 151 | override fun deleteTask(taskId: String): Completable { 152 | val selection = TaskEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?" 153 | val selectionArgs = arrayOf(taskId) 154 | databaseHelper.delete(TaskEntry.TABLE_NAME, selection, *selectionArgs) 155 | return Completable.complete() 156 | } 157 | 158 | companion object : SingletonHolderDoubleArg( 159 | ::TasksLocalDataSource 160 | ) 161 | } 162 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TasksPersistenceContract.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.data.source.local 18 | 19 | import android.provider.BaseColumns 20 | 21 | /** 22 | * The contract used for the db to save the tasks locally. 23 | */ 24 | object TasksPersistenceContract { 25 | /* Inner class that defines the table contents */ 26 | object TaskEntry : BaseColumns { 27 | const val TABLE_NAME = "tasks" 28 | const val COLUMN_NAME_ENTRY_ID = "entryid" 29 | const val COLUMN_NAME_TITLE = "title" 30 | const val COLUMN_NAME_DESCRIPTION = "description" 31 | const val COLUMN_NAME_COMPLETED = "completed" 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/remote/TasksRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.data.source.remote 18 | 19 | import com.example.android.architecture.blueprints.todoapp.data.Task 20 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource 21 | import io.reactivex.Completable 22 | import io.reactivex.Observable 23 | import io.reactivex.Single 24 | import java.util.LinkedHashMap 25 | import java.util.concurrent.TimeUnit 26 | 27 | /** 28 | * Implementation of the data source that adds a latency simulating network. 29 | */ 30 | object TasksRemoteDataSource : TasksDataSource { 31 | 32 | private const val SERVICE_LATENCY_IN_MILLIS = 5000 33 | private val tasksServiceData: MutableMap 34 | 35 | init { 36 | tasksServiceData = LinkedHashMap(2) 37 | addTask("Build tower in Pisa", "Ground looks good, no foundation work required.") 38 | addTask("Finish bridge in Tacoma", "Found awesome girders at half the cost!") 39 | } 40 | 41 | private fun addTask(title: String, description: String) { 42 | val newTask = Task(title = title, description = description) 43 | tasksServiceData.put(newTask.id, newTask) 44 | } 45 | 46 | override fun getTasks(): Single> { 47 | return Observable.fromIterable(tasksServiceData.values) 48 | .delay(SERVICE_LATENCY_IN_MILLIS.toLong(), TimeUnit.MILLISECONDS) 49 | .toList() 50 | } 51 | 52 | override fun getTask(taskId: String): Single { 53 | return Single.just(tasksServiceData[taskId]) 54 | .delay(SERVICE_LATENCY_IN_MILLIS.toLong(), TimeUnit.MILLISECONDS) 55 | } 56 | 57 | override fun saveTask(task: Task): Completable { 58 | tasksServiceData.put(task.id, task) 59 | return Completable.complete() 60 | } 61 | 62 | override fun completeTask(task: Task): Completable { 63 | val completedTask = Task(task.title!!, task.description, task.id, true) 64 | tasksServiceData.put(task.id, completedTask) 65 | return Completable.complete() 66 | } 67 | 68 | override fun completeTask(taskId: String): Completable { 69 | // Not required for the remote data source because the {@link TasksRepository} handles 70 | // converting from a {@code taskId} to a {@link task} using its cached data. 71 | return Completable.complete() 72 | } 73 | 74 | override fun activateTask(task: Task): Completable { 75 | val activeTask = Task(title = task.title!!, description = task.description!!, id = task.id) 76 | tasksServiceData.put(task.id, activeTask) 77 | return Completable.complete() 78 | } 79 | 80 | override fun activateTask(taskId: String): Completable { 81 | // Not required for the remote data source because the {@link TasksRepository} handles 82 | // converting from a {@code taskId} to a {@link task} using its cached data. 83 | return Completable.complete() 84 | } 85 | 86 | override fun clearCompletedTasks(): Completable { 87 | val it = tasksServiceData.entries.iterator() 88 | while (it.hasNext()) { 89 | val entry = it.next() 90 | if (entry.value.completed) { 91 | it.remove() 92 | } 93 | } 94 | return Completable.complete() 95 | } 96 | 97 | override fun refreshTasks() { 98 | // Not required because the {@link TasksRepository} handles the logic of refreshing the 99 | // tasks from all the available data sources. 100 | } 101 | 102 | override fun deleteAllTasks() { 103 | tasksServiceData.clear() 104 | } 105 | 106 | override fun deleteTask(taskId: String): Completable { 107 | tasksServiceData.remove(taskId) 108 | return Completable.complete() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.mvibase 2 | 3 | /** 4 | * Immutable object which contains all the required information for a business logic to process. 5 | */ 6 | interface MviAction 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviIntent.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.mvibase 2 | 3 | /** 4 | * Immutable object which represent an view's intent. 5 | */ 6 | interface MviIntent 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.mvibase 2 | 3 | /** 4 | * Immutable object resulting of a processed business logic. 5 | */ 6 | interface MviResult 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviView.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.mvibase 2 | 3 | import io.reactivex.Observable 4 | 5 | /** 6 | * Object representing a UI that will 7 | * a) emit its intents to a view model, 8 | * b) subscribes to a view model for rendering its UI. 9 | * 10 | * @param I Top class of the [MviIntent] that the [MviView] will be emitting. 11 | * @param S Top class of the [MviViewState] the [MviView] will be subscribing to. 12 | */ 13 | interface MviView { 14 | /** 15 | * Unique [Observable] used by the [MviViewModel] 16 | * to listen to the [MviView]. 17 | * All the [MviView]'s [MviIntent]s must go through this [Observable]. 18 | */ 19 | fun intents(): Observable 20 | 21 | /** 22 | * Entry point for the [MviView] to render itself based on a [MviViewState]. 23 | */ 24 | fun render(state: S) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.mvibase 2 | 3 | import io.reactivex.Observable 4 | 5 | /** 6 | * Object that will subscribes to a [MviView]'s [MviIntent]s, 7 | * process it and emit a [MviViewState] back. 8 | * 9 | * @param I Top class of the [MviIntent] that the [MviViewModel] will be subscribing 10 | * to. 11 | * @param S Top class of the [MviViewState] the [MviViewModel] will be emitting. 12 | */ 13 | interface MviViewModel { 14 | fun processIntents(intents: Observable) 15 | 16 | fun states(): Observable 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/mvibase/MviViewState.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.mvibase 2 | 3 | /** 4 | * Immutable object which contains all the required information to render a [MviView]. 5 | */ 6 | interface MviViewState 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.statistics 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviAction 4 | 5 | sealed class StatisticsAction : MviAction { 6 | object LoadStatisticsAction : StatisticsAction() 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsActionProcessorHolder.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.statistics 2 | 3 | import com.example.android.architecture.blueprints.todoapp.data.Task 4 | import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository 5 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviAction 6 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviResult 7 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewModel 8 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsAction.LoadStatisticsAction 9 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsResult.LoadStatisticsResult 10 | import com.example.android.architecture.blueprints.todoapp.util.flatMapIterable 11 | import com.example.android.architecture.blueprints.todoapp.util.schedulers.BaseSchedulerProvider 12 | import io.reactivex.Observable 13 | import io.reactivex.ObservableTransformer 14 | import io.reactivex.Single 15 | import io.reactivex.functions.BiFunction 16 | 17 | /** 18 | * Contains and executes the business logic for all emitted [MviAction] 19 | * and returns one unique [Observable] of [MviResult]. 20 | * 21 | * 22 | * This could have been included inside the [MviViewModel] 23 | * but was separated to ease maintenance, as the [MviViewModel] was getting too big. 24 | */ 25 | class StatisticsActionProcessorHolder( 26 | private val tasksRepository: TasksRepository, 27 | private val schedulerProvider: BaseSchedulerProvider 28 | ) { 29 | 30 | private val loadStatisticsProcessor = 31 | ObservableTransformer { actions -> 32 | actions.flatMap { 33 | tasksRepository.getTasks() 34 | // Transform one event of a List to an observable. 35 | .flatMapIterable() 36 | // Count all active and completed tasks and wrap the result into a Pair. 37 | .publish { shared -> 38 | Single.zip( 39 | shared.filter(Task::active).count().map(Long::toInt), 40 | shared.filter(Task::completed).count().map(Long::toInt), 41 | BiFunction { activeCount, completedCount -> 42 | LoadStatisticsResult.Success(activeCount, completedCount) 43 | } 44 | ).toObservable() 45 | } 46 | .cast(LoadStatisticsResult::class.java) 47 | // Wrap any error into an immutable object and pass it down the stream 48 | // without crashing. 49 | // Because errors are data and hence, should just be part of the stream. 50 | .onErrorReturn(LoadStatisticsResult::Failure) 51 | .subscribeOn(schedulerProvider.io()) 52 | .observeOn(schedulerProvider.ui()) 53 | // Emit an InFlight event to notify the subscribers (e.g. the UI) we are 54 | // doing work and waiting on a response. 55 | // We emit it after observing on the UI thread to allow the event to be emitted 56 | // on the current frame and avoid jank. 57 | .startWith(LoadStatisticsResult.InFlight) 58 | } 59 | } 60 | 61 | /** 62 | * Splits the [Observable] to match each type of [MviAction] to its corresponding business logic 63 | * processor. Each processor takes a defined [MviAction], returns a defined [MviResult]. 64 | * The global actionProcessor then merges all [Observable] back to one unique [Observable]. 65 | * 66 | * The splitting is done using [Observable.publish] which allows almost anything 67 | * on the passed [Observable] as long as one and only one [Observable] is returned. 68 | * 69 | * An security layer is also added for unhandled [MviAction] to allow early crash 70 | * at runtime to easy the maintenance. 71 | */ 72 | var actionProcessor = 73 | ObservableTransformer { actions -> 74 | actions.publish { shared -> 75 | // Match LoadStatisticsResult to loadStatisticsProcessor 76 | shared.ofType(LoadStatisticsAction::class.java).compose(loadStatisticsProcessor) 77 | .cast(StatisticsResult::class.java) 78 | .mergeWith( 79 | // Error for not implemented actions 80 | shared.filter { v -> v !is LoadStatisticsAction } 81 | .flatMap { w -> 82 | Observable.error( 83 | IllegalArgumentException("Unknown Action type: " + w)) 84 | }) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.statistics 18 | 19 | import android.os.Bundle 20 | import android.support.design.widget.NavigationView 21 | import android.support.v4.app.NavUtils 22 | import android.support.v4.view.GravityCompat 23 | import android.support.v4.widget.DrawerLayout 24 | import android.support.v7.app.AppCompatActivity 25 | import android.view.MenuItem 26 | import com.example.android.architecture.blueprints.todoapp.R 27 | import com.example.android.architecture.blueprints.todoapp.util.addFragmentToActivity 28 | 29 | /** 30 | * Show statistics for tasks. 31 | */ 32 | class StatisticsActivity : AppCompatActivity() { 33 | private lateinit var drawerLayout: DrawerLayout 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | 38 | setContentView(R.layout.statistics_act) 39 | 40 | // Set up the toolbar. 41 | setSupportActionBar(findViewById(R.id.toolbar)) 42 | supportActionBar?.run { 43 | setTitle(R.string.statistics_title) 44 | setHomeAsUpIndicator(R.drawable.ic_menu) 45 | setDisplayHomeAsUpEnabled(true) 46 | } 47 | 48 | // Set up the navigation drawer. 49 | drawerLayout = findViewById(R.id.drawer_layout) 50 | drawerLayout.setStatusBarBackground(R.color.colorPrimaryDark) 51 | 52 | findViewById(R.id.nav_view)?.let { setupDrawerContent(it) } 53 | 54 | if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { 55 | addFragmentToActivity(supportFragmentManager, StatisticsFragment(), R.id.contentFrame) 56 | } 57 | } 58 | 59 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 60 | when (item.itemId) { 61 | android.R.id.home -> { 62 | // Open the navigation drawer when the home icon is selected from the toolbar. 63 | drawerLayout.openDrawer(GravityCompat.START) 64 | return true 65 | } 66 | } 67 | return super.onOptionsItemSelected(item) 68 | } 69 | 70 | private fun setupDrawerContent(navigationView: NavigationView) { 71 | navigationView.setNavigationItemSelectedListener { menuItem -> 72 | when (menuItem.itemId) { 73 | R.id.list_navigation_menu_item -> NavUtils.navigateUpFromSameTask(this@StatisticsActivity) 74 | R.id.statistics_navigation_menu_item -> { 75 | // Do nothing, we're already on that screen 76 | } 77 | } 78 | // Close the navigation drawer when an item is selected. 79 | menuItem.isChecked = true 80 | drawerLayout.closeDrawers() 81 | true 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.statistics 18 | 19 | import android.arch.lifecycle.ViewModelProviders 20 | import android.os.Bundle 21 | import android.support.v4.app.Fragment 22 | import android.view.LayoutInflater 23 | import android.view.View 24 | import android.view.ViewGroup 25 | import android.widget.TextView 26 | import com.example.android.architecture.blueprints.todoapp.R 27 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviIntent 28 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviView 29 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewModel 30 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewState 31 | import com.example.android.architecture.blueprints.todoapp.util.ToDoViewModelFactory 32 | import io.reactivex.Observable 33 | import io.reactivex.disposables.CompositeDisposable 34 | import kotlin.LazyThreadSafetyMode.NONE 35 | 36 | /** 37 | * Main UI for the statistics screen. 38 | */ 39 | class StatisticsFragment : Fragment(), MviView { 40 | private lateinit var statisticsTV: TextView 41 | // Used to manage the data flow lifecycle and avoid memory leak. 42 | private val disposables: CompositeDisposable = CompositeDisposable() 43 | private val viewModel: StatisticsViewModel by lazy(NONE) { 44 | ViewModelProviders 45 | .of(this, ToDoViewModelFactory.getInstance(context!!)) 46 | .get(StatisticsViewModel::class.java) 47 | } 48 | 49 | override fun onCreateView( 50 | inflater: LayoutInflater, 51 | container: ViewGroup?, 52 | savedInstanceState: Bundle? 53 | ): View? { 54 | return inflater.inflate(R.layout.statistics_frag, container, false) 55 | .also { statisticsTV = it.findViewById(R.id.statistics) } 56 | } 57 | 58 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 59 | super.onViewCreated(view, savedInstanceState) 60 | bind() 61 | } 62 | 63 | /** 64 | * Connect the [MviView] with the [MviViewModel]. 65 | * We subscribe to the [MviViewModel] before passing it the [MviView]'s [MviIntent]s. 66 | * If we were to pass [MviIntent]s to the [MviViewModel] before listening to it, 67 | * emitted [MviViewState]s could be lost. 68 | */ 69 | private fun bind() { 70 | // Subscribe to the ViewModel and call render for every emitted state 71 | disposables.add( 72 | viewModel.states().subscribe { this.render(it) } 73 | ) 74 | // Pass the UI's intents to the ViewModel 75 | viewModel.processIntents(intents()) 76 | } 77 | 78 | override fun onDestroy() { 79 | super.onDestroy() 80 | disposables.dispose() 81 | } 82 | 83 | override fun intents(): Observable = initialIntent() 84 | 85 | /** 86 | * The initial Intent the [MviView] emit to convey to the [MviViewModel] 87 | * that it is ready to receive data. 88 | * This initial Intent is also used to pass any parameters the [MviViewModel] might need 89 | * to render the initial [MviViewState] (e.g. the task id to load). 90 | */ 91 | private fun initialIntent(): Observable { 92 | return Observable.just(StatisticsIntent.InitialIntent) 93 | } 94 | 95 | override fun render(state: StatisticsViewState) { 96 | if (state.isLoading) statisticsTV.text = getString(R.string.loading) 97 | if (state.error != null) { 98 | statisticsTV.text = resources.getString(R.string.statistics_error) 99 | } 100 | 101 | if (state.error == null && !state.isLoading) { 102 | showStatistics(state.activeCount, state.completedCount) 103 | } 104 | } 105 | 106 | private fun showStatistics(numberOfActiveTasks: Int, numberOfCompletedTasks: Int) { 107 | if (numberOfCompletedTasks == 0 && numberOfActiveTasks == 0) { 108 | statisticsTV.text = resources.getString(R.string.statistics_no_tasks) 109 | } else { 110 | val displayString = (resources.getString(R.string.statistics_active_tasks) 111 | + " " 112 | + numberOfActiveTasks 113 | + "\n" 114 | + resources.getString(R.string.statistics_completed_tasks) 115 | + " " 116 | + numberOfCompletedTasks) 117 | statisticsTV.text = displayString 118 | } 119 | } 120 | 121 | companion object { 122 | operator fun invoke(): StatisticsFragment = StatisticsFragment() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsIntent.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.statistics 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviIntent 4 | 5 | sealed class StatisticsIntent : MviIntent { 6 | object InitialIntent : StatisticsIntent() 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.statistics 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviResult 4 | 5 | sealed class StatisticsResult : MviResult { 6 | sealed class LoadStatisticsResult : StatisticsResult() { 7 | data class Success(val activeCount: Int, val completedCount: Int) : LoadStatisticsResult() 8 | data class Failure(val error: Throwable) : LoadStatisticsResult() 9 | object InFlight : LoadStatisticsResult() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.statistics 18 | 19 | import android.arch.lifecycle.ViewModel 20 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviAction 21 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviIntent 22 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviResult 23 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviView 24 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewModel 25 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewState 26 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsAction.LoadStatisticsAction 27 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsResult.LoadStatisticsResult 28 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsResult.LoadStatisticsResult.Failure 29 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsResult.LoadStatisticsResult.InFlight 30 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsResult.LoadStatisticsResult.Success 31 | import com.example.android.architecture.blueprints.todoapp.util.notOfType 32 | import io.reactivex.Observable 33 | import io.reactivex.ObservableTransformer 34 | import io.reactivex.disposables.CompositeDisposable 35 | import io.reactivex.functions.BiFunction 36 | import io.reactivex.subjects.PublishSubject 37 | 38 | /** 39 | * Listens to user actions from the UI ([StatisticsFragment]), retrieves the data and updates 40 | * the UI as required. 41 | * 42 | * @property actionProcessorHolder Contains and executes the business logic of all emitted actions. 43 | */ 44 | class StatisticsViewModel( 45 | private val actionProcessorHolder: StatisticsActionProcessorHolder 46 | ) : ViewModel(), MviViewModel { 47 | 48 | /** 49 | * Proxy subject used to keep the stream alive even after the UI gets recycled. 50 | * This is basically used to keep ongoing events and the last cached State alive 51 | * while the UI disconnects and reconnects on config changes. 52 | */ 53 | private val intentsSubject: PublishSubject = PublishSubject.create() 54 | private val statesObservable: Observable = compose() 55 | private val disposables = CompositeDisposable() 56 | 57 | /** 58 | * take only the first ever InitialIntent and all intents of other types 59 | * to avoid reloading data on config changes 60 | */ 61 | private val intentFilter: ObservableTransformer 62 | get() = ObservableTransformer { intents -> 63 | intents.publish { shared -> 64 | Observable.merge( 65 | shared.ofType(StatisticsIntent.InitialIntent::class.java).take(1), 66 | shared.notOfType(StatisticsIntent.InitialIntent::class.java) 67 | ) 68 | } 69 | } 70 | 71 | override fun processIntents(intents: Observable) { 72 | disposables.add(intents.subscribe(intentsSubject::onNext)) 73 | } 74 | 75 | override fun states(): Observable = statesObservable 76 | 77 | /** 78 | * Compose all components to create the stream logic 79 | */ 80 | private fun compose(): Observable { 81 | return intentsSubject 82 | .compose(intentFilter) 83 | .map(this::actionFromIntent) 84 | .compose(actionProcessorHolder.actionProcessor) 85 | // Cache each state and pass it to the reducer to create a new state from 86 | // the previous cached one and the latest Result emitted from the action processor. 87 | // The Scan operator is used here for the caching. 88 | .scan(StatisticsViewState.idle(), reducer) 89 | // When a reducer just emits previousState, there's no reason to call render. In fact, 90 | // redrawing the UI in cases like this can cause jank (e.g. messing up snackbar animations 91 | // by showing the same snackbar twice in rapid succession). 92 | .distinctUntilChanged() 93 | // Emit the last one event of the stream on subscription. 94 | // Useful when a View rebinds to the ViewModel after rotation. 95 | .replay(1) 96 | // Create the stream on creation without waiting for anyone to subscribe 97 | // This allows the stream to stay alive even when the UI disconnects and 98 | // match the stream's lifecycle to the ViewModel's one. 99 | .autoConnect(0) 100 | } 101 | 102 | /** 103 | * Translate an [MviIntent] to an [MviAction]. 104 | * Used to decouple the UI and the business logic to allow easy testings and reusability. 105 | */ 106 | private fun actionFromIntent(intent: StatisticsIntent): StatisticsAction { 107 | return when (intent) { 108 | is StatisticsIntent.InitialIntent -> LoadStatisticsAction 109 | } 110 | } 111 | 112 | override fun onCleared() { 113 | disposables.dispose() 114 | } 115 | 116 | companion object { 117 | /** 118 | * The Reducer is where [MviViewState], that the [MviView] will use to 119 | * render itself, are created. 120 | * It takes the last cached [MviViewState], the latest [MviResult] and 121 | * creates a new [MviViewState] by only updating the related fields. 122 | * This is basically like a big switch statement of all possible types for the [MviResult] 123 | */ 124 | private val reducer = BiFunction { previousState: StatisticsViewState, result: StatisticsResult -> 125 | when (result) { 126 | is LoadStatisticsResult -> when (result) { 127 | is Success -> 128 | previousState.copy( 129 | isLoading = false, 130 | activeCount = result.activeCount, 131 | completedCount = result.completedCount 132 | ) 133 | is Failure -> previousState.copy(isLoading = false, error = result.error) 134 | is InFlight -> previousState.copy(isLoading = true) 135 | } 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewState.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.statistics 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewState 4 | 5 | data class StatisticsViewState( 6 | val isLoading: Boolean, 7 | val activeCount: Int, 8 | val completedCount: Int, 9 | val error: Throwable? 10 | ) : MviViewState { 11 | companion object { 12 | fun idle(): StatisticsViewState { 13 | return StatisticsViewState( 14 | isLoading = false, 15 | activeCount = 0, 16 | completedCount = 0, 17 | error = null 18 | ) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.taskdetail 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviAction 4 | 5 | sealed class TaskDetailAction : MviAction { 6 | data class PopulateTaskAction(val taskId: String) : TaskDetailAction() 7 | data class DeleteTaskAction(val taskId: String) : TaskDetailAction() 8 | data class ActivateTaskAction(val taskId: String) : TaskDetailAction() 9 | data class CompleteTaskAction(val taskId: String) : TaskDetailAction() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.taskdetail 18 | 19 | import android.os.Bundle 20 | import android.support.v7.app.AppCompatActivity 21 | import com.example.android.architecture.blueprints.todoapp.R 22 | import com.example.android.architecture.blueprints.todoapp.util.addFragmentToActivity 23 | 24 | /** 25 | * Displays task details screen. 26 | */ 27 | class TaskDetailActivity : AppCompatActivity() { 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | 31 | setContentView(R.layout.taskdetail_act) 32 | 33 | // Set up the toolbar. 34 | setSupportActionBar(findViewById(R.id.toolbar)) 35 | supportActionBar?.run { 36 | setDisplayHomeAsUpEnabled(true) 37 | setDisplayShowHomeEnabled(true) 38 | } 39 | 40 | // Get the requested task id 41 | val taskId = intent.getStringExtra(EXTRA_TASK_ID) 42 | 43 | if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { 44 | addFragmentToActivity(supportFragmentManager, TaskDetailFragment(taskId), R.id.contentFrame) 45 | } 46 | } 47 | 48 | override fun onSupportNavigateUp(): Boolean { 49 | onBackPressed() 50 | return true 51 | } 52 | 53 | companion object { 54 | const val EXTRA_TASK_ID = "TASK_ID" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailIntent.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.taskdetail 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviIntent 4 | 5 | sealed class TaskDetailIntent : MviIntent { 6 | data class InitialIntent(val taskId: String) : TaskDetailIntent() 7 | data class DeleteTask(val taskId: String) : TaskDetailIntent() 8 | data class ActivateTaskIntent(val taskId: String) : TaskDetailIntent() 9 | data class CompleteTaskIntent(val taskId: String) : TaskDetailIntent() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.taskdetail 2 | 3 | import com.example.android.architecture.blueprints.todoapp.data.Task 4 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviResult 5 | 6 | sealed class TaskDetailResult : MviResult { 7 | 8 | sealed class PopulateTaskResult : TaskDetailResult() { 9 | data class Success(val task: Task) : PopulateTaskResult() 10 | data class Failure(val error: Throwable) : PopulateTaskResult() 11 | object InFlight : PopulateTaskResult() 12 | } 13 | 14 | sealed class ActivateTaskResult : TaskDetailResult() { 15 | data class Success(val task: Task) : ActivateTaskResult() 16 | data class Failure(val error: Throwable) : ActivateTaskResult() 17 | object InFlight : ActivateTaskResult() 18 | object HideUiNotification : ActivateTaskResult() 19 | } 20 | 21 | sealed class CompleteTaskResult : TaskDetailResult() { 22 | data class Success(val task: Task) : CompleteTaskResult() 23 | data class Failure(val error: Throwable) : CompleteTaskResult() 24 | object InFlight : CompleteTaskResult() 25 | object HideUiNotification : CompleteTaskResult() 26 | } 27 | 28 | sealed class DeleteTaskResult : TaskDetailResult() { 29 | object Success : DeleteTaskResult() 30 | data class Failure(val error: Throwable) : DeleteTaskResult() 31 | object InFlight : DeleteTaskResult() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewState.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.taskdetail 2 | 3 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewState 4 | 5 | data class TaskDetailViewState( 6 | val title: String, 7 | val description: String, 8 | val active: Boolean, 9 | val loading: Boolean, 10 | val error: Throwable?, 11 | val uiNotification: UiNotification? 12 | ) : MviViewState { 13 | enum class UiNotification { 14 | TASK_COMPLETE, 15 | TASK_ACTIVATED, 16 | TASK_DELETED 17 | } 18 | 19 | companion object { 20 | fun idle(): TaskDetailViewState { 21 | return TaskDetailViewState( 22 | title = "", 23 | description = "", 24 | active = false, 25 | loading = false, 26 | error = null, 27 | uiNotification = null 28 | ) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/ScrollChildSwipeRefreshLayout.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.tasks 18 | 19 | import android.content.Context 20 | import android.support.v4.widget.SwipeRefreshLayout 21 | import android.util.AttributeSet 22 | import android.view.View 23 | 24 | /** 25 | * Extends [SwipeRefreshLayout] to support non-direct descendant scrolling views. 26 | * 27 | * [SwipeRefreshLayout] works as expected when a scroll view is a direct child: it triggers 28 | * the refresh only when the view is on top. This class adds a way (@link #setScrollUpChild} to 29 | * define which view controls this behavior. 30 | */ 31 | class ScrollChildSwipeRefreshLayout : SwipeRefreshLayout { 32 | private var scrollUpChild: View? = null 33 | 34 | constructor(context: Context) : super(context) 35 | 36 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 37 | 38 | override fun canChildScrollUp(): Boolean { 39 | return scrollUpChild?.canScrollVertically(-1) ?: super.canChildScrollUp() 40 | } 41 | 42 | fun setScrollUpChild(view: View) { 43 | scrollUpChild = view 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.tasks 2 | 3 | import com.example.android.architecture.blueprints.todoapp.data.Task 4 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviAction 5 | 6 | sealed class TasksAction : MviAction { 7 | data class LoadTasksAction( 8 | val forceUpdate: Boolean, 9 | val filterType: TasksFilterType? 10 | ) : TasksAction() 11 | 12 | data class ActivateTaskAction(val task: Task) : TasksAction() 13 | 14 | data class CompleteTaskAction(val task: Task) : TasksAction() 15 | 16 | object ClearCompletedTasksAction : TasksAction() 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.tasks 18 | 19 | import android.content.Intent 20 | import android.os.Bundle 21 | import android.support.design.widget.NavigationView 22 | import android.support.v4.view.GravityCompat 23 | import android.support.v4.widget.DrawerLayout 24 | import android.support.v7.app.AppCompatActivity 25 | import android.view.MenuItem 26 | import com.example.android.architecture.blueprints.todoapp.R 27 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsActivity 28 | import com.example.android.architecture.blueprints.todoapp.util.addFragmentToActivity 29 | 30 | class TasksActivity : AppCompatActivity() { 31 | private lateinit var drawerLayout: DrawerLayout 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | setContentView(R.layout.tasks_act) 36 | 37 | // Set up the toolbar. 38 | setSupportActionBar(findViewById(R.id.toolbar)) 39 | supportActionBar!!.run { 40 | setHomeAsUpIndicator(R.drawable.ic_menu) 41 | setDisplayHomeAsUpEnabled(true) 42 | } 43 | 44 | // Set up the navigation drawer. 45 | drawerLayout = findViewById(R.id.drawer_layout) 46 | drawerLayout.setStatusBarBackground(R.color.colorPrimaryDark) 47 | val navigationView = findViewById(R.id.nav_view) 48 | if (navigationView != null) { 49 | setupDrawerContent(navigationView) 50 | } 51 | 52 | if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { 53 | addFragmentToActivity(supportFragmentManager, TasksFragment(), R.id.contentFrame) 54 | } 55 | } 56 | 57 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 58 | when (item.itemId) { 59 | android.R.id.home -> { 60 | // Open the navigation drawer when the home icon is selected from the toolbar. 61 | drawerLayout.openDrawer(GravityCompat.START) 62 | return true 63 | } 64 | } 65 | return super.onOptionsItemSelected(item) 66 | } 67 | 68 | private fun setupDrawerContent(navigationView: NavigationView) { 69 | navigationView.setNavigationItemSelectedListener { menuItem -> 70 | when (menuItem.itemId) { 71 | R.id.list_navigation_menu_item -> { 72 | // Do nothing, we're already on that screen 73 | } 74 | R.id.statistics_navigation_menu_item -> { 75 | val intent = Intent(this@TasksActivity, StatisticsActivity::class.java) 76 | startActivity(intent) 77 | } 78 | else -> { 79 | } 80 | } 81 | // Close the navigation drawer when an item is selected. 82 | menuItem.isChecked = true 83 | drawerLayout.closeDrawers() 84 | true 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.tasks 2 | 3 | import android.support.v4.content.ContextCompat 4 | import android.support.v4.view.ViewCompat 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.BaseAdapter 9 | import android.widget.CheckBox 10 | import android.widget.TextView 11 | import com.example.android.architecture.blueprints.todoapp.R 12 | import com.example.android.architecture.blueprints.todoapp.data.Task 13 | import io.reactivex.Observable 14 | import io.reactivex.subjects.PublishSubject 15 | 16 | class TasksAdapter(tasks: List) : BaseAdapter() { 17 | private val taskClickSubject = PublishSubject.create() 18 | private val taskToggleSubject = PublishSubject.create() 19 | private lateinit var tasks: List 20 | 21 | val taskClickObservable: Observable 22 | get() = taskClickSubject 23 | 24 | val taskToggleObservable: Observable 25 | get() = taskToggleSubject 26 | 27 | init { 28 | setList(tasks) 29 | } 30 | 31 | fun replaceData(tasks: List) { 32 | setList(tasks) 33 | notifyDataSetChanged() 34 | } 35 | 36 | private fun setList(tasks: List) { 37 | this.tasks = tasks 38 | } 39 | 40 | override fun getCount(): Int = tasks.size 41 | 42 | override fun getItem(position: Int): Task = tasks[position] 43 | 44 | override fun getItemId(position: Int): Long = position.toLong() 45 | 46 | override fun getView(position: Int, view: View?, viewGroup: ViewGroup): View { 47 | val rowView: View = view 48 | ?: LayoutInflater.from(viewGroup.context).inflate(R.layout.task_item, viewGroup, false) 49 | 50 | val task = getItem(position) 51 | 52 | rowView.findViewById(R.id.title).text = task.titleForList 53 | 54 | val completeCB = rowView.findViewById(R.id.complete) 55 | 56 | // Active/completed task UI 57 | completeCB.isChecked = task.completed 58 | if (task.completed) { 59 | ViewCompat.setBackground( 60 | rowView, 61 | ContextCompat.getDrawable(viewGroup.context, R.drawable.list_completed_touch_feedback)) 62 | } else { 63 | ViewCompat.setBackground( 64 | rowView, 65 | ContextCompat.getDrawable(viewGroup.context, R.drawable.touch_feedback)) 66 | } 67 | 68 | completeCB.setOnClickListener { taskToggleSubject.onNext(task) } 69 | 70 | rowView.setOnClickListener { taskClickSubject.onNext(task) } 71 | 72 | return rowView 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFilterType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.tasks 18 | 19 | /** 20 | * Used with the filter spinner in the tasks list. 21 | */ 22 | enum class TasksFilterType { 23 | /** 24 | * Do not filter tasks. 25 | */ 26 | ALL_TASKS, 27 | 28 | /** 29 | * Filters only the active (not completed yet) tasks. 30 | */ 31 | ACTIVE_TASKS, 32 | 33 | /** 34 | * Filters only the completed tasks. 35 | */ 36 | COMPLETED_TASKS 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksIntent.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.tasks 2 | 3 | import com.example.android.architecture.blueprints.todoapp.data.Task 4 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviIntent 5 | 6 | sealed class TasksIntent : MviIntent { 7 | object InitialIntent : TasksIntent() 8 | 9 | data class RefreshIntent(val forceUpdate: Boolean) : TasksIntent() 10 | 11 | data class ActivateTaskIntent(val task: Task) : TasksIntent() 12 | 13 | data class CompleteTaskIntent(val task: Task) : TasksIntent() 14 | 15 | object ClearCompletedTasksIntent : TasksIntent() 16 | 17 | data class ChangeFilterIntent(val filterType: TasksFilterType) : TasksIntent() 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksResult.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.tasks 2 | 3 | import com.example.android.architecture.blueprints.todoapp.data.Task 4 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviResult 5 | 6 | sealed class TasksResult : MviResult { 7 | sealed class LoadTasksResult : TasksResult() { 8 | data class Success(val tasks: List, val filterType: TasksFilterType?) : LoadTasksResult() 9 | data class Failure(val error: Throwable) : LoadTasksResult() 10 | object InFlight : LoadTasksResult() 11 | } 12 | 13 | sealed class ActivateTaskResult : TasksResult() { 14 | data class Success(val tasks: List) : ActivateTaskResult() 15 | data class Failure(val error: Throwable) : ActivateTaskResult() 16 | object InFlight : ActivateTaskResult() 17 | object HideUiNotification : ActivateTaskResult() 18 | } 19 | 20 | sealed class CompleteTaskResult : TasksResult() { 21 | data class Success(val tasks: List) : CompleteTaskResult() 22 | data class Failure(val error: Throwable) : CompleteTaskResult() 23 | object InFlight : CompleteTaskResult() 24 | object HideUiNotification : CompleteTaskResult() 25 | } 26 | 27 | sealed class ClearCompletedTasksResult : TasksResult() { 28 | data class Success(val tasks: List) : ClearCompletedTasksResult() 29 | data class Failure(val error: Throwable) : ClearCompletedTasksResult() 30 | object InFlight : ClearCompletedTasksResult() 31 | object HideUiNotification : ClearCompletedTasksResult() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewState.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.tasks 2 | 3 | import com.example.android.architecture.blueprints.todoapp.data.Task 4 | import com.example.android.architecture.blueprints.todoapp.mvibase.MviViewState 5 | import com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ALL_TASKS 6 | 7 | data class TasksViewState( 8 | val isLoading: Boolean, 9 | val tasksFilterType: TasksFilterType, 10 | val tasks: List, 11 | val error: Throwable?, 12 | val uiNotification: UiNotification? 13 | ) : MviViewState { 14 | enum class UiNotification { 15 | TASK_COMPLETE, 16 | TASK_ACTIVATED, 17 | COMPLETE_TASKS_CLEARED 18 | } 19 | 20 | companion object { 21 | fun idle(): TasksViewState { 22 | return TasksViewState( 23 | isLoading = false, 24 | tasksFilterType = ALL_TASKS, 25 | tasks = emptyList(), 26 | error = null, 27 | uiNotification = null 28 | ) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ActivityUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.architecture.blueprints.todoapp.util 18 | 19 | import android.annotation.SuppressLint 20 | import android.support.v4.app.Fragment 21 | import android.support.v4.app.FragmentManager 22 | 23 | /** 24 | * The `fragment` is added to the container view with id `frameId`. The operation is 25 | * performed by the `fragmentManager`. 26 | */ 27 | @SuppressLint("CommitTransaction") 28 | fun addFragmentToActivity( 29 | fragmentManager: FragmentManager, 30 | fragment: Fragment, 31 | frameId: Int) { 32 | fragmentManager.beginTransaction().run { 33 | add(frameId, fragment) 34 | commit() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ObservableUtils.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.util 2 | 3 | import io.reactivex.Observable 4 | import java.util.concurrent.TimeUnit 5 | 6 | /** 7 | * Emit an event immediately, then emit an other event after a delay has passed. 8 | * It is used for time limited UI state (e.g. a snackbar) to allow the stream to control 9 | * the timing for the showing and the hiding of a UI component. 10 | * 11 | * @param immediate Immediately emitted event 12 | * @param delayed Event emitted after a delay 13 | */ 14 | fun pairWithDelay(immediate: T, delayed: T): Observable { 15 | return Observable.timer(2, TimeUnit.SECONDS) 16 | .map { delayed } 17 | .startWith(immediate) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/RxExt.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.util 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.Single 5 | import io.reactivex.annotations.CheckReturnValue 6 | import io.reactivex.annotations.SchedulerSupport 7 | 8 | @CheckReturnValue 9 | @SchedulerSupport(SchedulerSupport.NONE) 10 | fun > Single.flatMapIterable(): Observable { 11 | return this.flatMapObservable { 12 | Observable.fromIterable(it) 13 | } 14 | } 15 | 16 | @CheckReturnValue 17 | @SchedulerSupport(SchedulerSupport.NONE) 18 | fun Observable.notOfType(clazz: Class): Observable { 19 | checkNotNull(clazz) { "clazz is null" } 20 | return filter { !clazz.isInstance(it) } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/SingletonHolderDoubleArg.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.util 2 | 3 | /** 4 | * Used to allow Singleton with arguments in Kotlin while keeping the code efficient and safe. 5 | * 6 | * See https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e 7 | */ 8 | open class SingletonHolderDoubleArg(creator: (A, B) -> T) { 9 | private var creator: ((A, B) -> T)? = creator 10 | @Volatile private var instance: T? = null 11 | 12 | fun getInstance(arg1: A, arg2: B): T { 13 | val i = instance 14 | if (i != null) { 15 | return i 16 | } 17 | 18 | return synchronized(this) { 19 | val i2 = instance 20 | if (i2 != null) { 21 | i2 22 | } else { 23 | val created = creator!!(arg1, arg2) 24 | instance = created 25 | created 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * Used to force [SingletonHolderDoubleArg.getInstance] to create a new instance next time it's called. 32 | * Used in tests. 33 | */ 34 | fun clearInstance() { 35 | instance = null 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/SingletonHolderSingleArg.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.util 2 | 3 | /** 4 | * Used to allow Singleton with arguments in Kotlin while keeping the code efficient and safe. 5 | * 6 | * See https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e 7 | */ 8 | open class SingletonHolderSingleArg(creator: (A) -> T) { 9 | private var creator: ((A) -> T)? = creator 10 | @Volatile private var instance: T? = null 11 | 12 | fun getInstance(arg: A): T { 13 | val i = instance 14 | if (i != null) { 15 | return i 16 | } 17 | 18 | return synchronized(this) { 19 | val i2 = instance 20 | if (i2 != null) { 21 | i2 22 | } else { 23 | val created = creator!!(arg) 24 | instance = created 25 | created 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/StringExt.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.util 2 | 3 | fun String?.isNullOrEmpty() = this == null || this.isEmpty() 4 | fun String?.isNotNullNorEmpty() = !this.isNullOrEmpty() -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ToDoViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.util 2 | 3 | import android.arch.lifecycle.ViewModel 4 | import android.arch.lifecycle.ViewModelProvider 5 | import android.content.Context 6 | import com.example.android.architecture.blueprints.todoapp.Injection 7 | import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskActionProcessorHolder 8 | import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskViewModel 9 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsActionProcessorHolder 10 | import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsViewModel 11 | import com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailActionProcessorHolder 12 | import com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailViewModel 13 | import com.example.android.architecture.blueprints.todoapp.tasks.TasksActionProcessorHolder 14 | import com.example.android.architecture.blueprints.todoapp.tasks.TasksViewModel 15 | 16 | class ToDoViewModelFactory private constructor( 17 | private val applicationContext: Context 18 | ) : ViewModelProvider.Factory { 19 | 20 | @Suppress("UNCHECKED_CAST") 21 | override fun create(modelClass: Class): T { 22 | if (modelClass == StatisticsViewModel::class.java) { 23 | return StatisticsViewModel( 24 | StatisticsActionProcessorHolder( 25 | Injection.provideTasksRepository(applicationContext), 26 | Injection.provideSchedulerProvider())) as T 27 | } 28 | if (modelClass == TasksViewModel::class.java) { 29 | return TasksViewModel( 30 | TasksActionProcessorHolder( 31 | Injection.provideTasksRepository(applicationContext), 32 | Injection.provideSchedulerProvider())) as T 33 | } 34 | if (modelClass == AddEditTaskViewModel::class.java) { 35 | return AddEditTaskViewModel( 36 | AddEditTaskActionProcessorHolder( 37 | Injection.provideTasksRepository(applicationContext), 38 | Injection.provideSchedulerProvider())) as T 39 | } 40 | if (modelClass == TaskDetailViewModel::class.java) { 41 | return TaskDetailViewModel( 42 | TaskDetailActionProcessorHolder( 43 | Injection.provideTasksRepository(applicationContext), 44 | Injection.provideSchedulerProvider())) as T 45 | } 46 | throw IllegalArgumentException("unknown model class " + modelClass) 47 | } 48 | 49 | companion object : SingletonHolderSingleArg(::ToDoViewModelFactory) 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/schedulers/BaseSchedulerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.util.schedulers 2 | 3 | import io.reactivex.Scheduler 4 | 5 | /** 6 | * Allow providing different types of [Scheduler]s. 7 | */ 8 | interface BaseSchedulerProvider { 9 | fun computation(): Scheduler 10 | 11 | fun io(): Scheduler 12 | 13 | fun ui(): Scheduler 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/schedulers/ImmediateSchedulerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.util.schedulers 2 | 3 | import io.reactivex.Scheduler 4 | import io.reactivex.schedulers.Schedulers 5 | 6 | /** 7 | * Implementation of the [BaseSchedulerProvider] making all [Scheduler]s immediate. 8 | */ 9 | class ImmediateSchedulerProvider : BaseSchedulerProvider { 10 | override fun computation(): Scheduler = Schedulers.trampoline() 11 | 12 | override fun io(): Scheduler = Schedulers.trampoline() 13 | 14 | override fun ui(): Scheduler = Schedulers.trampoline() 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/schedulers/SchedulerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.example.android.architecture.blueprints.todoapp.util.schedulers 2 | 3 | import io.reactivex.Scheduler 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import io.reactivex.schedulers.Schedulers 6 | 7 | /** 8 | * Provides different types of schedulers. 9 | */ 10 | object SchedulerProvider : BaseSchedulerProvider { 11 | override fun computation(): Scheduler = Schedulers.computation() 12 | 13 | override fun io(): Scheduler = Schedulers.io() 14 | 15 | override fun ui(): Scheduler = AndroidSchedulers.mainThread() 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/drawable-hdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/drawable-mdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/drawable-xhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/drawable-xxhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/drawable-xxxhdpi/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_assignment_turned_in_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_circle_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter_list.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_statistics.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_statistics_100dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_statistics_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_verified_user_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_completed_touch_feedback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/touch_feedback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/layout/addtask_act.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | 29 | 30 | 33 | 34 | 42 | 43 | 44 | 48 | 49 | 53 | 54 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/res/layout/addtask_frag.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 29 | 30 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_header.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 31 | 32 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/statistics_act.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 31 | 32 | 35 | 36 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/statistics_frag.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/task_item.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 32 | 33 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/taskdetail_act.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | 28 | 29 | 37 | 38 | 39 | 43 | 44 | 48 | 49 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/taskdetail_frag.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 32 | 33 | 39 | 40 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/tasks_act.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 31 | 32 | 35 | 36 | 44 | 45 | 46 | 50 | 51 | 55 | 56 | 65 | 66 | 67 | 68 | 69 | 70 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/res/layout/tasks_frag.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | 22 | 28 | 29 | 34 | 35 | 45 | 46 | 50 | 51 | 52 | 58 | 59 | 65 | 66 | 73 | 74 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/res/menu/drawer_actions.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 23 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/menu/filter_tasks.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 23 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/menu/taskdetail_fragment_menu.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/menu/tasks_fragment_menu.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 24 | 28 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldergod/android-architecture/1352ba59453721c9f46200a1f10cd76515940bf0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 21 | 64dp 22 | 23 | 24dp 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | #455A64 19 | #263238 20 | #D50000 21 | 22 | #CCCCCC 23 | 24 | #CFD8DC 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 16dp 20 | 16dp 21 | 22 | 8dp 23 | 24 | 16dp 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | TO-DO-MVI 19 | New TO-DO 20 | Edit TO-DO 21 | Task marked complete 22 | Task marked active 23 | Error while loading tasks 24 | Completed tasks cleared 25 | Filter 26 | Clear completed 27 | Delete task 28 | TO-DOs 29 | Title 30 | Enter your TO-DO here. 31 | TO DOs cannot be empty 32 | TO-DO saved 33 | TO-DO List 34 | Statistics 35 | You have no tasks. 36 | Active tasks: 37 | Completed tasks: 38 | Error loading statistics. 39 | No data 40 | LOADING 41 | 42 | 43 | @string/nav_all 44 | @string/nav_active 45 | @string/nav_completed 46 | 47 | All 48 | Active 49 | Completed 50 | All TO-DOs 51 | Active TO-DOs 52 | Completed TO-DOs 53 | You have no TO-DOs! 54 | You have no active TO-DOs! 55 | You have no completed TO-DOs! 56 | Add a TO-DO item + 57 | Refresh 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 28 | 29 |