【NowInAndroid架构拆解】(7)UI层解析——MainActivity构建过程

启动一件事情所需的动力越少,意志力就越多。------《微习惯》

前面分析过了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包含LoadingSuccess两个子数据类,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 的初始化

ComposeViewsetContent 方法会初始化 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 通过 MeasurePolicyPlaceable 完成测量和布局 ,最终通过 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展示出来的过程。

相关推荐
強云29 分钟前
界面架构- MVP(Qt)
qt·架构
赋创小助手11 小时前
Gartner预计2025年AI支出达6440亿美元:数据中心与服务器市场的关键驱动与挑战
运维·服务器·人工智能·科技·架构
magic 24511 小时前
MVC(Model-View-Controller)架构模式和三层架构介绍
架构·mvc
芯片SIPI设计12 小时前
HBM(高带宽内存)DRAM技术与架构
架构
拉丁解牛说技术12 小时前
AI大模型进阶系列(01)AI大模型的主流技术 | AI对普通人的本质影响是什么?
后端·架构·openai
r0ad13 小时前
文生图架构设计原来如此简单之交互流程优化
架构·aigc
热爱运维的小七13 小时前
从数据透视到AI分析,用四层架构解决运维难题
运维·人工智能·架构
桂月二二13 小时前
实时事件流处理架构的容错设计
架构·wpf
孪生质数-18 小时前
SQL server 2022和SSMS的使用案例1
网络·数据库·后端·科技·架构