From 1080fc21af530f52d7d46055360d565ba574a5aa Mon Sep 17 00:00:00 2001 From: lunkovartemij Date: Fri, 19 Feb 2021 19:54:29 +0300 Subject: [PATCH] saving viewModel state, saving fragment arguments, completed readme --- README.md | 36 +++++++++---- .../ui_generator_base/ComponentViewModel.kt | 5 ++ .../ru/impression/ui_generator_base/Ext.kt | 7 +++ .../ui_generator_base/SavedViewState.kt | 7 +++ .../ui_generator_base/StateDelegate.kt | 1 - .../ComponentClassBuilder.kt | 4 +- .../FragmentComponentClassBuilder.kt | 52 +++++++++++++++++-- .../ViewComponentClassBuilder.kt | 30 +++++++++++ 8 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 ui-generator-base/src/main/java/ru/impression/ui_generator_base/SavedViewState.kt diff --git a/README.md b/README.md index 5d35496..59d602f 100644 --- a/README.md +++ b/README.md @@ -114,22 +114,20 @@ As you can see, the codes for the Fragment and for the View are completely ident **Note:** you may need to build the project twice so that the binding adapters and component classes are generated correctly. -Also, in the case of a View, you can set `Prop.twoWay = true`, and then a two-way binding adapter will be generated for the View. It will send the value back when the annotated property changes. +Also, in the case of a View: +- You can set `Prop.twoWay = true`, and then a two-way binding adapter will be generated for the View. It will send the value back when the annotated property changes. ```kotlin @Prop(twoWay = true) var twoWayText: String? = null //a two-way binding adapter will be generated ``` - -And in the case of Fragments, you can pass callbacks to them: +- You can bind xml attribute to your state property: ```kotlin -// ChildFragmentViewModel.kt -@Prop -var callback: (() -> Unit)? = null - -// ChildFragment's parent Fragment -showFragment(ChildFragmentComponent().apply { callback = this@ParentFragment.viewModel.childFragmentCallback }) +var picture by state(null, attr = R.styleable.MyViewComponent_picture) +``` +```xml + ``` -But keep in mind that the source value of the callback must be in the ViewModel so that the callback does not refer to a Fragment or View. ### 2. Observable state @@ -182,6 +180,8 @@ class MyTextView : ComponentScheme({ ### 5. Coroutine support +#### suspend funs + Suppose that before you display some data, you need to load it first. Here's how you do it: ```kotlin var greeting: String? by state({ @@ -212,4 +212,20 @@ fun reloadGreeting() { } ``` +#### Flows + +Suppose you need to subscribe to the Flow and display all its elements. Here's how you do it: +```kotlin +var countDown: Int? by state(flow { + delay(1000) + emit(3) + delay(1000) + emit(2) + delay(1000) + emit(1) + delay(1000) + emit(0) +}) +``` + ***For detailed examples see module `app`.*** \ No newline at end of file diff --git a/ui-generator-base/src/main/java/ru/impression/ui_generator_base/ComponentViewModel.kt b/ui-generator-base/src/main/java/ru/impression/ui_generator_base/ComponentViewModel.kt index 139cd99..9e8d2e5 100644 --- a/ui-generator-base/src/main/java/ru/impression/ui_generator_base/ComponentViewModel.kt +++ b/ui-generator-base/src/main/java/ru/impression/ui_generator_base/ComponentViewModel.kt @@ -2,6 +2,7 @@ package ru.impression.ui_generator_base import android.os.Handler import android.os.Looper +import android.os.Parcelable import androidx.annotation.CallSuper import androidx.lifecycle.* import kotlin.reflect.KMutableProperty0 @@ -107,6 +108,10 @@ abstract class ComponentViewModel(val attrs: IntArray? = null) : ViewModel(), St protected open fun onLifecycleEvent(event: Lifecycle.Event) = Unit + open fun onSaveInstanceState(): Parcelable? = null + + open fun onRestoreInstanceState(savedInstanceState: Parcelable?) = Unit + public override fun onCleared() = Unit protected fun KMutableProperty0.set(value: T, renderImmediately: Boolean = false) { diff --git a/ui-generator-base/src/main/java/ru/impression/ui_generator_base/Ext.kt b/ui-generator-base/src/main/java/ru/impression/ui_generator_base/Ext.kt index 27e873e..9a8d548 100644 --- a/ui-generator-base/src/main/java/ru/impression/ui_generator_base/Ext.kt +++ b/ui-generator-base/src/main/java/ru/impression/ui_generator_base/Ext.kt @@ -3,11 +3,13 @@ package ru.impression.ui_generator_base import android.content.Context import android.content.ContextWrapper import android.graphics.drawable.Drawable +import android.os.Bundle import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.bundleOf import androidx.databinding.ViewDataBinding import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle @@ -30,6 +32,11 @@ val View.activity: AppCompatActivity? return contextWrapper } +fun Fragment.putArgument(key: String, value: Any?) { + val arguments = arguments ?: Bundle().also { arguments = it } + arguments.putAll(bundleOf(key to value)) +} + fun T.resolveAttrs(attrs: AttributeSet?) where T : Component<*, VM>, T : View { with(context.theme.obtainStyledAttributes(attrs, viewModel.attrs ?: return, 0, 0)) { try { diff --git a/ui-generator-base/src/main/java/ru/impression/ui_generator_base/SavedViewState.kt b/ui-generator-base/src/main/java/ru/impression/ui_generator_base/SavedViewState.kt new file mode 100644 index 0000000..5c6898d --- /dev/null +++ b/ui-generator-base/src/main/java/ru/impression/ui_generator_base/SavedViewState.kt @@ -0,0 +1,7 @@ +package ru.impression.ui_generator_base + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +class SavedViewState(val superState: Parcelable?, val viewModelState: Parcelable?): Parcelable \ No newline at end of file diff --git a/ui-generator-base/src/main/java/ru/impression/ui_generator_base/StateDelegate.kt b/ui-generator-base/src/main/java/ru/impression/ui_generator_base/StateDelegate.kt index c8c29c4..a253b77 100644 --- a/ui-generator-base/src/main/java/ru/impression/ui_generator_base/StateDelegate.kt +++ b/ui-generator-base/src/main/java/ru/impression/ui_generator_base/StateDelegate.kt @@ -60,7 +60,6 @@ open class StateDelegate( (parent as? ComponentViewModel)?.initSubscriptions(::collect) ?: collect() } - @Synchronized override fun getValue(thisRef: R, property: KProperty<*>) = value @Synchronized diff --git a/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/ComponentClassBuilder.kt b/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/ComponentClassBuilder.kt index 2e19b88..7a6e214 100644 --- a/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/ComponentClassBuilder.kt +++ b/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/ComponentClassBuilder.kt @@ -93,5 +93,7 @@ abstract class ComponentClassBuilder( val type: TypeMirror, val twoWay: Boolean, val attrChangedPropertyName: String - ) + ) { + val kotlinType = type.asTypeName().javaToKotlinType().copy(true) + } } \ No newline at end of file diff --git a/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/FragmentComponentClassBuilder.kt b/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/FragmentComponentClassBuilder.kt index 9d97a70..a2c6071 100644 --- a/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/FragmentComponentClassBuilder.kt +++ b/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/FragmentComponentClassBuilder.kt @@ -33,6 +33,7 @@ class FragmentComponentClassBuilder( propProperties.forEach { add( """ + val ${it.name} = ${it.name} if (${it.name} != null && ${it.name} !== viewModel.${it.name}) viewModel::${it.name}.%M(${it.name}) viewModel.onStateChanged(renderImmediately = true) @@ -73,19 +74,51 @@ class FragmentComponentClassBuilder( override fun TypeSpec.Builder.addRestMembers() { propProperties.forEach { addProperty(buildPropWrapperProperty(it)) } + addFunction(buildOnCreateFunction()) addFunction(buildOnCreateViewFunction()) addFunction(buildOnActivityCreatedFunction()) + addFunction(buildOnSaveInstanceStateFunction()) addFunction(buildOnDestroyViewFunction()) } private fun buildPropWrapperProperty(propProperty: PropProperty) = with( PropertySpec.builder( propProperty.name, - propProperty.type.asTypeName().javaToKotlinType().copy(true) + propProperty.kotlinType ) ) { mutable(true) initializer("null") + getter( + FunSpec.getterBuilder() + .addCode( + """ + return field ?: arguments?.get("${propProperty.name}") as? ${propProperty.kotlinType} + + """.trimIndent() + ) + .build() + ) + setter( + FunSpec.setterBuilder().addParameter("value", propProperty.kotlinType).addCode( + """ + field = value + %M("${propProperty.name}", value) + """.trimIndent(), + MemberName("ru.impression.ui_generator_base", "putArgument") + ).build() + ) + build() + } + + private fun buildOnCreateFunction() = with(FunSpec.builder("onCreate")) { + addModifiers(KModifier.OVERRIDE) + addParameter("savedInstanceState", ClassName("android.os", "Bundle").copy(true)) + addCode( + """ + super.onCreate(savedInstanceState) + viewModel.onRestoreInstanceState(savedInstanceState?.getParcelable("viewModelState"))""".trimIndent() + ) build() } @@ -110,10 +143,21 @@ class FragmentComponentClassBuilder( addCode( """ super.onActivityCreated(savedInstanceState) - - """.trimIndent() + viewModel.setComponent(this) + """.trimIndent() + ) + build() + } + + private fun buildOnSaveInstanceStateFunction() = with(FunSpec.builder("onSaveInstanceState")) { + addModifiers(KModifier.OVERRIDE) + addParameter("outState", ClassName("android.os", "Bundle")) + addCode( + """ + super.onSaveInstanceState(outState) + outState.putParcelable("viewModelState", viewModel.onSaveInstanceState()) + """.trimIndent() ) - addCode("viewModel.setComponent(this)") build() } diff --git a/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/ViewComponentClassBuilder.kt b/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/ViewComponentClassBuilder.kt index 08fba29..1da6602 100644 --- a/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/ViewComponentClassBuilder.kt +++ b/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/ViewComponentClassBuilder.kt @@ -62,6 +62,8 @@ class ViewComponentClassBuilder( addFunction(buildOnTwoWayPropChangedFunction()) addFunction(buildOnAttachedToWindowFunction()) addFunction(buildOnDetachedFromWindowFunction()) + addFunction(buildOnSaveInstanceStateFunction()) + addFunction(buildOnRestoreInstanceStateFunction()) addType(buildCompanionObject()) } @@ -191,6 +193,34 @@ class ViewComponentClassBuilder( build() } + private fun buildOnSaveInstanceStateFunction() = with(FunSpec.builder("onSaveInstanceState")) { + addModifiers(KModifier.OVERRIDE) + returns(ClassName("android.os", "Parcelable").copy(true)) + addCode( + """ + return %T(super.onSaveInstanceState(), viewModel.onSaveInstanceState()) + + """.trimIndent(), + ClassName("ru.impression.ui_generator_base", "SavedViewState") + ) + build() + } + + private fun buildOnRestoreInstanceStateFunction() = + with(FunSpec.builder("onRestoreInstanceState")) { + addModifiers(KModifier.OVERRIDE) + addParameter("state", ClassName("android.os", "Parcelable").copy(true)) + addCode( + """ + super.onRestoreInstanceState((state as? %T)?.superState) + viewModel.onRestoreInstanceState((state as? %T)?.viewModelState) + """.trimIndent(), + ClassName("ru.impression.ui_generator_base", "SavedViewState"), + ClassName("ru.impression.ui_generator_base", "SavedViewState") + ) + build() + } + private fun buildCompanionObject(): TypeSpec = with(TypeSpec.companionObjectBuilder()) { propProperties.forEach { addFunction(buildPropSetter(it))