通过前面的学习我们知道Jetpack Compose是一个声明式的UI框架,采用的是函数式编程思想,直观上来看就是一坨函数从上调到下。当函数需要数据时,会通过函数调用的参数来传递,一般来说这也没有什么问题。但当需要的数据特别多时,特别是对于一些非业务逻辑本身的数据(输入与输出),如上下文环境变量(Context,或者主题之类),都通过函数调用的参数传递就难免会让代码变得非常的庞杂和臃肿。这时候就可以使用CompositionLocal,一种在组合树中隐式的从上到下传递数据的方法,我们来具体地看一下。
注意: Compose中的函数通常叫做composable,可以简单的理解为同一个东西。从根composable开始,一个套一个的调用,自上而下的意思就是指composable的调用顺序,根函数在上,被调用的函数在下。函数调用是可以传递参数的,正常的从上到下的数据传递都是通过函数调用时的参数。
废话不多说,我们先来看一下什么是CompositionLocal以及它能做什么事情。
什么是CompositionLocal
CompositionLocal提供一种自上而下的数据传递方式,隐式的传递,也就是说,不用把数据放在参数里传递给子函数,子函数像在类中的方法访问域变量那样直接访问。我们来看一个粟子。
比如说,页面中有一个文案,可能需要定制字体颜色,常规方式是这样子写:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
文案的颜色,以及像背景等等这些东西是与整个App的配置相关的,或者与运行环境(如手机)的主题风格有关的,它并不是应用程序的业务逻辑。它属于上下文环境变量,其变化往往是由于运行环境变化而变化,或者是由于用户更改了应用的配置。这就非常适合使用CompositionLocal来转化为隐式数据传递,进而简化代码,使用后就变成酱婶儿的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
通过这个示例可以看出,定义了CompositionLocal以后,在被调用到的所有子函数中(desendants)都可以直接使用,就像访问全局变量那样。这就是CompositionLocal的作用,在指定的作用域中提供隐式的数据。
如何使用CompositionLocal
使用起来非常的方便,就像在类的方法中使用域变量那样,或者像使用全局变量那样,找到上级所定义的的CompositionLocal实例,然后引用其中的变量即可。
其实,已经在不知不觉中经常地使用了CompositionLocal,主题风格相关的MaterialTheme,Android的上下文LocalContext)以及像绘制时常用的LocalDensity),这些都是CompositionLocal实例,是由Compose定义好的顶层实例,在所有的composables中都可以直接使用。
需要理解的是CompositionLocal实例,本身并不是一个数据,它更像是一个集装箱,它本身是一个数据传递的机制,会在后面定义CompositionLocal实例时详细讲解。
另外,需要特别注意的是CompositionLocal是有作用域的,对其所有的子函数生效,并且也可以嵌套的,属性current)引用的是最近一层父函数(closest ancestor)中绑定的值。比如说:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
如何定义一个CompositionLocal
大部分时候Compose中定义好的就够我们用了,但也可以针对具体的场景来自定义一个CompositionLocal。分为两个步骤,创建实例,和绑定数据。
CompositionLocal实例是一个集装箱,用以封装需要从顶层往底层传递的数据,这些数据也不一定非要是常量,也是会改变的,比如应用或者页面上下文环境变化时,或者整个应用的配置发生变化时。
创建CompositionLocal实例
第一步是先创建一个实例,有两种方法创建一个CompositionLocal实例,这两种方式的主要区别就在于当数据变化时如何影响着重组(reComposition):
- compositionLocalOf) 当数据发生变化时仅会影响读取数据函数的重组;
- staticCompositionLocalOf) 当数组变化时,提供数据的所有子函数都会被重组。
可以发现,这两个方法的区别就在于影响重组的范围,前一个是影响着读取数据的地方,这与状态(State)一样;后一个则是影响着所有的被调用的composables。
1 2 3 4 5 6 7 |
|
注意: 为了更好的可读性和可维护性,CompositionLocal实例的命名应该以Local开头,如LocalColors。
把数据绑定到实例中去
光创建实例没什么用,创建完实例后,还需要把数组绑定到CompositionLocal实例中去,并同时指定作用域,这是非常关键的一步,不但决定了CompositionLocal中有什么数据,还决定了谁可以使用这些数据。使用CompositionLocalProvider)来绑定数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
函数CompositionLocalProvider接收一个CompositionLocal实例和一个composable lambda,这个lambda就是这个实例的作用域,也即此lambda所调用的所有composables都可以使用访问此实例的数据。参数LocalElevations就是实例,可以看前面的创建代码。LocalElevations provides elevation这是infix符号式写法,相当于LocalElevations.provides(elevation),elevation则是具体的数据,这意思就是在此lambda的作用域中,CompositionLocal实例LocalElevations会提供数据elevation。
1 2 3 4 5 6 7 |
|
这里多说一下,compositionLocalOf是更为精细化的控制(fine-grained control),因为,当数据发生变化时,在作用域中只有读取了数据的composables才会被重组;而staticCompositionLocalOf影响范围较大,因为(当数据发生变化 时)整个作用域内(即CompositionLocalProvider的lambda)的composables都会被重组。
什么时候该用CompositionLocal
CompositionLocal提供了一种新的数据传递方式,当数据需要在一定作用域内(Scoped)分享时,就可以考虑使用它。但并不是所有的场景都适用,具体来说,要符合以下标准:
- 数据要是非业务逻辑数据,也就是非代码所直接需要的输入和输出。
- 数据要有一个极其合理的初始值(默认值)。
- 数据不一定要是常量,也可以变化,但是业务逻辑并不是数据变化的原因。
- 数据的使用有一定的作用域。并且在作用域内所有的composables都可能会使用此数据。这一点很重要,如果仅仅是某个特定的composable使用,那就直接传参就好了。
适合使用CompositionLocal的数据有应用的主题风格,应用的配置信息,平台提供的上下文变量,平台的配置信息,或者对于一个局部来说是上下文变量的数据(比如说一个Dialog用到的数据)。除此之外,绝大部分时候不应该使用CompositionLocal。特别是涉及业务逻辑代码本身强相关的数据(输入与输出),一定不能使用CompositionLocal,这会让代码极难理解和维护,造成极难调试的Bug。
CompositionLocal的一个大的特点就是有明确的作用域,那么如何选择合适的作用域呢?总的来说应该让作用域越小越好。视数据的 影响范围和使用范围来决定其作用域:
- 整个应用级别的(App level),如主题风格,应用配置,平台的配置信息,或者会话数据(user sessions)。应该在根composable,即setContent处绑定数据。
- 整个页面级别的(Screen level),如平台上下文变量(Context或者Density),导航,或者页面内部的定制参数。
- 组件级别的(Component level),页面中某一个局部,比如Dialog的主题风格,或者Dialog并不直接操作,但却依赖的数据。
任何工具都是为了适合其的场景而生的,要了解清楚什么场景适合使用非常重要,切忌滥用。
它与状态(State)的区别是什么
从前面的文章降Compose十八掌之『鸿渐于陆』| State我们了解到,状态(State)是为了重组时数据不丢失,也就是说在composable多次运行时,数据能够得以留存。反过来状态变化了,也会触发重组,因为要刷新UI。状态是时间上的概念(使用此状态的composables)多次运行,状态持有的数据都得以保存,不会变成初始值。如果想要使用状态,必须当作参数传递过去。
而CompositionLocal是为了能在不同的composables中共享数据,不用参数传递,它是空间上的概念。
它们是为了解决不同的问题而设计出来的。可以把状态绑定到CompositionLocal中去,但其实没必要这么做,因为内部实现上已经会把绑定的数据封装为状态,因为CompositionLocal的数据是可能发生变化的,并且当变化时,也是要触发重组的。
它与全局变量的区别是什么
Kotlin语言是支持顶级全局变量(Top level globals)的,也就是与class平齐,在任何class之外的全局变量,其作用域是整个进程,任何导入(import)此变量的地方都可以访问。Compose是基于Kotlin语言的,所以Compose也是可以使用全局变量的。
全局变量的缺点是没有作用域,容易失控,仅应该用于常量,并且它也不能用于composables的重组。虽然全局变量可以跨越空间,让所有函数都能访问。全局变量是编程语言层面的东西,仅适用于常量的定义,也即整个应用程序运行期间确定不会发生改变的值。
这正是CompositionLocal有价值的地方,它能限定作用域,又可以触发重组,允许数据值改变,又可以跨越空间。这是Compose框架层面提供的工具,自然更适合在Compose中使用。
总结
调用函数时传递参数可谓是『明修栈道』,而CompositionLocal则是『暗渡陈仓』,提供了一种在组合(Composition)中自上而下隐式传递数据的一种方式,可以让函数调用只关心与逻辑相关的输入数据。恰当的使用CompositionLocal可以有效的管控上下文环境变量,并极大地简化代码,让代码更加的优雅和简洁。