Compose

    Jetpack Compose 是围绕可组合函数构建的。这些函数可让您以程序化方式定义应用的界面,只需描述应用界面的外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素、将其附加到父项等)。如需创建可组合函数,只需将 @Composable 注解添加到函数名称中即可。 
    Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包。Compose 提供声明性 API,让您可在不以命令方式改变前端视图的情况下呈现应用界面,从而使编写和维护应用界面变得更加容易。此术语需要一些解释说明,它的含义对应用设计非常重要。
声明性编程范式
   长期以来,Android 视图层次结构一直可以表示为界面 widget 树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前数据。最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变 widget 的内部状态。
   手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以出人意料的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护的复杂性会随着需要更新的视图数量而增长。
    在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程任务。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 是一个声明性界面框架。
    重新生成整个屏幕所面临的一个难题是,在时间、计算能力和电池用量方面可能成本高昂。为了减少在这方面耗费的资源,Compose 会智能地选择在任何给定时间需要重新绘制界面的哪些部分。这会对您设计界面组件的方式有一定影响,如重组中所述。
设置 Compose 编译器 Gradle 插件:
1、在 libs.versions.toml 文件中,移除对 Compose 编译器的所有引用
2、在“plugins”部分,添加以下新依赖项

[versions]
kotlin = "2.0.0"

[plugins]
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

// Add this line
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
    1、 在项目的根 build.gradle.kts 文件中,将以下内容添加到“plugins”部分:

plugins {
    // Existing plugins
    alias(libs.plugins.compose.compiler) apply false
 }
    在使用 Compose 的每个模块中,应用该插件:

plugins {
    // Existing plugins
    alias(libs.plugins.compose.compiler)
 }
    将以下定义添加到应用的 build.gradle 文件中:

android {
    buildFeatures {
        compose true
    }
}
    最后,将以下部分中您需要的 Compose BoM 和 Compose 库依赖项的子集添加到您的依赖项:

dependencies {

    val composeBom = platform("androidx.compose:compose-bom:2024.10.01")
    implementation(composeBom)
    androidTestImplementation(composeBom)

    // Choose one of the following:
    // Material Design 3
    implementation("androidx.compose.material3:material3")
    // or Material Design 2
    implementation("androidx.compose.material:material")
    // or skip Material Design and build directly on top of foundational components
    implementation("androidx.compose.foundation:foundation")
    // or only import the main APIs for the underlying toolkit systems,
    // such as input and measurement/layout
    implementation("androidx.compose.ui:ui")

    // Android Studio Preview support
    implementation("androidx.compose.ui:ui-tooling-preview")
    debugImplementation("androidx.compose.ui:ui-tooling")

    // UI Tests
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

    // Optional - Included automatically by material, only add when you need
    // the icons but not the material library (e.g. when using Material3 or a
    // custom design system based on Foundation)
    implementation("androidx.compose.material:material-icons-core")
    // Optional - Add full set of material icons
    implementation("androidx.compose.material:material-icons-extended")
    // Optional - Add window size utils
    implementation("androidx.compose.material3.adaptive:adaptive")

    // Optional - Integration with activities
    implementation("androidx.activity:activity-compose:1.9.2")
    // Optional - Integration with ViewModels
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5")
    // Optional - Integration with LiveData
    implementation("androidx.compose.runtime:runtime-livedata")
    // Optional - Integration with RxJava
    implementation("androidx.compose.runtime:runtime-rxjava2")

}
定义可组合函数
添加 @Composable使函数成为可组合函数。Composable 函数只能从其他 Composable 函数的范围内调用。

data class Message(var author: String,val body: String)

@Composable
fun MessageCard(msg: Message){
//    Column {  //vertical
//        Text(text = msg.author)
//        Text(text = msg.body)
//    }

    Row { // horizontal
        Text(text = msg.author)
        Text(text = msg.body)
    }

//    Box{ //stack
//        Text(text = msg.author)
//        Text(text = msg.body)
//    }

}
添加预览函数,使用@Preview注解可以在 Android Studio 中预览可组合函数,而无需构建应用并将其安装到 Android 设备或模拟器中。该注解必须用于不接受参数的可组合函数。因此,您无法直接预览 MessageCard 函数,而是需要创建另一个名为 PreviewMessageCard 的函数,由该函数使用适当的参数调用 MessageCard。请在 @Composable 上方添加 @Preview 注解。

@Preview(showBackground = true)
@Composable
fun PreviewMessageCard(){
    MessageCard(
        msg = Message("LiLei","Body")
    )
}
Column 函数可让您垂直排列元素。向 MessageCard 函数中添加一个 Column。使用 Row 水平排列各项,并使用 Box 堆叠元素。

@Composable
fun ShowImage(msg: Message){
    Row {
        Image(
            painter = painterResource(R.drawable.pic),
            contentDescription = "Contact profile picture"
        )

        Column {
            Text(text = msg.author)
            Text(text = msg.body)
        }
    }
}
配置布局
Compose 使用了修饰符。通过修饰符,您可以更改可组合项的大小、布局、外观,还可以添加高级互动,例如使元素可点击。

@Composable
fun EnrichMessageCard(msg: Message){

    //add padding around our message
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(R.drawable.pic),
            contentDescription = "LiLi",
            modifier = Modifier.size(40.dp)
                .clip(CircleShape)
                //add a border and set a color to the image
                .border(1.5.dp,MaterialTheme.colorScheme.primary,CircleShape)
        )

        //add a horizontal space between image and column
        Spacer(modifier = Modifier.width(8.dp))

        Column{
            Text(text = msg.author,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleSmall
            )
            Spacer(modifier = Modifier.height(4.dp))
            Surface(shape = MaterialTheme.shapes.medium,
                shadowElevation = 2.dp) {
                Text(text = msg.body,
                    style = MaterialTheme.typography.bodyMedium,
                    modifier = Modifier.padding(all = 4.dp)
                )
            }


        }

    }
}
预览不同的主题

@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO,
    name = "Light mode")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "Dark Mode")
@Composable
fun DarkThemeEnrichMessageCardPreview(){
    LiuHanzeKotlinTheme {
        Surface {
                EnrichMessageCard(msg = Message("Liu","DarkMode"))
        }
    }
}
列表和动画
使用LazyColumn和LazyRow创建列表。
可组合函数可以使用 remember 将本地状态存储在内存中,并跟踪传递给 mutableStateOf 的值的变化。该值更新时,系统会自动重新绘制使用此状态的可组合项(及其子项)。这称为重组。
通过使用 Compose 的状态 API(如 remember 和 mutableStateOf),系统会在状态发生任何变化时自动更新界面。
可以根据点击消息时消息的 isExpanded 状态,更改消息内容的背景颜色。您将使用 clickable 修饰符来处理可组合项上的点击事件。您会为背景颜色添加动画效果,使其值逐步从 MaterialTheme.colorScheme.surface 更改为 MaterialTheme.colorScheme.primary(反之亦然),而不只是切换 Surface 的背景颜色。为此,您将使用 animateColorAsState 函数。最后,您将使用 animateContentSize 修饰符顺畅地为消息容器大小添加动画效果。
mutableStateOf 会创建可观察的 MutableState<T>,后者是与 Compose 运行时集成的可观察类型。


    interface MutableState<T> : State<T> {
        override var value: T
    }
    对 value 所做的任何更改都会安排对读取 value 的所有可组合函数进行重组。  

    虽然 remember 可帮助您在重组后保持状态,但不会帮助您在配置更改(例如:屏幕旋转)后保持状态。为此,您必须使用 rememberSaveable。rememberSaveable 会自动保存可保存在 Bundle 中的任何值。对于其他值,您可以将其传入自定义 Saver 对象。  
    val mutableState by rememberSaveable { mutableStateOf(default) }

    添加到 Bundle 的所有数据类型都会自动保存。如果要保存无法添加到 Bundle 的内容,您有以下几种选择。
    最简单的解决方案是向对象添加 @Parcelize 注解。对象将变为可打包状态并且可以捆绑。例如,以下代码会创建可打包的 City 数据类型并将其保存到状态。

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}
    MapSaver
    ListSaver
    注意:在 Compose 中将可变对象(如 ArrayList<T> 或 mutableListOf())用作状态会导致用户在您的应用中看到不正确或过时的数据。不可观察的可变对象(如 ArrayList 或可变数据类)不能由 Compose 观察,且在发生变化后不会触发重组。建议您使用可观察的数据存储器(如 State<List<T>>)和不可变的 listOf(),而不是使用不可观察的可变对象。
    在可组合项中声明 MutableState 对象的方法有三种:

//by 委托语法需要以下导入:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }

//remember的另外一种用法:
val text = remember( state.visualTransformation, state.annotatedString, ) {
    state.visualTransformation.filter(state.annotatedString).text
}

//remember() 用来在 Compose 中“记住”某个计算结果,避免每次重组(recomposition)时重新计算。
//它有两个核心要点:

//若 remember() 的 关键参数(keys)没有变化
//→ 不会重新执行 block,而是使用上一次的结果。

//若 key 中任何一个发生变化
//→ remember 内的 lambda 会重新执行,产生新的值。

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
    
@Composable
fun EnrichMessageCard(msg: Message){
    //add padding around our message
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(R.drawable.pic),
            contentDescription = "LiLi",
            modifier = Modifier.size(40.dp)
                .clip(CircleShape)
                //add a border and set a color to the image
                .border(1.5.dp,MaterialTheme.colorScheme.primary,CircleShape)
        )
        //add a horizontal space between image and column
        Spacer(modifier = Modifier.width(8.dp))

        var isExpanded by remember { mutableStateOf(false) }

        //surfaceColor will be updated gradually from one color to other
        val surfaceColor by animateColorAsState(
            if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,
        )

        Column(modifier = Modifier.clickable{isExpanded = !isExpanded}) {
            Text(text = msg.author,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleSmall
            )
            Spacer(modifier = Modifier.height(4.dp))
            Surface(shape = MaterialTheme.shapes.medium,
                shadowElevation = 2.dp,
                color = surfaceColor,
                modifier = Modifier.animateContentSize().padding(1.dp)) {
                Text(text = msg.body,
                    style = MaterialTheme.typography.bodyMedium,
                    maxLines = if(isExpanded) Int.MAX_VALUE else 1,
                    modifier = Modifier.padding(all = 4.dp)
                )
            }
        }

    }
}

    @Composable
    fun ConversationTest(messages: List<Message>){
        LazyColumn {
            items(messages){
                EnrichMessageCard(it)
            }
        }
    }

    //调用
    ConversationTest(SampleData.conversationSample)

屏幕自适应
使用 Compose Material 3 自适应库的 currentWindowAdaptiveInfo() 顶级函数计算应用的 WindowSizeClass。该函数会返回一个 WindowAdaptiveInfo 实例,其中包含 windowSizeClass。每当窗口大小类发生变化时,应用都会收到更新:
Jetpack Compose 是一种现代的声明式方法,用于构建自适应应用,而不会复制多个布局文件,而且消除了维护方面的负担。

Compose Material 3 自适应库包含用于管理窗口大小类别、导航组件、多窗格布局以及可折叠设备折叠状态和合页位置的可组合项,例如:
    NavigationSuiteScaffold:根据应用窗口大小类和设备折叠状态,自动在导航栏和侧边导航栏之间切换。
    ListDetailPaneScaffold:实现列表-详情规范布局。
    根据应用窗口大小调整布局。在较大窗口大小类别中,在并排窗格中显示列表和列表项的详情;但在紧凑型和中等窗口大小类别中,只显示列表或详情。
    SupportingPaneScaffold:实现辅助窗格规范布局。
    在“较大”窗口大小类别中显示主要内容窗格和辅助窗格,但在“紧凑”和“中等”窗口大小类别中仅显示主要内容窗格。

Compose Material 3 自适应库是开发自适应应用的必备依赖项。
借助 Jetpack WindowManager 中的 WindowInfoTracker 接口,您可以获取设备的 DisplayFeature 对象列表。显示屏功能包括 FoldingFeature.State,用于指示设备是完全展开还是半开。
Compose Material 3 Adaptive 库提供了 currentWindowAdaptiveInfo() 顶级函数,该函数会返回包含 windowPosture 的 WindowAdaptiveInfo 实例。
用户通常会将外接键盘、触控板、鼠标和触控笔连接到大屏设备。这些外围设备可以提高用户的工作效率、输入精度、表达自我和提高无障碍功能。大多数 ChromeOS 设备都内置键盘和触控板。

自适应应用支持外部输入设备,但大部分工作是由 Android 框架为您完成的:
Jetpack Compose 1.7 及更高版本:默认支持键盘 Tab 键导航以及鼠标或触控板点击、选择和滚动。
Jetpack androidx.compose.material3 库:让用户能够使用触控笔向任何 TextField 组件写入内容。
键盘快捷键帮助程序:让用户能够发现 Android 平台和应用键盘快捷键。通过替换 onProvideKeyboardShortcuts() 窗口回调,在键盘快捷键帮助程序中发布应用的键盘快捷键。
为了全面支持各种尺寸的外形规格,自适应应用支持所有类型的输入。
重组
可组合函数符合跳过条件,除非:
    函数的返回值类型不是 Unit
    函数带有 @NonRestartableComposable 或 @NonSkippableComposable 注解
    必需参数的类型不稳定

某种类型必须符合以下协定,才能被视为稳定类型:
    对于相同的两个实例,其 equals 的结果将始终相同。
    如果类型的某个公共属性发生变化,组合将收到通知。
    所有公共属性类型也都是稳定。

有这样一些归入此协定的重要通用类型,即使未使用 @Stable 注解来显式标记为稳定的类型,Compose 编译器也会将其视为稳定的类型。
    所有基元值类型:Boolean、Int、Long、Float、Char 等。
    字符串
    所有函数类型 (lambda)
所有这些类型都可以遵循稳定协定,因为它们是不可变的。由于不可变类型绝不会发生变化,它们就永远不必通知组合更改方面的信息,因此遵循该协定就容易得多。

Compose 的 MutableState 类型是一种众所周知稳定但可变的类型。如果 MutableState 中存储了值,状态对象整体会被视为稳定对象,因为 State 的 .value 属性如有任何更改,Compose 就会收到通知。
当作为参数传递到可组合项的所有类型都很稳定时,系统会根据可组合项在界面树中的位置来比较参数值,以确保相等性。如果所有值自上次调用后未发生变化,则会跳过重组。
Compose 仅在可以证明稳定的情况下才会认为类型是稳定的。例如,接口通常被视为不稳定类型,并且具有可变公共属性的类型(实现可能不可变)的类型也被视为不稳定类型。
如果 Compose 无法推断类型是否稳定,但您想强制 Compose 将其视为稳定类型,请使用 @Stable 注解对其进行标记。

// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}
CompositionLocal
CompositionLocal 是一种在 Jetpack Compose 中通过 组合(composition)来管理和提供全局数据的方法。它可以用于传递应用程序的配置、主题信息、语言环境、用户信息等,避免了层层传递参数的麻烦。
CompositionLocal 本质上是 Compose 中的一种 作用域本地存储,数据在组件树中是局部可访问的,而不需要显式传递给每一个组件。


    // 定义一个 CompositionLocal 用来传递用户的名称
    val LocalUserName = compositionLocalOf { "Guest" }

    @Composable
    fun AppContent() {
        // 使用 CompositionLocalProvider 提供局部数据
        CompositionLocalProvider(LocalUserName provides "John Doe") {
            UserProfile() // 在这里访问 LocalUserName
        }
    }
    
    @Composable
    fun UserProfile() {
        // 访问 LocalUserName
        val userName = LocalUserName.current
        Text("Hello, $userName!")
    }

    //CompositionLocal 可以嵌套使用。内层的 CompositionLocalProvider 会覆盖外层的值。
    @Composable
    fun ParentComponent() {
        CompositionLocalProvider(LocalExampleData provides "Outer Value") {
            ChildComponent() // 访问 "Outer Value"
            CompositionLocalProvider(LocalExampleData provides "Inner Value") {
                ChildComponent() // 访问 "Inner Value"
            }
        }
    }

    //CompositionLocal 可以用于传递 ViewModel 或其他依赖。
    // 定义 CompositionLocal
    val LocalViewModel = compositionLocalOf<MyViewModel> { error("No ViewModel provided!") }

    // 提供 ViewModel
    @Composable
    fun App() {
        val viewModel = viewModel<MyViewModel>()
        CompositionLocalProvider(LocalViewModel provides viewModel) {
            Screen()
        }
    }

    // 使用 ViewModel
    @Composable
    fun Screen() {
        val viewModel = LocalViewModel.current
        val data = viewModel.data.collectAsState()
        Text(text = "Data from ViewModel: ${data.value}")
    }
NavController 实现页面跳转
首先,确保在你的 build.gradle 文件中添加了 navigation-compose 依赖:

dependencies {
    implementation "androidx.navigation:navigation-compose:2.8.6" // 请使用最新版本
}
一个简单的例子

    // HomeScreen
    @Composable
    fun HomeScreen(navController: NavController) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = "Home Screen", style = MaterialTheme.typography.h4)
            Spacer(modifier = Modifier.height(16.dp))
            Button(onClick = { navController.navigate("detail") }) {
                Text(text = "Go to Detail Screen")
            }
        }
    }
    
    // DetailScreen
    @Composable
    fun DetailScreen(navController: NavController) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = "Detail Screen", style = MaterialTheme.typography.h4)
            Spacer(modifier = Modifier.height(16.dp))
            Button(onClick = { navController.popBackStack() }) {
                Text(text = "Go Back to Home")
            }
        }
    }

在 MainActivity 中,设置 NavController 并使用 NavHost 定义导航图:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                NavControllerExampleApp()
            }
        }
    }
    
    @Composable
    fun NavControllerExampleApp() {
        val navController = rememberNavController() // 创建 NavController
        NavHost(
            navController = navController,
            startDestination = "home" // 设置起始目的地
        ) {
            // 定义导航图
            composable("home") { // HomeScreen 的路由
                HomeScreen(navController = navController)
            }
            composable("detail") { // DetailScreen 的路由
                DetailScreen(navController = navController)
            }
        }
    }

    //可以通过路由传递参数。例如,从 HomeScreen 传递一个字符串到 DetailScreen:
    // 修改导航图
    composable("detail/{message}") { backStackEntry ->
        val message = backStackEntry.arguments?.getString("message")
        DetailScreen(navController = navController, message = message)
    }

    // 修改导航操作
    navController.navigate("detail/Hello from HomeScreen")

路由参数传递
路由的使用分为 3 步:配置导航图 → 跳转路由 → 提取参数。

1. 第一步:在导航图中配置路由
用 Review.route(带占位符的模板)和 Review.arguments(参数配置)注册路由,关联 ReviewScreen:

// 可以为不同屏幕添加独特逻辑
object Review : Screen("review/{itemId}") {
    fun createRoute(itemId: Long) = "review/$itemId"
    fun extractItemId(backStackEntry: NavBackStackEntry): Long {
        return backStackEntry.arguments?.getLong("itemId") ?: -1L
    }
    val arguments = listOf(
        navArgument("itemId") { type = NavType.LongType }
    )
}

@Composable
fun AppNavHost(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = Screen.Home.route
    ) {
        // 其他路由...

        // 配置 Review 路由
        composable(
            route = Review.route, // 传入带占位符的路由模板:review/{itemId}
            arguments = Review.arguments // 传入参数配置:itemId 是 Long 类型
        ) { backStackEntry ->
            // 提取参数(调用封装好的 extractItemId 方法)
            val itemId = Review.extractItemId(backStackEntry)
            // 跳转到复习页面,传入提取到的 itemId
            ReviewScreen(itemId = itemId, navController = navController)
        }
    }
}
2. 第二步:从其他页面跳转至 Review 路由
用 Review.createRoute(itemId) 生成 “带实际参数的路由字符串”,调用 navController.navigate() 跳转:

// 示例:在 HomeScreen 中点击某个记忆项,跳转至 ReviewScreen
@Composable
fun HomeScreen(navController: NavHostController) {
    Button(
        onClick = {
            val targetItemId = 123L // 实际要传递的参数(如从数据库查询得到)
            // 生成实际路由:review/123
            val route = Review.createRoute(targetItemId)
            // 跳转路由
            navController.navigate(route)
        }
    ) {
        Text("复习记忆项 123")
    }
}
3. 第三步:在 ReviewScreen 中使用参数
通过 extractItemId 提取的 itemId,可以用于查询数据、渲染页面:

@Composable
fun ReviewScreen(itemId: Long, navController: NavHostController) {
    // 用 itemId 查询对应的记忆项(如从 Room 数据库获取)
    val memoryItem = remember { viewModel.getMemoryItemById(itemId) }

    Column {
        Text(text = "复习项 ID:$itemId")
        Text(text = "标题:${memoryItem?.title ?: "无"}")
        // 其他页面内容...
    }
}
如果路由有多个参数(如 detail/{id}/{title}),扩展 Review 的写法即可,示例:

object Detail : Screen("detail/{id}/{title}") {
    // 多参数构造路由
    fun createRoute(id: Long, title: String) = "detail/$id/$title"

    // 提取多个参数
    fun extractId(backStackEntry: NavBackStackEntry): Long {
        return backStackEntry.arguments?.getLong("id") ?: -1L
    }
    fun extractTitle(backStackEntry: NavBackStackEntry): String {
        return backStackEntry.arguments?.getString("title") ?: "未知标题"
    }

    // 多参数配置
    val arguments = listOf(
        navArgument("id") { type = NavType.LongType },
        navArgument("title") { type = NavType.StringType }
    )
}