MENU

Android声明式UI框架 Litho 初探 ——两种数据类型

September 5, 2020 • Read: 96 • Android

Litho中包含的的两种数据类型

Litho的两种属性分别是:

  • 不可变属性称为Props
  • 可变属性称为State

不可变属性Props

定义和使用props

Props属性:Component中使用@Prop注解的参数集合,具有单向性和不可变性,可以在左右的方法中访问它的指。在同一个Component中我们可以定义和访问相同的prop
下面这个例子,定义了两个Prop,一个string类型text,一个int类型index,text的注解中optional = true表示它是一个可选参数。

Component的生命周期方法被调用的时候,@Prop参数会保存component创建时从它们的父级传递过来的值(或者它们的默认值)

设置props

prop参数其实在前几篇文章中都有使用过,用起来也没有什么特别的地方,这里不在赘述,制作一个简单的说明。

Component中的prop参数会在编译时候自动加入到Builder中,以上面的代码举例:

 PropComponent.create(c).index(10)./*text("测试文本").*/build()

Prop的默认值

对于可选的Prop如果不设置值,就是java的默认值。或者你也可以使用@PropDefault注解然后添加默认值。

如果你使用Kotlin,那还需要加上@JvmFiel把该字段编辑为public才行。

 @MountSpec
object PropComponentSpec {
    @JvmField
    @PropDefault
    val prop1 = "default"
    @JvmField
    @PropDefault
    val prop2 = -1

资源类型

在Android开发中,我们经常会限定参数的类型。比如:

fun doSomething(@ColorInt color: Int, @StringRes str: Int, @DimenRes width: Int){}

Compontent的Prop中也有类似的操作,具体看代码:

fun onMount(
        c: ComponentContext, textView: TextView,
        @Prop(optional = true,resType = ResType.STRING) text: String?,
        @Prop index: Int
    ) {}

需要注意的是,Conpontent中修改一个Prop后,其他使用想用Prop的地方也需要修改

当你按照上面的方法修改并且build后,会自动生成Res,Attr,Dip,Px方法。

https://static.apkdv.com/usr/uploads/2020/09/3962129116.png

你可以像下面这样使用:

PropComponent.create(c).index(14).textRes(R.string.app_name).build()

ResType中包含以下这些类型:

https://static.apkdv.com/usr/uploads/2020/09/3962129116.png

可变属性State

定义和使用State

State 一般用在与用户交互的场景中,比如:点击、输入框、Checkbox。但是这些都是由当前Compontent内部感知,并更新State,他的父级并需要关心他的状态。正因为State是Compontent,所以当Compontent创建后,如果我们需要修改State,只能通过单独定义一个Prop属性来修改State的初始值

State 的声明和 Prop 区别不是很大:

@LayoutSpec
object StateComponentSpec {

     /**
     * 定义一个State参数isCheck
     */
    @OnCreateLayout
    fun onCreateLayout(c: ComponentContext, @State isCheck: Boolean): Component {
        return Column.create(c).child(
            Image.create(c).drawableRes(
                if (isCheck) android.R.drawable.checkbox_on_background
                else android.R.drawable.checkbox_off_background
            ).build()
        ).child(
            Text.create(c).text(if (isCheck) "Checked" else "Uncheck").textColor(Color.BLACK)
                .textSizeDip(16f).marginDip(YogaEdge.TOP, 10f).build()
        ).clickHandler(StateComponent.onClick(c)).build()
    }

      @OnUpdateState
    fun updateCheckedState(isCheck: StateValue<Boolean>) {
    
    }
}

State 初始化

State 需要在@OnCreateInitialState注解的方法中初始化:
OnCreateInitialState方法需要注意:

  1. 第一个参数必须是ComponentContext(大部分的Componetn方法都要求第一个参数必须是ComponentContext)
  2. State相关的参数的名称必须和其他生命周期方法中的@State参数保持一致,并且这些参数的类型必须是StateValue,其中泛型的类型与对应的@State一致
  3. 如果没有定义@OnCreateInitialState,State的值就是java默认值
  4. 只有在Component第一次被添加到Component树的时候才会调用一次@OnCreateInitialState方法.如果Component的key没有改变,后续对Component树布局的重新计算并不会重新调用@OnCreateInitialState方法
  5. 不需要自己调用@OnCreateInitialState方法.
@OnCreateInitialState
    fun updateCheckState(
        c: ComponentContext, isCheck: StateValue<Boolean>,
        @Prop initChecked: Boolean
    ) {
        isCheck.set(initChecked)
    }

更新 State

State 需要在@OnUpdateState注解的方法中更新:
OnUpdateState方法需要注意:

  1. 可以定义多个OnUpdateState方法来更新不同的 State ,但是OnUpdateState方法每次调用都会对它所在的Component重新计算一次。所以为了更好的性能,应该尽可能少的调用OnUpdateState,或者合并多个 State 的更新,来提升性能
  2. 和初始化时候一个样。State相关的参数的名称必须和其他生命周期方法中的@State参数保持一致,并且这些参数的类型必须是StateValue,其中泛型的类型与对应的@State一致
  3. 如果你的State的值需要依赖于Prop,你可以在@OnupdateState函数的参数中使用@Param声明,这样就可以在更新被触发的时候传递prop的值进来了.

跟我们的Check增加一个更新方法:

  @OnUpdateState
    fun updateCheckedState(isCheck: StateValue<Boolean>) {
        val check = isCheck.get()
        isCheck.set(check?.let { !check })
    }

合并多个 State 更新,并且使用 Param 来更新State:

 /**
     * 多个State合并更新,同时 使用Param来更新 State
     */
    @OnUpdateState
    fun updateCheckedStateTwo(isCheck: StateValue<Boolean>,isCheckTwo: StateValue<Boolean>,
    @Param checked:Boolean) {
        isCheck.set(!checked)
        isCheck.set(isCheckTwo.get())
    }

调用State更新

对于使用@OnUpdateState注解的方法,编译后自动生成两个更新方法:

  • 一个@OnUpdateState同名的方法,它会同步的调用state的更新。
  • 一个加上Async后缀的静态方法,它会异步的调用state的更新。

(图中的updateCheckedState,updateCheckedStateSync最终调用的都是同一个方法,所以这里说生成了两个方法)

让我们的点击事件调用更新方法:

@OnEvent(ClickEvent::class)
    fun onClick(c: ComponentContext, @State isCheck: Boolean) {
        StateComponent.updateCheckedStateAsync(c)
    }

关于调用更新方法有一下几点需要注意:

  1. LayoutSpec中避免在onCreateLayout中直接调用更新方法,因为更新会触发布局重新计算,而重新计算又会触发onCreateLayout,很容易造成死循环
  2. MountSpec中,不要在onMountonBind方法中直接调用更新方法,如果你真的需要在这类方法中更新State的值,那么应该使用下面会讲到的懒汉式State更新来替代.
  3. 当调用一个State更新方法的时候(StateComponent.updateCheckedStateAsync(c)),参数中的ComponentContext必须是当前需要更新传递过来的ComponentContext,因为它包含了现有的State等其他重要的信息,在重新计算的时候回替换原有的Component,生成新的Component.

懒汉式更新State

懒汉式更新可以更新 State 的值,但是又不会立刻触发Component的布局计算,当调用懒汉式更新后,Component将会保持现有的State值,在下次被别的机制(例如收到一个新的prop或者或者State的定期更新)触发是,才会更新State的值,在不需要立刻进行布局计算的情况下,懒汉式更新对想要更新内部Component信息并且在Component树的重新布局中保持这些信息是非常实用的.

要是用懒汉式更新,需要在 @State 注解中设置canUploadLazily = true

/**
 * 懒更新State
 */
@LayoutSpec
object LazilyUpdateComponentSpec {

    @OnCreateLayout
    fun onCreateLayout(
        c: ComponentContext,
        @State(canUpdateLazily = true) name: String
    ): Component {
        // 在这里直接调用 更新 State 方法
        LazilyUpdateComponent.lazyUpdateName(c,"UpdateName")
        return Column.create(c)
            .child(
                Text.create(c).text(name)
            ).build()
    }

    @OnCreateInitialState
    fun stateInit(c: ComponentContext, name: StateValue<String>, @Prop initName: String) {
        name.set(initName)
    }
}

调用:

 val component = LazilyUpdateComponent.create(c).initName("initName").build()

根据代码,我们在onCreateLayout方法中调用了更新State的方法,但是由于是懒更新,所以并不是对布局进行重新计算,所以界面上显示的还是初始化的值。

对上面的代码修改一下,增加一个点击事件,点击后更新另一个State的值:

/**
 * 懒汉式更新State
 */
@LayoutSpec
object LazilyUpdateComponentSpec {
    private const val TAG = "LazilyUpdateComponentSp"

    @OnCreateLayout
    fun onCreateLayout(
        c: ComponentContext,
        @State(canUpdateLazily = true) name: String,
        @State testData: String?
    ): Component {
        // 在这里直接调用 更新 State 方法
        LazilyUpdateComponent.lazyUpdateName(c, "UpdateName")
        Log.i(TAG, "onCreateLayout: $name")
        Log.i(TAG, "onCreateLayout: ${testData ?: ""}")
        return Column.create(c).clickHandler(LazilyUpdateComponent.onClick(c))
            .child(
                Text.create(c).text(name)
            ).build()
    }

    @OnCreateInitialState
    fun stateInit(c: ComponentContext, name: StateValue<String>, @Prop initName: String) {
        name.set(initName)
    }

    @OnEvent(ClickEvent::class)
    fun onClick(c: ComponentContext) {
        LazilyUpdateComponent.updateTestDataAsync(c)
    }

    @OnUpdateState
    fun updateTestData(testData: StateValue<String>) {
        testData.set("TestData")
    }
}

logcat:

点击前:
LazilyUpdateComponentSp: onCreateLayout: initName
LazilyUpdateComponentSp: onCreateLayout: 
···
点击后:
LazilyUpdateComponentSp: onCreateLayout: UpdateName
LazilyUpdateComponentSp: onCreateLayout: TestData

同时UI上也被更新: