Android Jetpack Compose 用计时器demo理解Compose UI 更新的关键-------状态管理(State)

目录

  • 概述
  • 1.什么是状态
  • 2.什么是单向数据流
  • 3.理解Stateless和Stateful
  • 4.使用Compose实现一个计数器
    • 4.1 实现计数器
    • 4.2 增加组件复用性-----状态上提
  • 总结

概述

我们都知道了Compose使用了声明式的开发范式,在这样的范式中,UI的职责更加的单一,只会对数据状态的变化作出反应,如果数据状态没有发生变化,则UI就永远不会自行的改变。假如我们把Composable的执行看成是一个函数的运算的话,那么状态就是函数的参数,输出就是生成的布局。由于唯一的参数决定唯一的输出,所以只有当函数的参数发生了变化,生成的布局才会相应的跟着变化。本文会通过一个计时器的小例子分别介绍如何能够更好的管理状态,让UI的可复用性更高,更容易维护。

1.什么是状态

如今的APP中的几乎所有的界面都是可以和用户进行交互的,一个不和用户交互的页面在现在的APP中基本见不到了,因为设计一个界面只为了展示一些特定的信息,那这个界面存在的意义是啥呢。所以动态可交互的页面是现在APP的主流。因为动态页面需要接收用户的操作(比如点击,长按,滑动等),然后通过UI的变化给用户作出反馈。比如弹一个对话框,给出一个Toast提示,跳转到新的页面等,所有的这些看得见的变化,其本质上都是内部数据的变化,而这些不断变化的数据就是UI的状态

我们都知道,在Android传统的视图体系中,状态大多数是以view的成员变量形式存在,例如TextView的mText就是当前TextView的状态,当我们想要更新TextView的文字时,需要首先获取到TextView的实例。然后调用TextView的setText方法更新TextView的文字。但是这样更新UI的问题也很明显,那就是当代码量增多的时候,这样的逻辑会变得特别复杂,很难维护,很难复用。这里,我们以一个简单的计数器例子体验下传统的视图体系中状态的更新管理存在的问题。
计时器的界面如下图所示:
在这里插入图片描述
如上图所示,计数器界面包含一个显示数字的TextView,和两个控制数字加,减的按钮,当点击“+” 或者是“-”按钮时,数字会随之增加或者减少。而变化的数字就是这个计数器的状态,这个状态保存在TextView的实例中,代码如下所示:

class CounterActivity : AppCompatActivity() {private lateinit var binding: ActivityCounterBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = DataBindingUtil.setContentView(this,R.layout.activity_counter)binding.btnAdd.setOnClickListener{binding.tvCount.text = "${Integer.valueOf(binding.tvCount.text.toString()) + 1}"}binding.btnSub.setOnClickListener {binding.tvCount.text = "${Integer.valueOf(binding.tvCount.text.toString()) - 1}"}}
}

如上面的代码所示,当点击按钮时,代码会直接修改TextView的text,更新U,而上面代码的问题也很明显,那就是计数器的逻辑与TextView耦合在一起,使得视图组件难以替换,计数逻辑也难以复用。而且随着事件源的增多,很容易出现重复代码。所以我们需要去优化上面的代码,如下所示:

class CounterActivity : AppCompatActivity() {private lateinit var binding: ActivityCounterBindingprivate var counter:Int = 0override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = DataBindingUtil.setContentView(this,R.layout.activity_counter)binding.btnAdd.setOnClickListener{counter++updateCounter(counter)}binding.btnSub.setOnClickListener {counter--updateCounter(counter)}}private fun updateCounter(counter: Int){binding.tvCount.text = "$counter"}
}

如上所示:我们新增了counter成员来计数,将计数器的状态从TextView的mText上提到Activity的counter,状态上提后,所有的修改都从TextView流向counter,所以计算逻辑对TextView的依赖就没有了,组件就更容易替换了,比如,我们想把TextView替换成我们自定义的TextView,就直接替换就可以了,基本不用做过多的修改。状态的上移使得TextView的职责变简单了,那么这个计数器完美了吗,答案是不完美,因为计数逻辑在Activity中难以复用,而且点击按钮后需要手动调用updateCounter(counter: Int)函数,Button的职责还是不够简单,所以需要继续了解单向数据流的概念。

2.什么是单向数据流

单向数据流顾明思义就是数据单向流动,比如规定数据只能从父组件流向子组件,子组件可以使用父组件的数据,但是不能直接修改它,如果想要修改数据,需通过事件来通知父组件修改。如下图所示:

在这里插入图片描述
我们使用单向数据流的架构改造下计数器,代码如下所示:

class CounterActivity : AppCompatActivity() {private lateinit var binding: ActivityCounterBindingprivate val viewModel by viewModels<CounterViewModel>()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = DataBindingUtil.setContentView(this, R.layout.activity_counter)binding.btnAdd.setOnClickListener {viewModel.increment()}binding.btnSub.setOnClickListener {viewModel.decrement()}viewModel.counterLiveData.observe(this) { counter ->binding.tvCount.text = "$counter"}}
}class CounterViewModel : ViewModel() {private var _counter = MutableLiveData(0)var counterLiveData: LiveData<Int> = _counterfun increment() {_counter.value = _counter.value!! + 1}fun decrement() {_counter.value = _counter.value!! - 1}
}

在上面的代码中,计数器被改造成了一个单向数据流的MVVM架构,状态从Activity提到了ViewModel,而LiveData将状态包装成了一个可观察的对象,Activity作为观察者监听counter的变化来更新UI。通过观察者模式降低了Button的职责,点击Button时,只需要调用increment()或者decrement()方法就行了,至于计数器增加多少,减少多少都不需要Button管。

其实我们都知道,Android的MVVM架构可以在Data BInding的加持下与View建立双向绑定,但是双向绑定会导致数据流向混乱,维护难度加大,因此在比较大的项目中会采用单向数据流的结构,因为单向数据流由于数据来源单一,数据变动可溯源,所以单向数据流架构下的逻辑会更加清晰

3.理解Stateless和Stateful

Compose在设计之初就已经贯彻了单向数据流的设计思想,由于Composable只是一个函数,不会像View那样轻易封装私有状态,这样状态随处定义的情况就得到了抑制;而且Compose的状态像LiveData一样能够被观察,当状态变化后,相关联的UI会自动刷新,不需要像传统的视图那样命令式的逐个通知。那么也许会有读者想问,既然Compose是一个函数,调用后也不会返回任何实例,那么Composable是如何实现UI刷新呢?

这里我们以一个Compose的项目中渲染Text文本的Composable方法来简单介绍Compose的UI刷新。我们使用Android Studio新建一个Compose项目,然后会默认生成一个示例函数。

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {Text(text = "Hello $name!",modifier = modifier)
}

如上面代码所示,当Greeting方法想要更新显示的文字时,只能是再次调用Greeting方法,并且传入新的name,内部的Text组件也会再次被调用显示最新的文字。这个通过Composable重新执行来更新界面的过程被称为“重组”。Compose通过重组实现UI的刷新,而重组正是由于Composable的状态变化所触发的。
从函数fun Greeting(name: String, modifier: Modifier = Modifier)中可以看出Greeting函数内部除了参数以外,没有依赖其他的状态,,像这种只依赖参数的Composable被称为Stateless Composable。而有的Composable内部持有或者访问了某些状态,这种称为Stateful Composable在这里我们需要记住的是,Stateless Composable的重组只能来自上层Composable的调用,而Stateful Composable 的重组来自其依赖的状态的变化

那Compose 如何实现Stateless Composable和Stateful Composable的呢?其实很简单,方法就是当Stateless的参数没有变化时就不会参与调用方的重组,重组的范围局限在Stateless外部。我们可以对Greetin函数反编译后,看下Compose 编译器具体做了些什么。如下图所示:
在这里插入图片描述

从上图可知,编译器在@Composable函数体内进行了插桩处理,在Text调用之前对参数进行判断,如果参数没有变化,则跳过对Text的调用。这就是为啥当参数不变时,Staless不参与重组的本质原因

4.使用Compose实现一个计数器

4.1 实现计数器

在前面我们使用了传统的View实现了一个计数器,并且对其进行了优化改进,使其拥有更好的复用性,并且我们也理解了Compose的stateless和stateful的区别,本节咱们就使用Compose UI实现一个计数器,并像之前对传统View进行优化的方式来慢慢的优化我们的Compose计数器组件。

运行截图如下所示:
在这里插入图片描述
首先我们使用的是Stateful Composable来实现计数器,代码如下:

class ComposeCounterAct : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {MyComposeTheme {// A surface container using the 'background' color from the themeSurface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colorScheme.background) {CounterComponent()}}}}@Composablefun CounterComponent() {Column(modifier = Modifier.padding(16.dp)) {var counter by remember { mutableStateOf(0) }Text("$counter",Modifier.fillMaxWidth(),textAlign = TextAlign.Center)Row {Button(onClick = { counter-- },modifier = Modifier.weight(1f)) {Text("-")}Spacer(Modifier.width(16.dp))Button(onClick = { counter++ },modifier = Modifier.weight(1f)) {Text("+")}}}}
}

在上面的代码中,我们在内部创建了状态counter,用于记录最新的计数值,代码先是读取counter的值并在Text中显示,然后再Button的onClick中对counter进行了修改,CounterComponent函数中依赖对counter的读写,所以上面的代码是一个Stateful Composable

在上面的代码中我们还可以看到状态的创建时使用了这样一行代码:
var counter by remember { mutableStateOf(0) }
这是创建了一个状态,在Compose中使用State描述一个状态,泛型T是状态的具体类型

interface State<out T> {val value: T
}

State是一个可观察的对象,当Composable对State的value值进行读取的同时会与State建立订阅关系,当value发生变化时,作为监听者的Composable会自动刷新UI。所以在上面的代码中,当counter状态发生变化时,CounterComponent函数便会发生重组。

而有时候Composable需要对State的value进行修改,就比如咱们的计数器例子,点击按钮可以修改counter的值,使用使用的是MutableState ,MutableState 表示状态是可修改的,其包裹的数据是一个可修改的var类型。

创建MutableState有三种方式,第一种是:
val counter:MutableState<Int> = mutableStateOf(0)

第二种方式:
val(counter,setCounter) = mutableStateOf(0)
这里的counter是一个Int 类型的数据,后续使用的时候可以直接访问,无需使用点操作符获取value,而需要更新值的地方需要使用setCounter(xx)完成

第三种方式,也是我们常用的方式:
var counter by mutableStateOf(0)
这种方式通过对counter的读写会通过getValue和setValue这两个运算符的重写最终代理为对value的操作,通过by关键字,可以像访问一个普通的Int变量一样对状态进行读写。

注意:当使用by代理创建State时需要额外引入扩展方法:import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue若是IDE无法自动导入上面的依赖,手动添加就可以

4.2 增加组件复用性-----状态上提

我们之前已经知道了状态上提的概念,在传统的视图体系种通过将状态从View上提到Activity或者ViewModel可以促进视图与逻辑的解耦,Compose也可以通过状态提升来优化代码,由于Stateless不耦合任何业务逻辑,所以功能更加纯粹,对于stateful的可复用性更好。状态上提通常的做法就是将内部状态移除,通过参数传入需要显示的状态,以及需要给调用方的事件,代码如下:

class ComposeCounterAct : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {MyComposeTheme {// A surface container using the 'background' color from the themeSurface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colorScheme.background) {CounterDemo()}}}}@Composablefun CounterDemo() {var counter by remember { mutableStateOf(0) }CounterComponent(counter = counter, onIncrement = { counter++ }) {Log.d("zhongxj", "counter: $counter")if (counter > 0) {counter--}}}@Composablefun CounterComponent(counter: Int, // 重组时传入当前需要显示的计数onIncrement: () -> Unit,// 回调点击加号的事件onDecrement: () -> Unit // 回调单击减号的事件) {Column(modifier = Modifier.padding(16.dp)) {Text("$counter",Modifier.fillMaxWidth(),textAlign = TextAlign.Center)Row {Button(onClick = { onDecrement() },modifier = Modifier.weight(1f)) {Text("-")}Spacer(Modifier.width(16.dp))Button(onClick = { onIncrement() },modifier = Modifier.weight(1f)) {Text("+")}}}}
}

在上面的代码中,CounterDemo在调用CounterComponent时为其注入counter以及onincrement()与onDecrement()的回调实现,CounterComponent不再耦合具体业务,完全面向调用方传入的参数变成,这与面向对象编程中的依赖倒置差不多。
CounterComponent经过状态上提后,职责更加单一,可复用性与可测试性都得到了提高,而且,状态上提有助于单一数据源模型的打造。

总结

本文到此先告一段落,因为Compose的状态管理还有很多内容,后面再慢慢道来,本文只是简单的通过一个计数器的例子介绍状态的管理在传统View和Compose UI的实现,以及如何优化这些状态管理,使我们的代码更具复用性,我们写代码很多时候都希望,write once,run everywhere。学习Compose的State非常有必要,后面的内容敬请期待。

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

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

相关文章

es5的实例__proto__(原型链) prototype(原型对象) {constructor:构造函数}

现在看这张图开始变得云里雾里&#xff0c;所以简单回顾一下 prototype 的基本内容&#xff0c;能够基本读懂这张图的脉络。 先介绍一个基本概念&#xff1a; function Person() {}Person.prototype.name KK;let person1 new Person();在上面的例子中&#xff0c; Person …

腾讯混元助手使用指南

一、腾讯混元助手简介 腾讯混元助手是什么&#xff1f; 腾讯混元助手是由腾讯研发的大语言模型的平台产品&#xff0c;具备跨领域知识和自然语言理解能力&#xff0c;实现基于人机自然语言对话的方式&#xff0c;理解用户指令并执行任务&#xff0c;帮助用户实现人获取信息&am…

SpringBoot整合Websocket(Java websocket怎么使用)

目录 1 Websocket是什么2 Websocket可以做什么3 Springboot整合Websocket3.1 服务端3.2 客户端 1 Websocket是什么 WebSocket 是一种基于 TCP 协议的全双工通信协议&#xff0c;可以在浏览器和服务器之间建立实时、双向的数据通信。可以用于在线聊天、在线游戏、实时数据展示等…

算法通关村第十七关:青铜挑战-贪心其实很简单

青铜挑战-贪心其实很简单 1. 难以解释的贪心算法 贪心学习法则&#xff1a;直接做题&#xff0c;不考虑贪不贪心 贪心(贪婪)算法 是指在问题尽心求解时&#xff0c;在每一步选择中都采取最好或者最优&#xff08;最有利&#xff09;的选择&#xff0c;从而希望能够导致结果最…

【爬虫笔记】Python爬虫简单运用爬取代理IP

一、前言 近些年来&#xff0c;网络上的爬虫越来越多&#xff0c;很多网站都针对爬虫进行了限制&#xff0c;封禁了一些不规则的请求。为了实现正常的网络爬虫任务&#xff0c;爬虫常用代理IP来隐藏自己的真实IP&#xff0c;避免被服务器封禁。本文将介绍如何使用Python爬虫来…

百度智能云千帆大模型丨未来人手必备的代码助手

文章目录 1. 前言2. 千帆大模型平台3. 十分友好的功能4. comate代码助手5. 总结 1. 前言 我之前给大家推荐过Poe这个网站&#xff0c;它用的人比较少&#xff0c;但一旦接触后会发现它其实挺强大的。 因为它是一个可以同时支持好几个大模型的在线聚合平台。常用的GPT4&#x…

基于阻塞队列的生产消费模型

目录 一、线程同步 1.生产消费模型&#xff08;或生产者消费者模型&#xff09; 2.认识同步 &#xff08;1&#xff09;生产消费模型中的同步 &#xff08;2&#xff09;生产者消费者模型的特点 二、条件变量 1.认识条件变量 2.条件变量的使用 3.代码改造 三、基于阻…

uniapp移动端h5设计稿还原

思路 动态设置html的font-size大小 实现步骤 先创建一个public.css文件&#xff0c;设置初始的font-size大小 /* 注意这样写 只能使用css文件, scss 是不支持的, setProperty 只适用于原生css上 */ html {--gobal-font-size: 0.45px; } .gobal-font-size {font-size: var(--g…

leetcode 655. 输出二叉树(java)

输出二叉树 题目描述代码演示 题目描述 难度 - 中等 leetcode 655. 输出二叉树 给你一棵二叉树的根节点 root &#xff0c;请你构造一个下标从 0 开始、大小为 m x n 的字符串矩阵 res &#xff0c;用以表示树的 格式化布局 。构造此格式化布局矩阵需要遵循以下规则&#xff1a…

Python接口自动化封装导出excel方法和读写excel数据

一、首先需要思考&#xff0c;我们在页面导出excel&#xff0c;用python导出如何写入文件的 封装前需要确认python导出excel接口返回的是一个什么样的数据类型 如下&#xff1a;我们先看下不对返回结果做处理&#xff0c;直接接收数据类型是一个对象&#xff0c;无法获取返回值…

IOC和注解

想要学好spring&#xff0c;必须时时刻刻想着&#xff0c;spring的本质就是一个容器&#xff0c;放java对象的容器&#xff0c;java对象在spring容器中也叫做bean对象。 文章目录 一、spring介绍1、什么是框架2、框架的作用![在这里插入图片描述](https://img-blog.csdnimg.cn…

这几招真管用!找回丢失的iPhone的好方法!

你昂贵的iPhone不见了。它丢了吗?它被偷了吗?如果你把iPhone弄丢了,你可以从各种其他来源找到它,包括iPad、Mac、iCloud和Apple Watch。 你可以使用iCloud网站上的苹果“查找我的”应用程序、你的任何其他苹果设备或你家人注册的设备来追踪它。或者从“查找我的”应用程序…

Java基础知识点汇总

一、Java基础知识点整体框架 详细知识点见链接资源&#xff0c;注&#xff1a;框架是用Xmind App完成&#xff0c;查看需下载。 二、基础知识各部分概况 2.1 认识Java 2.2 数据类型和变量 2.3 运算符 2.4 程序逻辑控制 2.5 方法的使用 2.6 数组的定义和使用 2.7 类和对象 2.8 …

移植STM32官方加密库STM32Cryptographic

感谢这位博主&#xff0c;文章具有很高的参考价值&#xff1a; STM32F1做RSA&#xff0c;AES数据加解密&#xff0c;MD5信息摘要处理_我以为我爱了的博客-CSDN博客 概述 ST官方在很多年前就推出了自己的加密库&#xff0c;配合ST芯片用起来非常方便&#xff0c;支持ST的所有…

借助CIFAR10模型结构理解卷积神经网络及Sequential的使用

CIFAR10模型搭建 CIFAR10模型结构 0. input : 332x32&#xff0c;3通道32x32的图片 --> 特征图(Feature maps) : 3232x32即经过32个35x5的卷积层&#xff0c;输出尺寸没有变化&#xff08;有x个特征图即有x个卷积核。卷积核的通道数与输入的通道数相等&#xff0c;即35x5&am…

SpringCloud(十)——ElasticSearch简单了解(一)初识ElasticSearch和RestClient

文章目录 1. 初始ElasticSearch1.1 ElasticSearch介绍1.2 安装并运行ElasticSearch1.3 运行kibana1.4 安装IK分词器 2. 操作索引库和文档2.1 mapping属性2.2 创建索引库2.3 对索引库的查、删、改2.4 操作文档 3. RestClient3.1 初始化RestClient3.2 操作索引库3.3 操作文档 1. …

网络技术二十二:NATPPP

NAT 转换流程 产生背景 定义 分类 常用命令 PPP PPP会话建立过程 认证 PPP会话流程

第 3 章 栈和队列(循环队列的顺序存储结构实现)

1. 背景说明 和顺序栈相类似&#xff0c;在队列的顺序存储结构中&#xff0c;除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外&#xff0c; 尚需附设两个指针 front 和 rear 分别指示队列头元素及队列尾元素的位置。约定&#xff1a;初始化建空队列时&#x…

qt nodeeditor编译安装

目录 1. 下载源码 2. Qt creator编译源码 2.1 编译debug模式 &#xff08;MinGW&#xff09; 2.2 编译release模式 &#xff08;MinGW&#xff09; 1. 下载源码 https://github.com/paceholder/nodeeditor/archive/refs/tags/3.0.10.zip 2. Qt creator编译源码 解压文件…

Java 数据库改了一个字段, 前端传值后端接收为null问题解决

前端传值后端为null的原因可能有很多种&#xff0c;我遇到一个问题是&#xff0c;数据库修改了一个字段&#xff0c;前端传值了&#xff0c;但是后台一直接收为null值&#xff0c; 原因排查&#xff1a; 1、字段没有匹配上&#xff0c;数据库字段和前端字段传值不一致 2、大…