Litho 是什么

Litho 是一个用于在 Android 上构建高效用户界面(UI)的声明性框架。但不同以往的 UI 框架,它的底层是 Yoga, 它通过将不需要交互的 UI 转换为 Drawable 来渲染视图,通过 Yoga 来完成组件布局的异步或同步(可根据场景定制)测量和计算,实现了布局的扁平化。加速了 UI 渲染速度

在 Litho 中,使用组件(Component)来构建 UI,而不是直接与传统的 Android 视图进行交互。组件本质上是一个函数,它接受不可变的输入(称为属性 props),并返回描述用户界面的组件层次结构。

如果有 Flutter 开发经验,那么 Litho 的开发方式有点类似

接下来的教程都将结合代码进行讲解

基础配置

gradle

apply plugin: 'kotlin-kapt'

dependencies 中加入

    // Litho
    implementation 'com.facebook.litho:litho-core:0.37.1'
    implementation 'com.facebook.litho:litho-widget:0.37.1'
    kapt 'com.facebook.litho:litho-processor:0.37.1'
    // SoLoader
    implementation 'com.facebook.soloader:soloader:0.9.0'
    // For integration with Fresco
    implementation 'com.facebook.litho:litho-fresco:0.37.1'
    // Sections
    implementation 'com.facebook.litho:litho-sections-core:0.37.1'
    implementation 'com.facebook.litho:litho-sections-widget:0.37.1'
    compileOnly 'com.facebook.litho:litho-sections-annotations:0.37.1'
    kapt 'com.facebook.litho:litho-sections-processor:0.37.1'

初始化 SoLoader.Litho 依赖,SoLoader 用于加载底层布局引擎 Yoga

SoLoader.init(this, false);

使用基础 Component

Component Specs

Litho 中的视图单元叫做 Component,可以直观的翻译为组件

组件分为两种类型 :
Layout Spec:将其他组件组合到特定的布局中。这相当于 Android 上的 ViewGroup 。

Mount Spec:可以渲染 View 或 Drawable 组件。
现在,让我们来看看 Layout Spec 的整体结构:

Component 的类名必须以 Spec 结尾,不然会报错

/**
 * Component
 * 组件 Spec 只是一个普通的java类,带有一些特殊的注解。
 * 组件 Spec 是完全无状态的,没有任何类成员。
 * 使用 @Prop 标注的参数将自动成为组件构建器的一部分。
 */
@LayoutSpec // 将其他组件组合到特定的布局中。这相当于 Android 上的 ViewGroup
class MainLithoViewSpec {
    /**
     * @OnCreateLayout 注解的方法必须具有 ComponentContext 作为其第一个参数
     * 后跟使用 @Prop 标注的参数列表。注解处理器将在构建时对参数列表以及API中其他约束条件进行验证。
     */
    @OnCreateLayout
    fun onCreateLayout(
        context: ComponentContext,
        @Prop color: Int,
        @Prop title: String
    ): Component {
        return Column.create(context)
            .paddingDip(YogaEdge.ALL, 16f)
            .backgroundColor(Color.DKGRAY)
            .child(
                Text.create(context).text(title)
                    .textColor(color)
                    .textSizeDip(25f)
            )
            .child(
                Text.create(context).text("这是小标题")
                    .textColor(Color.GREEN)
                    .textSizeDip(16f)
            )
            .build()

    }
}

在 Activity 中使用

···
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val c = ComponentContext(this);
        // 这两方式都可以,但是第一种方式需要编译
        val component2 = MainLithoView.create(c).color(Color.WHITE).title("这是一个Title")
        
        val component = MainLithoViewSpec.onCreateLayout(c, Color.WHITE, "这是一个Title")
        // 这里不在使用xml,使用 Litho的Component
        setContentView(LithoView.create(c, component));
    }
···

组件 Spec 类在编译时期会生成与 Spec 名相同但没有 Spec 后缀的 ComponentLifecycle 子类。例如,MainLithoViewSpec 类会生成一个 MainLithoView 类。

生成的类种暴露的唯一 API 是 create(...)方法,它为 spec 类中声明的 @Props 返回相应的 Component.Builder。
在运行时,特定类型的所有组件实例共享相同的 ComponentLifecycle 引用。这意味着每个组件类型只有一个 spec 实例,而不是每个组件实例。

MountSpec 相比于 Layout Spec 更复杂一些,它拥有自己的生命周期

目前我自己的理解是 LayoutSpec 中你可以使用官方提供的一些组件来构建 UI,但是官方组件毕竟数量有限不可能全部实现 UI 设计。这时候 MountSpec 的作用就凸显出来了。MountSpec 把 Android 上的 View 转化

Mount Specs

Mount Specs 用来生成渲染具体 View 或者 Drawable 的组件。
Mount spec 必须使用 @MountSpec 注解来标注,并至少实现一个标注了 @onCreateMountContent 的方法。

Mount Spec 相比于 Layout Spec 更复杂一些,它拥有自己的生命周期:

  • @OnPrepare,准备阶段,进行一些初始化操作。
  • @OnMeasure,负责布局的计算。
  • @OnBoundsDefined,在布局计算完成后挂载视图前做一些操作。
  • @OnCreateMountContent,创建需要挂载的视图。
  • @OnMount,挂载视图,完成布局相关的设置。
  • @OnBind,绑定视图,完成数据和视图的绑定。
  • @OnUnBind,解绑视图,主要用于重置视图的数据相关的属性,防止出现复用问题。
  • @OnUnmount,卸载视图,主要用于重置视图的布局相关的属性,防止出现复用问题

Android 小伙伴应该对上面这几个状态比较熟悉

android 图文ui 框架 android框架布局_UI

下面这个代码,只是一个单纯的 ColorDrawable,你也可以替换成你需要实习的 View 例如 ImageView:

/**
 * 挂载操作有一个非常类似于Android的RecyclerView Adapter的API。
 * 它有一个 onCreateMountContent 方法,用于在回收池为空时创建和初始化 View 和 Drawable 内容 onMount 使用当前信息对复用的内容进行更新。
 *
 * 预分配
 * 当挂载 MountSpec 组件时,其 View 或 Drawable 内容需要从回收池中初始化或重用。
 * 如果池为空,那么将创建一个新实例,这可能会使UI线程过于繁忙并丢弃一个或多个帧。为了缓解这种情况,Litho 可以预先分配一些实例并放入回收池中。
 *
 */
@MountSpec(poolSize = 0, canPreallocate = true, isPureRender = true)
class MainColorViewSpec {
    private const val TAG = "MainColorViewSpec"


    // onCreateMountContent 的返回类型应该始终与 onMount 的第二个参数的类型匹配。它们必须是 View 或 Drawable 子类。参数在构建时进行校验。
    // onCreateMountContent 不能接收 @Prop 或任何带有其他注解的参数。
    @OnCreateMountContent
    fun onCreateMountContent(context: Context): ColorDrawable {
        Log.d(TAG, "OnCreateMountContent() 在组件挂接到宿主 View 之前运行")
        return ColorDrawable()
    }

    /**
     * 挂载必须在主线程,因为需要处理 Android View。
     * @OnMount 方法不知执行耗时操作,原因跟上面类似,Android 主线程不能执行耗时操作
     * 在任何 @MountSpec 方法中使用Output <?> 会自动为之后的阶段创建一个输入。在这种情况下,@OnPrepare 输出为 @OnMount 的输入。
     */
    @OnMount
    fun onMount(
        context: ComponentContext,
        colorDrawable: ColorDrawable,
        @FromPrepare color: Int // 名称必须对应
    ) {
        Log.d(TAG, "OnMount() 在组件挂接到宿主 View 之前运行")
        colorDrawable.color = color
    }


    // 该方法在执行布局计算之前只运行一次,并且可以在后台线程中执行。
    @OnPrepare
    fun onPrepare(
        context: ComponentContext,
        @Prop colorName: Int,
        color: Output<Int> // 名称必须对应
    ) {
        Log.d(TAG, "onPrepare() 在布局测量之前运行")
        color.set(colorName)
    }


    /**
     * 如果要在布局计算过程中自定义组件的测量,就要实现 @OnMeasure 方法。
     * 假设想要 ColorComponent 具有默认宽度,并在其高度未定义时强制执行特定的高宽比。
     */
    @OnMeasure
    fun onMeasure(
        context: ComponentContext,
        layout: ComponentLayout,
        widthSpec: Int,
        heightSpec: Int,
        size: Size
    ) {
        Log.d(TAG, "onMeasure() 在布局测量期间选择性运行")
        if (SizeSpec.getMode(widthSpec) == SizeSpec.UNSPECIFIED) {
            size.width = 40
        } else {
            size.width = SizeSpec.getSize(widthSpec)
        }

        // If height is undefined, use 1.5 aspect ratio.
        if (SizeSpec.getMode(heightSpec) == SizeSpec.UNSPECIFIED) {
            size.height = (size.width * 1.5).toInt()
        } else {
            size.height = SizeSpec.getSize(heightSpec)
        }
    }


    @OnBoundsDefined
    fun onBoundsDefined(c: ComponentContext, layout: ComponentLayout) {
        Log.d(TAG, "onBoundsDefined() 在布局测量之后运行")
    }


    @OnBind
    fun onBind(c: ComponentContext, view: ColorDrawable) {
        Log.d(TAG, "onBind() 在组件挂接到宿主 View 后运行")
    }

    @OnUnbind
    fun onUnbind(c: ComponentContext, view: ColorDrawable) {
        Log.d(TAG, "onUnbind() 在将组件从宿主 View 分离之前运行")
    }

    @OnUnmount
    fun onUnmount(context: ComponentContext, mountedView: ColorDrawable) {
        Log.d(TAG, "OnUnmount() 在组件从宿主 View 分离后,选择性运行")
    }

    /**
     * Mount Spec可以使用@ShouldUpdate注释定义一个方法来避免在更新时进行重新测试和重新挂载。
     * @ShouldUpdate 的调用的前提是component是"纯渲染函数'。
     * 一个组件如果是纯渲染函数,那么它的渲染结果只取决于它的prop和状态.
     * 这意味着在@OnMount期间,组件不应该访问任何可变的全局变量。
     * 一个@MountSpec可以通过使用@MountSpec注释的pureRender参数来定自己为"纯渲染的"。
     * 只有纯渲染的Component可以假设当prop不更改时就不需要重新挂载
     */
    @ShouldUpdate(onMount = true)
    fun shouldUpdate(@Prop(optional = true) someStringProp: Diff<String>): Boolean {
        return someStringProp.previous.equals(someStringProp.next)
    }
}

使用:

val component2  = MainColorView.create(c)
            .widthDip(26f)
            .heightDip(46f)
            //colorName 就是我们定义的属性
            .colorName(Color.GREEN).build()

运行后打印的 log:

MainColorViewSpec: onPrepare() 在布局测量之前运行
MainColorViewSpec: onBoundsDefined() 在布局测量之后运行
MainColorViewSpec: OnCreateMountContent() 在组件挂接到宿主 View 之前运行
MainColorViewSpec: OnMount() 在组件挂接到宿主 View 之前运行
MainColorViewSpec: onBind() 在组件挂接到宿主 View 后运行
MainColorViewSpec: onUnbind() 在将组件从宿主 View 分离之前运行

到这里 MountSpec 的基本用法就讲完了。有了这两个 Component 就乐意做很多事了。

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 方法。

android 图文ui 框架 android框架布局_android 图文ui 框架_02

你可以像下面这样使用:

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

ResType 中包含以下这些类型:

 

android 图文ui 框架 android框架布局_Android_03

 

可变属性 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 的方法,但是由于是懒更新,所以并不是对布局进行重新计算,所以界面上显示的还是初始化的值。

 

android 图文ui 框架 android框架布局_android 图文ui 框架_04

 

对上面的代码修改一下,增加一个点击事件,点击后更新另一个 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 上也被更新:

 

android 图文ui 框架 android框架布局_UI_05

 

Litho 底层使用的是 Yoga,Yoga 是 Facebook 的另一个开源项目,它是一个跨 iOS、Android、Windows 平台在内的布局引擎,兼容 Flexbox 布局方式。

所以只要熟悉 Flexbox 布局,那么在使用 Litho 进行 UI 布局时基本毫无压力。

如果熟悉 Flutter 开发,那在使用 Litho 时,会有一些似曾相识的感觉,Litho 中的 Row 与 Column 相关属性与 Flutter 中的 Row 与 Column 几乎无二。

本来想写一点示例代码,但是感觉没什么可写的。下面这个链接是 Yoga 官网的 playground。 
https://yogalayout.com/playground 
你可以通过它可视化的调整 UI,构建你需要的 layout。同时可以生成相应的 Litho 代码

在线可视化构建 UI:

在线可视化构建 UI:

android 图文ui 框架 android框架布局_Android_06

 

直接生成的 Litho 代码:

android 图文ui 框架 android框架布局_UI_07

 

在 Flexbox 中可以通过 positionType (ABSOLUTE) 属性来实现 Android 中的 FrameLayout 效果:

@OnCreateLayout
    fun createLayout(c: ComponentContext): Component {
        return Column.create(c)
            .child(

                SolidColor.create(c)
                    .color(Color.MAGENTA)
                    .widthDip(100f)
                    .heightDip(100f)
            )
            .child(
                Text.create(c)
                    .text("FrameLayout")
                    .marginDip(YogaEdge.TOP, 30f)
                    .positionType(YogaPositionType.ABSOLUTE)
            )
            .build();
    }

运行效果: 

android 图文ui 框架 android框架布局_Android_08

 

在最开始入门介绍中,我们曾经用 SingleComponentSection 完成了一个简单的列表,当时的做法是使用 for 构造出了多个子 Component。其实在 Litho 中提供了一个性能更好的方式,专门处理这种数据(这种数据其实就是类似于 Android 中的 adapter 与其绑定的数据)。

Litho 中专门处理这种模板与列表支持的组件叫做

DataDiffSection 的使用

DataDiffSection。下面用 DataDiffSection 我们重构一下之前写的 MainListViewSpec

  • 首先生成我们的数据:
val data = arrayListOf<Int>()
        for (i in 0 until 32) {
            data.add(i)
        }
  • 在 MainListViewSpec 中增加一个创建 ListItemView 组件的方法:
@OnEvent(RenderEvent::class)
    fun onRender(c: SectionContext, @FromEvent model: Int): RenderInfo {
        return ComponentRenderInfo.create().component(
            ListItemView.create(c)
                .color(if (model % 2 == 0) Color.WHITE else Color.LTGRAY)
                .title(if (model % 2 == 0) "hello word" else model.toString())
                .build()
        ).build()
    }
  • 接下来改造一下 onCreateChildren 方法:
@OnCreateChildren
    fun createChildren(c: SectionContext, @Prop listData: ArrayList<Int>):Children {
        return Children.create()
            .child(
                DataDiffSection.create<Int>(c)
                    .data(listData)
                    .renderEventHandler(MainListView.onRender(c))
            )
            .build()
    }
  • 最后运行一下:
···
 val recycleView = RecyclerCollectionComponent.create(c).disablePTR(false)
            .section(MainListView.create(SectionContext(this)).listData(data)).build()
        setContentView(LithoView.create(c, recycleView))
···

可能大部分到这里都有点蒙,DataDiffSection 到底是从哪里来的呢?
DataDiffSection 有点类似于 Android 的 DiffUtil 它是一个内置的一个事件:

  1. 每当一个 Item 被渲染的时候,DataDiffSection 会产生一个 RenderEvent。
  2. 创建 DataDiffSection 的时候,我们要传入自己的 renderEventHandler,就是上面代码中的 MainListView.onRender(c)

可以看到效果跟之前的没有区别:

标题

 

嵌套一个横向滚动的列表

在最开始我们是使用 SingleComponentSection 构建列表的。这里如果需要嵌套一个横向滚动的列表,同样也可以用 SingleComponentSection 来完成:

val config =  ListRecyclerConfiguration.create()
            .orientation(LinearLayoutManager.HORIZONTAL)
            .reverseLayout(false)
            .snapMode(SnapUtil.SNAP_TO_CENTER)
            .build()

 RecyclerCollectionComponent.create(c)
    .disablePTR(true)
    .recyclerConfiguration(config)
    .section(DataDiffSection.create<Int>(c)
        .data(listData)
        .renderEventHandler(SectionItem.onRender(c)))
    .canMeasureRecycler(true)


标题