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

这篇具有很好参考价值的文章主要介绍了Android Jetpack Compose 用计数器demo理解Compose UI 更新的关键-------状态管理(State)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

概述

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

1.什么是状态

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

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

class CounterActivity : AppCompatActivity() {
    private lateinit var binding: ActivityCounterBinding
    override 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: ActivityCounterBinding
    private var counter:Int = 0
    override 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.什么是单向数据流

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

Android Jetpack Compose 用计数器demo理解Compose UI 更新的关键-------状态管理(State),Android Jetpack Compose,Kotlin--将Android进行到底,android jetpack,ui,compose ui,compose state,状态管理
我们使用单向数据流的架构改造下计数器,代码如下所示:

class CounterActivity : AppCompatActivity() {
    private lateinit var binding: ActivityCounterBinding
    private 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> = _counter

    fun 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 编译器具体做了些什么。如下图所示:
Android Jetpack Compose 用计数器demo理解Compose UI 更新的关键-------状态管理(State),Android Jetpack Compose,Kotlin--将Android进行到底,android jetpack,ui,compose ui,compose state,状态管理

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

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

4.1 实现计数器

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

运行截图如下所示:
Android Jetpack Compose 用计数器demo理解Compose UI 更新的关键-------状态管理(State),Android Jetpack Compose,Kotlin--将Android进行到底,android jetpack,ui,compose ui,compose state,状态管理
首先我们使用的是Stateful Composable来实现计数器,代码如下:

class ComposeCounterAct : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    CounterComponent()

                }
            }
        }
    }

    @Composable
    fun 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 theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    CounterDemo()
                }
            }
        }
    }

    @Composable
    fun CounterDemo() {
        var counter by remember { mutableStateOf(0) }
        CounterComponent(counter = counter, onIncrement = { counter++ }) {
            Log.d("zhongxj", "counter: $counter")
            if (counter > 0) {counter--}
        }
    }

    @Composable
    fun 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非常有必要,后面的内容敬请期待。文章来源地址https://www.toymoban.com/news/detail-721353.html

到了这里,关于Android Jetpack Compose 用计数器demo理解Compose UI 更新的关键-------状态管理(State)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • go限流、计数器固定窗口算法/计数器滑动窗口算法

    问题1:后端接口只能支撑每10秒1w个请求,要怎么来保护它呢? 问题2:发短信的接口,不超过100次/时,1000次/24小时,要怎么实现? 所谓固定窗口,就是只设置了一个时间段,给这个时间段加上一个计数器。 常见的就是统计每秒钟的请求量。 这里就是一个QPS计数器。 在这一

    2024年04月26日
    浏览(39)
  • FPGA拾忆_(3):调用IP 计数器&BCD计数器

    调用IP计数器: 每来一个cin(进位输入)信号,计数器输出值加一,当计数值为9且cin为1时,输出一个时钟长度的cout(进位输出)信号。 首先采用调用quartus种IP的方式,具体步骤: Tools----IP Catalog: 然后会调出IP目录窗口: 通过搜索counter来添加计数器模块,需要设置的内容

    2024年02月03日
    浏览(56)
  • verilog手撕代码5——计数器(置位、加减、环形、扭环形、格雷码计数器实现)

    2023.5.12 编写一个十六进制计数器模块,计数器输出信号递增每次到达0,给出指示信号 zero ,当置位信号 set 有效时,将当前输出置为输入的数值 set_num 。 注意 :这里zero=1和num=0是同一拍输出的,按道理如果根据num=0,然后去输出zero=1应该延迟一拍。所以这里考虑将number延迟一

    2024年02月07日
    浏览(54)
  • LR中监控ORACLE数据库常用计数器(如何自定义Oracle计数器)

    目录 一、添加自定义计数器的方法 1、要创建自定义查询,请执行以下操作: 2、配置文件示例对象 二、常用自定义计数器列表 三、LR中监控ORACLE数据库常用计数器遇到问题及处理 1. 在安装路径的Mercury LoadRunnerdatmonitors找到vmon.cfg文件,打开。 2. 在vmon.cfg文件的第三行中,

    2024年02月15日
    浏览(53)
  • 【FPGA】Verilog:时序电路设计 | 二进制计数器 | 计数器 | 分频器 | 时序约束

    前言: 本章内容主要是演示Vivado下利用Verilog语言进行电路设计、仿真、综合和下载 示例:计数器与分频器   ​​ 功能特性: 采用 Xilinx Artix-7 XC7A35T芯片  配置方式:USB-JTAG/SPI Flash 高达100MHz 的内部时钟速度  存储器:2Mbit SRAM   N25Q064A SPI Flash(样图旧款为N25Q032A) 通用

    2024年02月02日
    浏览(62)
  • 用74LS73设计四位二进制加法计数器和8421BCD加法计数器

     (1)用2片74LS73实现该电路,由CP端输入单脉冲,设计并画出4位异步二进制加法计数器电路图。  (2)由CP端输入单脉冲,测试并记录Q1~Q4端状态及波形。 四位二进制加法计数器状态迁移表如下: Q 4n Q 3n Q 2n Q 1n Q 4n+1 Q 3n+1 Q 2n+1 Q 1n+1 0 0 0 0 0 0 0 1 0 0 0 1 0 0 1 0 0 0 1 0 0 0 1 1 0 0 1 1 0 1 0

    2024年02月10日
    浏览(90)
  • 定时器/计数器中定时/计数初值的计算

             寄存器TMOD是单片机的一个特殊功能寄存器,其功能是控制定时器/计数器T0、T1的工作方式。它的字节地址为89H, 不可以对它进行位操作。          只能进行字节操作, 即给寄存器整体赋值的方法 设置初始值 ,如 TMOD=0x01 。在上电和复位时,寄存器TMOD的初始

    2024年02月10日
    浏览(50)
  • 数电:计数器

    同步计数器和异步计数器的区别 1.同步计数器的外部时钟端都连在一起,而异步计数器没有 2.同步计数器在外部信号到来时触发器同时翻转,而异步计数器的触发器为串行连接,工作频率较低 3.异步计数器输出状态的建立,要比CP慢一个传输时间,容易存在竞争冒险 竞争冒险

    2024年02月06日
    浏览(46)
  • 【时序逻辑电路】——计数器

    🦄🦄欢迎大家来学习数字电路——时序逻辑电路。 🦄🦄在这里我们会讲解二进制计数器、十进制计数器和集成计数器74LS161,希望通过我们的学习会让你更明白数字电路中的奥秘。 目录 🔑一、计数器  🏆1.二进制计数器  (1)异步二进制加法计数器 (2)同步二进制加法

    2024年02月02日
    浏览(59)
  • Linux性能计数器

      性能计数器是在大多数现代cpu上可用的特殊硬件寄存器。这些寄存器统计特定类型的hw事件的数量:例如执行的指令、遭受的cachemisses或错误预测的分支,而不会减慢内核或应用程序的速度。这些寄存器还可以在事件数量达到阈值时触发中断——因此可以用于分析在该CP

    2024年02月11日
    浏览(64)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包