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,可以直观的翻译为组件
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 类。
MountSpec 相比于 Layout Spec 更复杂一些,它拥有自己的生命周期
目前我自己的理解是 LayoutSpec 中你可以使用官方提供的一些组件来构建 UI,但是官方组件毕竟数量有限不可能全部实现 UI 设计。这时候 MountSpec 的作用就凸显出来了。MountSpec 把 Android 上的 View 转化
Mount Specs
Mount Spec 相比于 Layout Spec 更复杂一些,它拥有自己的生命周期:
- @OnPrepare,准备阶段,进行一些初始化操作。
- @OnMeasure,负责布局的计算。
- @OnBoundsDefined,在布局计算完成后挂载视图前做一些操作。
- @OnCreateMountContent,创建需要挂载的视图。
- @OnMount,挂载视图,完成布局相关的设置。
- @OnBind,绑定视图,完成数据和视图的绑定。
- @OnUnBind,解绑视图,主要用于重置视图的数据相关的属性,防止出现复用问题。
- @OnUnmount,卸载视图,主要用于重置视图的布局相关的属性,防止出现复用问题
Android 小伙伴应该对上面这几个状态比较熟悉

下面这个代码,只是一个单纯的 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
// 名称必须对应 ) { 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
): 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 方法。

你可以像下面这样使用:
PropComponent.create(c).index(14).textRes(R.string.app_name).build()
ResType 中包含以下这些类型:

可变属性 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
) { } }
State 初始化
State 需要在 @OnCreateInitialState 注解的方法中初始化: OnCreateInitialState 方法需要注意:
- 第一个参数必须是
ComponentContext(大部分的 Componetn 方法都要求第一个参数必须是ComponentContext) - State 相关的参数的名称必须和其他生命周期方法中的 @State 参数保持一致,并且这些参数的类型必须是
StateValue, 其中泛型的类型与对应的 @State 一致 - 如果没有定义
@OnCreateInitialState,State 的值就是 java 默认值 - 只有在
Component第一次被添加到 Component 树的时候才会调用一次@OnCreateInitialState方法。如果 Component 的 key 没有改变,后续对 Component 树布局的重新计算并不会重新调用 @OnCreateInitialState 方法 - 不需要自己调用
@OnCreateInitialState方法.
@OnCreateInitialState fun updateCheckState( c: ComponentContext, isCheck: StateValue
, @Prop initChecked: Boolean ) { isCheck.set(initChecked) }
更新 State
State 需要在 @OnUpdateState 注解的方法中更新: OnUpdateState 方法需要注意:
- 可以定义多个
OnUpdateState方法来更新不同的 State ,但是OnUpdateState方法每次调用都会对它所在的Component重新计算一次。所以为了更好的性能,应该尽可能少的调用OnUpdateState,或者合并多个 State 的更新,来提升性能 - 和初始化时候一个样。State 相关的参数的名称必须和其他生命周期方法中的 @State 参数保持一致,并且这些参数的类型必须是
StateValue, 其中泛型的类型与对应的 @State 一致 - 如果你的 State 的值需要依赖于 Prop, 你可以在
@OnupdateState函数的参数中使用@Param声明,这样就可以在更新被触发的时候传递 prop 的值进来了.
跟我们的 Check 增加一个更新方法:
@OnUpdateState fun updateCheckedState(isCheck: StateValue
) { val check = isCheck.get() isCheck.set(check?.let { !check }) }
合并多个 State 更新,并且使用 Param 来更新 State:
/ * 多个State合并更新,同时 使用Param来更新 State */ @OnUpdateState fun updateCheckedStateTwo(isCheck: StateValue
,isCheckTwo: StateValue
, @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) }
关于调用更新方法有一下几点需要注意:
- LayoutSpec 中避免在
onCreateLayout中直接调用更新方法,因为更新会触发布局重新计算,而重新计算又会触发onCreateLayout,很容易造成死循环 - MountSpec 中,不要在
onMount、onBind方法中直接调用更新方法,如果你真的需要在这类方法中更新 State 的值,那么应该使用下面会讲到的懒汉式 State 更新来替代. - 当调用一个 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
, @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
, @Prop initName: String) { name.set(initName) } @OnEvent(ClickEvent::class) fun onClick(c: ComponentContext) { LazilyUpdateComponent.updateTestDataAsync(c) } @OnUpdateState fun updateTestData(testData: StateValue
) { testData.set("TestData") } }
logcat:
点击前: LazilyUpdateComponentSp: onCreateLayout: initName LazilyUpdateComponentSp: onCreateLayout: ··· 点击后: LazilyUpdateComponentSp: onCreateLayout: UpdateName LazilyUpdateComponentSp: onCreateLayout: TestData
同时 UI 上也被更新:

Litho 底层使用的是 Yoga,Yoga 是 Facebook 的另一个开源项目,它是一个跨 iOS、Android、Windows 平台在内的布局引擎,兼容 Flexbox 布局方式。
所以只要熟悉 Flexbox 布局,那么在使用 Litho 进行 UI 布局时基本毫无压力。
如果熟悉 Flutter 开发,那在使用 Litho 时,会有一些似曾相识的感觉,Litho 中的 Row 与 Column 相关属性与 Flutter 中的 Row 与 Column 几乎无二。
在线可视化构建 UI:
在线可视化构建 UI:

直接生成的 Litho 代码:

在 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(); }
运行效果:

在最开始入门介绍中,我们曾经用 SingleComponentSection 完成了一个简单的列表,当时的做法是使用 for 构造出了多个子 Component。其实在 Litho 中提供了一个性能更好的方式,专门处理这种数据(这种数据其实就是类似于 Android 中的 adapter 与其绑定的数据)。
Litho 中专门处理这种模板与列表支持的组件叫做
DataDiffSection 的使用
DataDiffSection。下面用 DataDiffSection 我们重构一下之前写的 MainListViewSpec。
- 首先生成我们的数据:
val data = arrayListOf
() 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
):Children { return Children.create() .child( DataDiffSection.create
(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 它是一个内置的一个事件:
- 每当一个 Item 被渲染的时候,DataDiffSection 会产生一个 RenderEvent。
- 创建 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
(c) .data(listData) .renderEventHandler(SectionItem.onRender(c))) .canMeasureRecycler(true)

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/203936.html原文链接:https://javaforall.net
