Facebook出品的Android声明式开源新框架Litho文档翻译-仓库架构

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

参与

仓库架构


这是一个描述仓库中什么东西在哪儿的快速细分文档.

/docs/


这个目录存放了你现在看到的这些GitHub页的Jekyll文件.


/lib/


在这个子文件夹中可以找到很多扩展库.他们大致可以被分为两个类别.

  • 可拉取的库:这些库都存放在jCenter上./lib/中相应的子文件夹中只包含了一个写有拉取这个库的命令的BUCK文件.
  • 打包的库:这些库都被完整的存放在文件夹里.这是正确的buck方式.然而,他们极大的增加了仓库的大小,因此它们只在绝对必要的时候才被包含进来.


/sample-barebones/


在这里可以找到准系统教程的成品源码.如果你修改了这个教程,你必须在这里更新源码.


/sample/


在这个文件夹下可以找到Litho示例程序的代码.它包含了playground,你必须使用它来进行所有的测试/调试报告.


/litho-*/


Litho被切分成为了几个子工程,所以终端用户可以选择框架中他们需要的部分来使用.可用的工程如下:

  • litho-annotation是一个纯净的,包含了用户使用注释处理器所必须的注释的java库.
  • litho-core包含了核心框架代码
  • litho-fresco包含了用户使用Fresco图形库所需的Component.
  • litho-it包含了对框架的集成测试.引入这个子工程是非常必要的,因为它可以避免循环引用.
  • litho-it-powermock包含了框架使用PowerMock的集成测试.查看文件夹下的README可以了解更多.
  • litho-processor包含了单例的注释处理器
  • litho-stetho包含了Stetho的集成,它可以让你更轻松的开发和调试.
  • litho-stubs包含了一些为了显示列表的魔法效果所需的Android框架类.
  • litho-testing包含了测试Litho Component的工具.
  • litho-widget包含了几个常用的Android控件的mount spec.


/COMPONENTS_DEFS和/BUCK


这些文件定义了如何构建Litho.BUCK文件是buck的输入,/COMPONENTS_DEFS文件包含了一些buck在仓库中查找目标所需的常量.它会被导入到/BUCK.




回到导航页




Facebook出品的Android声明式开源新框架Litho文档翻译-如何参与

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

参与

如何参与


Facebook Litho团队非常欢迎你的参与.

我们使用GitHub的Pull系统进行操作.请在这里查看详细信息.




回到导航页




Facebook出品的Android声明式开源新框架Litho文档翻译-开发者选项

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

工具

开发者选项


除了Stetho之外,我们还提供了两个编译时标志,用于可视化你的应用程序的Component层级结构.这类似于Android的内部设置:显示View边界,但是由于Litho并不是总是使用Android View,所以我们实现了我们自己的方式使它变得更加实用.

ComponentsConfiguration类中有两个字段来控制这些.


debugHighlightInteractiveBounds

高亮显示Component的交互边界以及Component的扩展触摸边界(如果存在的话).


debugHighlightMountBounds

高亮显示可挂载的drawable和view的边界.由框架自动添加(例如,在Component被点击的时候)的View将会被使用不同的颜色来高亮显示.


这些是默认关闭的.如果你想要在你的程序中打开它们,你可以在你的程序中的任何地方覆写他们:

1
2
ComponentsConfiguration.debugHighlightInteractiveBounds = true;
ComponentsConfiguration.debugHighlightMountBounds = true;




回到导航页




Facebook出品的Android声明式开源新框架Litho文档翻译-调试

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

工具

调试


Stetho


Stetho是一个非常好的Android调试工具,我们已经确保了它能够与Litho一起使用.为了在Stetho中启用Litho调试,需要在你的Application实现中的onCreate()方法里添加下列的代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class SampleApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
Stetho.initialize(
Stetho.newInitializerBuilder(this)
.enableWebKitInspector(new LithoWebKitInspector(this))
.build());
}
}

这会使Litho完全集成进stetho中.启用了Litho支持之后,你只用打开你的app然后再你的浏览器中打开chrome://inspect

点击你想查看的程序的查看链接(我们使用的是Litho sample app).这将会打开一个UI查看器,你可以在其中查看你程序的View和Component的层级结构.

当查看一个Litho Component的时候,你也可以直接在查看器中更改你的UI中的内容!这样就可以在不重新编译和运行你的程序的情况下,通过调整margin,padding或者其他属性来实现快速设计迭代.你也可以使用它来快速测试你的UI是否能处理不同长度的文本.




回到导航页




Facebook出品的Android声明式开源新框架Litho文档翻译-最佳实践

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

最佳实践


编码风格


指引:

  • 使用NAMEComponentSpec命名你的Spec来生成一个名叫NAMEComponent的Component.
  • ComponentContext参数应该被命名为c,这样能避免你的布局代码变得冗长并且能够使它变得可读性更强.
  • 在适当的地方使用资源类型(ResType.STRING, ResType.COLOR, ResType.DIMEN_SIZE等等)能够让用Android资源设置prop变得更简单.
  • 先定义所有必须的prop然后再定义可选的prop(optional=ture).
  • 使用静态导入所有的enum(YogaEdge,YogaAlign,YogaJustify等等)来减少你的布局代码,并使其变得可读性更强.
  • 对withLayout()下的prop不使用额外的缩进等级
  • 生命周期方法,例如@OnCreateLayout,是静态的并且对包外对象是私有的.
  • 如果可能的话,对可选的孩子Component使用内联选项,这样能使你的布局结构变得更加流畅.
  • 如果你正在构建一个孩子容器,在下一行添加该容器.这使布局代码显得更加结构化.

使用推荐编码风格编码的实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@LayoutSpec
class MyComponentSpec {
@OnCreateLayout
static ComponentLayout onCreateLayout(
ComponentContext c,
@Prop(resType = STRING) String title,
@Prop(optional = true) Drawable image) {
return Row.create(c)
.alignItems(CENTER)
.paddingRes(R.dimen.some_dimen)
.child(
Image.create(c)
.drawable(image)
.withLayout()
.width(40)
.height(40)
.marginRes(RIGHT, R.dimen.my_margin))
.child(TextUtils.isEmpty(title) ? null :
Text.create(c)
.text(title)
.textColorAttr(R.attr.textColorTertiary)
.withLayout()
.marginDip(5)
.flexGrow(1f))
.build();
}
}


Prop vs State


Litho Component一共有两种类型的数据模型:PropState.在你需要使用它们的时候,明白两种模型的差别是非常重要的.

Prop的作用是在树中把数据从Component传递给它的孩子们.Prop在定义一个Component模型的静态部分中是非常有用的,因为Prop是不可变的.

State主要是用于处理那些与Component交互产生的更新或者那些只能被Component拦截到的更新.State是由组件自己管理的并且从外部是不可见的,Component的父级完全不知道它孩子的State的信息.

以下是关于如何决定对Component中的某些数据是采用prop还是State的简要介绍:

  • 数据是否定义了一个属性并且值会保持不变?如果是,则它应该是一个Prop.
  • 数据是否应该由父级定义并且传递下来?如果是,则它应该是一个Prop.
  • 它是否应该在用户与这个Component交互后改变它的值(例如点击)?如果是,它应该是一个State.
  • 你是否能够通过其他已有的Prop和State的值来计算出它的值?如果是,你就不应该使用State.

比起使用由上至下传递的Prop的方式,使用更多State的方式会让你的应用程序的复杂度升高,变得更加难以维护和更加难以推导出数据流.你应该尝试控制你的Compoennt中的State在最小的数量并且保持的数据流是从上到下的.如果你有多个兄弟Component并且他们的State是互相依赖的,这时你应该确定一个父级Component来替代孩子们持有并且管理它们的state.

让我们举个拥有单选按钮的列表的例子,列表中不能同时选中多个项.列表中所有的单选按钮的state都取决于其他项被点击和选中的状态.比起让所有的单选按钮都使用state,你应该在父Component中持有”哪一个列表项被点击”的state,并且通过prop从上而下的把这个信息传递给它的孩子们.


不可变性


Component本质上是一系列方法的集合,数据通过不可变的参数的方式传递给这些方法.当Component的prop或者state更新后,框架将会使用更新后的信息创建一个新的Component实例,因为先前的Component是不能改变的.

虽然Component本身是不可变的,但是使用可变的对象来表示prop和state很容易就会使Component变得线程不安全.Litho在后台线程中计算布局,如果构成Component的Prop或者State的对象在另一个线程中被改变了,将会导致同样的Component渲染出不一样的效果.

你必须保证你的prop和state是使用天生不可变的原始类型,如果做不到的话,也必须确保你使用的对象是不可变的.




回到导航页




Facebook出品的Android声明式开源新框架Litho文档翻译-创建一个ComponentTree

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

进阶指引

创建一个ComponentTree


使用Component指引中,我们看到了如何创建一个根Component并且把它传递给一个LithoView,接着LithoView将会处理使用给出的根Component创建ComponentTree的其他步骤.ComponentTree会使用线程安全的方法来管理你的Component的生命周期,使得你可以在任何的线程中创建和使用它们.虽然通常你可以不需要去创建和管理你自己的ComponentTree,但是也有部分情况你需要自己去做.下面就会讲到是你应该如何创建一个ComponentTree,传递给它一个根Component,并且把它添加到一个LithoView中去.这个ComponentTree的create()方法会返回一个暴露出ComponentTree的设置方法的Builder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final LithoView lithoView = new LithoView(this);
final ComponentContext context = new ComponentContext(this);
final Component text = Text.create(context)
.text("Hello World")
.textSizeDip(50)
.build();
final ComponentTree componentTree = ComponentTree.create(context, text).build();
lithoView.setComponentTree(componentTree);
setContentView(lithoView);
}




回到导航页




Facebook出品的Android声明式开源新框架Litho文档翻译-增量式挂载

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

进阶指引

增量式挂载


手动增量式挂载


如果你没有使用Recycler,你也仍然可以集成增量式挂载到你的现有的UI实现里.你必须在每次LithoView的可见区域发生变化的时候显式的通知框架,通过调用:

1
myLithoView.performIncrementalMount();

举例来说,如果你在ListView中使用Component,你可以在一个OnScrollListener中调用performIncrementalMount().




回到导航页




Facebook出品的Android声明式开源新框架Litho文档翻译-TreeProp

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

进阶指引

TreeProp


@TreeProp是那些自动并且静默的从父Component传递给子Component的prop.

TreeProp非常方便,它能够在一个布局树上分享上下文数据或者工具而又不需要显式的传递prop给每一个层级结构上的Component.

预加载器就是一个很好的例子,它能够在渲染图片之前从网络上预抓取图片.由于图片在程序中非常常见,预加载器也被广泛的应用.我们可以不用@Prop把预加载器传递给整个布局树,而是把预加载器的实现定义给那些需要使用它的Component.


声明一个TreeProp


每一个TreeProp都是在一个被注释为@OnCreateTreeProp的方法中被声明和定义的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@LayoutSpec
public class ParentComponentSpec {
@OnCreateTreeProp
static Prefetcher onCreatePrefetcher(
ComponentContext c,
@Prop Prefetcher prefetcher) {
return prefetcher;
}
@OnCreateLayout
static ComponentLayout onCreateLayout(
ComponentContext c,
@Prop Uri imageUri) {
return ChildComponent.create(c)
.imageUri(imageUri)
.buildWithLayout();
}
}

你只能为任何给定类型声明一个@TreeProp.如果一个ParentComponent的子类也定义了一个Prefetcher类型的@TreeProp,它将会覆写它的所有子类的相应的@TreeProp值(但是不包括它自己的值).


使用一个TreeProp


孩子component可以使用@TreeProp注释来声明一个和父亲Component的@OnCreateTreeProp方法中同样类型的参数的方式来访问TreeProp的值.

1
2
3
4
5
6
7
8
9
10
11
12
13
@LayoutSpec
class ChildComponentSpec {
@OnCreateLayout
static ComponentLayout onCreateLayout(
ComponentContext context,
@TreeProp Prefetcher prefetcher,
@Prop Uri imageUri) {
if (prefetcher != null) {
prefetcher.prefetch(imageUri);
}
...
}
}




回到导航页




Facebook出品的Android声明式开源新框架Litho文档翻译-自定义布局

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

进阶指引

自定义布局


Litho依赖Yoga(一个可以创建复杂UI的强大的布局引擎)来进行布局计算.然而,也会存在一些例外情况,这些时候只使用Yoga还不够,你可能还需要自己实现测量和布局的工作.

Litho提供了一套自定义布局的API,用来在ComponentTree被创建的和被测量时访问尺寸信息,或者在一个隔离的环境中测量Component.

重要,请注意:这些API会造成不可忽略的性能开销,因此,我们建议仅仅在确定有必要使用它们的时候再去使用它们.


用例


  • 一个Component布局树依赖于它自己的大小 和/或 它的孩子的大小.例如这种情况:一个Component布局只能在它的孩子们的大小符合父亲的限制的时候才使用它们,如果不符合,这个布局就需要使用作为后备的孩子来代替.
  • 一个容器的孩子们必须根据它们的大小或者它们父亲的大小进行绝对位置的手动定位.Yoga可以绝对定位孩子在父亲中的位置.然而,这个位置本身可能取决于使用父母的大小限制计算后的孩子的大小尺寸.如果需要,margin或者padding也需要手动被计算在内.


大小约束


在使用API之前,你需要已经熟悉了传统Android View的onMeasure方法并且了解什么是MeasureSpec,因为Litho使用了一个类似的概念,名叫SizeSpec.

类似于Android的MeasureSpec,Litho的SizeSpec也是由size和mode组成的.可用的mode与MeasureSpec一样,有三种:UNSPECIFIED,EXACTLY和AT_MOST.


测量一个Component


我们可以使用给定的SizeSpec在隔离环境下测量Component.结果将会被填充进一个作为参数传递进去的Size对象中.

在下面的例子中,我们使用一个UNSPECIFIED的SizeSpec去测量一个Text Component,这表示文字的一行能够是无限长的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final Component<Text> textComponent = Text.create(c)
.textSizeSp(16)
.text(“Some text to measure.”)
.build();
final Size outputSize = new Size();
textComponent.measure(
c,
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
outputSize);
final int textComponentWidth = outputSize.width;
final int textComponentHeight = outputSize.height;


SizeSpec


在布局创建期间,API可以提供Component将要被测量时使用的SizeSpec信息.为了获取这些信息,我们需要用一个新的@OnCreateLayoutWithSizeSpec注释替代原来的@OnCreateLayout.新的注释方法的参数,除了标准的ComponentContext,还有两个整型,它们分别代表width spec和height spec.

在下面的例子中,我们测量了一个Text Component来检查给定的文字是否适合可用的空间.如果不适合,则使用一个Image Component代替Text Component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@LayoutSpec
class MyComponentSpec {
@OnCreateLayoutWithSizeSpec
static ComponentLayout onCreateLayoutWithSizeSpec(
ComponentContext c,
int widthSpec,
int heightSpec,
@Prop SomeObject someProp) {
final Component<Text> textComponent = Text.create(c)
.textSizeSp(16)
.text(“Some text to measure.”)
.build();
// UNSPECIFIED sizeSpecs will measure the text as being one line only,
// having unlimited width.
final Size textOutputSize = new Size();
textComponent.measure(
c,
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
SizeSpec.makeSizeSpec(0, UNSPECIFIED),
textOutputSize);
// Small component to use in case textComponent doesn’t fit within
// the current layout.
final Component<Image> imageComponent = Image.create(c)
.srcRes(R.drawable.some_icon)
.build();
// Assuming SizeSpec.getMode(widthSpec) == EXACTLY or AT_MOST.
final int layoutWidth = SizeSpec.getSize(widthSpec);
final boolean textFits = (textOutputSize.width <= layoutWidth);
return Layout.create(c, textFits ? textComponent : imageComponent)
.build();
}
}




回到导航页




Facebook出品的Android声明式开源新框架Litho文档翻译-单元测试

欢迎转载,转载请标明出处.
英文原文文档地址: Litho-doc

测试

单元测试


Litho通过流AssertJ方法提供了测试helper.可用的有:

为了使用这些测试功能,你需要在编译的时候包含可选的litho-testing包.可用的包是:com.facebook.litho:litho-testing:+

为了演示这些类的用法,下面展示一个示例,它是一个包含了图标和简单描述文字的Component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Displays who liked the post.
*
* 1 - 3 likers => Comma separated names (e.g. Jane, Mike, Doug)
* > 3 likers => Comma separated number denoting the like count
*/
@LayoutSpec
class LikersComponentSpec {
@OnCreateLayout
protected static ComponentLayout onCreateLayout(
ComponentContext c,
@Prop List<User> likers) {
return Row.create(c)
.alignItems(FLEX_START)
.child(
Image.create(c)
.srcRes(R.drawable.like))
.child(
Text.create(c)
.text(formatLikers(likers))
.textSizeSp(12)
.ellipsize(TruncateAt.END))
.build();
}
private static String formatLikers(List<User> likers) {
...
}
}

在我们的测试中,我们想要验证文字和图标的渲染效果.


安装


Component的测试框架提供了一个Junit @Rule,它覆写了Styleables,并且允许轻松的访问ComponentContext.

1
2
3
4
5
6
7
/**
* Tests {@link LikersComponent}
*/
@RunWith(RobolectricTestRunner.class)
public class LikersComponentTest {
@Rule
public ComponentsRule mComponentsRule = new ComponentsRule();


测试组件的渲染


Component框架包括了一组AssertJ风格的helper类来验证你Component的属性.在后台,它们将为你挂载Component.

你也可以在ComponentContext和Context组上,或者ComponentBuilder被build()消耗之前使用断言.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
public void testTwoLikers() {
ComponentContext c = mComponentsRule.getContext();
ImmutableList<User> likers =
ImmutableList.of(new User("Jane"), new User("Mike"));
Component<LikersComponent> component =
LikersComponent
.create(c)
.likers(likers)
.build();
assertThat(c, component).hasText("Jane, Mike");
}
@Test
public void testLikeIcon() {
ComponentContext c = mComponentsRule.getContext();
Drawable likeIcon = c.getResources().getDrawable(R.drawable.like);
ImmutableList<User> likers =
ImmutableList.of(new User("Jane"), new User("Mike"));
LikersComponent.Builder componentBuilder =
LikersComponent
.create(c)
.likers(likers);
assertThat(componentBuilder).hasDrawable(likeIcon);
}


测试子Component的渲染


比起在你的Component的内容渲染上使用断言,可能一种更有效的方式是测试子Component的渲染情况.SubComponent是一个方便的类,可以更简单的比较Component的类型.你依然可以使用AssertJ来验证子Component是否存在.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class StoryTest {
...
@Test
public void testStoryLayout() {
ComponentContext c = mComponentsRule.getContext();
Story story = ...
Component<StoryComponent> component =
StoryComponent.create(c)
.story(story);
assertThat(subComponents).hasSubComponents(
SubComponent.of(HeaderComponent.class),
SubComponent.of(MessageComponent.class),
SubComponent.of(LikersComponent.class),
SubComponent.of(FeedbackComponent.class));
}
@Test
public void testStoryWithZeroLikes() {
ComponentContext c = mComponentsRule.getContext();
Story storyWithZeroLikes = ...;
Component<StoryComponent> component = StoryComponent.create(c)
.story(storyWithZeroLikes)
.build();
assertThat(component)
.doesNotContainSubComponent(SubComponent.of(LikersComponent.class));
}
}


额外的断言


还有一些断言是可用于Component和LithoView的.他们都会在你的Component创建的树上进行操作.因此,断言一个你的Component上的Drawable的存在将会从提供的开始点遍历view的层级结构.


注意事项


在进行Litho单元测试时,请注意,Yoga的本地库必须要被加载,这可能会由于你的构建系统的选择,而产生一些问题.比如,使用Gradle和Robolectric时,你可能会遇到问题,因为Robolectric对于每一个测试组件都使用了一个新的拥有不同设置的ClassLoader.对于PowerMock也是一样,对于每一个基本组件,它都提供了一个ClassLoader,并且规定他们都为不可重用状态.

JVM有两个非常重要的相关限制:

  1. 一个共享的库在一个进程中只能被加载一次.
  2. ClassLoader不共享加载的库的信息

因为这些,在测试运行中使用多个ClassLoader是非常有问题的,因为每一个实例都会尝试加载Yoga,但是除了第一个会成功之外,其它的都会报libyoga.so already loaded in another classloader(libyoga.so已经在另一个classloader被加载过了)的异常.

避免这个的唯一方法是避免使用多个ClassLoader,如果必须使用新的ClassLoader,则使用fork进程的方式.

Gradle允许你限制一个进程在作废之前可以执行的测试类的数量.如果你设置这个值为1,我们就避免了ClassLoader的重用:

1
2
3
4
5
6
7
8
9
10
android {
[...]
testOptions {
unitTests.all {
forkEvery = 1
maxParallelForks = Math.ceil(Runtime.runtime.availableProcessors() * 1.5)
}
}
}

使用BUCK,可以通过指定每一个测试目标的名称为会导致并行进程加速的名称来打到这个效果.或者,你也可以设置fork_mode为per_test,具体请参照这里的描述.

最后,根据你的构建系统和你的项目中已经存在的限制,你可能需要调整你的测试运行器使用ClassLoader的方式.然而,这不是Litho独有的问题,而是在Android项目中混合使用native代码和java代码所导致的不幸后果.




回到导航页