diff --git a/.circleci/config.yml b/.circleci/config.yml index 278aff2f8..ec9ee1554 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -131,7 +131,7 @@ workflows: - test_navigation_basic_sample - test_navigation_advanced_sample - test_paging_sample - # flaky, deprecated - test_paging_network_sample + - test_paging_network_sample - test_persistence_content_provider_sample # inst tests with flavors - test_persistence_migrations_sample # temporarily disabled due to #804 - test_workmanager_sample diff --git a/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.properties b/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.properties +++ b/BasicRxJavaSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/BasicRxJavaSample/versions.gradle b/BasicRxJavaSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/BasicRxJavaSample/versions.gradle +++ b/BasicRxJavaSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.properties b/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.properties +++ b/BasicRxJavaSampleKotlin/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/BasicRxJavaSampleKotlin/versions.gradle b/BasicRxJavaSampleKotlin/versions.gradle index b5770b8d9..5dc879294 100644 --- a/BasicRxJavaSampleKotlin/versions.gradle +++ b/BasicRxJavaSampleKotlin/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/BasicSample/gradle/wrapper/gradle-wrapper.properties b/BasicSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/BasicSample/gradle/wrapper/gradle-wrapper.properties +++ b/BasicSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/BasicSample/versions.gradle b/BasicSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/BasicSample/versions.gradle +++ b/BasicSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/GithubBrowserSample/app/build.gradle b/GithubBrowserSample/app/build.gradle index 23db2b769..fd15b3b60 100644 --- a/GithubBrowserSample/app/build.gradle +++ b/GithubBrowserSample/app/build.gradle @@ -15,7 +15,6 @@ */ apply plugin: 'com.android.application' -apply plugin: 'jacoco' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' @@ -65,10 +64,6 @@ android { } } -jacoco { - toolVersion = "0.7.4+" -} - dependencies { implementation deps.app_compat implementation deps.recyclerview diff --git a/GithubBrowserSample/gradle/wrapper/gradle-wrapper.properties b/GithubBrowserSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/GithubBrowserSample/gradle/wrapper/gradle-wrapper.properties +++ b/GithubBrowserSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/GithubBrowserSample/versions.gradle b/GithubBrowserSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/GithubBrowserSample/versions.gradle +++ b/GithubBrowserSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/LiveDataSample/gradle/wrapper/gradle-wrapper.properties b/LiveDataSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/LiveDataSample/gradle/wrapper/gradle-wrapper.properties +++ b/LiveDataSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/LiveDataSample/versions.gradle b/LiveDataSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/LiveDataSample/versions.gradle +++ b/LiveDataSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/MADSkillsNavigationSample/gradle/wrapper/gradle-wrapper.properties b/MADSkillsNavigationSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/MADSkillsNavigationSample/gradle/wrapper/gradle-wrapper.properties +++ b/MADSkillsNavigationSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/MADSkillsNavigationSample/versions.gradle b/MADSkillsNavigationSample/versions.gradle new file mode 100644 index 000000000..5dc879294 --- /dev/null +++ b/MADSkillsNavigationSample/versions.gradle @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * Shared file between builds so that they can all use the same dependencies and + * maven repositories. + **/ +ext.deps = [:] +def versions = [:] +versions.activity = '1.1.0' +versions.android_gradle_plugin = '4.0.0' +versions.annotations = "1.0.0" +versions.apache_commons = "2.5" +versions.appcompat = "1.2.0-alpha02" +versions.arch_core = "2.1.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" +versions.benchmark = "1.1.0-alpha01" +versions.cardview = "1.0.0" +versions.constraint_layout = "2.0.0-alpha2" +versions.core_ktx = "1.1.0" +versions.coroutines = "1.4.1" +versions.dagger = "2.16" +versions.dexmaker = "2.2.0" +versions.espresso = "3.2.0" +versions.fragment = "1.2.0" +versions.glide = "4.8.0" +versions.hamcrest = "1.3" +versions.junit = "4.12" +versions.kotlin = "1.4.21" +versions.lifecycle = "2.2.0" +versions.material = "1.0.0" +versions.mockito = "2.25.0" +versions.mockito_all = "1.10.19" +versions.mockito_android = "2.25.0" +versions.mockwebserver = "3.8.1" +versions.navigation = "2.3.0-alpha01" +versions.okhttp_logging_interceptor = "3.9.0" +versions.paging = "3.1.0-alpha04" +versions.recyclerview = "1.2.0-beta01" +versions.retrofit = "2.9.0" +versions.robolectric = "4.2" +versions.room = "2.4.0-alpha05" +versions.rx_android = "2.0.1" +versions.rxjava2 = "2.1.3" +versions.timber = "4.7.1" +versions.transition = "1.3.0" +versions.truth = "1.0.1" +versions.work = "2.6.0" +ext.versions = versions + +def build_versions = [:] +build_versions.min_sdk = 14 +build_versions.compile_sdk = 29 +build_versions.target_sdk = 29 +build_versions.build_tools = "29.0.3" +ext.build_versions = build_versions + +def deps = [:] + +def activity = [:] +activity.activity_ktx = "androidx.activity:activity-ktx:$versions.activity" +deps.activity = activity + +deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin" + +deps.annotations = "androidx.annotation:annotation:$versions.annotations" + +deps.app_compat = "androidx.appcompat:appcompat:$versions.appcompat" + +def arch_core = [:] +arch_core.runtime = "androidx.arch.core:core-runtime:$versions.arch_core" +arch_core.testing = "androidx.arch.core:core-testing:$versions.arch_core" +deps.arch_core = arch_core + +def atsl = [:] +atsl.core = "androidx.test:core:$versions.atsl_core" +atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" +atsl.runner = "androidx.test:runner:$versions.atsl_runner" +atsl.rules = "androidx.test:rules:$versions.atsl_rules" +deps.atsl = atsl + +deps.benchmark = "androidx.benchmark:benchmark-junit4:$versions.benchmark" +deps.benchmark_gradle = "androidx.benchmark:benchmark-gradle-plugin:$versions.benchmark" + +deps.cardview = "androidx.cardview:cardview:$versions.cardview" + +deps.constraint_layout = "androidx.constraintlayout:constraintlayout:$versions.constraint_layout" + +deps.core_ktx = "androidx.core:core-ktx:$versions.core_ktx" + +def coroutines = [:] +coroutines.android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines" +coroutines.test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.coroutines" +deps.coroutines = coroutines + +def dagger = [:] +dagger.runtime = "com.google.dagger:dagger:$versions.dagger" +dagger.android = "com.google.dagger:dagger-android:$versions.dagger" +dagger.android_support = "com.google.dagger:dagger-android-support:$versions.dagger" +dagger.compiler = "com.google.dagger:dagger-compiler:$versions.dagger" +dagger.android_support_compiler = "com.google.dagger:dagger-android-processor:$versions.dagger" +deps.dagger = dagger + +deps.dexmaker = "com.linkedin.dexmaker:dexmaker-mockito:$versions.dexmaker" + +def espresso = [:] +espresso.core = "androidx.test.espresso:espresso-core:$versions.espresso" +espresso.contrib = "androidx.test.espresso:espresso-contrib:$versions.espresso" +espresso.intents = "androidx.test.espresso:espresso-intents:$versions.espresso" +deps.espresso = espresso + +def fragment = [:] +fragment.runtime = "androidx.fragment:fragment:${versions.fragment}" +fragment.runtime_ktx = "androidx.fragment:fragment-ktx:${versions.fragment}" +fragment.testing = "androidx.fragment:fragment-testing:${versions.fragment}" +deps.fragment = fragment + +def glide = [:] +glide.runtime = "com.github.bumptech.glide:glide:$versions.glide" +glide.compiler = "com.github.bumptech.glide:compiler:$versions.glide" +deps.glide = glide + +deps.hamcrest = "org.hamcrest:hamcrest-all:$versions.hamcrest" + +deps.junit = "junit:junit:$versions.junit" + +def kotlin = [:] +kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin" +kotlin.test = "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin" +kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" +kotlin.allopen = "org.jetbrains.kotlin:kotlin-allopen:$versions.kotlin" +deps.kotlin = kotlin + +def lifecycle = [:] +lifecycle.runtime = "androidx.lifecycle:lifecycle-runtime:$versions.lifecycle" +lifecycle.java8 = "androidx.lifecycle:lifecycle-common-java8:$versions.lifecycle" +lifecycle.compiler = "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle" +lifecycle.viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle" +lifecycle.livedata_ktx = "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle" +deps.lifecycle = lifecycle + +deps.material = "com.google.android.material:material:$versions.material" + +def mockito = [:] +mockito.core = "org.mockito:mockito-core:$versions.mockito" +mockito.all = "org.mockito:mockito-all:$versions.mockito_all" +mockito.android = "org.mockito:mockito-android:$versions.mockito_android" +deps.mockito = mockito + +deps.mock_web_server = "com.squareup.okhttp3:mockwebserver:$versions.mockwebserver" + +def navigation = [:] +navigation.runtime = "androidx.navigation:navigation-runtime:$versions.navigation" +navigation.runtime_ktx = "androidx.navigation:navigation-runtime-ktx:$versions.navigation" +navigation.fragment = "androidx.navigation:navigation-fragment:$versions.navigation" +navigation.fragment_ktx = "androidx.navigation:navigation-fragment-ktx:$versions.navigation" +navigation.testing = "androidx.navigation:navigation-testing:$versions.navigation" +navigation.ui = "androidx.navigation:navigation-ui:$versions.navigation" +navigation.ui_ktx = "androidx.navigation:navigation-ui-ktx:$versions.navigation" +navigation.safe_args_plugin = "androidx.navigation:navigation-safe-args-gradle-plugin:$versions.navigation" +deps.navigation = navigation + +deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp_logging_interceptor}" + +deps.paging_runtime = "androidx.paging:paging-runtime:$versions.paging" + +deps.recyclerview = "androidx.recyclerview:recyclerview:$versions.recyclerview" + +def retrofit = [:] +retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit" +retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit" +retrofit.mock = "com.squareup.retrofit2:retrofit-mock:$versions.retrofit" +deps.retrofit = retrofit + +deps.robolectric = "org.robolectric:robolectric:$versions.robolectric" + +def room = [:] +room.runtime = "androidx.room:room-runtime:$versions.room" +room.compiler = "androidx.room:room-compiler:$versions.room" +room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" +room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" +room.testing = "androidx.room:room-testing:$versions.room" +deps.room = room + +deps.rx_android = "io.reactivex.rxjava2:rxandroid:$versions.rx_android" + +deps.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2" + +deps.timber = "com.jakewharton.timber:timber:$versions.timber" + +deps.transition = "androidx.transition:transition:$versions.transition" + +deps.truth = "com.google.truth:truth:$versions.truth" + +def work = [:] +work.runtime = "androidx.work:work-runtime:$versions.work" +work.testing = "androidx.work:work-testing:$versions.work" +work.firebase = "androidx.work:work-firebase:$versions.work" +work.runtime_ktx = "androidx.work:work-runtime-ktx:$versions.work" +deps.work = work + +ext.deps = deps + +def addRepos(RepositoryHandler handler) { + handler.google() + handler.jcenter() + handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } +} +ext.addRepos = this.&addRepos diff --git a/NavigationAdvancedSample/app/src/androidTest/java/com/example/android/navigationadvancedsample/BottomNavigationTest.kt b/NavigationAdvancedSample/app/src/androidTest/java/com/example/android/navigationadvancedsample/BottomNavigationTest.kt index 225753fa4..0260c9cda 100644 --- a/NavigationAdvancedSample/app/src/androidTest/java/com/example/android/navigationadvancedsample/BottomNavigationTest.kt +++ b/NavigationAdvancedSample/app/src/androidTest/java/com/example/android/navigationadvancedsample/BottomNavigationTest.kt @@ -112,24 +112,6 @@ class BottomNavigationTest { assertDeeperThirdScreen() } - @Test - fun bottomNavView_itemReselected_goesBackToStart() { - openThirdScreen() - - assertThirdScreen() - - onView(withContentDescription(R.string.sign_up)) - .perform(click()) - - assertDeeperThirdScreen() - - // Reselect the current item - openThirdScreen() - - // Verify that it popped the back stack until the start destination. - assertThirdScreen() - } - private fun assertSecondScreen() { onView(allOf(withText(R.string.title_list), isDescendantOfA(withId(R.id.action_bar)))) .check(matches(isDisplayed())) diff --git a/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/MainActivity.kt b/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/MainActivity.kt index 0b838a34c..60c6d29a7 100644 --- a/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/MainActivity.kt +++ b/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/MainActivity.kt @@ -18,10 +18,12 @@ package com.example.android.navigationadvancedsample import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController +import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView /** @@ -29,48 +31,30 @@ import com.google.android.material.bottomnavigation.BottomNavigationView */ class MainActivity : AppCompatActivity() { - private var currentNavController: LiveData? = null + private lateinit var navController: NavController + private lateinit var appBarConfiguration: AppBarConfiguration override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - if (savedInstanceState == null) { - setupBottomNavigationBar() - } // Else, need to wait for onRestoreInstanceState - } - override fun onRestoreInstanceState(savedInstanceState: Bundle?) { - super.onRestoreInstanceState(savedInstanceState) - // Now that BottomNavigationBar has restored its instance state - // and its selectedItemId, we can proceed with setting up the - // BottomNavigationBar with Navigation - setupBottomNavigationBar() - } + val navHostFragment = supportFragmentManager.findFragmentById( + R.id.nav_host_container + ) as NavHostFragment + navController = navHostFragment.navController - /** - * Called on first creation and when restoring state. - */ - private fun setupBottomNavigationBar() { + // Setup the bottom navigation view with navController val bottomNavigationView = findViewById(R.id.bottom_nav) + bottomNavigationView.setupWithNavController(navController) - val navGraphIds = listOf(R.navigation.home, R.navigation.list, R.navigation.form) - - // Setup the bottom navigation view with a list of navigation graphs - val controller = bottomNavigationView.setupWithNavController( - navGraphIds = navGraphIds, - fragmentManager = supportFragmentManager, - containerId = R.id.nav_host_container, - intent = intent + // Setup the ActionBar with navController and 3 top level destinations + appBarConfiguration = AppBarConfiguration( + setOf(R.id.titleScreen, R.id.leaderboard, R.id.register) ) - - // Whenever the selected controller changes, setup the action bar. - controller.observe(this, Observer { navController -> - setupActionBarWithNavController(navController) - }) - currentNavController = controller + setupActionBarWithNavController(navController, appBarConfiguration) } override fun onSupportNavigateUp(): Boolean { - return currentNavController?.value?.navigateUp() ?: false + return navController.navigateUp(appBarConfiguration) } } diff --git a/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt b/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt deleted file mode 100644 index 9c5cbb3fb..000000000 --- a/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2019, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.navigationadvancedsample - -import android.content.Intent -import android.util.SparseArray -import androidx.core.util.forEach -import androidx.core.util.set -import androidx.fragment.app.FragmentManager -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.navigation.NavController -import androidx.navigation.fragment.NavHostFragment -import com.google.android.material.bottomnavigation.BottomNavigationView - -/** - * Manages the various graphs needed for a [BottomNavigationView]. - * - * This sample is a workaround until the Navigation Component supports multiple back stacks. - */ -fun BottomNavigationView.setupWithNavController( - navGraphIds: List, - fragmentManager: FragmentManager, - containerId: Int, - intent: Intent -): LiveData { - - // Map of tags - val graphIdToTagMap = SparseArray() - // Result. Mutable live data with the selected controlled - val selectedNavController = MutableLiveData() - - var firstFragmentGraphId = 0 - - // First create a NavHostFragment for each NavGraph ID - navGraphIds.forEachIndexed { index, navGraphId -> - val fragmentTag = getFragmentTag(index) - - // Find or create the Navigation host fragment - val navHostFragment = obtainNavHostFragment( - fragmentManager, - fragmentTag, - navGraphId, - containerId - ) - - // Obtain its id - val graphId = navHostFragment.navController.graph.id - - if (index == 0) { - firstFragmentGraphId = graphId - } - - // Save to the map - graphIdToTagMap[graphId] = fragmentTag - - // Attach or detach nav host fragment depending on whether it's the selected item. - if (this.selectedItemId == graphId) { - // Update livedata with the selected graph - selectedNavController.value = navHostFragment.navController - attachNavHostFragment(fragmentManager, navHostFragment, index == 0) - } else { - detachNavHostFragment(fragmentManager, navHostFragment) - } - } - - // Now connect selecting an item with swapping Fragments - var selectedItemTag = graphIdToTagMap[this.selectedItemId] - val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId] - var isOnFirstFragment = selectedItemTag == firstFragmentTag - - // When a navigation item is selected - setOnNavigationItemSelectedListener { item -> - // Don't do anything if the state is state has already been saved. - if (fragmentManager.isStateSaved) { - false - } else { - val newlySelectedItemTag = graphIdToTagMap[item.itemId] - if (selectedItemTag != newlySelectedItemTag) { - // Pop everything above the first fragment (the "fixed start destination") - fragmentManager.popBackStack(firstFragmentTag, - FragmentManager.POP_BACK_STACK_INCLUSIVE) - val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag) - as NavHostFragment - - // Exclude the first fragment tag because it's always in the back stack. - if (firstFragmentTag != newlySelectedItemTag) { - // Commit a transaction that cleans the back stack and adds the first fragment - // to it, creating the fixed started destination. - fragmentManager.beginTransaction() - .setCustomAnimations( - R.anim.nav_default_enter_anim, - R.anim.nav_default_exit_anim, - R.anim.nav_default_pop_enter_anim, - R.anim.nav_default_pop_exit_anim) - .attach(selectedFragment) - .setPrimaryNavigationFragment(selectedFragment) - .apply { - // Detach all other Fragments - graphIdToTagMap.forEach { _, fragmentTagIter -> - if (fragmentTagIter != newlySelectedItemTag) { - detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!) - } - } - } - .addToBackStack(firstFragmentTag) - .setReorderingAllowed(true) - .commit() - } - selectedItemTag = newlySelectedItemTag - isOnFirstFragment = selectedItemTag == firstFragmentTag - selectedNavController.value = selectedFragment.navController - true - } else { - false - } - } - } - - // Optional: on item reselected, pop back stack to the destination of the graph - setupItemReselected(graphIdToTagMap, fragmentManager) - - // Handle deep link - setupDeepLinks(navGraphIds, fragmentManager, containerId, intent) - - // Finally, ensure that we update our BottomNavigationView when the back stack changes - fragmentManager.addOnBackStackChangedListener { - if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) { - this.selectedItemId = firstFragmentGraphId - } - - // Reset the graph if the currentDestination is not valid (happens when the back - // stack is popped after using the back button). - selectedNavController.value?.let { controller -> - if (controller.currentDestination == null) { - controller.navigate(controller.graph.id) - } - } - } - return selectedNavController -} - -private fun BottomNavigationView.setupDeepLinks( - navGraphIds: List, - fragmentManager: FragmentManager, - containerId: Int, - intent: Intent -) { - navGraphIds.forEachIndexed { index, navGraphId -> - val fragmentTag = getFragmentTag(index) - - // Find or create the Navigation host fragment - val navHostFragment = obtainNavHostFragment( - fragmentManager, - fragmentTag, - navGraphId, - containerId - ) - // Handle Intent - if (navHostFragment.navController.handleDeepLink(intent) - && selectedItemId != navHostFragment.navController.graph.id) { - this.selectedItemId = navHostFragment.navController.graph.id - } - } -} - -private fun BottomNavigationView.setupItemReselected( - graphIdToTagMap: SparseArray, - fragmentManager: FragmentManager -) { - setOnNavigationItemReselectedListener { item -> - val newlySelectedItemTag = graphIdToTagMap[item.itemId] - val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag) - as NavHostFragment - val navController = selectedFragment.navController - // Pop the back stack to the start destination of the current navController graph - navController.popBackStack( - navController.graph.startDestination, false - ) - } -} - -private fun detachNavHostFragment( - fragmentManager: FragmentManager, - navHostFragment: NavHostFragment -) { - fragmentManager.beginTransaction() - .detach(navHostFragment) - .commitNow() -} - -private fun attachNavHostFragment( - fragmentManager: FragmentManager, - navHostFragment: NavHostFragment, - isPrimaryNavFragment: Boolean -) { - fragmentManager.beginTransaction() - .attach(navHostFragment) - .apply { - if (isPrimaryNavFragment) { - setPrimaryNavigationFragment(navHostFragment) - } - } - .commitNow() - -} - -private fun obtainNavHostFragment( - fragmentManager: FragmentManager, - fragmentTag: String, - navGraphId: Int, - containerId: Int -): NavHostFragment { - // If the Nav Host fragment exists, return it - val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment? - existingFragment?.let { return it } - - // Otherwise, create it and return it. - val navHostFragment = NavHostFragment.create(navGraphId) - fragmentManager.beginTransaction() - .add(containerId, navHostFragment, fragmentTag) - .commitNow() - return navHostFragment -} - -private fun FragmentManager.isOnBackStack(backStackName: String): Boolean { - val backStackCount = backStackEntryCount - for (index in 0 until backStackCount) { - if (getBackStackEntryAt(index).name == backStackName) { - return true - } - } - return false -} - -private fun getFragmentTag(index: Int) = "bottomNavigation#$index" diff --git a/NavigationAdvancedSample/app/src/main/res/layout/activity_main.xml b/NavigationAdvancedSample/app/src/main/res/layout/activity_main.xml index 65fbe9dc1..dd210b7a6 100644 --- a/NavigationAdvancedSample/app/src/main/res/layout/activity_main.xml +++ b/NavigationAdvancedSample/app/src/main/res/layout/activity_main.xml @@ -24,9 +24,12 @@ + android:layout_weight="1" + app:defaultNavHost="true" + app:navGraph="@navigation/nav_graph" /> + + + + + + + + diff --git a/NavigationAdvancedSample/gradle/wrapper/gradle-wrapper.properties b/NavigationAdvancedSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/NavigationAdvancedSample/gradle/wrapper/gradle-wrapper.properties +++ b/NavigationAdvancedSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/NavigationAdvancedSample/versions.gradle b/NavigationAdvancedSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/NavigationAdvancedSample/versions.gradle +++ b/NavigationAdvancedSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/NavigationBasicSample/app/src/main/res/layout/activity_main.xml b/NavigationBasicSample/app/src/main/res/layout/activity_main.xml index e6d45d065..aed8e0bbf 100644 --- a/NavigationBasicSample/app/src/main/res/layout/activity_main.xml +++ b/NavigationBasicSample/app/src/main/res/layout/activity_main.xml @@ -24,7 +24,7 @@ android:background="@android:color/background_light" tools:context="com.example.android.navigationsample.MainActivity"> - + differ.submitData(pagingData) + } + } + + // Wait for initial load to finish. + advanceUntilIdle() + + assertThat(differ.snapshot()).containsExactly( + CheeseListItem.Separator('A'), + CheeseListItem.Item(cheeses[0]), + CheeseListItem.Separator('B'), + CheeseListItem.Item(cheeses[1]), + CheeseListItem.Separator('C'), + CheeseListItem.Item(cheeses[2]), + ) + + // runBlockingTest checks for leaking jobs, so we have to cancel the one we started. + job.cancel() + } +} + +class CheeseDaoFake(val cheeses: List) : CheeseDao { + override fun allCheesesByName(): PagingSource { + return object : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + return LoadResult.Page( + data = cheeses, + prevKey = null, + nextKey = null, + ) + } + + // Ignored in test. + override fun getRefreshKey(state: PagingState): Int? = null + } + } + + override fun insert(cheeses: List) {} + override fun insert(cheese: Cheese) {} + override fun delete(cheese: Cheese) {} +} + +val noopListUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) {} + override fun onRemoved(position: Int, count: Int) {} + override fun onMoved(fromPosition: Int, toPosition: Int) {} + override fun onChanged(position: Int, count: Int, payload: Any?) {} +} diff --git a/PagingSample/app/src/androidTest/java/paging/android/example/com/pagingsample/MainActivityTest.java b/PagingSample/app/src/androidTest/java/paging/android/example/com/pagingsample/MainActivityTest.java deleted file mode 100644 index a97da3293..000000000 --- a/PagingSample/app/src/androidTest/java/paging/android/example/com/pagingsample/MainActivityTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package paging.android.example.com.pagingsample; - -import android.app.Activity; -import androidx.arch.core.executor.testing.CountingTaskExecutorRule; -import android.content.Intent; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.recyclerview.widget.RecyclerView; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * Simply sanity test to ensure that activity launches without any issues and shows some data. - */ -@RunWith(AndroidJUnit4.class) -public class MainActivityTest { - @Rule - public final CountingTaskExecutorRule testRule = new CountingTaskExecutorRule(); - - @Test - public void showSomeResults() throws InterruptedException, TimeoutException { - Intent intent = new Intent(ApplicationProvider.getApplicationContext(), MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Activity activity = InstrumentationRegistry.getInstrumentation().startActivitySync(intent); - testRule.drainTasks(10, TimeUnit.SECONDS); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - final RecyclerView recyclerView = activity.findViewById(R.id.cheeseList); - waitForAdapterChange(recyclerView); - assertThat(recyclerView.getAdapter(), notNullValue()); - waitForAdapterChange(recyclerView); - assertThat(recyclerView.getAdapter().getItemCount() > 0, is(true)); - } - - private void waitForAdapterChange(final RecyclerView recyclerView) throws InterruptedException { - final CountDownLatch latch = new CountDownLatch(1); - InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> - recyclerView.getAdapter().registerAdapterDataObserver( - new RecyclerView.AdapterDataObserver() { - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - latch.countDown(); - } - - @Override - public void onChanged() { - latch.countDown(); - } - })); - if (recyclerView.getAdapter().getItemCount() > 0) { - return;//already loaded - } - assertThat(latch.await(10, TimeUnit.SECONDS), is(true)); - } -} diff --git a/PagingSample/app/src/androidTest/java/paging/android/example/com/pagingsample/MainActivityTest.kt b/PagingSample/app/src/androidTest/java/paging/android/example/com/pagingsample/MainActivityTest.kt new file mode 100644 index 000000000..bcdb41f24 --- /dev/null +++ b/PagingSample/app/src/androidTest/java/paging/android/example/com/pagingsample/MainActivityTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package paging.android.example.com.pagingsample + +import android.content.Intent +import androidx.annotation.UiThread +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Simply sanity test to ensure that activity launches without any issues and shows some data. + */ +@RunWith(AndroidJUnit4::class) +class MainActivityTest { + + @Test + @UiThread + fun showSomeResults() { + val intent = Intent(ApplicationProvider.getApplicationContext(), MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ActivityScenario.launch(intent) + onView(withId(R.id.cheeseList)).check { view, noViewFoundException -> + if (noViewFoundException != null) { + throw noViewFoundException + } + + val recyclerView = view as RecyclerView + assertThat(recyclerView.adapter).isNotNull() + assertThat(recyclerView.adapter!!.itemCount).isGreaterThan(0) + } + } +} diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseAdapter.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseAdapter.kt index 1c073de6a..f03825990 100644 --- a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseAdapter.kt +++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseAdapter.kt @@ -16,53 +16,63 @@ package paging.android.example.com.pagingsample -import androidx.recyclerview.widget.DiffUtil import android.view.ViewGroup import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil /** * A simple PagedListAdapter that binds Cheese items into CardViews. - *

+ * * PagedListAdapter is a RecyclerView.Adapter base class which can present the content of PagedLists * in a RecyclerView. It requests new pages as the user scrolls, and handles new PagedLists by * computing list differences on a background thread, and dispatching minimal, efficient updates to * the RecyclerView to ensure minimal UI thread work. - *

+ * * If you want to use your own Adapter base class, try using a PagedListAdapterHelper inside your * adapter instead. * * @see androidx.paging.PagedListAdapter * @see androidx.paging.AsyncPagedListDiffer */ -class CheeseAdapter : PagingDataAdapter(diffCallback) { +class CheeseAdapter : PagingDataAdapter(diffCallback) { override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) { holder.bindTo(getItem(position)) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder = - CheeseViewHolder(parent) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder { + return CheeseViewHolder(parent) + } companion object { /** * This diff callback informs the PagedListAdapter how to compute list differences when new * PagedLists arrive. - *

+ * * When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to * detect there's only a single item difference from before, so it only needs to animate and * rebind a single view. * * @see DiffUtil */ - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Cheese, newItem: Cheese): Boolean = - oldItem.id == newItem.id + val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean { + return if (oldItem is CheeseListItem.Item && newItem is CheeseListItem.Item) { + oldItem.cheese.id == newItem.cheese.id + } else if (oldItem is CheeseListItem.Separator && newItem is CheeseListItem.Separator) { + oldItem.name == newItem.name + } else { + oldItem == newItem + } + } /** * Note that in kotlin, == checking on data classes compares all contents, but in Java, * typically you'll implement Object#equals, and use it to compare object contents. */ - override fun areContentsTheSame(oldItem: Cheese, newItem: Cheese): Boolean = - oldItem == newItem + override fun areContentsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean { + return oldItem == newItem + } } } } + diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDb.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDb.kt index d4ec91363..e3f851c69 100644 --- a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDb.kt +++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseDb.kt @@ -57,7 +57,6 @@ abstract class CheeseDb : RoomDatabase() { } } - private val CHEESE_DATA = arrayListOf( "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi", "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseListItem.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseListItem.kt new file mode 100644 index 000000000..1dfabf5d5 --- /dev/null +++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseListItem.kt @@ -0,0 +1,9 @@ +package paging.android.example.com.pagingsample + +/** + * Common UI model between the [Cheese] data class and separators. + */ +sealed class CheeseListItem(val name: String) { + data class Item(val cheese: Cheese) : CheeseListItem(cheese.name) + data class Separator(private val letter: Char) : CheeseListItem(letter.toUpperCase().toString()) +} \ No newline at end of file diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewHolder.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewHolder.kt index 03dec4257..a47f62ceb 100644 --- a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewHolder.kt +++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewHolder.kt @@ -16,27 +16,37 @@ package paging.android.example.com.pagingsample -import androidx.recyclerview.widget.RecyclerView +import android.graphics.Typeface import android.view.LayoutInflater import android.view.ViewGroup import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView /** - * A simple ViewHolder that can bind a Cheese item. It also accepts null items since the data may - * not have been fetched before it is bound. + * A simple ViewHolder that can bind a Cheese or Separator item. It also accepts null items since + * the data may not have been fetched before it is bound. */ -class CheeseViewHolder(parent :ViewGroup) : RecyclerView.ViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.cheese_item, parent, false)) { - +class CheeseViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.cheese_item, parent, false) +) { + var cheese: Cheese? = null + private set private val nameView = itemView.findViewById(R.id.name) - var cheese : Cheese? = null /** * Items might be null if they are not paged in yet. PagedListAdapter will re-bind the * ViewHolder when Item is loaded. */ - fun bindTo(cheese : Cheese?) { - this.cheese = cheese - nameView.text = cheese?.name + fun bindTo(item: CheeseListItem?) { + if (item is CheeseListItem.Separator) { + nameView.text = "${item.name} Cheeses" + nameView.setTypeface(null, Typeface.BOLD) + } else { + nameView.text = item?.name + nameView.setTypeface(null, Typeface.NORMAL) + } + cheese = (item as? CheeseListItem.Item)?.cheese + nameView.text = item?.name } -} \ No newline at end of file +} + diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModel.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModel.kt index b7557b781..afdb5aad0 100644 --- a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModel.kt +++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModel.kt @@ -16,25 +16,23 @@ package paging.android.example.com.pagingsample -import android.app.Application import androidx.lifecycle.AndroidViewModel -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * A simple [AndroidViewModel] that provides a [Flow]<[PagingData]> of delicious cheeses. */ -class CheeseViewModel(app: Application) : AndroidViewModel(app) { - private val dao = CheeseDb.get(app).cheeseDao() - +class CheeseViewModel(private val dao: CheeseDao) : ViewModel() { /** * We use the Kotlin [Flow] property available on [Pager]. Java developers should use the * RxJava or LiveData extension properties available in `PagingRx` and `PagingLiveData`. */ - val allCheeses = Pager( - PagingConfig( + val allCheeses: Flow> = Pager( + config = PagingConfig( /** * A good page size is a value that fills at least a few screens worth of content on a * large device so the User is unlikely to see a null item. @@ -66,6 +64,30 @@ class CheeseViewModel(app: Application) : AndroidViewModel(app) { ) { dao.allCheesesByName() }.flow + .map { pagingData -> + pagingData + // Map cheeses to common UI model. + .map { cheese -> CheeseListItem.Item(cheese) } + .insertSeparators { before: CheeseListItem?, after: CheeseListItem? -> + if (before == null && after == null) { + // List is empty after fully loaded; return null to skip adding separator. + null + } else if (after == null) { + // Footer; return null here to skip adding a footer. + null + } else if (before == null) { + // Header + CheeseListItem.Separator(after.name.first()) + } else if (!before.name.first().equals(after.name.first(), ignoreCase = true)){ + // Between two items that start with different letters. + CheeseListItem.Separator(after.name.first()) + } else { + // Between two items that start with the same letter. + null + } + } + } + .cachedIn(viewModelScope) fun insert(text: CharSequence) = ioThread { dao.insert(Cheese(id = 0, name = text.toString())) diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModelFactory.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModelFactory.kt new file mode 100644 index 000000000..b4d1697e7 --- /dev/null +++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/CheeseViewModelFactory.kt @@ -0,0 +1,23 @@ +package paging.android.example.com.pagingsample + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +/** + * A [ViewModelProvider.Factory] that provides dependencies to [CheeseViewModel], + * allowing tests to switch out [CheeseDao] implementation via constructor injection. + */ +class CheeseViewModelFactory( + private val app: Application +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(CheeseViewModel::class.java)) { + val cheeseDao = CheeseDb.get(app).cheeseDao() + @Suppress("UNCHECKED_CAST") // Guaranteed to succeed at this point. + return CheeseViewModel(cheeseDao) as T + } + + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/MainActivity.kt b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/MainActivity.kt index bfd046111..7577d2590 100644 --- a/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/MainActivity.kt +++ b/PagingSample/app/src/main/java/paging/android/example/com/pagingsample/MainActivity.kt @@ -24,9 +24,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.activity_main.* import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import paging.android.example.com.pagingsample.databinding.ActivityMainBinding /** * Shows a list of Cheeses, with swipe-to-delete, and an input field at the top to add. @@ -35,15 +35,18 @@ import kotlinx.coroutines.launch * is updated automatically using paging components. */ class MainActivity : AppCompatActivity() { - private val viewModel by viewModels() + lateinit var binding: ActivityMainBinding + private set + private val viewModel by viewModels { CheeseViewModelFactory(application) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) // Create adapter for the RecyclerView val adapter = CheeseAdapter() - cheeseList.adapter = adapter + binding.cheeseList.adapter = adapter // Subscribe the adapter to the ViewModel, so the items in the adapter are refreshed // when the list changes @@ -58,9 +61,17 @@ class MainActivity : AppCompatActivity() { private fun initSwipeToDelete() { ItemTouchHelper(object : ItemTouchHelper.Callback() { // enable the items to swipe to the left or right - override fun getMovementFlags(recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder): Int = + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + val cheeseViewHolder = viewHolder as CheeseViewHolder + return if (cheeseViewHolder.cheese != null) { makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) + } else { + makeMovementFlags(0, 0) + } + } override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean = false @@ -72,24 +83,24 @@ class MainActivity : AppCompatActivity() { viewModel.remove(it) } } - }).attachToRecyclerView(cheeseList) + }).attachToRecyclerView(binding.cheeseList) } private fun addCheese() { - val newCheese = inputText.text.trim() + val newCheese = binding.inputText.text.trim() if (newCheese.isNotEmpty()) { viewModel.insert(newCheese) - inputText.setText("") + binding.inputText.setText("") } } private fun initAddButtonListener() { - addButton.setOnClickListener { + binding.addButton.setOnClickListener { addCheese() } // when the user taps the "Done" button in the on screen keyboard, save the item. - inputText.setOnEditorActionListener { _, actionId, _ -> + binding.inputText.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { addCheese() return@setOnEditorActionListener true @@ -97,7 +108,7 @@ class MainActivity : AppCompatActivity() { false // action that isn't DONE occurred - ignore } // When the user clicks on the button, or presses enter, save the item. - inputText.setOnKeyListener { _, keyCode, event -> + binding.inputText.setOnKeyListener { _, keyCode, event -> if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { addCheese() return@setOnKeyListener true diff --git a/PagingSample/gradle/wrapper/gradle-wrapper.properties b/PagingSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/PagingSample/gradle/wrapper/gradle-wrapper.properties +++ b/PagingSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/PagingSample/versions.gradle b/PagingSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/PagingSample/versions.gradle +++ b/PagingSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/PagingWithNetworkSample/app/build.gradle b/PagingWithNetworkSample/app/build.gradle index 399282e7d..ca13b5b28 100644 --- a/PagingWithNetworkSample/app/build.gradle +++ b/PagingWithNetworkSample/app/build.gradle @@ -15,11 +15,7 @@ */ apply plugin: 'com.android.application' - apply plugin: 'kotlin-android' - -apply plugin: 'kotlin-android-extensions' - apply plugin: 'kotlin-kapt' android { @@ -33,20 +29,28 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + + buildFeatures { + viewBinding = true + } + buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { jvmTarget = "1.8" freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"] } + sourceSets { androidTest.java.srcDirs += "src/test-common/java" test.java.srcDirs += "src/test-common/java" @@ -69,6 +73,7 @@ dependencies { implementation deps.material implementation deps.room.runtime implementation deps.room.ktx + implementation deps.room.paging implementation deps.lifecycle.livedata_ktx implementation deps.lifecycle.runtime implementation deps.paging_runtime @@ -79,18 +84,13 @@ dependencies { implementation deps.glide.runtime // Android Testing Support Library's runner and rules - androidTestImplementation deps.atsl.core + androidTestImplementation deps.atsl.ext_junit androidTestImplementation deps.atsl.runner androidTestImplementation deps.atsl.rules androidTestImplementation deps.arch_core.testing - - androidTestImplementation deps.junit - androidTestImplementation deps.retrofit.mock - androidTestImplementation deps.mockito.core - androidTestImplementation deps.mockito.android + androidTestImplementation deps.espresso.contrib testImplementation deps.junit - testImplementation deps.retrofit.mock - testImplementation deps.arch_core.testing - testImplementation deps.mockito.core + testImplementation deps.coroutines.test + testImplementation deps.kotlin.test } diff --git a/PagingWithNetworkSample/app/src/androidTest/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivityTest.kt b/PagingWithNetworkSample/app/src/androidTest/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivityTest.kt index 23f27461b..156250a18 100644 --- a/PagingWithNetworkSample/app/src/androidTest/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivityTest.kt +++ b/PagingWithNetworkSample/app/src/androidTest/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivityTest.kt @@ -18,10 +18,11 @@ package com.android.example.paging.pagingwithnetwork.reddit.ui import android.app.Application import android.content.Intent -import androidx.arch.core.executor.testing.CountingTaskExecutorRule import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider -import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers.withId import com.android.example.paging.pagingwithnetwork.R import com.android.example.paging.pagingwithnetwork.reddit.DefaultServiceLocator import com.android.example.paging.pagingwithnetwork.reddit.ServiceLocator @@ -32,17 +33,11 @@ import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPost import com.android.example.paging.pagingwithnetwork.reddit.ui.SubRedditViewModel.Companion.DEFAULT_SUBREDDIT import com.android.example.paging.pagingwithnetwork.repository.FakeRedditApi import com.android.example.paging.pagingwithnetwork.repository.PostFactory -import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.CoreMatchers.notNullValue -import org.hamcrest.MatcherAssert.assertThat +import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException /** * Simple sanity test to ensure data is displayed @@ -55,10 +50,8 @@ class RedditActivityTest(private val type: RedditPostRepository.Type) { fun params() = arrayOf(IN_MEMORY_BY_ITEM, IN_MEMORY_BY_PAGE) } - @get:Rule - var testRule = CountingTaskExecutorRule() - private val postFactory = PostFactory() + @Before fun init() { val fakeApi = FakeRedditApi() @@ -68,45 +61,30 @@ class RedditActivityTest(private val type: RedditPostRepository.Type) { val app = ApplicationProvider.getApplicationContext() // use a controlled service locator w/ fake API ServiceLocator.swap( - object : DefaultServiceLocator(app = app, useInMemoryDb = true) { - override fun getRedditApi(): RedditApi = fakeApi - } + object : DefaultServiceLocator(app = app, useInMemoryDb = true) { + override fun getRedditApi(): RedditApi = fakeApi + } ) } @Test - @Throws(InterruptedException::class, TimeoutException::class) fun showSomeResults() { - val intent = RedditActivity.intentFor( + ActivityScenario.launch( + RedditActivity.intentFor( context = ApplicationProvider.getApplicationContext(), type = type + ).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } ) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val activity = InstrumentationRegistry.getInstrumentation().startActivitySync(intent) - val recyclerView = activity.findViewById(R.id.list) - assertThat(recyclerView.adapter, notNullValue()) - waitForAdapterChange(recyclerView) - assertThat(recyclerView.adapter?.itemCount, `is`(3)) - } - private fun waitForAdapterChange(recyclerView: RecyclerView) { - val latch = CountDownLatch(1) - InstrumentationRegistry.getInstrumentation().runOnMainSync { - recyclerView.adapter?.registerAdapterDataObserver( - object : RecyclerView.AdapterDataObserver() { - override fun onChanged() { - latch.countDown() - } + onView(withId(R.id.list)).check { view, noViewFoundException -> + if (noViewFoundException != null) { + throw noViewFoundException + } - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - latch.countDown() - } - }) - } - testRule.drainTasks(1, TimeUnit.SECONDS) - if (recyclerView.adapter?.itemCount ?: 0 > 0) { - return + val recyclerView = view as RecyclerView + assertEquals(3, recyclerView.adapter?.itemCount) } - assertThat(latch.await(10, TimeUnit.SECONDS), `is`(true)) } } \ No newline at end of file diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/MainActivity.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/MainActivity.kt index 16affd809..98828a587 100644 --- a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/MainActivity.kt +++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/MainActivity.kt @@ -18,25 +18,28 @@ package com.android.example.paging.pagingwithnetwork import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.android.example.paging.pagingwithnetwork.databinding.ActivityMainBinding import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository import com.android.example.paging.pagingwithnetwork.reddit.ui.RedditActivity -import kotlinx.android.synthetic.main.activity_main.* /** * chooser activity for the demo. */ class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - withDatabase.setOnClickListener { + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.withDatabase.setOnClickListener { show(RedditPostRepository.Type.DB) } - networkOnly.setOnClickListener { + binding.networkOnly.setOnClickListener { show(RedditPostRepository.Type.IN_MEMORY_BY_ITEM) } - networkOnlyWithPageKeys.setOnClickListener { + binding.networkOnlyWithPageKeys.setOnClickListener { show(RedditPostRepository.Type.IN_MEMORY_BY_PAGE) } } diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/PageKeyedRemoteMediator.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/PageKeyedRemoteMediator.kt index 04562ef52..b7550b1ec 100644 --- a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/PageKeyedRemoteMediator.kt +++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inDb/PageKeyedRemoteMediator.kt @@ -24,6 +24,12 @@ class PageKeyedRemoteMediator( private val postDao: RedditPostDao = db.posts() private val remoteKeyDao: SubredditRemoteKeyDao = db.remoteKeys() + override suspend fun initialize(): InitializeAction { + // Require that remote REFRESH is launched on initial load and succeeds before launching + // remote PREPEND / APPEND. + return InitializeAction.LAUNCH_INITIAL_REFRESH + } + override suspend fun load( loadType: LoadType, state: PagingState diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/InMemoryByItemRepository.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/InMemoryByItemRepository.kt index 0a87713ee..993aad589 100644 --- a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/InMemoryByItemRepository.kt +++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/InMemoryByItemRepository.kt @@ -27,14 +27,14 @@ import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPost */ class InMemoryByItemRepository(private val redditApi: RedditApi) : RedditPostRepository { override fun postsOfSubreddit(subReddit: String, pageSize: Int) = Pager( - PagingConfig( - pageSize = pageSize, - enablePlaceholders = false - ) + PagingConfig( + pageSize = pageSize, + enablePlaceholders = false + ) ) { ItemKeyedSubredditPagingSource( - redditApi = redditApi, - subredditName = subReddit + redditApi = redditApi, + subredditName = subReddit ) }.flow } diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/ItemKeyedSubredditPagingSource.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/ItemKeyedSubredditPagingSource.kt index c6fe4dba3..d8217bd47 100644 --- a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/ItemKeyedSubredditPagingSource.kt +++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byItem/ItemKeyedSubredditPagingSource.kt @@ -22,6 +22,7 @@ import androidx.paging.PagingSource.LoadParams.Prepend import androidx.paging.PagingSource.LoadResult.Page import androidx.paging.PagingState import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi +import com.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byPage.PageKeyedSubredditPagingSource import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost import retrofit2.HttpException import java.io.IOException @@ -32,26 +33,25 @@ import java.io.IOException * Note that this is not the correct consumption of the Reddit API but rather shown here as an * alternative implementation which might be more suitable for your backend. * - * @see [com.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byPage.PageKeyedSubredditPagingSource] + * @see [PageKeyedSubredditPagingSource] */ class ItemKeyedSubredditPagingSource( - private val redditApi: RedditApi, - private val subredditName: String + private val redditApi: RedditApi, + private val subredditName: String ) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { return try { val items = redditApi.getTop( - subreddit = subredditName, - after = if (params is Append) params.key else null, - before = if (params is Prepend) params.key else null, - limit = params.loadSize + subreddit = subredditName, + after = if (params is Append) params.key else null, + before = if (params is Prepend) params.key else null, + limit = params.loadSize ).data.children.map { it.data } Page( - data = items, - prevKey = items.firstOrNull()?.name, - nextKey = items.lastOrNull()?.name + data = items, + prevKey = items.firstOrNull()?.name, + nextKey = items.lastOrNull()?.name ) } catch (e: IOException) { LoadResult.Error(e) diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/InMemoryByPageKeyRepository.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/InMemoryByPageKeyRepository.kt index 95ddf0c96..20e1c328c 100644 --- a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/InMemoryByPageKeyRepository.kt +++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/InMemoryByPageKeyRepository.kt @@ -27,12 +27,11 @@ import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPost */ class InMemoryByPageKeyRepository(private val redditApi: RedditApi) : RedditPostRepository { override fun postsOfSubreddit(subReddit: String, pageSize: Int) = Pager( - PagingConfig(pageSize) + PagingConfig(pageSize) ) { PageKeyedSubredditPagingSource( - redditApi = redditApi, - subredditName = subReddit + redditApi = redditApi, + subredditName = subReddit ) }.flow } - diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/PageKeyedSubredditPagingSource.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/PageKeyedSubredditPagingSource.kt index d260a3e4f..af20356d6 100644 --- a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/PageKeyedSubredditPagingSource.kt +++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/repository/inMemory/byPage/PageKeyedSubredditPagingSource.kt @@ -20,6 +20,7 @@ import androidx.paging.PagingSource import androidx.paging.PagingSource.LoadParams.Append import androidx.paging.PagingSource.LoadParams.Prepend import androidx.paging.PagingSource.LoadResult.Page +import androidx.paging.PagingState import com.android.example.paging.pagingwithnetwork.reddit.api.RedditApi import com.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byItem.ItemKeyedSubredditPagingSource import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost @@ -55,4 +56,14 @@ class PageKeyedSubredditPagingSource( LoadResult.Error(e) } } + + override fun getRefreshKey(state: PagingState): String? { + return state.anchorPosition?.let { anchorPosition -> + // This loads starting from previous page, but since PagingConfig.initialLoadSize spans + // multiple pages, the initial load will still load items centered around + // anchorPosition. This also prevents needing to immediately launch prepend due to + // prefetchDistance. + state.closestPageToPosition(anchorPosition)?.prevKey + } + } } diff --git a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt index 0cd4117c3..33dfd9667 100644 --- a/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt +++ b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt @@ -29,10 +29,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import com.android.example.paging.pagingwithnetwork.GlideApp -import com.android.example.paging.pagingwithnetwork.R +import com.android.example.paging.pagingwithnetwork.databinding.ActivityRedditBinding import com.android.example.paging.pagingwithnetwork.reddit.ServiceLocator +import com.android.example.paging.pagingwithnetwork.reddit.paging.asMergedLoadStates import com.android.example.paging.pagingwithnetwork.reddit.repository.RedditPostRepository -import kotlinx.android.synthetic.main.activity_reddit.* import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -44,14 +44,8 @@ import kotlinx.coroutines.flow.filter * The intent arguments can be modified to make it use a different repository (see MainActivity). */ class RedditActivity : AppCompatActivity() { - companion object { - const val KEY_REPOSITORY_TYPE = "repository_type" - fun intentFor(context: Context, type: RedditPostRepository.Type): Intent { - val intent = Intent(context, RedditActivity::class.java) - intent.putExtra(KEY_REPOSITORY_TYPE, type.ordinal) - return intent - } - } + lateinit var binding: ActivityRedditBinding + private set private val model: SubRedditViewModel by viewModels { object : AbstractSavedStateViewModelFactory(this, null) { @@ -74,7 +68,9 @@ class RedditActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_reddit) + binding = ActivityRedditBinding.inflate(layoutInflater) + setContentView(binding.root) + initAdapter() initSwipeToRefresh() initSearch() @@ -83,14 +79,14 @@ class RedditActivity : AppCompatActivity() { private fun initAdapter() { val glide = GlideApp.with(this) adapter = PostsAdapter(glide) - list.adapter = adapter.withLoadStateHeaderAndFooter( + binding.list.adapter = adapter.withLoadStateHeaderAndFooter( header = PostsLoadStateAdapter(adapter), footer = PostsLoadStateAdapter(adapter) ) lifecycleScope.launchWhenCreated { - adapter.loadStateFlow.collectLatest { loadStates -> - swipe_refresh.isRefreshing = loadStates.refresh is LoadState.Loading + adapter.loadStateFlow.collect { loadStates -> + binding.swipeRefresh.isRefreshing = loadStates.mediator?.refresh is LoadState.Loading } } @@ -102,20 +98,25 @@ class RedditActivity : AppCompatActivity() { lifecycleScope.launchWhenCreated { adapter.loadStateFlow - // Only emit when REFRESH LoadState for RemoteMediator changes. + // Use a state-machine to track LoadStates such that we only transition to + // NotLoading from a RemoteMediator load if it was also presented to UI. + .asMergedLoadStates() + // Only emit when REFRESH changes, as we only want to react on loads replacing the + // list. .distinctUntilChangedBy { it.refresh } - // Only react to cases where Remote REFRESH completes i.e., NotLoading. + // Only react to cases where REFRESH completes i.e., NotLoading. .filter { it.refresh is LoadState.NotLoading } - .collect { list.scrollToPosition(0) } + // Scroll to top is synchronous with UI updates, even if remote load was triggered. + .collect { binding.list.scrollToPosition(0) } } } private fun initSwipeToRefresh() { - swipe_refresh.setOnRefreshListener { adapter.refresh() } + binding.swipeRefresh.setOnRefreshListener { adapter.refresh() } } private fun initSearch() { - input.setOnEditorActionListener { _, actionId, _ -> + binding.input.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_GO) { updatedSubredditFromInput() true @@ -123,7 +124,7 @@ class RedditActivity : AppCompatActivity() { false } } - input.setOnKeyListener { _, keyCode, event -> + binding.input.setOnKeyListener { _, keyCode, event -> if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { updatedSubredditFromInput() true @@ -134,10 +135,19 @@ class RedditActivity : AppCompatActivity() { } private fun updatedSubredditFromInput() { - input.text.trim().toString().let { + binding.input.text.trim().toString().let { if (it.isNotBlank() && model.shouldShowSubreddit(it)) { model.showSubreddit(it) } } } + + companion object { + const val KEY_REPOSITORY_TYPE = "repository_type" + fun intentFor(context: Context, type: RedditPostRepository.Type): Intent { + val intent = Intent(context, RedditActivity::class.java) + intent.putExtra(KEY_REPOSITORY_TYPE, type.ordinal) + return intent + } + } } diff --git a/PagingWithNetworkSample/lib/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/SubRedditViewModel.kt b/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/SubRedditViewModel.kt similarity index 100% rename from PagingWithNetworkSample/lib/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/SubRedditViewModel.kt rename to PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/SubRedditViewModel.kt diff --git a/PagingWithNetworkSample/app/src/test-common/java/com/android/example/paging/pagingwithnetwork/repository/FakeRedditApi.kt b/PagingWithNetworkSample/app/src/test-common/java/com/android/example/paging/pagingwithnetwork/repository/FakeRedditApi.kt index 6e25d6ad5..8abb5ada8 100644 --- a/PagingWithNetworkSample/app/src/test-common/java/com/android/example/paging/pagingwithnetwork/repository/FakeRedditApi.kt +++ b/PagingWithNetworkSample/app/src/test-common/java/com/android/example/paging/pagingwithnetwork/repository/FakeRedditApi.kt @@ -38,10 +38,10 @@ class FakeRedditApi : RedditApi { } private fun findPosts( - subreddit: String, - limit: Int, - after: String? = null, - before: String? = null + subreddit: String, + limit: Int, + after: String? = null, + before: String? = null ): List { // only support paging forward if (before != null) return emptyList() @@ -52,13 +52,13 @@ class FakeRedditApi : RedditApi { } private fun findSubReddit(subreddit: String) = - model.getOrDefault(subreddit, SubReddit()) + model.getOrDefault(subreddit, SubReddit()) override suspend fun getTop( - @Path("subreddit") subreddit: String, - @Query("limit") limit: Int, - @Query("after") after: String?, - @Query("before") before: String? + @Path("subreddit") subreddit: String, + @Query("limit") limit: Int, + @Query("after") after: String?, + @Query("before") before: String? ): RedditApi.ListingResponse { failureMsg?.let { throw IOException(it) @@ -66,11 +66,11 @@ class FakeRedditApi : RedditApi { val items = findPosts(subreddit, limit, after, before) val nextAfter = items.lastOrNull()?.data?.name return RedditApi.ListingResponse( - RedditApi.ListingData( - children = items, - after = nextAfter, - before = null - ) + RedditApi.ListingData( + children = items, + after = nextAfter, + before = null + ) ) } diff --git a/PagingWithNetworkSample/app/src/test/java/com/android/example/paging/pagingwithnetwork/reddit/repository/SubredditPagingSourceTest.kt b/PagingWithNetworkSample/app/src/test/java/com/android/example/paging/pagingwithnetwork/reddit/repository/SubredditPagingSourceTest.kt new file mode 100644 index 000000000..6a3e9d09e --- /dev/null +++ b/PagingWithNetworkSample/app/src/test/java/com/android/example/paging/pagingwithnetwork/reddit/repository/SubredditPagingSourceTest.kt @@ -0,0 +1,64 @@ +package com.android.example.paging.pagingwithnetwork.reddit.repository + +import androidx.paging.PagingSource.LoadParams.Refresh +import androidx.paging.PagingSource.LoadResult.Page +import com.android.example.paging.pagingwithnetwork.reddit.ui.SubRedditViewModel.Companion.DEFAULT_SUBREDDIT +import com.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byItem.ItemKeyedSubredditPagingSource +import com.android.example.paging.pagingwithnetwork.reddit.repository.inMemory.byPage.PageKeyedSubredditPagingSource +import com.android.example.paging.pagingwithnetwork.repository.FakeRedditApi +import com.android.example.paging.pagingwithnetwork.repository.PostFactory +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class SubredditPagingSourceTest { + private val postFactory = PostFactory() + private val fakePosts = listOf( + postFactory.createRedditPost(DEFAULT_SUBREDDIT), + postFactory.createRedditPost(DEFAULT_SUBREDDIT), + postFactory.createRedditPost(DEFAULT_SUBREDDIT) + ) + private val fakeApi = FakeRedditApi().apply { + fakePosts.forEach { post -> addPost(post) } + } + + @Test + fun itemKeyedSubredditPagingSource() = runBlockingTest { + val pagingSource = ItemKeyedSubredditPagingSource(fakeApi, DEFAULT_SUBREDDIT) + assertEquals( + expected = Page( + data = listOf(fakePosts[0], fakePosts[1]), + prevKey = fakePosts[0].name, + nextKey = fakePosts[1].name + ), + actual = pagingSource.load( + Refresh( + key = null, + loadSize = 2, + placeholdersEnabled = false + ) + ), + ) + } + + @Test + fun pageKeyedSubredditPagingSource() = runBlockingTest { + val pagingSource = PageKeyedSubredditPagingSource(fakeApi, DEFAULT_SUBREDDIT) + assertEquals( + expected = Page( + data = listOf(fakePosts[0], fakePosts[1]), + prevKey = null, + nextKey = fakePosts[1].name + ), + actual = pagingSource.load( + Refresh( + key = null, + loadSize = 2, + placeholdersEnabled = false + ) + ), + ) + } +} \ No newline at end of file diff --git a/PagingWithNetworkSample/benchmark/build.gradle b/PagingWithNetworkSample/benchmark/build.gradle index 3ccd7774f..a9d414289 100644 --- a/PagingWithNetworkSample/benchmark/build.gradle +++ b/PagingWithNetworkSample/benchmark/build.gradle @@ -1,11 +1,6 @@ apply plugin: 'com.android.library' - apply plugin: 'kotlin-android' - -apply plugin: 'kotlin-android-extensions' - apply plugin: 'kotlin-kapt' - apply plugin: 'androidx.benchmark' android { @@ -29,6 +24,10 @@ android { testBuildType = "release" + buildFeatures { + viewBinding = true + } + buildTypes { release { isDefault = true diff --git a/PagingWithNetworkSample/benchmark/src/androidTest/java/com/example/benchmark/BenchmarkActivity.kt b/PagingWithNetworkSample/benchmark/src/androidTest/java/com/example/benchmark/BenchmarkActivity.kt index 51eba5221..a9d102e56 100644 --- a/PagingWithNetworkSample/benchmark/src/androidTest/java/com/example/benchmark/BenchmarkActivity.kt +++ b/PagingWithNetworkSample/benchmark/src/androidTest/java/com/example/benchmark/BenchmarkActivity.kt @@ -16,29 +16,35 @@ package com.example.benchmark import android.os.Bundle +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingSource +import androidx.paging.PagingState import com.android.example.paging.pagingwithnetwork.GlideApp import com.android.example.paging.pagingwithnetwork.reddit.ui.PostsAdapter import com.android.example.paging.pagingwithnetwork.reddit.vo.RedditPost -import kotlinx.android.synthetic.main.activity_benchmark.* +import com.example.benchmark.databinding.ActivityBenchmarkBinding import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class BenchmarkActivity : AppCompatActivity() { val testExecutor = TestExecutor() + @VisibleForTesting + lateinit var binding: ActivityBenchmarkBinding + private set override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_benchmark) + binding = ActivityBenchmarkBinding.inflate(layoutInflater) + setContentView(binding.root) val glide = GlideApp.with(this) val adapter = PostsAdapter(glide) - list.adapter = adapter + binding.list.adapter = adapter val config = PagingConfig( pageSize = 5, @@ -68,4 +74,7 @@ class MockPagingSource : PagingSource() { val key = params.key ?: 0 return LoadResult.Page(List(200) { generatePost() }.toList(), key - 1, key + 1) } + + // Unused in benchmark. + override fun getRefreshKey(state: PagingState): Int? = null } \ No newline at end of file diff --git a/PagingWithNetworkSample/benchmark/src/androidTest/java/com/example/benchmark/PostsAdapterBenchmark.kt b/PagingWithNetworkSample/benchmark/src/androidTest/java/com/example/benchmark/PostsAdapterBenchmark.kt index 9469a6f06..e1a1d474a 100644 --- a/PagingWithNetworkSample/benchmark/src/androidTest/java/com/example/benchmark/PostsAdapterBenchmark.kt +++ b/PagingWithNetworkSample/benchmark/src/androidTest/java/com/example/benchmark/PostsAdapterBenchmark.kt @@ -19,10 +19,9 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.recyclerview.widget.RecyclerView import androidx.test.annotation.UiThreadTest +import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest -import androidx.test.rule.ActivityTestRule -import kotlinx.android.synthetic.main.activity_benchmark.* import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test @@ -35,21 +34,21 @@ class PostsAdapterBenchmark { val benchmarkRule = BenchmarkRule() @get:Rule - val activityRule = ActivityTestRule(BenchmarkActivity::class.java) + val activityRule = ActivityScenarioRule(BenchmarkActivity::class.java) - @UiThreadTest @Test + @UiThreadTest fun scrollItem() { - val activity = activityRule.activity - - // If RecyclerView has children, the items are attached, bound, and gone through layout. - // Ready to benchmark. - assertTrue("RecyclerView expected to have children", activity.list.childCount > 0) + activityRule.scenario.onActivity { activity -> + // If RecyclerView has children, the items are attached, bound, and gone through layout. + // Ready to benchmark. + assertTrue("RecyclerView expected to have children", activity.binding.list.childCount > 0) - benchmarkRule.measureRepeated { - activity.list.scrollByOneItem() - runWithTimingDisabled { - activity.testExecutor.flush() + benchmarkRule.measureRepeated { + activity.binding.list.scrollByOneItem() + runWithTimingDisabled { + activity.testExecutor.flush() + } } } } diff --git a/PagingWithNetworkSample/gradle/wrapper/gradle-wrapper.properties b/PagingWithNetworkSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/PagingWithNetworkSample/gradle/wrapper/gradle-wrapper.properties +++ b/PagingWithNetworkSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/PagingWithNetworkSample/lib/build.gradle b/PagingWithNetworkSample/lib/build.gradle index 50d143b47..7a13a9ff6 100644 --- a/PagingWithNetworkSample/lib/build.gradle +++ b/PagingWithNetworkSample/lib/build.gradle @@ -1,9 +1,5 @@ apply plugin: 'com.android.library' - apply plugin: 'kotlin-android' - -apply plugin: 'kotlin-android-extensions' - apply plugin: 'kotlin-kapt' android { @@ -17,18 +13,20 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + + buildFeatures { + viewBinding = true + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { jvmTarget = "1.8" freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"] } - - buildFeatures { - viewBinding = true - } } dependencies { diff --git a/PagingWithNetworkSample/lib/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/paging/LoadStatesMerger.kt b/PagingWithNetworkSample/lib/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/paging/LoadStatesMerger.kt new file mode 100644 index 000000000..1bd0f7753 --- /dev/null +++ b/PagingWithNetworkSample/lib/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/paging/LoadStatesMerger.kt @@ -0,0 +1,180 @@ +package com.android.example.paging.pagingwithnetwork.reddit.paging + +import androidx.annotation.VisibleForTesting +import androidx.paging.CombinedLoadStates +import androidx.paging.LoadState +import androidx.paging.LoadState.NotLoading +import androidx.paging.LoadState.Loading +import androidx.paging.LoadStates +import androidx.paging.PagingSource.LoadResult.Error +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.scan +import androidx.paging.PagingDataAdapter +import androidx.paging.RemoteMediator +import androidx.paging.PagingSource +import androidx.paging.LoadType.REFRESH +import androidx.paging.LoadType +import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.NOT_LOADING +import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.REMOTE_STARTED +import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.REMOTE_ERROR +import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.SOURCE_ERROR +import com.android.example.paging.pagingwithnetwork.reddit.paging.MergedState.SOURCE_LOADING + +/** + * Converts the raw [CombinedLoadStates] [Flow] from [PagingDataAdapter.loadStateFlow] into a new + * [Flow] of [CombinedLoadStates] that track [CombinedLoadStates.mediator] states as they are + * synchronously applied in the UI. Any [Loading] state triggered by [RemoteMediator] will only + * transition back to [NotLoading] after the fetched items have been synchronously shown in UI by a + * successful [PagingSource] load of type [REFRESH]. + * + * Note: This class assumes that the [RemoteMediator] implementation always invalidates + * [PagingSource] on a successful fetch, even if no data was modified (which Room does by default). + * Using this class without this guarantee can cause [LoadState] to get indefinitely stuck as + * [Loading] in cases where invalidation doesn't happen because the fetched network data represents + * exactly what is already cached in DB. + */ +@OptIn(ExperimentalCoroutinesApi::class) +fun Flow.asMergedLoadStates(): Flow { + val syncRemoteState = LoadStatesMerger() + return scan(syncRemoteState.toLoadStates()) { _, combinedLoadStates -> + syncRemoteState.updateFromCombinedLoadStates(combinedLoadStates) + syncRemoteState.toLoadStates() + } +} + +/** + * Track the combined [LoadState] of [RemoteMediator] and [PagingSource], so that each load type + * is only set to [NotLoading] when [RemoteMediator] load is applied on presenter-side. + */ +private class LoadStatesMerger { + var refresh: LoadState = NotLoading(endOfPaginationReached = false) + private set + var prepend: LoadState = NotLoading(endOfPaginationReached = false) + private set + var append: LoadState = NotLoading(endOfPaginationReached = false) + private set + var refreshState: MergedState = NOT_LOADING + private set + var prependState: MergedState = NOT_LOADING + private set + var appendState: MergedState = NOT_LOADING + private set + + fun toLoadStates() = LoadStates( + refresh = refresh, + prepend = prepend, + append = append + ) + + /** + * For every new emission of [CombinedLoadStates] from the original [Flow], update the + * [MergedState] of each [LoadType] and compute the new [LoadState]. + */ + fun updateFromCombinedLoadStates(combinedLoadStates: CombinedLoadStates) { + computeNextLoadStateAndMergedState( + sourceRefreshState = combinedLoadStates.source.refresh, + sourceState = combinedLoadStates.source.refresh, + remoteState = combinedLoadStates.mediator?.refresh, + currentMergedState = refreshState, + ).also { + refresh = it.first + refreshState = it.second + } + computeNextLoadStateAndMergedState( + sourceRefreshState = combinedLoadStates.source.refresh, + sourceState = combinedLoadStates.source.prepend, + remoteState = combinedLoadStates.mediator?.prepend, + currentMergedState = prependState, + ).also { + prepend = it.first + prependState = it.second + } + computeNextLoadStateAndMergedState( + sourceRefreshState = combinedLoadStates.source.refresh, + sourceState = combinedLoadStates.source.append, + remoteState = combinedLoadStates.mediator?.append, + currentMergedState = appendState, + ).also { + append = it.first + appendState = it.second + } + } + + /** + * Compute which [LoadState] and [MergedState] to transition, given the previous and current + * state for a particular [LoadType]. + */ + private fun computeNextLoadStateAndMergedState( + sourceRefreshState: LoadState, + sourceState: LoadState, + remoteState: LoadState?, + currentMergedState: MergedState, + ): Pair { + if (remoteState == null) return sourceState to NOT_LOADING + + return when (currentMergedState) { + NOT_LOADING -> when (remoteState) { + is Loading -> Loading to REMOTE_STARTED + is Error<*, *> -> remoteState to REMOTE_ERROR + else -> NotLoading(remoteState.endOfPaginationReached) to NOT_LOADING + } + REMOTE_STARTED -> when { + remoteState is Error<*, *> -> remoteState to REMOTE_ERROR + sourceRefreshState is Loading -> Loading to SOURCE_LOADING + else -> Loading to REMOTE_STARTED + } + REMOTE_ERROR -> when (remoteState) { + is Error<*, *> -> remoteState to REMOTE_ERROR + else -> Loading to REMOTE_STARTED + } + SOURCE_LOADING -> when { + sourceRefreshState is Error<*, *> -> sourceRefreshState to SOURCE_ERROR + remoteState is Error<*, *> -> remoteState to REMOTE_ERROR + sourceRefreshState is NotLoading -> { + NotLoading(remoteState.endOfPaginationReached) to NOT_LOADING + } + else -> Loading to SOURCE_LOADING + } + SOURCE_ERROR -> when (sourceRefreshState) { + is Error<*, *> -> sourceRefreshState to SOURCE_ERROR + else -> sourceRefreshState to SOURCE_LOADING + } + } + } +} + +/** + * State machine used to compute [LoadState] values in [LoadStatesMerger]. + * + * This allows [LoadStatesMerger] to track whether to block transitioning to [NotLoading] from the + * [Loading] state if it was triggered by [RemoteMediator], until [PagingSource] invalidates and + * completes [REFRESH]. + */ +private enum class MergedState { + /** + * Idle state; defer to remote state for endOfPaginationReached. + */ + NOT_LOADING, + + /** + * Remote load triggered; start listening for source refresh. + */ + REMOTE_STARTED, + + /** + * Waiting for remote in error state to get retried + */ + REMOTE_ERROR, + + /** + * Source refresh triggered by remote invalidation, once this completes we can be sure + * the next generation was loaded. + */ + SOURCE_LOADING, + + /** + * Remote load completed, but waiting for source refresh in error state to get retried. + */ + SOURCE_ERROR, +} diff --git a/PagingWithNetworkSample/versions.gradle b/PagingWithNetworkSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/PagingWithNetworkSample/versions.gradle +++ b/PagingWithNetworkSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/PersistenceContentProviderSample/app/build.gradle b/PersistenceContentProviderSample/app/build.gradle index 4cbe66127..e78eed190 100644 --- a/PersistenceContentProviderSample/app/build.gradle +++ b/PersistenceContentProviderSample/app/build.gradle @@ -34,6 +34,10 @@ android { } } } + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } buildTypes { release { minifyEnabled false diff --git a/PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.properties b/PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.properties +++ b/PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/PersistenceContentProviderSample/versions.gradle b/PersistenceContentProviderSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/PersistenceContentProviderSample/versions.gradle +++ b/PersistenceContentProviderSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/PersistenceMigrationsSample/gradle/wrapper/gradle-wrapper.properties b/PersistenceMigrationsSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/PersistenceMigrationsSample/gradle/wrapper/gradle-wrapper.properties +++ b/PersistenceMigrationsSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/PersistenceMigrationsSample/versions.gradle b/PersistenceMigrationsSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/PersistenceMigrationsSample/versions.gradle +++ b/PersistenceMigrationsSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/README.md b/README.md index bf82790a7..a58265c67 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ how to use ViewModels and Room together with RxJava, in Kotlin. * **[WorkManagerSample](https://github.com/googlesamples/android-architecture-components/tree/master/WorkManagerSample)** - Shows how to use WorkManager to do background work, in Kotlin. +* **[WorkManagerMultiprocessSample](https://github.com/googlesamples/android-architecture-components/tree/master/WorkManagerMultiprocessSample)** - Shows how to use WorkManager in apps that manage multiple processes. + ### Reporting Issues diff --git a/ViewBindingSample/app/build.gradle b/ViewBindingSample/app/build.gradle index 8051db3e4..cc9cd01d2 100644 --- a/ViewBindingSample/app/build.gradle +++ b/ViewBindingSample/app/build.gradle @@ -34,8 +34,8 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - viewBinding { - enabled = true + buildFeatures { + viewBinding true } } diff --git a/ViewBindingSample/gradle/wrapper/gradle-wrapper.properties b/ViewBindingSample/gradle/wrapper/gradle-wrapper.properties index 99f79addf..db78223b8 100644 --- a/ViewBindingSample/gradle/wrapper/gradle-wrapper.properties +++ b/ViewBindingSample/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/ViewBindingSample/versions.gradle b/ViewBindingSample/versions.gradle index b5770b8d9..5dc879294 100644 --- a/ViewBindingSample/versions.gradle +++ b/ViewBindingSample/versions.gradle @@ -27,10 +27,10 @@ versions.annotations = "1.0.0" versions.apache_commons = "2.5" versions.appcompat = "1.2.0-alpha02" versions.arch_core = "2.1.0" -versions.atsl_core = "1.2.0" -versions.atsl_junit = "1.1.1" -versions.atsl_rules = "1.2.0" -versions.atsl_runner = "1.2.0" +versions.atsl_core = "1.3.0" +versions.atsl_junit = "1.1.2" +versions.atsl_rules = "1.3.0" +versions.atsl_runner = "1.3.0" versions.benchmark = "1.1.0-alpha01" versions.cardview = "1.0.0" versions.constraint_layout = "2.0.0-alpha2" @@ -39,7 +39,7 @@ versions.coroutines = "1.4.1" versions.dagger = "2.16" versions.dexmaker = "2.2.0" versions.espresso = "3.2.0" -versions.fragment = "1.2.1" +versions.fragment = "1.2.0" versions.glide = "4.8.0" versions.hamcrest = "1.3" versions.junit = "4.12" @@ -52,17 +52,17 @@ versions.mockito_android = "2.25.0" versions.mockwebserver = "3.8.1" versions.navigation = "2.3.0-alpha01" versions.okhttp_logging_interceptor = "3.9.0" -versions.paging = "3.0.0-alpha11" +versions.paging = "3.1.0-alpha04" versions.recyclerview = "1.2.0-beta01" versions.retrofit = "2.9.0" versions.robolectric = "4.2" -versions.room = "2.3.0-alpha01" +versions.room = "2.4.0-alpha05" versions.rx_android = "2.0.1" versions.rxjava2 = "2.1.3" versions.timber = "4.7.1" versions.transition = "1.3.0" versions.truth = "1.0.1" -versions.work = "2.2.0" +versions.work = "2.6.0" ext.versions = versions def build_versions = [:] @@ -92,6 +92,7 @@ deps.arch_core = arch_core def atsl = [:] atsl.core = "androidx.test:core:$versions.atsl_core" atsl.ext_junit = "androidx.test.ext:junit:$versions.atsl_junit" +atsl.ext_junit_ktx = "androidx.test.ext:junit-ktx:$versions.atsl_junit" atsl.runner = "androidx.test:runner:$versions.atsl_runner" atsl.rules = "androidx.test:rules:$versions.atsl_rules" deps.atsl = atsl @@ -195,6 +196,7 @@ def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" room.ktx = "androidx.room:room-ktx:$versions.room" +room.paging = "androidx.room:room-paging:$versions.room" room.rxjava2 = "androidx.room:room-rxjava2:$versions.room" room.testing = "androidx.room:room-testing:$versions.room" deps.room = room diff --git a/WorkManagerMultiprocessSample/.gitignore b/WorkManagerMultiprocessSample/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/WorkManagerMultiprocessSample/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/WorkManagerMultiprocessSample/README.md b/WorkManagerMultiprocessSample/README.md new file mode 100644 index 000000000..54ff13add --- /dev/null +++ b/WorkManagerMultiprocessSample/README.md @@ -0,0 +1,26 @@ +Android Jetpack WorkManager Multiprocess Sample +================================== + +This is a simple application written in Kotlin that shows how to implement advanced concepts of [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager/). + +## APIs / Concepts included in sample + +* [Custom WorkManager Configuration and Initialization](https://developer.android.com/topic/libraries/architecture/workmanager/advanced/custom-configuration) +* RemoteCoroutineWorker +* RemoteListenableWorker + +## Reporting Issues + +You can report an [Issue](https://github.com/googlesamples/android-architecture-components/issues) on the samples using this repository. + +If you find an issue with WorkManager, please report it using the [WorkManager Issue Tracker](https://issuetracker.google.com/issues/new?component=409906&template=1094197). + + +## License +Copyright 2021 The Android Open Source Project, Inc. + +Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/WorkManagerMultiprocessSample/app/.gitignore b/WorkManagerMultiprocessSample/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/WorkManagerMultiprocessSample/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/WorkManagerMultiprocessSample/app/build.gradle b/WorkManagerMultiprocessSample/app/build.gradle new file mode 100644 index 000000000..b1cb264a4 --- /dev/null +++ b/WorkManagerMultiprocessSample/app/build.gradle @@ -0,0 +1,65 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "com.example.background.multiprocess" + minSdkVersion 23 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.google.android.material:material:1.3.0' + implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.work:work-runtime-ktx:2.6.0-alpha02' + implementation 'androidx.work:work-multiprocess:2.6.0-alpha02' + testImplementation 'androidx.test:core:1.3.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' +} \ No newline at end of file diff --git a/WorkManagerMultiprocessSample/app/proguard-rules.pro b/WorkManagerMultiprocessSample/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/WorkManagerMultiprocessSample/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/WorkManagerMultiprocessSample/app/src/main/AndroidManifest.xml b/WorkManagerMultiprocessSample/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..dc60e46cf --- /dev/null +++ b/WorkManagerMultiprocessSample/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WorkManagerMultiprocessSample/app/src/main/java/com/example/background/multiprocess/ExampleRemoteCoroutineWorker.kt b/WorkManagerMultiprocessSample/app/src/main/java/com/example/background/multiprocess/ExampleRemoteCoroutineWorker.kt new file mode 100644 index 000000000..8a2863bbf --- /dev/null +++ b/WorkManagerMultiprocessSample/app/src/main/java/com/example/background/multiprocess/ExampleRemoteCoroutineWorker.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.background.multiprocess + +import android.content.Context +import android.util.Log +import androidx.work.WorkerParameters +import androidx.work.multiprocess.RemoteCoroutineWorker +import androidx.work.multiprocess.RemoteListenableWorker + +/** + * Example of implementing a RemoteCoroutineWorker. This worker simply returns Success. + * + * Use RemoteCoroutineWorker if your worker is implemented in Kotlin, otherwise use + * [RemoteListenableWorker] if your worker is implemented in Java. + */ +class ExampleRemoteCoroutineWorker(context: Context, parameters: WorkerParameters) : + RemoteCoroutineWorker(context, parameters) { + + override suspend fun doRemoteWork(): Result { + + Log.d(TAG, "Starting ExampleRemoteCoroutineWorker") + + // Do some work here + + return Result.success() + } + + companion object { + private const val TAG = "CoroutineWorker" + } +} \ No newline at end of file diff --git a/WorkManagerMultiprocessSample/app/src/main/java/com/example/background/multiprocess/ExampleRemoteListenableWorker.java b/WorkManagerMultiprocessSample/app/src/main/java/com/example/background/multiprocess/ExampleRemoteListenableWorker.java new file mode 100644 index 000000000..213e20c69 --- /dev/null +++ b/WorkManagerMultiprocessSample/app/src/main/java/com/example/background/multiprocess/ExampleRemoteListenableWorker.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.background.multiprocess; + +import android.content.Context; +import android.util.Log; + +import androidx.concurrent.futures.CallbackToFutureAdapter; +import androidx.work.WorkerParameters; +import androidx.work.multiprocess.RemoteListenableWorker; + +import com.google.common.util.concurrent.ListenableFuture; + +/** + * Example of implementing a RemoteListenableWorker. This worker simply returns Success. + *

+ * Use RemoteListenableWorker if your worker is implemented in Java, otherwise use + * RemoteCoroutineWorker if your worker is implemented in Kotlin. + */ +public class ExampleRemoteListenableWorker extends RemoteListenableWorker { + + private static final String TAG = "ListenableWorker"; + + public ExampleRemoteListenableWorker(Context appContext, WorkerParameters workerParams) { + super(appContext, workerParams); + } + + @Override + public ListenableFuture startRemoteWork() { + return CallbackToFutureAdapter.getFuture(completer -> { + Log.i(TAG, "Starting ExampleRemoteListenableWorker"); + + // Do some work here. + + return completer.set(Result.success()); + }); + } +} diff --git a/WorkManagerMultiprocessSample/app/src/main/java/com/example/background/multiprocess/MainActivity.kt b/WorkManagerMultiprocessSample/app/src/main/java/com/example/background/multiprocess/MainActivity.kt new file mode 100644 index 000000000..eeec194a7 --- /dev/null +++ b/WorkManagerMultiprocessSample/app/src/main/java/com/example/background/multiprocess/MainActivity.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.background.multiprocess + +import android.content.ComponentName +import android.os.Bundle +import android.widget.Button +import androidx.appcompat.app.AppCompatActivity +import androidx.work.Data +import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.multiprocess.RemoteCoroutineWorker +import androidx.work.multiprocess.RemoteListenableWorker +import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_CLASS_NAME +import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME +import androidx.work.multiprocess.RemoteWorkerService + +/** + * This class demonstrates the ability to schedule two different workers that will run in specified + * processes. + * + * See [buildOneTimeWorkRemoteWorkRequest] to understand how to designate the process a Worker runs + * in. + */ +class MainActivity : AppCompatActivity() { + + private val PACKAGE_NAME = "com.example.background.multiprocess" + + private var workManager: WorkManager? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + workManager = WorkManager.getInstance(this@MainActivity) + + findViewById