Jetpack Compose “Hello Android!”初探

1,129 阅读7分钟

1.创建Compose应用模板

新建一个Compose项目,运行起来是这样的效果。

clipboard.png

你在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体系中,ActivityonCreate方法会调用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呢?作者认为,这么做有两点好处:

  1. 便于迁移,之前的Activity不用更改继承就能直接使用setContent方法,很方便。
  2. 确实没必要,而且可以有效利用FragmentActivity等之类Activity的特性。

推荐大家以后在写kotlin代码的时候,也思考一下能否利用kotlin扩展函数实现一些巧妙的设计。

好了题外话就说到这里,继续向下看,因为只是概览,所以本节不会讲的过于深入。 看这个扩展方法的参数,第一个参数是一个CompositionContext,是个Context,但不是View体系中的Context,而且可空,默认还没传,所以是干嘛使的?
先不说,等会儿下面用到了再说。
这第二个参数就比较好理解了,忽略@Composable注解,这就是一个lambda表达式参数,所以我们才能像setContent {}这样去使用setContent方法,大括号中的内容就是lambda表达式。

接着看setContent方法,首先通过在decorViewfindViewById寻找ComposeViewdecorView是什么就不过多介绍了,毕竟大家都是有经验的开发人员。
至于这个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的操作似乎也没什么奇怪。
接下来,依次调用的ComposeViewsetParentCompositionContextsetContentsetOwners方法,其中:

  • 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")
    }
}

clipboard2.png 在加了@Composable注解的函数上加上@Preview注解,就能不用再次构建就可以看到预览效果了,这个Composable函数有个条件是不能有参数,所以我们一般将需要传参数的Composable函数外面套一个不需要参数的Composable函数用来预览,不过预览效果目前为止和xml比还是差一点。

好了,本节内容就到这里,接下来会介绍各个Composable控件的使用。