1.创建Compose应用模板
新建一个Compose项目,运行起来是这样的效果。
你在MainActivity中可以看到类似这样的代码(不同的Compose版本可能有区别)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeDemoTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hell $name!")
Text(text = "Hell $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
ComposeDemoTheme {
Greeting("Android")
}
}
从代码里可以看到熟悉的onCreate方法,然后,然后别的东西就有点奇怪了,下面我们来简要介绍一下这块代码都做了什么。
1.1 setContent做了什么?
在传统的View体系中,Activity的onCreate方法会调用setContentView,传入一个layoutid,或者直接传进去一个View,就构建出了当前Activity的界面。Compose中的setContent方法名字差不多,我们推测是不是作用和setContentView差不多呢?嗯,是的,作用确实差不多,我们点进去看看。
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
发现这个setContent方法竟然是ComponentActivity的一个扩展方法,看着似乎有点奇怪,为什么是扩展方法而不是ComponentActivity的成员方法呢?作者推测,因为ComponentActiivty是androidx下的通用Activity,包括FragmentActivity等都继承自它,继承关系是这样的AppCompatActivity:FragmentActivity:ComponentActivity,
这对那些只想用androidx而不想用Compose的开发者明显是不友好的。那为什么不能像FragmentActivity一样新写一个ComposeActivity继承自ComponentActivity呢?作者认为,这么做有两点好处:
- 便于迁移,之前的
Activity不用更改继承就能直接使用setContent方法,很方便。 - 确实没必要,而且可以有效利用
FragmentActivity等之类Activity的特性。
推荐大家以后在写kotlin代码的时候,也思考一下能否利用kotlin扩展函数实现一些巧妙的设计。
好了题外话就说到这里,继续向下看,因为只是概览,所以本节不会讲的过于深入。
看这个扩展方法的参数,第一个参数是一个CompositionContext,是个Context,但不是View体系中的Context,而且可空,默认还没传,所以是干嘛使的?
先不说,等会儿下面用到了再说。
这第二个参数就比较好理解了,忽略@Composable注解,这就是一个lambda表达式参数,所以我们才能像setContent {}这样去使用setContent方法,大括号中的内容就是lambda表达式。
接着看setContent方法,首先通过在decorView中findViewById寻找ComposeView,decorView是什么就不过多介绍了,毕竟大家都是有经验的开发人员。
至于这个ComposeView是什么?
他就是个ViewGroup,继承关系是这样的ComposeView:AbstractComposeView:ViewGroup
继续向下看,判断composeView是否为空,我们先看为空的情况,因为首次判断肯定是为空的。
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
ComposeView为空的话首先new了一个ComposeView,刚我们已经知道ComposeView就是个View,所以new一个View的操作似乎也没什么奇怪。
接下来,依次调用的ComposeView的setParentCompositionContext,setContent,setOwners方法,其中:
-
setParentCompositionContext该方法设置了CompositionContext,也就是setContent方法的第一个参数,其实这个方法的真正作用是给这个ComposeView设置它父节点的CompositionContext,如果参数为空,这个context将由这个View所依赖的Window决定,是不是读起来有点拗口呢?因为是我照着这个方法的注释翻译的,CopositionContext在构建Compose页面的时候会用到,现阶段不做深入了解。 -
setContent又见到一个setContent方法,不过这个方法是ComposeView的,参数是我们在Activity中传进来的lambda表达式,这里可以简单理解为将所要构建的页面步骤传给ComposeView,然后由它来渲染页面。 -
setOwners设置一些必要的监听,因为监听只需要设置一次,所以我们看到在上一个为true的代码块中就只有setParentCompositionContext和setContent,而没有setOwners和setContextView。 -
ComponentActivity.setContextView没错,就是你以为的那个setContentView,就是原本应该在Activity的onCreate方法中的setContentView,这里进行的操作很简单,就是将new出来的这个ComposeView塞进decorView的content中,这样下次再findviewById,ComposeView就不为空了。
总结:setContent方法将lambda表达式传给ComposeView,然后将ComposeView塞进了decorView的Content中,然后由ComposeView根据我们传进去的lambda表达式,进行页面的渲染工作。
1.2 ComposeDemoTheme,Surface,Greeting都是什么?
都是函数。
是的,没错,都是函数,但不是普通函数,而是加了@Composable注解的函数。
抛开这个注解,发现这就是函数一层一层的调用。
setContent {
ComposeDemoTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
setContent方法中传给ComposeView的lambda表达式在某一时刻会被invoke,然后lambda函数里又调用了ComposeDemoTheme函数,ComposeDemoTheme函数后面又传进来一个lambda表达式,这个表达式在某一时刻调用又执行了其他函数,就这样一层一层调用函数,我们的Compose界面就构建出来了。有一点需要注意,函数一旦加了@Composable注解,就只能在别的加了@Composable注解的函数中被调用,我们注意到setContent的尾部lambda参数也是加了@Composable注解的,想一想也是应该的,Composable函数是为了构建界面的,而普通函数并没有办法构建界面,也就不能调用加了Composable注解的函数。
1.3 @Composable注解是干嘛的
注解相信大家都不陌生,这个@Composable和我们之前使用的注解在用法上也没用任何区别,不过实现上的区别还是有的,我们之前用的注解或是通过注解处理器,或是通过运行时反射来实现一些修改代码执行的操作,而@Composable注解是通过编译器插件,在编译的时候就对加了@Composable注解的函数进行一些特殊处理,我们在这里不做过多探究,通过观察我们还发现:
- 自动生成的这个
Greeting函数没有返回值,因为Compose函数只需要执行就行了,确实不需要也不应该加返回值。 - 函数名称的首字母大写了,让人乍一看还以为是个类,而且Compose自带的一些控件函数全部首字母大写。嗯,大概是为了和普通函数区分开,建议我们在新建Composable函数的时候首字母也大写。 注意,不要给普通函数添加Composable注解,刚提到加了注解在编译时会对这些加了注解的函数进行修改,而普通函数不需要这些修改。
1.4 @Preview注解是干嘛的?
这个注解顾名思义,就是用来预览Composable函数执行效果的。
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
ComposeDemoTheme {
Greeting("Android")
}
}
在加了
@Composable注解的函数上加上@Preview注解,就能不用再次构建就可以看到预览效果了,这个Composable函数有个条件是不能有参数,所以我们一般将需要传参数的Composable函数外面套一个不需要参数的Composable函数用来预览,不过预览效果目前为止和xml比还是差一点。
好了,本节内容就到这里,接下来会介绍各个Composable控件的使用。