diff options
author | Yigit Boyar <yboyar@google.com> | 2020-10-01 09:59:28 -0700 |
---|---|---|
committer | Manuel Vicente Vivo <mvivo@google.com> | 2020-12-03 10:59:30 +0000 |
commit | 11f3864741c0a335cc34ce18e1b65034cba517b1 (patch) | |
tree | 7f9a630288fd654445db6b5c39593160854900a8 | |
parent | de1bae70c096f01e4604c30c5fccc0e732a9724f (diff) | |
download | data-binding-11f3864741c0a335cc34ce18e1b65034cba517b1.tar.gz |
Adds StateFlow support to data binding
Bug: 162839058
Test: KotlinTestApp
Change-Id: I835d768cbc6d8bf44566bb15aae8fe786a005ed0
38 files changed, 978 insertions, 138 deletions
diff --git a/BUILD.bazel b/BUILD.bazel index a6ad530d..0b207b5e 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -288,10 +288,11 @@ maven_repo( "//prebuilts/tools/common/m2/repository/android/arch/lifecycle/extensions/1.0.0:aar", "//prebuilts/tools/common/m2/repository/android/arch/lifecycle/runtime/1.0.3:aar", "//prebuilts/tools/common/m2/repository/androidx/annotation/annotation/1.0.0:jar", + "//prebuilts/tools/common/m2/repository/androidx/annotation/annotation/1.1.0:jar", "//prebuilts/tools/common/m2/repository/androidx/appcompat/appcompat/1.0.0:aar", "//prebuilts/tools/common/m2/repository/androidx/appcompat/appcompat/1.0.2:aar", - "//prebuilts/tools/common/m2/repository/androidx/arch/core/core-common/2.0.0:jar", - "//prebuilts/tools/common/m2/repository/androidx/arch/core/core-runtime/2.0.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/arch/core/core-common/2.1.0:jar", + "//prebuilts/tools/common/m2/repository/androidx/arch/core/core-runtime/2.1.0:aar", "//prebuilts/tools/common/m2/repository/androidx/asynclayoutinflater/asynclayoutinflater/1.0.0:aar", "//prebuilts/tools/common/m2/repository/androidx/cardview/cardview/1.0.0:aar", "//prebuilts/tools/common/m2/repository/androidx/collection/collection/1.0.0:jar", @@ -306,14 +307,19 @@ maven_repo( "//prebuilts/tools/common/m2/repository/androidx/interpolator/interpolator/1.0.0:aar", "//prebuilts/tools/common/m2/repository/androidx/legacy/legacy-support-core-ui/1.0.0:aar", "//prebuilts/tools/common/m2/repository/androidx/legacy/legacy-support-core-utils/1.0.0:aar", - "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-common/2.0.0:jar", - "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-extensions/2.0.0:aar", - "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-livedata-core/2.0.0:aar", - "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-livedata/2.0.0:aar", - "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-process/2.0.0:aar", - "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-runtime/2.0.0:aar", - "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-service/2.0.0:aar", - "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-viewmodel/2.0.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-common/2.2.0:jar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-extensions/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-livedata-core-ktx/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-livedata-core/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-livedata-ktx/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-livedata/2.0.0:aar", #Workaround as loader:1.0.0 needs it + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-livedata/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-process/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-runtime-ktx/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-runtime/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-service/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-viewmodel-ktx/2.2.0:aar", + "//prebuilts/tools/common/m2/repository/androidx/lifecycle/lifecycle-viewmodel/2.2.0:aar", "//prebuilts/tools/common/m2/repository/androidx/loader/loader/1.0.0:aar", "//prebuilts/tools/common/m2/repository/androidx/localbroadcastmanager/localbroadcastmanager/1.0.0:aar", "//prebuilts/tools/common/m2/repository/androidx/print/print/1.0.0:aar", @@ -335,9 +341,33 @@ maven_repo( "//prebuilts/tools/common/m2/repository/com/android/support/support-media-compat/26.1.0:aar", "//prebuilts/tools/common/m2/repository/com/android/support/support-v4/26.1.0:aar", "//prebuilts/tools/common/m2/repository/com/android/support/support-vector-drawable/26.1.0:aar", + "//prebuilts/tools/common/m2/repository/com/github/gundy/semver4j/0.16.4:jar", "//prebuilts/tools/common/m2/repository/com/google/android/material/material/1.0.0:aar", "//prebuilts/tools/common/m2/repository/com/google/code/findbugs/jsr305/1.3.9:jar", "//prebuilts/tools/common/m2/repository/com/google/errorprone/error_prone_annotations/2.1.3:jar", + "//prebuilts/tools/common/m2/repository/de/undercouch/gradle-download-task/4.0.2:jar", + "//prebuilts/tools/common/m2/repository/org/antlr/antlr4-runtime/4.5.2-1:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/intellij/deps/trove4j/1.0.20181211:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-android-extensions/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-annotation-processing-gradle/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-build-common/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-compiler-embeddable/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-compiler-runner/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-daemon-client/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-daemon-embeddable/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-gradle-plugin-api/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-gradle-plugin-model/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-gradle-plugin/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-script-runtime/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-scripting-common/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-scripting-compiler-embeddable/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-scripting-compiler-impl-embeddable/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-scripting-jvm/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-util-io/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlin/kotlin-util-klib/1.4.10:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.4.1:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.3.7:jar", + "//prebuilts/tools/common/m2/repository/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1:jar", ], ) @@ -350,6 +380,7 @@ gradle_build( "//prebuilts/studio/sdk:platform-tools", "//prebuilts/studio/sdk:build-tools/latest", "//tools/buildSrc/base:version.properties", + "//tools/buildSrc/base:dependencies.properties", "//tools/data-binding:databinding.properties", ] + glob( ["extensions/**"], diff --git a/compilationTests/build.gradle b/compilationTests/build.gradle index 9398c5ea..a42cc26e 100644 --- a/compilationTests/build.gradle +++ b/compilationTests/build.gradle @@ -21,6 +21,7 @@ dependencies { testCompile 'com.android.tools:annotations:24.5.0' testCompile libs.guava testCompile libs.kotlin_stdlib + testCompile 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' } afterEvaluate { diff --git a/compilationTests/gradle/wrapper/gradle-wrapper.properties b/compilationTests/gradle/wrapper/gradle-wrapper.properties index b6f6d38a..73a14a3f 100644 --- a/compilationTests/gradle/wrapper/gradle-wrapper.properties +++ b/compilationTests/gradle/wrapper/gradle-wrapper.properties @@ -4,4 +4,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=../../../../../../../external/gradle/gradle-4.1-bin.zip +distributionUrl=../../../../../../../external/gradle/gradle-6.6-bin.zip diff --git a/compilationTests/src/test/java/androidx/databinding/compilationTest/ObservableGetDetectionTest.kt b/compilationTests/src/test/java/androidx/databinding/compilationTest/ObservableGetDetectionTest.kt index 1a0440dc..001410b8 100644 --- a/compilationTests/src/test/java/androidx/databinding/compilationTest/ObservableGetDetectionTest.kt +++ b/compilationTests/src/test/java/androidx/databinding/compilationTest/ObservableGetDetectionTest.kt @@ -26,7 +26,8 @@ import org.junit.runners.Parameterized class ObservableGetDetectionTest( private val type: String, private val resolvedType: String, // e..g if it is ObservableInt, resolvedType is Int - private val getter: String + private val getter: String, + private val constructor: String ) : BaseCompilationTest() { @Test fun detectGetterCallsOnObservables() { @@ -71,7 +72,7 @@ class ObservableGetDetectionTest( package com.example; import androidx.databinding.*; public class MyClass { - public final $type value = new $type(); + public final $type value = $constructor; } """.trimIndent()) writeFile("/app/src/main/res/layout/observable_get.xml", @@ -100,7 +101,7 @@ class ObservableGetDetectionTest( package com.example; import androidx.databinding.*; public class MyClass { - public final $type value = new $type(); + public final $type value = $constructor; @InverseMethod("fromString") public static String convertToString($resolvedType value) { throw new RuntimeException(""); @@ -134,19 +135,31 @@ class ObservableGetDetectionTest( @Parameterized.Parameters(name = "{0}") @JvmStatic fun params() = arrayOf( - arrayOf("ObservableByte", "byte"), - arrayOf("ObservableBoolean", "boolean"), - arrayOf("ObservableChar", "char"), - arrayOf("ObservableShort", "short"), - arrayOf("ObservableInt", "int"), - arrayOf("ObservableLong", "long"), - arrayOf("ObservableFloat", "float"), - arrayOf("ObservableDouble", "double"), - arrayOf("ObservableField<String>", "String") + arrayOf("androidx.databinding.ObservableByte", "byte"), + arrayOf("androidx.databinding.ObservableBoolean", "boolean"), + arrayOf("androidx.databinding.ObservableChar", "char"), + arrayOf("androidx.databinding.ObservableShort", "short"), + arrayOf("androidx.databinding.ObservableInt", "int"), + arrayOf("androidx.databinding.ObservableLong", "long"), + arrayOf("androidx.databinding.ObservableFloat", "float"), + arrayOf("androidx.databinding.ObservableDouble", "double"), + arrayOf("androidx.databinding.ObservableField<String>", "String") ).map { - arrayOf("androidx.databinding.${it[0]}", it[1], "get()") + arrayOf(it[0], it[1], "get()", "new ${it[0]}()") } + arrayOf( - arrayOf("androidx.lifecycle.MutableLiveData<String>", "String", "getValue()") + arrayOf( + "androidx.lifecycle.MutableLiveData<String>", + "String", + "getValue()", + "new androidx.lifecycle.MutableLiveData<String>()" + ) + ) + arrayOf( + arrayOf( + "kotlinx.coroutines.flow.MutableStateFlow<String>", + "String", + "getValue()", + "kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow(\"\")" + ) ) } -}
\ No newline at end of file +} diff --git a/compilationTests/src/test/resources/app_build.gradle b/compilationTests/src/test/resources/app_build.gradle index ab61387d..ed269e3a 100644 --- a/compilationTests/src/test/resources/app_build.gradle +++ b/compilationTests/src/test/resources/app_build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { compileSdkVersion rootProject.latestCompileSdk @@ -26,6 +27,7 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - implementation "androidx.lifecycle:lifecycle-livedata:2.0.0" + implementation "androidx.fragment:fragment:1.2.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1" !@{DEPENDENCIES} } diff --git a/compilationTests/src/test/resources/commonBuildScript.gradle b/compilationTests/src/test/resources/commonBuildScript.gradle index 35f21b4c..cd94804a 100644 --- a/compilationTests/src/test/resources/commonBuildScript.gradle +++ b/compilationTests/src/test/resources/commonBuildScript.gradle @@ -27,6 +27,7 @@ rootProject.buildscript { dependencies { classpath "com.android.tools.build:gradle:${buildVersion}" + classpath libs.kotlin_gradle_plugin } } diff --git a/compiler/src/main/java/android/databinding/tool/expr/Expr.java b/compiler/src/main/java/android/databinding/tool/expr/Expr.java index 73770a55..c956dfcd 100644 --- a/compiler/src/main/java/android/databinding/tool/expr/Expr.java +++ b/compiler/src/main/java/android/databinding/tool/expr/Expr.java @@ -191,14 +191,19 @@ abstract public class Expr implements VersionProvider, LocationScopeProvider { return getResolvedType().isObservable(); } - public String getUpdateRegistrationCall() { + public String getUpdateRegistrationCall(int id, String value) { if (!isObservable()) { L.e("The expression isn't observable!"); } + String lastParams = id + ", " + value + ");"; if (getResolvedType().isLiveData()) { - return "updateLiveDataRegistration"; + return "updateLiveDataRegistration(" + lastParams; } - return "updateRegistration"; + if (getResolvedType().isStateFlow()) { + return "androidx.databinding.ViewDataBindingKtx." + + "updateStateFlowRegistration(this, " + lastParams; + } + return "updateRegistration(" + lastParams; } public void setUnwrapObservableFields(boolean unwrapObservableFields) { diff --git a/compiler/src/main/java/android/databinding/tool/reflection/ModelAnalyzer.kt b/compiler/src/main/java/android/databinding/tool/reflection/ModelAnalyzer.kt index d2a86ee8..37f0e8a6 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/ModelAnalyzer.kt +++ b/compiler/src/main/java/android/databinding/tool/reflection/ModelAnalyzer.kt @@ -53,6 +53,12 @@ abstract class ModelAnalyzer protected constructor(@JvmField val libTypes: LibTy val mutableLiveDataType by lazy(LazyThreadSafetyMode.NONE) { loadClassErasure(libTypes.mutableLiveData) } + val stateFlowType by lazy(LazyThreadSafetyMode.NONE) { + loadClassErasure(libTypes.stateFlow) + } + val mutableStateFlowDataType by lazy(LazyThreadSafetyMode.NONE) { + loadClassErasure(libTypes.mutableStateFlow) + } val viewDataBindingType by lazy(LazyThreadSafetyMode.NONE) { val klass = findClass(libTypes.viewDataBinding, null) Preconditions.checkNotNull(klass, "Cannot find %s class." + @@ -141,6 +147,19 @@ abstract class ModelAnalyzer protected constructor(@JvmField val libTypes: LibTy } } + private val dataBindingKtxClass by lazy { + findClass(libTypes.dataBindingKtx, null) + } + + fun checkDataBindingKtx() { + Preconditions.checkNotNull( + dataBindingKtxClass, """Data binding ktx is not enabled. + | + |Add dataBinding.addKtx = true to your build.gradle to enable it.""" + .trimMargin() + ) + } + fun findClass(className: String, imports: ImportBag?): ModelClass? { return classFinderCache.find(className, imports) } diff --git a/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.kt b/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.kt index 99b7f8c8..fb600443 100644 --- a/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.kt +++ b/compiler/src/main/java/android/databinding/tool/reflection/ModelClass.kt @@ -176,7 +176,8 @@ abstract class ModelClass { return modelAnalyzer.observableType.isAssignableFrom(this) || modelAnalyzer.observableListType.isAssignableFrom(this) || modelAnalyzer.observableMapType.isAssignableFrom(this) || - (modelAnalyzer.liveDataType?.isAssignableFrom(this) ?: false) + (modelAnalyzer.liveDataType?.isAssignableFrom(this) ?: false) || + (modelAnalyzer.stateFlowType?.isAssignableFrom(this) ?: false) } /** @@ -206,24 +207,43 @@ abstract class ModelClass { } /** + * @return whether or not this is a StateFlow + */ + val isStateFlow by lazy(LazyThreadSafetyMode.NONE) { + val modelAnalyzer = ModelAnalyzer.getInstance() + val isStateFlow = modelAnalyzer.stateFlowType?.isAssignableFrom(erasure()) ?: false + if (isStateFlow) { + modelAnalyzer.checkDataBindingKtx() + } + isStateFlow + } + + /** + * @return whether or not this is a MutableStateFlow + */ + val isMutableStateFlow by lazy(LazyThreadSafetyMode.NONE) { + ModelAnalyzer.getInstance().mutableStateFlowDataType?.isAssignableFrom(erasure()) ?: false + } + + /** * @return the name of the simple getter method when this is an ObservableField or LiveData or - * `null` for any other type + * Flow or `null` for any other type */ val observableGetterName: String? get() = when { isObservableField -> "get" - isLiveData -> "getValue" + isLiveData || isStateFlow -> "getValue" else -> null } /** * @return the name of the simple setter method when this is an ObservableField or - * MutableLiveData or `null` for any other type. + * MutableLiveData or MutableStateFlow or `null` for any other type. */ val observableSetterName: String? get() = when { isObservableField -> "set" - isMutableLiveData -> "setValue" + isMutableLiveData || isMutableStateFlow -> "setValue" else -> null } @@ -651,4 +671,4 @@ abstract class ModelClass { return fieldName } } -}
\ No newline at end of file +} diff --git a/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt b/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt index 26b8203a..1130f32d 100644 --- a/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt +++ b/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt @@ -754,7 +754,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder, val libTypes: LibTypes block("public void ${it.setterName}(${if (it.resolvedType.isPrimitive) "" else "@Nullable "}$argType ${it.readableName})") { val used = it.isIsUsedInCallback || it.isUsed if (used && it.isObservable) { - nl("${it.updateRegistrationCall}(${it.id}, ${it.readableName});"); + nl(it.getUpdateRegistrationCall(it.id, it.readableName)) } nl("this.${it.fieldName} = ${it.readableName};") if (used) { @@ -1098,7 +1098,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder, val libTypes: LibTypes app("", assignment) } it.value.filter { it.isObservable }.forEach { expr: Expr -> - tab("${expr.updateRegistrationCall}(${expr.id}, ${expr.executePendingLocalName});") + tab(expr.getUpdateRegistrationCall(expr.id, expr.executePendingLocalName)) } } diff --git a/compilerCommon/src/main/kotlin/android/databinding/tool/LibTypes.kt b/compilerCommon/src/main/kotlin/android/databinding/tool/LibTypes.kt index 3b848a29..4a4a4726 100644 --- a/compilerCommon/src/main/kotlin/android/databinding/tool/LibTypes.kt +++ b/compilerCommon/src/main/kotlin/android/databinding/tool/LibTypes.kt @@ -60,10 +60,22 @@ class LibTypes(val useAndroidX: Boolean) { convert("android.arch.lifecycle.MutableLiveData") } + val stateFlow by lazy(LazyThreadSafetyMode.NONE) { + "kotlinx.coroutines.flow.StateFlow" + } + + val mutableStateFlow by lazy(LazyThreadSafetyMode.NONE) { + "kotlinx.coroutines.flow.MutableStateFlow" + } + val dataBindingComponent by lazy(LazyThreadSafetyMode.NONE) { convert("android.databinding.DataBindingComponent") } + val dataBindingKtx by lazy(LazyThreadSafetyMode.NONE) { + "androidx.databinding.ViewDataBindingKtx" + } + val dataBinderMapper by lazy(LazyThreadSafetyMode.NONE) { convert("android.databinding.DataBinderMapper") } @@ -210,4 +222,4 @@ class LibTypes(val useAndroidX: Boolean) { "android.arch.persistence." to "androidx.sqlite." ) } -}
\ No newline at end of file +} diff --git a/compilerCommon/src/test/kotlin/android/databinding/tool/store/AndroidXConversionTest.kt b/compilerCommon/src/test/kotlin/android/databinding/tool/store/AndroidXConversionTest.kt index 07a757d8..68fdd9ba 100644 --- a/compilerCommon/src/test/kotlin/android/databinding/tool/store/AndroidXConversionTest.kt +++ b/compilerCommon/src/test/kotlin/android/databinding/tool/store/AndroidXConversionTest.kt @@ -54,4 +54,4 @@ class AndroidXConversionTest { `is`("androidx.recyclerview.widget.RecyclerView") ) } -}
\ No newline at end of file +} diff --git a/extensions-support/gradle/wrapper/gradle-wrapper.properties b/extensions-support/gradle/wrapper/gradle-wrapper.properties index 273873f6..fa1cc2e4 100644 --- a/extensions-support/gradle/wrapper/gradle-wrapper.properties +++ b/extensions-support/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=../../../../external/gradle/gradle-6.5-bin.zip +distributionUrl=../../../../external/gradle/gradle-6.7.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/extensions/baseAdapters/build.gradle b/extensions/baseAdapters/build.gradle index 85ab1dbd..603925b3 100644 --- a/extensions/baseAdapters/build.gradle +++ b/extensions/baseAdapters/build.gradle @@ -96,4 +96,4 @@ task prebuild(type : Copy) { rename { String fileName -> "databinding-adapters.aar" } -}
\ No newline at end of file +} diff --git a/extensions/build.gradle b/extensions/build.gradle index 8b77e813..1bfd88b6 100644 --- a/extensions/build.gradle +++ b/extensions/build.gradle @@ -51,6 +51,8 @@ buildscript { databindingProperties.load(new FileInputStream("$projectDir/../databinding.properties")) Properties buildToolsProperties = new Properties() buildToolsProperties.load(new FileInputStream("$projectDir/../../buildSrc/base/version.properties")) + Properties dependencyProperties = new Properties() + dependencyProperties.load(new FileInputStream("$projectDir/../../buildSrc/base/dependencies.properties")) def runningInIde = project.hasProperty('android.injected.invoked.from.ide') // this is done by bazel but if we are in IDE it also configures so we need to distinguish @@ -107,7 +109,7 @@ buildscript { } ext.dataBindingConfig = databindingProperties def supportLibVersion = "1.0.0" - def lifecycleVersion = "2.0.0" + def lifecycleVersion = "2.2.0" ext.libs = [:] ext.libs.android_support_annotations = "androidx.annotation:annotation:$supportLibVersion" ext.libs.android_support_collection = "androidx.collection:collection:$supportLibVersion" @@ -115,9 +117,14 @@ buildscript { ext.libs.android_support_appcompat_v7 = "androidx.appcompat:appcompat:$supportLibVersion" ext.libs.android_arch_lifecycle_extensions = "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" ext.libs.android_arch_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion" + ext.libs.android_arch_lifecycle_runtime_ktx = "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + ext.libs.kotlin_gradle_plugin = dependencyProperties.kotlin_gradle_plugin + ext.libs.kotlin_stdlib = dependencyProperties.kotlin_stdlib + ext.libs.coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1" dependencies { classpath "com.android.tools.build:gradle:$TOOLS_VERSION" + classpath rootProject.ext.libs.kotlin_gradle_plugin } if (!autoConfigured) { @@ -177,8 +184,8 @@ subprojects { failOnVersionConflict() preferProjectModules() - force 'androidx.lifecycle:lifecycle-viewmodel:2.0.0' - force 'androidx.lifecycle:lifecycle-livedata-core:2.0.0' + force 'androidx.lifecycle:lifecycle-viewmodel:2.2.0' + force 'androidx.lifecycle:lifecycle-livedata-core:2.2.0' force 'androidx.fragment:fragment:1.0.0' } } diff --git a/extensions/databindingKtx/build.gradle b/extensions/databindingKtx/build.gradle new file mode 100644 index 00000000..4b8ae3e2 --- /dev/null +++ b/extensions/databindingKtx/build.gradle @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion dataBindingConfig.compileSdkVersion + buildToolsVersion dataBindingConfig.buildToolsVersion + + defaultConfig { + minSdkVersion 14 + targetSdkVersion dataBindingConfig.targetSdkVersion + } + compileOptions { + sourceCompatibility dataBindingConfig.javaTargetCompatibility + targetCompatibility dataBindingConfig.javaSourceCompatibility + } + buildTypes { + release { + minifyEnabled false + } + } + packagingOptions { + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/NOTICE.txt' + } +} + +configurations { + jarArchives +} + +dependencies { + implementation project(':library') + implementation rootProject.ext.libs.kotlin_stdlib + implementation rootProject.ext.libs.coroutines + api rootProject.ext.libs.android_arch_lifecycle_extensions + api rootProject.ext.libs.android_arch_lifecycle_runtime_ktx +} + +//create jar tasks +android.libraryVariants.all { variant -> + def name = variant.buildType.name + + if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) { + return // Skip debug builds. + } + def suffix = name.capitalize() + + def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.srcDirs + } + + artifacts.add('archives', sourcesJarTask); +} +uploadArchives { + repositories { + mavenDeployer { + pom.artifactId = 'databinding-ktx' + pom.project { + licenses { + license { + name dataBindingConfig.licenseName + url dataBindingConfig.licenseUrl + distribution dataBindingConfig.licenseDistribution + } + } + } + } + } +} + +task prebuildAar(type : Copy) { + dependsOn uploadArchives + from "$buildDir/outputs/aar/databinding-ktx-release.aar" + into dataBindingConfig.prebuildFolder + rename { String fileName -> + "databinding-ktx.aar" + } +} diff --git a/extensions/databindingKtx/src/main/AndroidManifest.xml b/extensions/databindingKtx/src/main/AndroidManifest.xml new file mode 100644 index 00000000..60e62e57 --- /dev/null +++ b/extensions/databindingKtx/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<manifest package="androidx.databinding.ktx" /> diff --git a/extensions/databindingKtx/src/main/java/androidx/databinding/ViewDataBindingKtx.kt b/extensions/databindingKtx/src/main/java/androidx/databinding/ViewDataBindingKtx.kt new file mode 100644 index 00000000..509b44ab --- /dev/null +++ b/extensions/databindingKtx/src/main/java/androidx/databinding/ViewDataBindingKtx.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 androidx.databinding; + +import androidx.annotation.RestrictTo +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import java.lang.ref.ReferenceQueue + +/** + * Helper methods for data binding Ktx features. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +object ViewDataBindingKtx { + + /** + * Method object extracted out to attach a listener to a bound StateFlow object. + */ + private val CREATE_STATE_FLOW_LISTENER = + CreateWeakListener { viewDataBinding, localFieldId, referenceQueue -> + StateFlowListener(viewDataBinding, localFieldId, referenceQueue) + .listener + } + + @Suppress("unused") // called by generated code + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @JvmStatic + fun updateStateFlowRegistration( + viewDataBinding: ViewDataBinding, + localFieldId: Int, + observable: Flow<*>? + ): Boolean { + viewDataBinding.mInStateFlowRegisterObserver = true + try { + return viewDataBinding.updateRegistration( + localFieldId, observable, CREATE_STATE_FLOW_LISTENER + ) + } finally { + viewDataBinding.mInStateFlowRegisterObserver = false + } + } + + internal class StateFlowListener( + binder: ViewDataBinding?, + localFieldId: Int, + referenceQueue: ReferenceQueue<ViewDataBinding> + ) : ObservableReference<Flow<Any?>> { + + private var _lifecycleOwner : LifecycleOwner? = null + private var observerJob : Job? = null + private val listener = WeakListener<Flow<Any?>>( + binder, localFieldId, this, referenceQueue + ) + override fun getListener(): WeakListener<Flow<Any?>> { + return listener + } + + override fun addListener(target: Flow<Any?>?) { + val owner = _lifecycleOwner ?: return + if (target != null) { + startCollection(owner, target) + } + } + + override fun removeListener(target: Flow<Any?>?) { + observerJob?.cancel() + observerJob = null + } + + private fun startCollection(owner: LifecycleOwner, flow: Flow<Any?>) { + observerJob?.cancel() + observerJob = owner.lifecycleScope.launchWhenCreated { + flow.collect { + listener.binder?.handleFieldChange(listener.mLocalFieldId, listener.target, 0) + } + } + } + + override fun setLifecycleOwner(lifecycleOwner: LifecycleOwner?) { + if (_lifecycleOwner === lifecycleOwner) { + return + } + observerJob?.cancel() + _lifecycleOwner = lifecycleOwner + val target = listener.target + if (lifecycleOwner != null && target != null) { + startCollection(lifecycleOwner, target) + } + } + } +} diff --git a/extensions/library/build.gradle b/extensions/library/build.gradle index 3963d839..774c49d6 100644 --- a/extensions/library/build.gradle +++ b/extensions/library/build.gradle @@ -68,10 +68,10 @@ configurations { dependencies { implementation rootProject.ext.libs.android_support_collection - implementation "androidx.databinding:databinding-common:${dataBindingConfig.version}" + api "androidx.databinding:databinding-common:${dataBindingConfig.version}" api project(':viewbinding') - compileOnly rootProject.ext.libs.android_arch_lifecycle_extensions api rootProject.ext.libs.android_arch_lifecycle_runtime + compileOnly rootProject.ext.libs.android_arch_lifecycle_extensions } //create jar tasks diff --git a/extensions/library/src/main/java/androidx/databinding/CreateWeakListener.java b/extensions/library/src/main/java/androidx/databinding/CreateWeakListener.java new file mode 100644 index 00000000..398e468e --- /dev/null +++ b/extensions/library/src/main/java/androidx/databinding/CreateWeakListener.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 androidx.databinding; + +import androidx.annotation.RestrictTo; +import java.lang.ref.ReferenceQueue; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface CreateWeakListener { + WeakListener create( + ViewDataBinding viewDataBinding, + int localFieldId, + ReferenceQueue<ViewDataBinding> referenceQueue); +} diff --git a/extensions/library/src/main/java/androidx/databinding/ObservableReference.java b/extensions/library/src/main/java/androidx/databinding/ObservableReference.java new file mode 100644 index 00000000..447d47e9 --- /dev/null +++ b/extensions/library/src/main/java/androidx/databinding/ObservableReference.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 androidx.databinding; + +import androidx.annotation.RestrictTo; +import androidx.lifecycle.LifecycleOwner; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface ObservableReference<T> { + WeakListener<T> getListener(); + void addListener(T target); + void removeListener(T target); + void setLifecycleOwner(LifecycleOwner lifecycleOwner); +} diff --git a/extensions/library/src/main/java/androidx/databinding/ViewDataBinding.java b/extensions/library/src/main/java/androidx/databinding/ViewDataBinding.java index 5df3a3cd..9612bd65 100644 --- a/extensions/library/src/main/java/androidx/databinding/ViewDataBinding.java +++ b/extensions/library/src/main/java/androidx/databinding/ViewDataBinding.java @@ -92,8 +92,13 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind */ private static final CreateWeakListener CREATE_PROPERTY_LISTENER = new CreateWeakListener() { @Override - public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) { - return new WeakPropertyListener(viewDataBinding, localFieldId).getListener(); + public WeakListener create( + ViewDataBinding viewDataBinding, + int localFieldId, + ReferenceQueue<ViewDataBinding> referenceQueue + ) { + return new WeakPropertyListener(viewDataBinding, localFieldId, referenceQueue) + .getListener(); } }; @@ -102,8 +107,13 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind */ private static final CreateWeakListener CREATE_LIST_LISTENER = new CreateWeakListener() { @Override - public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) { - return new WeakListListener(viewDataBinding, localFieldId).getListener(); + public WeakListener create( + ViewDataBinding viewDataBinding, + int localFieldId, + ReferenceQueue<ViewDataBinding> referenceQueue + ) { + return new WeakListListener(viewDataBinding, localFieldId, referenceQueue) + .getListener(); } }; @@ -112,8 +122,13 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind */ private static final CreateWeakListener CREATE_MAP_LISTENER = new CreateWeakListener() { @Override - public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) { - return new WeakMapListener(viewDataBinding, localFieldId).getListener(); + public WeakListener create( + ViewDataBinding viewDataBinding, + int localFieldId, + ReferenceQueue<ViewDataBinding> referenceQueue + ) { + return new WeakMapListener(viewDataBinding, localFieldId, referenceQueue) + .getListener(); } }; @@ -122,8 +137,13 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind */ private static final CreateWeakListener CREATE_LIVE_DATA_LISTENER = new CreateWeakListener() { @Override - public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) { - return new LiveDataListener(viewDataBinding, localFieldId).getListener(); + public WeakListener create( + ViewDataBinding viewDataBinding, + int localFieldId, + ReferenceQueue<ViewDataBinding> referenceQueue + ) { + return new LiveDataListener(viewDataBinding, localFieldId, referenceQueue) + .getListener(); } }; @@ -272,6 +292,14 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind private boolean mInLiveDataRegisterObserver; /** + * When StateFlow first collects for chances, it notifies immediately that there was a + * change. This flag identifies that we've just started observing StateFlow and we should ignore + * the change notification. + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + protected boolean mInStateFlowRegisterObserver; + + /** * Needed for backwards binary compatibility. * b/122936785 * @hide @@ -539,9 +567,10 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind return mRoot; } - private void handleFieldChange(int mLocalFieldId, Object object, int fieldId) { - if (mInLiveDataRegisterObserver) { - // We're in LiveData registration, which always results in a field change + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + protected void handleFieldChange(int mLocalFieldId, Object object, int fieldId) { + if (mInLiveDataRegisterObserver || mInStateFlowRegisterObserver) { + // We're in LiveData or StateFlow registration, which always results in a field change // that we can ignore. The value will be read immediately after anyway, so // there is no need to be dirty. return; @@ -602,7 +631,8 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind return listener.getTarget(); } - private boolean updateRegistration(int localFieldId, Object observable, + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + protected boolean updateRegistration(int localFieldId, Object observable, CreateWeakListener listenerCreator) { if (observable == null) { return unregisterFrom(localFieldId); @@ -679,7 +709,7 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind } WeakListener listener = mLocalFieldObservers[localFieldId]; if (listener == null) { - listener = listenerCreator.create(this, localFieldId); + listener = listenerCreator.create(this, localFieldId, sReferenceQueue); mLocalFieldObservers[localFieldId] = listener; if (mLifecycleOwner != null) { listener.setLifecycleOwner(mLifecycleOwner); @@ -1374,66 +1404,16 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind ); } - private interface ObservableReference<T> { - WeakListener<T> getListener(); - void addListener(T target); - void removeListener(T target); - void setLifecycleOwner(LifecycleOwner lifecycleOwner); - } - - private static class WeakListener<T> extends WeakReference<ViewDataBinding> { - private final ObservableReference<T> mObservable; - protected final int mLocalFieldId; - private T mTarget; - - public WeakListener(ViewDataBinding binder, int localFieldId, - ObservableReference<T> observable) { - super(binder, sReferenceQueue); - mLocalFieldId = localFieldId; - mObservable = observable; - } - - public void setLifecycleOwner(LifecycleOwner lifecycleOwner) { - mObservable.setLifecycleOwner(lifecycleOwner); - } - - public void setTarget(T object) { - unregister(); - mTarget = object; - if (mTarget != null) { - mObservable.addListener(mTarget); - } - } - - public boolean unregister() { - boolean unregistered = false; - if (mTarget != null) { - mObservable.removeListener(mTarget); - unregistered = true; - } - mTarget = null; - return unregistered; - } - - public T getTarget() { - return mTarget; - } - - protected ViewDataBinding getBinder() { - ViewDataBinding binder = get(); - if (binder == null) { - unregister(); // The binder is dead - } - return binder; - } - } - private static class WeakPropertyListener extends Observable.OnPropertyChangedCallback implements ObservableReference<Observable> { final WeakListener<Observable> mListener; - public WeakPropertyListener(ViewDataBinding binder, int localFieldId) { - mListener = new WeakListener<Observable>(binder, localFieldId, this); + public WeakPropertyListener( + ViewDataBinding binder, + int localFieldId, + ReferenceQueue<ViewDataBinding> referenceQueue + ) { + mListener = new WeakListener<Observable>(binder, localFieldId, this, referenceQueue); } @Override @@ -1473,8 +1453,14 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind implements ObservableReference<ObservableList> { final WeakListener<ObservableList> mListener; - public WeakListListener(ViewDataBinding binder, int localFieldId) { - mListener = new WeakListener<ObservableList>(binder, localFieldId, this); + public WeakListListener( + ViewDataBinding binder, + int localFieldId, + ReferenceQueue<ViewDataBinding> referenceQueue + ) { + mListener = new WeakListener<ObservableList>( + binder, localFieldId, this, referenceQueue + ); } @Override @@ -1535,8 +1521,14 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind implements ObservableReference<ObservableMap> { final WeakListener<ObservableMap> mListener; - public WeakMapListener(ViewDataBinding binder, int localFieldId) { - mListener = new WeakListener<ObservableMap>(binder, localFieldId, this); + public WeakMapListener( + ViewDataBinding binder, + int localFieldId, + ReferenceQueue<ViewDataBinding> referenceQueue + ) { + mListener = new WeakListener<ObservableMap>( + binder, localFieldId, this, referenceQueue + ); } @Override @@ -1573,8 +1565,12 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind final WeakListener<LiveData<?>> mListener; LifecycleOwner mLifecycleOwner; - public LiveDataListener(ViewDataBinding binder, int localFieldId) { - mListener = new WeakListener(binder, localFieldId, this); + public LiveDataListener( + ViewDataBinding binder, + int localFieldId, + ReferenceQueue<ViewDataBinding> referenceQueue + ) { + mListener = new WeakListener(binder, localFieldId, this, referenceQueue); } @Override @@ -1618,10 +1614,6 @@ public abstract class ViewDataBinding extends BaseObservable implements ViewBind } } - private interface CreateWeakListener { - WeakListener create(ViewDataBinding viewDataBinding, int localFieldId); - } - /** * This class is used by generated subclasses of {@link ViewDataBinding} to track the * included layouts contained in the bound layout. This class is an implementation diff --git a/extensions/library/src/main/java/androidx/databinding/WeakListener.java b/extensions/library/src/main/java/androidx/databinding/WeakListener.java new file mode 100644 index 00000000..0af2313b --- /dev/null +++ b/extensions/library/src/main/java/androidx/databinding/WeakListener.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 androidx.databinding; + +import android.annotation.TargetApi; + +import androidx.annotation.RestrictTo; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.OnLifecycleEvent; +import android.content.res.ColorStateList; +import androidx.databinding.CallbackRegistry.NotifierCallback; +import android.graphics.drawable.Drawable; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.text.TextUtils; +import android.util.LongSparseArray; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; +import android.util.SparseLongArray; +import android.view.Choreographer; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnAttachStateChangeListener; +import android.view.ViewGroup; +import androidx.viewbinding.ViewBinding; + +import androidx.databinding.library.R; +import androidx.databinding.ObservableReference; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Map; + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class WeakListener<T> extends WeakReference<ViewDataBinding> { + private final ObservableReference<T> mObservable; + protected final int mLocalFieldId; + private T mTarget; + + public WeakListener( + ViewDataBinding binder, + int localFieldId, + ObservableReference<T> observable, + ReferenceQueue<ViewDataBinding> referenceQueue + ) { + super(binder, referenceQueue); + mLocalFieldId = localFieldId; + mObservable = observable; + } + + public void setLifecycleOwner(LifecycleOwner lifecycleOwner) { + mObservable.setLifecycleOwner(lifecycleOwner); + } + + public void setTarget(T object) { + unregister(); + mTarget = object; + if (mTarget != null) { + mObservable.addListener(mTarget); + } + } + + public boolean unregister() { + boolean unregistered = false; + if (mTarget != null) { + mObservable.removeListener(mTarget); + unregistered = true; + } + mTarget = null; + return unregistered; + } + + public T getTarget() { + return mTarget; + } + + @Nullable + protected ViewDataBinding getBinder() { + ViewDataBinding binder = get(); + if (binder == null) { + unregister(); // The binder is dead + } + return binder; + } +} diff --git a/extensions/settings.gradle b/extensions/settings.gradle index 90a9cf9a..c8e81194 100644 --- a/extensions/settings.gradle +++ b/extensions/settings.gradle @@ -20,6 +20,7 @@ include ':library' include ':baseAdapters' include ':viewbinding' +include ':databindingKtx' if (hasProperty("includeDoclava")) { File externalRoot = new File(rootDir, '../../../external') include ':doclava' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 59b5f892..5028f28f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/integration-tests/DynamicApp/app/build.gradle b/integration-tests/DynamicApp/app/build.gradle index 98bac453..ab7384a3 100644 --- a/integration-tests/DynamicApp/app/build.gradle +++ b/integration-tests/DynamicApp/app/build.gradle @@ -44,8 +44,8 @@ android { dependencies { api 'androidx.appcompat:appcompat:1.0.0' - api 'androidx.lifecycle:lifecycle-extensions:2.0.0' + api 'androidx.lifecycle:lifecycle-extensions:2.2.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' -}
\ No newline at end of file +} diff --git a/integration-tests/KotlinTestApp/app/build.gradle b/integration-tests/KotlinTestApp/app/build.gradle index 76c4bfe0..752b3508 100644 --- a/integration-tests/KotlinTestApp/app/build.gradle +++ b/integration-tests/KotlinTestApp/app/build.gradle @@ -23,6 +23,7 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion rootProject.latestCompileSdk + defaultConfig { applicationId "androidx.databinding.kotlintestapp" minSdkVersion 14 @@ -41,14 +42,16 @@ android { } dependencies { - def supportVersion = "1.0.0" - def lifecycleVersion = "2.0.0" + def supportVersion = "1.2.0" + def lifecycleVersion = "2.2.0" def testingVersion = "1.1.0" implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(":library1") implementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.kotlinVersion" implementation "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion" - implementation "androidx.lifecycle:lifecycle-livedata:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1" implementation "androidx.appcompat:appcompat:$supportVersion" testImplementation 'junit:junit:4.12' androidTestImplementation("androidx.test:runner:$testingVersion") { diff --git a/integration-tests/KotlinTestApp/app/src/androidTest/java/androidx/databinding/kotlintestapp/StateFlowTest.kt b/integration-tests/KotlinTestApp/app/src/androidTest/java/androidx/databinding/kotlintestapp/StateFlowTest.kt new file mode 100644 index 00000000..36a6e8f6 --- /dev/null +++ b/integration-tests/KotlinTestApp/app/src/androidTest/java/androidx/databinding/kotlintestapp/StateFlowTest.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 androidx.databinding.kotlintestapp + +import androidx.databinding.kotlintestapp.databinding.StateFlowBinding +import androidx.databinding.kotlintestapp.vo.StateFlowContainer +import androidx.databinding.kotlintestapp.vo.StateFlowViewModel +import androidx.test.runner.AndroidJUnit4 +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class StateFlowTest { + + @Suppress("MemberVisibilityCanPrivate") + @Rule + @JvmField + val rule = BindingActivityRule<StateFlowBinding>(R.layout.state_flow) + + private val stateFlow = MutableStateFlow("") + private val stateFlowHolder = MutableStateFlow(stateFlow) + private val stateFlowContainer = StateFlowContainer() + private val stateFlowViewModel = StateFlowViewModel() + + @Test + fun testStateFlow() { + setupView() + + assertEquals("", rule.binding.textView1.text.toString()) + assertEquals("", rule.binding.included.textView1.text.toString()) + assertEquals("", rule.binding.textView2.text.toString()) + assertEquals("", rule.binding.included.textView2.text.toString()) + + val stateFlowExpected = "Hello world!" + stateFlow.value = stateFlowExpected + val containerExpected = "Hi there!" + stateFlowContainer.stateFlow.value = containerExpected + rule.executePendingBindings() + + assertEquals(stateFlowExpected, rule.binding.textView1.text.toString()) + assertEquals(stateFlowExpected, rule.binding.included.textView1.text.toString()) + assertEquals(containerExpected, rule.binding.textView2.text.toString()) + assertEquals(containerExpected, rule.binding.included.textView2.text.toString()) + } + + @Test + fun noLifecycleOwner() { + setupView(includeLifecycle = false) + + assertEquals("", rule.binding.textView1.text.toString()) + assertEquals("", rule.binding.included.textView1.text.toString()) + assertEquals("", rule.binding.textView2.text.toString()) + assertEquals("", rule.binding.included.textView2.text.toString()) + + stateFlow.value = "Hello world!" + stateFlowContainer.stateFlow.value = "Hi there!" + rule.executePendingBindings() + + assertEquals("", rule.binding.textView1.text.toString()) + assertEquals("", rule.binding.included.textView1.text.toString()) + assertEquals("", rule.binding.textView2.text.toString()) + assertEquals("", rule.binding.included.textView2.text.toString()) + } + + @Test + fun testMutableStateFlow() { + setupView() + + assertEquals("", rule.binding.textView2.text.toString()) + assertEquals("", rule.binding.included.textView2.text.toString()) + + val expected = "EditText changed" + rule.runOnUiThread { + rule.binding.editText1.setText(expected) + } + rule.executePendingBindings() + + assertEquals(expected, rule.binding.textView2.text.toString()) + assertEquals(expected, rule.binding.included.textView2.text.toString()) + assertEquals(expected, stateFlowContainer.stateFlow.value) + } + + @Test + fun stateFlowChanged() { + setupView() + + stateFlow.value = "foo" + rule.executePendingBindings() + assertEquals("foo", rule.binding.textView1.text) + + rule.runOnUiThread { + val anotherStateFlow = MutableStateFlow("new value") + rule.binding.stateFlow = anotherStateFlow + rule.executePendingBindings() + } + + assertEquals("new value", rule.binding.textView1.text) + } + + @Test + fun stateFlowOfStateFlowChanged() { + setupView() + + stateFlow.value = "foo" + rule.executePendingBindings() + assertEquals("foo", rule.binding.textView3.text) + + stateFlow.value = "bar" + rule.executePendingBindings() + assertEquals("bar", rule.binding.textView3.text) + + rule.runOnUiThread { + val anotherStateFlow = MutableStateFlow("new value") + stateFlowHolder.value = anotherStateFlow + rule.executePendingBindings() + } + + assertEquals("new value", rule.binding.textView3.text) + } + + @Test + fun testStateInOperator() { + setupView() + assertEquals("Hi there", rule.binding.textView4.text.toString()) + } + + private fun setupView(includeLifecycle: Boolean = true) { + rule.runOnUiThread { + rule.binding.apply { + if (includeLifecycle) { + lifecycleOwner = rule.activity + } + stateFlow = this@StateFlowTest.stateFlow + stateFlowOfStateFlow = this@StateFlowTest.stateFlowHolder + stateFlowContainer = this@StateFlowTest.stateFlowContainer + stateFlowViewModel = this@StateFlowTest.stateFlowViewModel + } + } + + rule.executePendingBindings() + } +} diff --git a/integration-tests/KotlinTestApp/app/src/main/AndroidManifest.xml b/integration-tests/KotlinTestApp/app/src/main/AndroidManifest.xml index 364d420e..3eaa3208 100644 --- a/integration-tests/KotlinTestApp/app/src/main/AndroidManifest.xml +++ b/integration-tests/KotlinTestApp/app/src/main/AndroidManifest.xml @@ -36,4 +36,4 @@ </activity> </application> -</manifest>
\ No newline at end of file +</manifest> diff --git a/integration-tests/KotlinTestApp/app/src/main/java/androidx/databinding/kotlintestapp/vo/StateFlowContainer.kt b/integration-tests/KotlinTestApp/app/src/main/java/androidx/databinding/kotlintestapp/vo/StateFlowContainer.kt new file mode 100644 index 00000000..25531fa2 --- /dev/null +++ b/integration-tests/KotlinTestApp/app/src/main/java/androidx/databinding/kotlintestapp/vo/StateFlowContainer.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 androidx.databinding.kotlintestapp.vo + +import kotlinx.coroutines.flow.MutableStateFlow + +class StateFlowContainer { + val stateFlow = MutableStateFlow("") +} diff --git a/integration-tests/KotlinTestApp/app/src/main/java/androidx/databinding/kotlintestapp/vo/StateFlowViewModel.kt b/integration-tests/KotlinTestApp/app/src/main/java/androidx/databinding/kotlintestapp/vo/StateFlowViewModel.kt new file mode 100644 index 00000000..358cb7ba --- /dev/null +++ b/integration-tests/KotlinTestApp/app/src/main/java/androidx/databinding/kotlintestapp/vo/StateFlowViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 androidx.databinding.kotlintestapp.vo + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn + +class StateFlowViewModel : ViewModel() { + + val stateFlow = flow { + emit("Hi there") + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") +} diff --git a/integration-tests/KotlinTestApp/app/src/main/res/layout/adapter_from_library.xml b/integration-tests/KotlinTestApp/app/src/main/res/layout/adapter_from_library.xml index c301f861..8a354e23 100644 --- a/integration-tests/KotlinTestApp/app/src/main/res/layout/adapter_from_library.xml +++ b/integration-tests/KotlinTestApp/app/src/main/res/layout/adapter_from_library.xml @@ -16,4 +16,4 @@ android:id="@+id/library1TextView" app:setTextViaLibrary1="@{text}"/> </LinearLayout> -</layout>
\ No newline at end of file +</layout> diff --git a/integration-tests/KotlinTestApp/app/src/main/res/layout/state_flow.xml b/integration-tests/KotlinTestApp/app/src/main/res/layout/state_flow.xml new file mode 100644 index 00000000..79a05885 --- /dev/null +++ b/integration-tests/KotlinTestApp/app/src/main/res/layout/state_flow.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<layout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <data> + <variable + name="stateFlow" + type="kotlinx.coroutines.flow.StateFlow<String>" /> + + <variable + name="stateFlowOfStateFlow" + type="kotlinx.coroutines.flow.StateFlow<kotlinx.coroutines.flow.StateFlow<String>>" /> + + <variable + name="stateFlowContainer" + type="androidx.databinding.kotlintestapp.vo.StateFlowContainer" /> + + <variable + name="stateFlowViewModel" + type="androidx.databinding.kotlintestapp.vo.StateFlowViewModel" /> + </data> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/textView1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{stateFlow}" /> + + <TextView + android:id="@+id/textView2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{stateFlowContainer.stateFlow}" /> + + <TextView + android:id="@+id/textView3" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{stateFlowOfStateFlow}" /> + + <TextView + android:id="@+id/textView4" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{stateFlowViewModel.stateFlow}" /> + + <EditText + android:id="@+id/editText1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@={stateFlowContainer.stateFlow}" /> + + <include layout="@layout/state_flow_included" + android:id="@+id/included" + app:stateFlow="@{stateFlow}" + app:stateFlowContainer="@{stateFlowContainer}" /> + + </LinearLayout> +</layout> diff --git a/integration-tests/KotlinTestApp/app/src/main/res/layout/state_flow_included.xml b/integration-tests/KotlinTestApp/app/src/main/res/layout/state_flow_included.xml new file mode 100644 index 00000000..68c498e2 --- /dev/null +++ b/integration-tests/KotlinTestApp/app/src/main/res/layout/state_flow_included.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<layout xmlns:android="http://schemas.android.com/apk/res/android"> + + <data> + <variable + name="stateFlow" + type="kotlinx.coroutines.flow.StateFlow<String>" /> + + <variable + name="stateFlowContainer" + type="androidx.databinding.kotlintestapp.vo.StateFlowContainer" /> + </data> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/textView1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{stateFlow}" /> + + <TextView + android:id="@+id/textView2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@{stateFlowContainer.stateFlow}" /> + + <EditText + android:id="@+id/editText1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@={stateFlowContainer.stateFlow}" /> + + </LinearLayout> +</layout> diff --git a/integration-tests/KotlinTestApp/build.gradle b/integration-tests/KotlinTestApp/build.gradle index ff3a7d1b..d499539e 100644 --- a/integration-tests/KotlinTestApp/build.gradle +++ b/integration-tests/KotlinTestApp/build.gradle @@ -33,4 +33,4 @@ if (!rootProject.ext.runningInIde) { project.apply from: "../../commonHeader.gradle" project.apply from: "../../commonLocalRepo.gradle" } -}
\ No newline at end of file +} diff --git a/integration-tests/KotlinTestApp/library1/build.gradle b/integration-tests/KotlinTestApp/library1/build.gradle index 1fc6a245..2d7055bb 100644 --- a/integration-tests/KotlinTestApp/library1/build.gradle +++ b/integration-tests/KotlinTestApp/library1/build.gradle @@ -12,13 +12,14 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - `*/ + */ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { compileSdkVersion rootProject.latestCompileSdk + defaultConfig { minSdkVersion 14 targetSdkVersion rootProject.latestCompileSdk diff --git a/integration-tests/TestApp/app/build.gradle b/integration-tests/TestApp/app/build.gradle index 993d61ee..f42560a6 100644 --- a/integration-tests/TestApp/app/build.gradle +++ b/integration-tests/TestApp/app/build.gradle @@ -32,7 +32,7 @@ android { dependencies { def supportVersion = "1.0.0" - def lifecycleVersion = "2.0.0" + def lifecycleVersion = "2.2.0" implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "androidx.annotation:annotation:$supportVersion" compileOnly 'javax.annotation:jsr250-api:1.0' diff --git a/integration-tests/ViewBindingWithDataBindingTestApp/app/build.gradle b/integration-tests/ViewBindingWithDataBindingTestApp/app/build.gradle index e768a35f..d9620cf7 100644 --- a/integration-tests/ViewBindingWithDataBindingTestApp/app/build.gradle +++ b/integration-tests/ViewBindingWithDataBindingTestApp/app/build.gradle @@ -43,7 +43,7 @@ android { dependencies { def supportVersion = "1.0.0" - def lifecycleVersion = "2.0.0" + def lifecycleVersion = "2.2.0" def testingVersion = "1.1.0" implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.kotlinVersion" |