(一)Jetpack Compose 从入门到会写

基本概念

Compose 名称由来

众所周知,继承在功能拓展上表现的很脆弱,容易类、函数爆炸,通过代理和包装进行组合会更健壮。
Compose 意为组合,使用上也是把 Compose 函数以 模拟函数调用层级关系的方式 组合到一起,最终映射为 ViewTree。

声明示UI与命名式UI

命名式UI:

需要手动更新。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:text="Hello"android:layout_width="wrap_content"android:layout_height="wrap_content"/><ImageViewandroid:src="..."android:layout_width="wrap_content"android:layout_height="wrap_content"/></LinearLayout>

声明式UI:

只需要把界面设置出来,不需要手动更新。(随着数据自动更新)

Column() {Text(text = "Hello")Image(bitmap = ...)
}
声明式UI如何实现自动订阅
  var text by mutableStateOf("hello")//与ui绑定Text(text = text)//赋值,ui会收到订阅LaunchedEffect(Unit) {text = "good"}
Compose和databind的区别

databinding只能更新界面元素中的值,而compose可以更新任何内容,包括结构。

    //文案是否可以被显示 var ifShow by mutableStateOf(false)   var text by mutableStateOf("hello")if (ifShow) {Text(text = text)}

比如这里if(showImage) ,如果为false,则Text从视图结构里被移除,跟原本的setVisible是有区别的。

关于层级嵌套

Android布局嵌套层级太深,会导致性能损耗,因为重复测量。

(比如父view是Linelayout,宽度为wrap_content,会取子view的最大宽度为宽度。如果有一个子View为match_parent,就会先以0为强制宽度测量一次这个子view,再测量其他子view的最大宽度,再以最大宽度测量这个match_parent的子view得出最终尺寸。在有多重嵌套的场景下,测量次数会指数级攀升,测量次数 = O^2n,O为层级数,因此每增加一层,布局时间翻一倍)

Compose规避了这个问题,因为在Compose中,子项只能测量一次,测量两次就会引发运行时异常。

Intrinsic Measurement 固有尺寸测量。

Compose允许父组件在测量前,先粗略测量子组件的最大最小大小尺寸,再统一进行测量。测量次数 = O^n。

因此在Compose中,N级嵌套布局,和同一层级嵌套布局,性能是一样的。

实现简单APP主页

实现一个列表

实现一个竖向列表,item为头像+昵称

首先需要定义好数据源:

//数据源
data class User(val name: String, val job: String)
object UserData {val messages = listOf(User("小王", "厨师"),User("大明", "司机"),User("小李 ", "外卖员"),)
}

然后实现每一列的布局ui:


//ui item
@Composable
private fun UserItem(user: User) {//Row 是以行排布的元素组件,列是ColumnRow(modifier = Modifier.padding(all = 8.dp),verticalAlignment = Alignment.CenterVertically) {//头像Image(painter = painterResource(id = R.mipmap.mine),contentDescription = null,modifier = Modifier.size(30.dp).clip(CircleShape))Spacer(Modifier.padding(horizontal = 8.dp))Column() {//名字Text(text = user.name)//间隔Spacer(Modifier.padding(vertical = 8.dp))//职业Text(text = user.job,)}}
}

最后,需要将布局Item组成一个Conversation列表,加入到Activity的布局中:

//Activity
class ComposeListActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {Conversation(userList = messages)}}
}//实现一个列表
@Composable
fun Conversation(userList: List<User>) {LazyColumn {items(userList) { it ->UserItem(user = it)}}
}

实现底部导航栏

首先新建一个Activity,在setContent中声明底部导航栏(结构为一个Row+三个Icon)

class ComposeHomeActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {NavBar()}}@Composablefun NavBar() {Row {Icon(painter = painterResource(id = R.mipmap.home), contentDescription = null)Icon(painter = painterResource(id = R.mipmap.project), contentDescription = null)Icon(painter = painterResource(id = R.mipmap.mine), contentDescription = null)}}
}

此时运行的效果是这样的:

略显丑陋,接下来给它们补充一些间距,布局,以及颜色:

@Composable
fun CustomApp() {Row(Modifier.height(84.dp).padding(16.dp),verticalAlignment = Alignment.CenterVertically) {NavItem(iconRes = R.mipmap.home, desc = "主页", Color.Blue)NavItem(iconRes = R.mipmap.project, desc = "项目", Color.Gray)NavItem(iconRes = R.mipmap.mine, desc = "我的", Color.Gray)}
}// 避免给每个Icon重复设置,将Icon抽成独立的自定义参数组件
@Composable
fun RowScope.NavItem(@DrawableRes iconRes: Int, desc: String, tint: Color) {Icon(painter = painterResource(id = iconRes), contentDescription = desc,Modifier.size(24.dp).weight(1f),  //宽度比重,必须要在RowScope下才能设置。tint = tint)
}

运行:

可以看到效果好了很多。那么如何实现不同页面之间的切换呢?

首先定义好三个不同的页面:

@Composablefun ProjectScreen(modifier: Modifier = Modifier) {Box(modifier.fillMaxSize()) {Text(text = "Favorites Screen", modifier = Modifier.align(Alignment.Center))}}@Composablefun UserScreen(modifier: Modifier = Modifier) {Box(modifier.fillMaxSize()) {Text(text = "Settings Screen", modifier = Modifier.align(Alignment.Center))}}@Composablefun HomeScreen(userList: List<User>) {//这里是前面实现的列表页面LazyColumn {items(userList) { it ->UserItem(user = it)}}}

然后,在底部栏按钮外部,增加Scaffold组件

Scaffold:一个组合元素,可以轻松地在同一个位置添加AppBar、BottomAppBar等。

  • 增加页面数据类Screen,用来标识三个页面。
  • 增加currentScreen变量,维护当前页面状态。
  • 给每一个NavItem设置点击监听事件,在每一个item点击时设置currentScreen的变化。
  • Scaffold收到currentScreen变化通知,切换不同的页面。
  sealed class Screen {object Home : Screen()object Project : Screen()object User : Screen()}@Composablefun CustomApp() {//维护当前页面状态var currentScreen by remember { mutableStateOf<Screen>(Screen.Home) }Scaffold(bottomBar = {Row(Modifier.height(84.dp).padding(16.dp),verticalAlignment = Alignment.CenterVertically) {NavItem(iconRes = R.mipmap.home, desc = "主页", Color.Blue) {currentScreen = Screen.Home}NavItem(iconRes = R.mipmap.project, desc = "项目", Color.Gray) {currentScreen = Screen.Project}NavItem(iconRes = R.mipmap.mine, desc = "我的", Color.Gray) {currentScreen = Screen.User}}}) { innerPadding ->when (currentScreen) {Screen.Home -> HomeScreen(UserData.messages)Screen.Project -> ProjectScreen(Modifier.padding(innerPadding))Screen.User -> UserScreen(Modifier.padding(innerPadding))}}}// 避免给每个Icon重复设置,将Icon抽成独立的自定义参数组件@Composablefun RowScope.NavItem(@DrawableRes iconRes: Int,desc: String,tint: Color,click: () -> Unit,) {Icon(painter = painterResource(id = iconRes), contentDescription = desc,Modifier.size(24.dp).weight(1f).clickable {click.invoke()},  //宽度比重,必须要在RowScope下才能设置。tint = tint,)}

实现效果:

底部栏BottomNavigation

比起自己实现导航栏+左右滑动页面,最好采用系统api来实现,效果和稳定性更优。这里可以先引入Navigation的库:

//Navigation
implementation "androidx.navigation:navigation-compose:2.8.0-alpha06"

结合Jetpack Compose的一些常用元素:

BottomNavigation:用于创建底部导航栏。

BottomNavigationItem:底部导航栏的一个单独项。

@Composablefun MyApp() {var currentScreen by remember { mutableStateOf<Screen>(Screen.Home) }Scaffold(bottomBar = {BottomNavigation {BottomNavigationItem(icon = { Icon(Icons.Filled.Home, contentDescription = "Home") },label = { Text("Home") },selected = currentScreen == Screen.Home,onClick = { currentScreen = Screen.Home })BottomNavigationItem(icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorites") },label = { Text("Favorites") },selected = currentScreen == Screen.Project,onClick = { currentScreen = Screen.Project })BottomNavigationItem(icon = { Icon(Icons.Filled.Settings, contentDescription = "Settings") },label = { Text("Settings") },selected = currentScreen == Screen.User,onClick = { currentScreen = Screen.User })}}) { innerPadding ->when (currentScreen) {Screen.Home -> HomeScreen(Modifier.padding(innerPadding))Screen.Project -> ProjectScreen(Modifier.padding(innerPadding))Screen.User -> UserScreen(Modifier.padding(innerPadding))}}}

实现效果:

Android原生自定义View在Compose中使用

在第二个页面ProjectScreen中,尝试加入之前写好的自定义View:

@Composablefun ProjectScreen(modifier: Modifier = Modifier) {AndroidView(modifier = Modifier.size(150.dp),  factory = { context ->// Creates viewCircularAnimProgressView(context)},update = { view ->// 视图已膨胀或此块中读取的状态已更新// 如有必要,在此处添加逻辑// 由于selectedItem在此处阅读,AndroidView将重新组合// 每当状态发生变化时// 撰写示例->查看通信})}

可以通过AndroidView组件实现,在factory中返回通过原生方式实现的自定义View即可实现。

并且在状态更新时,会回调update事件,可以在这里对View进行一些刷新操作。

运行效果:

与MVI结合使用

带来的优化

举个栗子:

用MVI实现一个计数器,通常需要定义一个状态类来存储计数器的状态:

状态类CounterState

事件类CounterEvent:来表示计数器的事件。

数据类CounterViewModel:用于处理计数器应用程序的业务逻辑。

//状态
data class CounterState(val count: Int)//事件
sealed class CounterEvent {object IncrementCounter : CounterEvent()
}//ViewModel
class CounterViewModel : ViewModel() {private val _state = MutableLiveData(CounterState(0))val state: LiveData<CounterState> = _statefun handleEvent(event: CounterEvent) {when (event) {is CounterEvent.IncrementCounter -> incrementCounter()}}private fun incrementCounter() {val currentCount = _state.value?.count ?: 0_state.value = CounterState(currentCount + 1)}
}// UI databind部分
//1.在xml中定义 btn 和 text...
val button = findViewById(R.id.btn)
val text = findViewById(R.id.text)
// 2.显示绑定viewmodel.state.observe(this) {count->btn.text = "Click Count: $count"}//3.点击监听button.setOnclickListener {viewmodel.handleEvent(...)}

UI部分如果使用Compose实现,代码会比较简洁,因为在UI初始化的时候就建立了绑定。

@Composable
fun Counter() {    Button(onClick = { viewmodel.handleEvent(...) }) //点击 Text(text = "Click Count: " + viewmodel.state.value.count) //显示
}

这样做的优点是,保证了框架的唯一性

由于每个view是在一开始的时候就被数据源赋值的,无法被多处调用随意修改,所以保证了框架不会被随意打乱。更好的保证了代码的低耦合等特点。

MVI结合Compose

举个栗子,用MVI+Compose实现一个登录功能,整体结构如下:


class ComposeMviActivity : ComponentActivity() {private lateinit var userViewModel: UserViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)userViewModel =ViewModelProvider(this, UserViewModelFactory(application)).get(UserViewModel::class.java)setContent {val userState = userViewModel.state.valueval username = remember { mutableStateOf(TextFieldValue("")) }val password = remember { mutableStateOf(TextFieldValue("")) }Column(modifier = Modifier.padding(16.dp)) {Text(text = "Welcome")TextField(value = username.value,onValueChange = { username.value = it })TextField(value = password.value,onValueChange = { password.value = it },visualTransformation = if (password.value.text.isNotEmpty()) PasswordVisualTransformation() else VisualTransformation.None,keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),)Button(onClick = {userViewModel.onIntent(UserIntent.Login(username.value.text,password.value.text))},modifier = Modifier.padding(top = 8.dp)) {Text(text = "Login")}Button(onClick = { userViewModel.onIntent(UserIntent.Logout) },modifier = Modifier.padding(top = 8.dp)) {Text(text = "Logout")}if (userState.success) {Text(text = "登录成功")} else {Text(text = "未登录:" + userState.error)}}}}}class UserViewModelFactory(private val application: Application) :ViewModelProvider.AndroidViewModelFactory(application) {override fun <T : ViewModel> create(modelClass: Class<T>): T {//val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)val userModel = UserModel()return UserViewModel(userModel) as T}
}
sealed class UserIntent {data class Login(val username: String, val password: String) : UserIntent()object Logout: UserIntent()
}data class UserViewState(val success: Boolean = false, val error: String = "")
class UserModel() {fun login(username: String, password: String): Boolean {return "123" == username && "456" == password}fun logout() {//清空本地账号密码}
}
class UserViewModel(private val userModel: UserModel): ViewModel() {private val _state = mutableStateOf(UserViewState())val state: State<UserViewState> = _statefun onIntent(intent: UserIntent) {when(intent) {is UserIntent.Login -> {val isSuccessful = userModel.login(intent.username, intent.password)if (isSuccessful) {_state.value = UserViewState(success = true)} else {_state.value = UserViewState(error = "Invalid username or password")}}is UserIntent.Logout -> {userModel.logout()_state.value = UserViewState(success = true)}}}
}

总结

首先掌握Compose的重要性不言而喻,SwiftUI也好,Flutter也好,声明式UI一统移动端是大势所趋。

写之前笔者对Compose的了解基本为0,作为Android开发,之前也只是写过一点flutter的demo,但是从开始学习到完成本文一共花了大约不到1天,由此可见Compose的入手门槛其实并不高,只要花时间看一看写一写,是比较容易快速上手的。

但是深入理解依然是需要花功夫和心血的,整理了一些后续的学习方向:

● Compose实现自定义View
● Compose动画
● Compose绘制原理(https://kstack.corp.kuaishou.com/article/6353)
● Compose事件分发
● Compose滑动嵌套
● Compose Widget Lifecycle
● Compose UI 交互刷新流程

会在以后的工作和学习过程中,持续补充,希望感兴趣的同学可以一起加入进来,共同进步。

最后~感谢读到这里,再会!


参考资料:

Jetpack Compose  |  Android Developers——Compose官方教程

写在开头 | 你好 Compose——jetpack compose中文教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/310098.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue.js------vue基础

1. 能够了解更新监测, key作用, 虚拟DOM, diff算法2. 能够掌握设置动态样式3. 能够掌握过滤器, 计算属性, 侦听器4. 能够完成品牌管理案例 一.Vue基础_更新监测和key 1.v-for更新监测 目标&#xff1a;目标结构变化, 触发v-for的更新 情况1: 数组翻转情况2: 数组截取情况3…

记录--病理切片图像处理

简介 数字病理切片&#xff0c;也称为全幻灯片成像&#xff08;Whole Slide Imaging&#xff0c;WSI&#xff09;或数字切片扫描&#xff0c;是将传统的玻片病理切片通过高分辨率扫描仪转换为数字图像的技术。这种技术对病理学领域具有革命性的意义&#xff0c;因为它允许病理…

C语言文件操作详解

1. 什么是文件&#xff1a; 文件是计算机中存储数据的一种方式&#xff0c;它可以包含文本、图像、音频、视频等各种形式的信息。在计算机系统中&#xff0c;文件被组织成一个个独立的单元&#xff0c;可以通过文件名来标识和访问。文件可以存储在计算机的硬盘、固态硬盘、光盘…

SWM341系列应用(RTC、FreeRTOS\RTTHREAD应用和Chip ID)

SWM341系列RTC应用 22.1、RTC的时钟基准 --liuzc 2023-8-17 现象:客户休眠发现RTC走的不准&#xff0c;睡眠2小时才走了5分钟。 分析与解决&#xff1a;经过排查RTC的时钟源是XTAL_32K&#xff0c;由于睡眠时时设置XTAL->CR0&#xff1b;&#xff0c;会把XTAL_32K给关…

【Canvas与艺术】绘制磨砂黄铜材质Premium Quality徽章

【关键点】 渐变色的使用、斜纹的实现、底图的寻觅 【成果图】 ​​​​​​​ 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><tit…

C++ PTA 天梯赛 L1-003 个位数统计 L1-005 考试座位号 【范围for循环】【. 与 -> 访问成员】

L1-003 个位数统计 最后一个测试点考察的是当N特别大时&#xff0c;如果用整数存会数据溢出&#xff0c;改成字符串可以增大范围 知识点&#xff1a; 1.范围 for 循环&#xff0c;它对于遍历容器&#xff08;比如字符串&#xff09;中的元素非常方便。在这里&#xff0c;N 是…

TinyEMU源码分析之中断处理

TinyEMU源码分析之中断处理 1 触发中断2 查询中断2.1 查询中断使能与pending状态&#xff08;mie和mip&#xff09;2.2 查询中断总开关与委托&#xff08;mstatus和mideleg&#xff09;2.2.1 M模式2.2.2 S模式2.2.3 U模式 3 处理中断3.1 获取中断编号3.2 检查委托3.3 进入中断3…

⑤-1 学习PID--什么是PID

​ PID 算法可以用于温度控制、水位控制、飞行姿态控制等领域。后面我们通过PID 控制电机进行说明。 自动控制系统 在直流有刷电机的基础驱动中&#xff0c;如果电机负载不变&#xff0c;我们只要设置固定的占空比&#xff08;电压&#xff09;&#xff0c;电机的速度就会稳定在…

HTML5 新增语义标签及属性

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 ✍HTML5 新增语义标签及属性&#x1f48e;1 HTML5 新增的块级语义化标签&…

使用ADO.NET访问数据库

目录 访问数据库的步骤 &#xff11;、建立数据库 &#xff12;、设置链接参数 &#xff08;1&#xff09;web网页和数据库连接的方法一 &#xff08;2&#xff09;web网页和数据库连接的方法二 &#xff13;、建立链接对象 &#xff14;、显示数据库 &#xff15;、数…

openwrt局域网配置多个IP

在局域网配置过程中&#xff0c;若是DHCP服务器关闭&#xff0c;又忘记了配置的ip&#xff0c;将很难访问到路由器重新进行配置。这种情况可以在路由器出厂时做一个备用ip去避免。 1.配置 以下是备用ip的配置方法&#xff0c;以SKYLAB的SKW99 WIFI模组为例进行说明&#xff1…

Android使用shape属性绘制边框内渐变色

目录 先上效果图实现方法shape属性介绍代码结果 先上效果图 这是使用AndroidStudio绘制的带有渐变色的边框背景色 实现方法 项目中由于UI设计需求&#xff0c;需要给按钮、控件设置带有背景色效果的。以下是UI效果图。 这里我们使用shape属性来绘制背景效果。 shape属性介…

ffmpeg命令与批处理编程

(一) CMD脚本查找所有文件 powershell与cmd转换 powershell与cmd虽然同为windows命令&#xff0c;但许多命令并不通用。 CMD换行符 a 在CMD下&#xff0c;可以用^作为换行符&#xff0c;类似于Linux下的\。举例如下&#xff1a; start pemu.exe ^ -net nic,vlan1,macaddr…

最新版IntelliJ IDEA 2024.1安装和配置教程 详细图文解说版安装教程

IntelliJ IDEA 2024.1 最新版如何快速入门体验?IntelliJ IDEA 2024.1 安装和配置教程 图文解说版 文章目录 IntelliJ IDEA 2024.1 最新版如何快速入门体验?IntelliJ IDEA 2024.1 安装和配置教程 图文解说版前言 第一步&#xff1a; IntelliJ IDEA 2024.1安装教程第 0 步&…

如何通过Linux pciehp sysfs接口控制PCIe Slot电源状态?-2

NVME SSD电源状态判断 通过pciehp sysfs接口对NVMe SSD所在的PCIe插槽进行Power On/Off操作时&#xff0c;确实会间接影响到NVMe SSD本身的电源状态。因为NVMe SSD是作为PCIe设备连接到特定插槽上的&#xff0c;插槽电源状态的变化通常会直接影响到与其相连的设备。 当对PCIe…

Python学习笔记16 - 函数

函数的创建和调用 函数调用的参数传递 函数的返回值 函数的参数定义 变量的作用域 递归函数 斐波那契数列 总结

fiddler常用操作汇总

1、过滤 2、查看数据包内容 3、弱网测试 弱网测试其实就是提前设置好一个值&#xff0c;在这个环境下进行测试就行了。 &#xff08;1&#xff09;进入定制规则页面&#xff1a; (2) 点击CtrlF调起搜索&#xff0c;在Find what 中输入300进行查找&#xff0c;更改上行、下行网…

【Java】SpringBoot快速整合mongoDB

目录 1.什么是mongoDB&#xff1f; 2.Docker安装mongoDB 3.SpringBoot整合mongoDB步骤 4.验证 1.什么是mongoDB&#xff1f; MongoDB是一种非关系型数据库&#xff0c;被广泛用于大型数据存储和分布式系统的构建。MongoDB支持的数据模型比传统的关系型数据库更加灵活&#x…

UDTF函数 explode

场景&#xff1a; 原hive数据形式 split 处理到一个Array 形式 使用explode炸开后的效果是 explode结合侧面视图达到targeType 目标形式&#xff1a; 一进多出 explode 将hive 中复杂的 array 炸成多行 因为炸开后&#xff0c; movie 列值少于categoryname 列所以这里为了达到…

SpringBoot的旅游管理系统+论文+ppt+免费远程调试

项目介绍: 基于SpringBoot旅游网站 旅游管理系统 本旅游管理系统采用的数据库是Mysql&#xff0c;使用SpringBoot框架开发。在设计过程中&#xff0c;充分保证了系统代码的良好可读性、实用性、易扩展性、通用性、便于后期维护、操作方便以及页面简洁等特点。 &#xff08;1&…