Android Compose开发

来自:博客园
时间:2024-02-27
阅读:

 

好处

Compose 编译后不是转化为原生的 Android 上的 View 去显示,而是依赖于平台的Canvas ,在这点上和 Flutter 有点相似,简单地说可以理解为 Compose 是全新的一套 View 。

声明式 UI,通过对比可以看到 Kotin DSL 有诸多好处:

  • 有着近似 XML 的结构化表现力
  • 较少的字符串,更多的强类型,更安全
  • 可提取 linearLayoutParams 这样的对象方便复用
  • 在布局中同步嵌入 onClick 等事件处理
  • 如需要还可以嵌入 if ,for 这样的控制语句
  • 减少 findViewById 等函数遍历树
  • 加速开发View 与 Compose 之间可以相互调用,兼容现有的所有代码。借助 AS 可以实时预览界面,轻松执行界面检查。
  • 另外 Compose 里的代码基本都是可以被混淆的,所以开启混淆之后代码的压缩率也很高。
  • 手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以出人意料的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护的复杂性会随着需要更新的视图数量而增长。

入门

Jetpack Compose 中的 match_parent 相当于什么?

Compose 编程思想  |  Jetpack Compose  |  Android Developers

Compose 布局基础知识  |  Jetpack Compose  |  Android Developers

原创:写给初学者的Jetpack Compose教程,基础控件和布局

原创:写给初学者的Jetpack Compose教程,Modifier

原创:写给初学者的Jetpack Compose教程,使用State让界面动起来

原创:写给初学者的Jetpack Compose教程,Lazy Layout

Composable

告诉编译器:此函数旨在将数据转换为界面。
所有的 Composable 函数还有一个约定俗成的习惯,就是函数的命名首字母需要大写。
@Preview 注解,这个注解表示这个函数是用来快速预览 UI 样式的。

@Composable 注解用于标记一个函数为可组合函数。可组合函数是一种特殊的函数,不需要返回任何 UI 元素,因为可组合函数描述的是所需的屏幕状态,而不是构造界面 widget;而如果按我们以前的 XML 编程方式,必须在方法中返回 UI 元素才能使用它(如返回 View 类型)。

@Composable 注解的函数之间可以相互调用,因为这样 Compose 框架才能正确处理依赖关系。另外,@Composable 函数中也可以调用普通函数,而普通函数中却不能直接调用@Composable 函数。 这里可以类比下 kotlin 中 suspend 挂起函数的用法,其用法是相似的

布局

Compose 通过只测量一次子项来实现高性能。单遍测量对性能有利,使 Compose 能够高效地处理较深的界面树。
Android Compose开发
父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整

其他组件

CollapsingToolbarScaffold
stickyHeader
HorizontalPager
BottomNavigationBar
Scaffold
PullRefreshIndicator
TopAppBar

列表

列表和网格  |  Jetpack Compose  |  Android Developers

verticalScroll

我们可以使用 verticalScroll() 修饰符使 Column 可滚动

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

延迟列表

使用 Compose 的 LazyColumn 和 LazyRow。这些可组合项只会呈现屏幕上显示的元素,因此,对于较长的列表,使用它们会非常高效。

import androidx.compose.foundation.lazy.items

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageRow(message)
        }
    }
}

还有一个名为 [itemsIndexed ()]( https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/package-summary?hl=zh-cn# (androidx. compose. foundation. lazy. LazyListScope). itemsIndexed (kotlin. collections. List, kotlin. Function2, kotlin. Function2, kotlin. Function3)) 的 items () 扩展函数的变体,用于提供索引

内容内边距

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

如需在列表项之间添加间距,可以使用 Arrangement.spacedBy ()。以下示例在每个列表项之间添加了 4.dp 的间距:

verticalArrangement = Arrangement.spacedBy(4.dp),

性能

早期 Lazy Layout 的性能很差,滚动的时候巨卡无比,确实很难让人用得下去。
但是在 Compose 1.5版本中,Google 做了大量的性能优化工作,所以如果你现在再来尝试一次,你会发现性能已经不是什么问题了。

修饰符

借助修饰符,您可以修饰或扩充可组合项。您可以使用修饰符来执行以下操作:

  • 更改可组合项的大小、布局、行为和外观
  • 添加信息,如无障碍标签
  • 处理用户输入
  • 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放

修饰符是标准的 Kotlin 对象。您可以通过调用某个 Modifier 类函数来创建修饰符:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}
  • 修饰符顺序很重要
  • 提取和重复使用修饰符
  • clickable 使可组合项响应用户输入,并显示涟漪。
  • padding 在元素周围留出空间。
  • fillMaxWidth 使可组合项填充其父项为它提供的最大宽度。
  • size() 指定元素的首选宽度和高度。

偏移量

要相对于原始位置放置布局,请添加 offset 修饰符,并在 x 轴和 y 轴中设置偏移量。偏移量可以是正数,也可以是非正数。padding 和 offset 之间的区别在于,向可组合项添加 offset 不会改变其测量结果:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

offset 修饰符根据布局方向水平应用。在从左到右的上下文中,正 offset 会将元素向右移,而在从右到左的上下文中,它会将元素向左移。
Android Compose开发

requiredSize

请注意,如果指定的尺寸不符合来自布局父项的约束条件,则可能不会采用该尺寸。如果您希望可组合项的尺寸固定不变,而不考虑传入的约束条件,请使用 requiredSize 修饰符:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

在此示例中,即使父项的 height 设置为 100.dpImage 的高度还是 150.dp,因为 requiredSize 修饰符优先级较高。

滚动

在 View 中的话,通常可以在需要滚动的内容之外再嵌套一层 ScrollView 布局,这样 ScrollView 中的内容就可以滚动了。
而 Compose 则不需要再进行额外的布局嵌套,只需要借助 modifier 参数即可,代码如下所示:

@Composable  
fun SimpleWidgetColumn() {  
    Row(  
        modifier = Modifier  
            .fillMaxSize()  
            .horizontalScroll(rememberScrollState()),  
        verticalAlignment = Alignment.CenterVertically,  
    ) {  
        ...  
    }  
}

添加间距Spacer

Spacer(modifier = Modifier.width(8.dp))

Button

如何才能给 Button 指定文字内容呢?它可以和 Text 配合在一起使用。

Button(
    onClick = { /* ... */ },
    // Uses ButtonDefaults.ContentPadding by default
    contentPadding = PaddingValues(
        start = 20.dp,
        top = 12.dp,
        end = 20.dp,
        bottom = 12.dp
    )
) {
    // Inner content including an icon and a text label
    Icon(
        Icons.Filled.Favorite,
        contentDescription = "Favorite",
        modifier = Modifier.size(ButtonDefaults.IconSize)
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

Context

要想弹出 Toast 需要有 Context 参数才行。在 Composable 函数当中获取 Context 对象,可以调用 LocalContext. current 获得。

@Composable  
fun SimpleWidgetColumn() {  
    Column {  
        ...  
        val context = LocalContext.current  
        Button(onClick = {  
            Toast.makeText(context, "This is Toast", Toast.LENGTH_SHORT).show()  
        }) {  
            Text(  
                text = "This is Button",  
                color = Color.White,  
                fontSize = 26.sp  
            )  
        }  
    }  
}

文字图片

Compose 中的文字  |  Jetpack Compose  |  Android Developers

自定义图片  |  Jetpack Compose  |  Android Developers

val imageModifier = Modifier
    .size(150.dp)
    .border(BorderStroke(1.dp, Color.Black))
    .background(Color.Yellow)
Image(
    painter = painterResource(id = R.drawable.dog),
    contentDescription = stringResource(id = R.string.dog_content_description),
    contentScale = ContentScale.Fit,
    modifier = imageModifier
)

TextField

@Composable  
fun SimpleWidgetColumn() {  
    Column {  
        ...  
        TextField(  
            value = "",  
            onValueChange = {},  
            placeholder = {  
                Text(text = "Type something here")  
            },  
            colors = TextFieldDefaults.textFieldColors(  
                backgroundColor = Color.White  
            )  
        )  
    }  
}

重组

  • 为了跟踪这种状态变化,您必须使用 remember 和 mutableStateOf 函数。
  • remember 和 mutableStateOf 在 Composable 函数中几乎永远都是配套使用的。
  • 使用 by 关键字替代了之前的等号,用委托的方式来为 count 变量赋值。count 的类型是 MutableState<Int>,而改用 by 关键字赋值之后,count 的类型就变成了 Int。既然都是 Int 了,那么我们就可以直接对这个值进行读写操作了,而不用像之前那样再调用它的 getValue ()和 setValue ()函数,是不是代码变得更简单了?注意导包
import androidx. compose. runtime. getValue
import androidx. compose. runtime. setValue
  • rememberSaveable 函数是 remember 函数的一个1增强版,它唯一和 remember 不同的地方就是在于其包裹的数据在手机横竖屏旋转时会被保留下来。

实例

import androidx. compose. foundation. clickable
import androidx. compose. runtime. getValue
import androidx. compose. runtime. mutableStateOf
import androidx. compose. runtime. remember
import androidx. compose. runtime. setValue

class MainActivity : ComponentActivity () {
   override fun onCreate (savedInstanceState: Bundle?) {
       super.onCreate (savedInstanceState)
       setContent {
           ComposeTutorialTheme {
               Conversation (SampleData. conversationSample)
           }
       }
   }
}

@Composable
fun MessageCard (msg: Message) {
    Row (modifier = Modifier.padding (all = 8. dp)) {
        Image (
            painter = painterResource (R.drawable. profile_picture),
            contentDescription = null,
            modifier = Modifier
                .size (40. dp)
                .clip (CircleShape)
                .border (1.5. dp, MaterialTheme. colors. secondaryVariant, CircleShape)
        )
        Spacer (modifier = Modifier.width (8. dp))
		
        var isExpanded by remember { mutableStateOf (false) }

        Column (modifier = Modifier. clickable { isExpanded = !isExpanded }) {
            Text (
                text = msg. author,
                color = MaterialTheme. colors. secondaryVariant,
                style = MaterialTheme. typography. subtitle2
            )

            Spacer (modifier = Modifier.height (4. dp))

            Surface (
                shape = MaterialTheme. shapes. medium,
                elevation = 1. dp,
            ) {
                Text (
                    text = msg. body,
                    modifier = Modifier.padding (all = 4. dp),
                    maxLines = if (isExpanded) Int. MAX_VALUE else 1,
                    style = MaterialTheme. typography. body2
                )
            }
        }
    }
}

状态提升

以下是你应该考虑的状态提升最少应该到达哪个层级的关键因素:

  1. 如果有多个 Composable 函数需要读取同一个 State 对象,那么至少要将 State 提升到这些 Composable 函数共有的父级函数当中。

  2. 如果有多个 Composable 函数需要对同一个 State 对象进行写入,那么至少要将 State 提升到所有执行写入的 Composable 函数里调用层级最高的那一层。

  3. 如果某个事件的触发会导致两个或更多的 State 发生变更,那么这些 State 都应该提升到相同的层级。

viewmodel

首先我们要引入如下两个库,这是 Compose 为了适配 ViewModel 和 LiveData 而专门设计的库:

dependencies {
    implementation "androidx. lifecycle: lifecycle-viewmodel-compose: 2.6.2"
    implementation "androidx. compose. runtime: runtime-livedata: 1.5.1"
}

传统 LiveData 的用法在 Compose 中并不好使,因为传统 LiveData 依赖于监听某个值的变化,并对相应的界面进行更新,而 Compose 的界面更新则依赖于重组。
因此,我们需要将 LiveData 转换成 State 才行,observeAsState ()函数就是用来做这个事情的,参数中传入的0表示它的初始值。

import androidx. lifecycle. viewmodel. compose. viewModel

@Composable
fun CallCounter (modifier: Modifier = Modifier, viewModel: MainViewModel = viewModel ()) {
    val count by viewModel.count.observeAsState (0)
    val doubleCount by viewModel.doubleCount.observeAsState (0)
    Column {
        Counter (
            count = count,
            onIncrement = { viewModel.incrementCount () },
            modifier.fillMaxWidth ()
        )
        Counter (
            count = doubleCount,
            onIncrement = { viewModel.incrementDoubleCount () },
            modifier.fillMaxWidth ()
        )
    }
}

互相调用

Interoperability API  |  Jetpack Compose  |  Android Developers

Jetpack Compose和View的互操作性 - 圣骑士wind - 博客园

ComposeView 的 setContent (content: @Composable () -> Unit) 方法只有一个 content 参数,而这个参数是一个添加了 @Composable 注解的匿名函数,也就是说,在其中我们可以正常的使用 compose 了。

bind. jointGifPreviewRecyclerView. setContent {
	Test ()
}

Android View

@Composable
fun CustomView () {
    val state = remember { mutableStateOf (0) }
 
    //widget. Button
    AndroidView (
        factory = { ctx ->
            //Here you can construct your View
            android.widget.Button (ctx). apply {
                text = "My Button"
                layoutParams = LinearLayout.LayoutParams (MATCH_PARENT, WRAP_CONTENT)
                setOnClickListener {
                    state. value++
                }
            }
        },
        modifier = Modifier.padding (8. dp)
    )
    //widget. TextView
    AndroidView (factory = { ctx ->
        //Here you can construct your View
        TextView (ctx). apply {
            layoutParams = LinearLayout.LayoutParams (MATCH_PARENT, WRAP_CONTENT)
        }
    }, update = {
        it. text = "You have clicked the buttons: " + state.value.toString () + " times"
    })
}

这里的桥梁是AndroidView, 它是一个 composable 方法:

@Composable
fun <T : View> AndroidView (
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
)

Compose 和 View 的结合, 主要是靠两个桥梁.
还挺有趣的:

  • ComposeView其实是个 Android View.
  • AndroidView其实是个 Composable 方法.

Compose 和 View 可以互相兼容的特点保证了项目可以逐步迁移, 并且也给够了安全感, 像极了当年 java 项目迁移 kotlin,至于什么学习曲线, 经验不足, 反正早晚都要学的, 整点新鲜的也挺好

项目学习

fmtjava/Compose_Eyepetizer: 一款基于 Jetpack Compose 实现的精美仿开眼视频App(提供Kotlin、Flutter、React Native、小程序版本 ? )

其他

//ViewPager2, 通过将此状态对象保存在组件中,可以确保当组件重新合成时,分页状态不会丢失。

val pagerState = rememberPagerState ()

HorizontalPager 是一种用于构建横向滚动页面的组件。它允许您在应用程序中创建水平滑动的页面布局,类似于 ViewPager 或 RecyclerView。

HorizontalPager (pageCount = 4,  
    userScrollEnabled = false,  
    state = pagerState,  
    modifier = Modifier  
        .fillMaxSize ()  
        .background (Color. White)  
        .padding (padding)) { pageIndex ->  
    when (pageIndex) {  
        0 -> DailyPage ()  
        1 -> DiscoverPage ()  
        2 -> HotPage ()  
        3 -> PersonPage ()  
    }  
}

使用记忆的协程作用域可以确保在组件重新合成时,作用域内的协程会自动取消

val scope = rememberCoroutineScope ()

weight 用于设置子元素的权重,权重越大,占据的空间就越大

Column (modifier = Modifier.weight (1f) 

thickness 分割线的厚度

Divider (thickness = 0.5. dp, modifier = Modifier.padding (top = 5. dp))

这是一个小圆点

Box (modifier = Modifier  
    .padding (2. dp)  
    .clip (CircleShape)  
    .background (color)  
    .size (8. dp))

text 加一个背景

Box (modifier = Modifier  
    .padding (end = 15. dp, bottom = 10. dp)  
    .background (Black_54, shape = RoundedCornerShape (5. dp))  
    .padding (5. dp)  
    .align (Alignment. BottomEnd), contentAlignment = Alignment. Center) {  
    Text (text = DateUtils.formatDateMsByMS ((itemData. duration * 1000). toLong ()),  
        color = Color. White,  
        fontWeight = FontWeight. Bold)

或者这样写

Text (text = DateUtils.formatDateMsByMS ((itemData. duration * 1000). toLong ()),  
    color = Color. White,  
    fontWeight = FontWeight. Bold,  
    modifier = Modifier  
        .padding (end = 15. dp, bottom = 10. dp)  
        .background (Black_54, shape = RoundedCornerShape (5. dp))  
        .padding (5. dp)  
        .align (Alignment. BottomEnd))

注意:这里有2个 padding

Android Compose开发

padding

在 Compose 中,确实没有margin修饰符,只有padding修饰符。如果您想在Text组件周围创建间距,可以使用padding修饰符来实现类似的效果。在您提供的示例代码中,Modifier.padding (top = 3. dp)将在Text组件的顶部添加3dp 的内边距,从而创建了与margin类似的效果。

Text (text = itemData. author?. description ?: "",  
    color = Color. White,  
    fontSize = 13. sp, modifier = Modifier.padding (top = 3. dp))

或者:

Spacer (modifier = Modifier.height (10. dp))

zIndex

zIndex 是指定视图的层级顺序的属性。它控制了视图在屏幕上的显示顺序。具有较高 zIndex 值的视图将显示在具有较低 zIndex 值的视图之上。

默认情况下,视图的 zIndex 值为0。如果设置一个较大的正值,则视图将显示在其他视图的上方。如果设置一个较小的负值,则视图将显示在其他视图的下方。当两个视图的 zIndex 相同时,它们将按照它们在布局文件中的顺序进行绘制。

通过调整视图的 zIndex 属性,您可以控制视图的叠加顺序,从而达到覆盖或隐藏其他视图的效果。

DiscoverTabPageWidget (pagerState, modifier = Modifier  
    .weight (1f)  
    .zIndex (-1f))

LaunchedEffect

LaunchedEffect 是 Jetpack Compose 中的一个函数,用于在协程中执行副作用操作。副作用操作通常包括异步任务、网络请求、数据库操作或其他可能会阻塞主线程的操作。

LaunchedEffect 函数是一个协程构建器,它接受一个或多个参数,并在代码块中执行异步操作。它会自动在适当的时间启动和取消协程,确保在 Compose 组件的生命周期内正确处理副作用。当组件被创建时,LaunchedEffect 会启动协程,当组件被销毁时,它会自动取消协程。

LaunchedEffect 的参数可以是任何对象,用于标识不同的副作用操作。通常使用简单的数据类对象作为参数,例如 key1 = Unit

LaunchedEffect 的代码块中,你可以执行各种需要在后台进行的操作,例如网络请求、数据库访问、文件读写等。由于这些操作是在协程中执行的,因此它们不会阻塞主线程,确保应用保持响应性。

需要注意的是,LaunchedEffect 函数只能在 Compose 函数内部调用,例如在 @Composable 注解的函数内部使用。如果你尝试在非 Compose 函数中调用它,将会出现编译错误。

总结起来,LaunchedEffect 是一个用于在协程中执行副作用操作的函数,它确保在 Compose 组件的生命周期内正确处理副作用。它是 Jetpack Compose 中处理异步任务和副作用的重要工具之一

itemData?. run {  
    LaunchedEffect (key1 = Unit) {  
        viewModel.getRelateVideoList (itemData. id)  
        viewModel.saveVideo (itemData)  
    }  
}

DisposableEffect

用于在组件创建和销毁时执行一些副作用操作。当组件被销毁时,onDispose 代码块内的操作会被执行

val coroutineScope = rememberCoroutineScope ()

DisposableEffect (Unit) {  
    val timer = Timer ()  
    timer.schedule (object : TimerTask () {  
        override fun run () {  
            coroutineScope. launch {  
                pagerState.animateScrollToPage (pagerState. currentPage + 1)  
            }  
        }  
    }, 3000, 3000)  
    onDispose {  
        timer.cancel ()  
    }  
}

pading3

这段代码是基于 Jetpack Compose 的 Paging 3 库编写的,用于创建一个可流式访问的分页数据流。

Pager 是 Paging 3 库中的一个类,用于管理分页数据。它接受一个 PagingConfig 对象和一个 pagingSourceFactory 函数作为参数。

PagingConfig 对象用于配置分页的行为和属性,其中包括:

pageSize:每一页的数据大小。
initialLoadSize:初始加载的数据大小。
prefetchDistance:在达到列表末尾之前开始预取下一页的距离。
pagingSourceFactory 函数用于创建一个实现 PagingSource 接口的数据源。这里的 DailyPagingSource 是自定义的数据源,它接受 bannerList 和 refreshing 参数,并根据这些参数来获取分页数据。

通过 flow 属性,我们可以将 Pager 对象转换为一个流,以便进行流式访问。然后使用 cachedIn () 函数,将流缓存在 viewModelScope 中,以便在组件重新合成时保留数据状态。

综上所述,这段代码的作用是创建一个可流式访问的分页数据流,并将其缓存在 viewModelScope 中,以便在组件生命周期内保留数据状态。

val pageFlow = Pager (config = PagingConfig (pageSize = 10,  
    initialLoadSize = 10,  
    prefetchDistance = 1), pagingSourceFactory = {  
    DailyPagingSource (bannerList, refreshing)  
}). flow.cachedIn (viewModelScope)
返回顶部
顶部