启动一件事情所需的动力越少,意志力就越多。------《微习惯》
前面分析过了Model、ViewModel层,在系列的尾声部分,我将继续对UI层进行拆解。相比较于传统的XML布局模式,UI是Jetpack Compose体系当中差别最大的地方。像Model、ViewModel这些模块,在传统的MVP、MVVM里面都有类似概念。而声明式布局是我在理解项目过程中遭遇最多困难的地方。
因此,我将用2篇文章对NowInAndroid项目的UI层进行分析理解,第一篇文章从宏观角度,以MainActivity
作为起点,探究Compose视图树的构建过程;第二篇文章则聚焦具体的ForYou
页面,学习页面布局搭建、数据展现以及跳转的过程。
程序的入口:MainActivity
kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var lazyStats: dagger.Lazy<JankStats>
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor
@Inject
lateinit var analyticsHelper: AnalyticsHelper
@Inject
lateinit var userNewsResourceRepository: UserNewsResourceRepository
private val viewModel: MainActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
...
setContent {
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
CompositionLocalProvider(
LocalAnalyticsHelper provides analyticsHelper,
LocalTimeZone provides currentTimeZone,
) {
NiaTheme(
darkTheme = themeSettings.darkTheme,
androidTheme = themeSettings.androidTheme,
disableDynamicTheming = themeSettings.disableDynamicTheming,
) {
NiaApp(appState)
}
}
}
}
}
NIA是一个单Activity的应用,与传统写法一样,程序的入口在MainActivity的onCreate
中。在分析它之前,我们先看一下MainActivityViewModel
类。
包装首页加载状态的 MainActivityViewModel
kotlin
@HiltViewModel // Hilt注入点,用以注入参数 UserDataRepository
class MainActivityViewModel @Inject constructor(
userDataRepository: UserDataRepository,
) : ViewModel() {
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userData.map { // 读取userData并转换为StateFlow
Success(it)
}.stateIn(
scope = viewModelScope,
initialValue = Loading, // 初始态为Loading
started = SharingStarted.WhileSubscribed(5_000),
)
}
MainActivityViewModel
里面只维护了一个uiState
状态变量,它的类型是MainActivityUiState
------这是一个密封接口(sealed interface
)。
kotlin
sealed interface MainActivityUiState { // 密封接口,有Loading、Success两个状态
data object Loading : MainActivityUiState
data class Success(val userData: UserData) : MainActivityUiState {
override val shouldDisableDynamicTheming = !userData.useDynamicColor
override val shouldUseAndroidTheme: Boolean = when (userData.themeBrand) {
ThemeBrand.DEFAULT -> false
ThemeBrand.ANDROID -> true
}
override fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) =
when (userData.darkThemeConfig) {
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemDarkTheme
DarkThemeConfig.LIGHT -> false
DarkThemeConfig.DARK -> true
}
}
/**
* Returns `true` if the state wasn't loaded yet and it should keep showing the splash screen.
*/
fun shouldKeepSplashScreen() = this is Loading // Kotlin的接口中允许添加函数实现
/**
* Returns `true` if the dynamic color is disabled.
*/
val shouldDisableDynamicTheming: Boolean get() = true
/**
* Returns `true` if the Android theme should be used.
*/
val shouldUseAndroidTheme: Boolean get() = false
/**
* Returns `true` if dark theme should be used.
*/
fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) = isSystemDarkTheme
}
该sealed class包含Loading
和Success
两个子数据类,Loading
不含有成员变量,其函数使用接口定义的默认实现。Success
则包装userData
,并且读取了userData
中的配置项,这些配置项用来更新UI显示主题等。
UserData包含两类信息:1.用户收藏、已读的文章列表和订阅的主题列表;2.用户的主题色彩设置。
在onCreate里使用setContent创建布局
kotlin
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
lifecycleScope.launch { // 关联到Activity的lifecycle
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { // 启动协程绑定生命周期
combine( // 并发执行2个耗时操作
isSystemInDarkTheme(),
viewModel.uiState,
) { systemDark, uiState ->
ThemeSettings(
darkTheme = uiState.shouldUseDarkTheme(systemDark),
androidTheme = uiState.shouldUseAndroidTheme,
disableDynamicTheming = uiState.shouldDisableDynamicTheming,
)
}
.onEach { themeSettings = it } // 生成ThemeSettings并赋值给局部变量
.map { it.darkTheme }
.distinctUntilChanged()
.collect { darkTheme -> // 在collect中进行终结操作
trace("niaEdgeToEdge") {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
lightScrim = android.graphics.Color.TRANSPARENT,
darkScrim = android.graphics.Color.TRANSPARENT,
) { darkTheme },
navigationBarStyle = SystemBarStyle.auto(
lightScrim = lightScrim,
darkScrim = darkScrim,
) { darkTheme },
)
}
}
}
}
// 在返回Success前,展示SplashScreen
splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() }
// 重头戏!等价于setContentView(R.layout.XXXX)
setContent {
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
CompositionLocalProvider( // 重头戏!在最后一个lambda参数中,应用provides所声明的变量
LocalAnalyticsHelper provides analyticsHelper,
LocalTimeZone provides currentTimeZone,
) {
NiaTheme(
darkTheme = themeSettings.darkTheme,
androidTheme = themeSettings.androidTheme,
disableDynamicTheming = themeSettings.disableDynamicTheming,
) {
NiaApp(appState) // 最终调用到了@Composable函数NiaApp
}
}
}
}
setContent内部实现
在onCreate
中调用setContent{...}
,用于将 Compose UI
绑定到 Activity/Fragment
,替代传统 setContentView(R.layout.xml)
,直接用 Kotlin 代码声明 UI。通过调用setContent
,启动Compose运行时
,进而对UI组件的 生命周期和状态 进行管理。
setContent { ... }
是 Jetpack Compose 的核心入口,负责将 Compose UI 绑定到 Android 的窗口系统中。其底层实现融合了 Compose 的声明式编程模型
和 Android 的传统视图机制
。以下是其内部工作原理的深入解析:
1. 类与接口的协作
setContent
的实现依赖于以下几个关键类:
- ComponentActivity :AndroidX 提供的 Activity 基类,提供了
setContent
扩展函数。 - ComposeView:继承自 ViewGroup,是 Compose UI 的容器。
- Composition :管理 Compose 组件树与底层视图的绑定。
- Composer :负责 Compose 的组件树构建和重组。
2. 内部流程详解
步骤 1:调用 setContent
当在 ComponentActivity
中调用 setContent
时,会触发以下操作:
kotlin
// ComponentActivity.kt
public fun ComponentActivity.setContent(composable: @Composable () -> Unit) {
// 1. 创建 ComposeView 实例
val composeView = ComposeView(this)
// 2. 将 ComposeView 设置为 Activity 的根视图
setContentView(composeView)
// 3. 将 Composable Lambda 绑定到 ComposeView
composeView.setContent(composable) // 这里composable是一个lambda函数
}
步骤 2:ComposeView 的初始化
ComposeView
的 setContent
方法会初始化 Compose 运行时环境
:
kotlin
// ComposeView.kt
fun ComposeView.setContent(composable: @Composable () -> Unit) {
// 1. 创建 CompositionContext,管理生命周期
val context = CompositionContext()
// 2. 创建 Composition,关联到当前 View
val composition = Composition(ViewCompositionStrategy, context)
// 3. 将 Composable 函数绑定到 Composition
composition.setContent(composable)
// 4. 将 Composition 与 ComposeView 关联
this.setComposition(composition)
}
步骤 3:Composition 的运作
- 首次组合(Initial Composition)
Compose 运行时执行传入的 composable Lambda
,生成一颗由 LayoutNode
构成的组件树。例如:
kotlin
setContent {
Text("Hello Compose")
}
会生成一个表示 Text 组件
的 LayoutNode
。
- 布局与绘制
LayoutNode
通过 MeasurePolicy
和 Placeable
完成测量和布局 ,最终通过 Android 的 Canvas
绘制到 ComposeView 的 Surface
上。
- 重组(Recomposition)
当状态(如 mutableStateOf
)变化时,Composition 会标记需要更新的组件,重新执行受影响的 Composable 函数,生成新的 LayoutNode 树,并通过差量更新优化渲染。
3. 与传统视图系统的桥接
Compose 的 UI 最终会通过以下方式嵌入到 Android 的视图系统中:
- ComposeView 作为传统 View
ComposeView 继承自 ViewGroup,因此可以像普通视图一样添加到 Activity 或 Fragment 中。
- AndroidComposeView 的内部渲染
ComposeView 内部通过 AndroidComposeView
(一个私有类)与 Android 的 View 系统交互:
- 重写 onMeasure 和 onLayout,将 Compose 的布局逻辑嵌入到传统视图的测量/布局流程中。
- 通过 DisplayListCanvas 将 Compose 的绘制指令转换为 Android 的绘制操作。
4. 与 XML 布局的混合使用
Compose 可以与传统 View 混合使用,通过在xml文件中预留一个ComposeView
,可以在Activity中进行动态绑定,添加Compose组件。
xml
<!-- activity_main.xml -->
<FrameLayout>
<TextView android:id="@+id/text_view" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
kotlin
// 在 Activity 中动态绑定
val composeView = findViewById<ComposeView>(R.id.compose_view)
composeView.setContent {
Text("Hello Hybrid UI")
}
小结
以上就是MainActivity
的初始化过程,本文着重分析了Compose UI
是如何与传统xml布局向结合,把自身绑定在Activity的ContentView上。
在下一篇文章里,我将从路由入手,分析 ForYou
Tab展示出来的过程。