-
Jetpack Compose是什么
如果有跨端开发经验的同学,理解和学习compose可能没有那么大的压力。简单地说,compose可以让Android的原生开发也可以使用类似rn的jsx的语法来开发UI界面。以往,我们开发Android原生页面的时候,通常是在xml中画相关的UI控件,然后在activity中通过findViewById拿到相关的控件对象,接着根据业务需求调用该控件对象的相关API方法。这种方式官方称之为命令式UI。再加上大前端浪潮的兴起,React、Vue等声明式UI技术的的日益成熟,以往那套继承自View的组件体系已经越发难以维护(目前View.java已超过3万行代码),因此Compose这门技术就应运而生了。 -
Jetpack Compose的优势
这点感兴趣的同学就到官网看下就行了,反正就是一堆好处。值得注意的是compose最低兼容到API21。 -
Compose API设计原则
由于Compose在编程范式上与传统视图体系有着根本的不同,在开始深入学习Compose之前,有必要对Compose API的设计原则做一个介绍。无论是Compose预置的Composable还是开发者自己定义的Composable,都应该遵守这些原则。- 一切皆为函数
Compose声明式UI的基础是Composable函数。Composable函数通过多级嵌套形成结构化的函数调用链,函数调用链经过运行后生成一颗UI视图树。视图树一旦生成便不可随意改变。视图的刷新依靠Composable函数的反复执行(重组)来实现。当需要显示的数据发生变化时,Compoable基于新参数再次执行,更新底层的视图树,最终完成视图的刷新。
Composable函数只能在Composable函数中调用,这与挂起函数只能在协程或其他挂起函数中调用类似,都是在编译期保证的。在Compose的世界中,一切组件都是函数,由于没有类的概念,因此不会有任何继承的层次结构,所有组件都是顶层函数,可以在DSL中直接调用。 - 组合优于继承
Composable作为函数相互没有继承关系,这有利于促使开发者使用组合的视角去思考问题。 - 单一数据源
在传统的命令行UI中(以EditText为例),文字的变化来源多处,有可能来自用户的输入,也可能来自代码某处的setText。状态变化可能不止一个来源,即所谓的多数据源。多数据源下的状态变化不容易跟踪,且会使后期难以维护。单一数据源决定了Composable数据流的单向流动,数据(name)总是自上而下(由父组件流向子组件)流动,而事件(onNameChange)总是自下而上(由子组件流向父组件)传递。
- 一切皆为函数
-
Compose与View的关系
在传统视图体系中由View与ViewGroup构成视图树,而Compose中也有同样一颗视图树,它由LayoutNode构成,由Composition负责管理。
两种树的节点类型不同,但是它们并非全没关系,依然可以共存于一棵树中。就像DOM节点与View也不同,但是可以通过WebView显示在一棵树上,Compose也可以借助这样一个连接点挂载在View树上。使用Android Studio自带的Layout Inspector可以看到这个连接点就是ComposeView,它就是连接View与Compose的桥梁。
ComposeView有一个唯一子节点AndroidComposeView,它既是一个ViewGroup,也是LayoutNode视图树的持有者,它实现了LayoutNode视图结构与View视图结构的连接。既然AndroidComposeView已经承担了两套体系的连接,那为什么还要多一层ComposeView呢?
ComposeView继承自AbstractComposeView,而后者有三个子类,分别对应着Activity窗口、Dialog窗口与PopupWindow窗口。Android平台存在所谓Window的概念,我们在很多场景下会有多窗口需求,例如在页面中弹出一个对话窗。AbstractComposeView的子类负责Android平台各类窗口的适配并生成对应的Composition, ComposeView作为其中一个子类负责Activity窗口的适配。总体来说,ComposeView负责对Android平台的Activity窗口的适配,AndroidComposeView负责连接LayoutNode视图系统与View视图系统。如此的职责划分可以实现上层视图适配与下层窗口适配逻辑的解耦。
Composition是视图树的创建者。从Composable函数到视图树的生成经历这个过程:
第一步Composable函数执行后填充SlotTable, SlotTable中记录着Composable执行过程的状态信息;
第二步基于SlotTable生成和更新LayoutNode视图树。Composition负责从Composable执行到视图树生成(更新)的整个过程。
ComposeView接入View视图后,内部的UI工作都在Compose侧闭环处理,来自AndroidComposeView的绘制、测量布局与手势事件分发等都下沉到LayoutNode去完成。ComposeView作为View可以挂载到原有View视图树中的任意位置。因此一个传统视图项目可以通过ComposeView阶段性地接入Compose。一个纯Compose页面就是将ComposeView直接挂载到ContentView上面。
- 不只是Android UI框架
Compose还包含一些其他的库,如下图所示:
Compose从上到下分为四层,每一层都可以被单独使用,在不同维度提供能力支持。可以只使用Compose的Runtime层构建任何基于数据驱动能力的系统或类库。在这样清晰的分层结构下,我们甚至可以隔离那些平台相关代码,自底向上自己来实现跨平台的UI系统。
- Compose开发环境搭建
- 创建一个新的Compose项目
这个比较简单,使用AndroidStudio创建项目时,选择compose项目,然后无脑下一步即可,如下图所示:
- 已有项目引入Compose
按照下面步骤改造已有项目:- 在工程中引入Compose相关依赖:
dependencies { implementation platform('androidx.compose:compose-bom:2022.10.00') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.material3:material3' }
- 在build.gradle文件的android闭包中添加如何下配置:
buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion '1.3.2' } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } }
- 修改相关的xml布局文件
<?xml version="1.0" encoding="utf-8"?> <androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> </androidx.compose.ui.platform.ComposeView>
- 在activity中查找该View并使用setContent即可
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<ComposeView>(R.id.root).setContent { Text(text = "Hello Compose") } } }
- 在工程中引入Compose相关依赖:
- 在Compose中使用View组件
不少功能性的传统视图控件在Compose中没有对应的Composable实现,例如SurfaceView、WebView、MapView等。因此在Compose中可能会有使用传统View控件的需求。Compose提供了名为AndroidView的Composable组件,允许在Composable中插入任意基于继承自View的传统视图控件。class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AndroidView(factory = { context -> WebView(context).apply { settings.javaScriptEnabled = true webViewClient = WebViewClient() loadUrl("https://www.baidu.com/") } }, modifier = Modifier.fillMaxSize()) } } }
- 创建一个新的Compose项目