Compose 中 TextField 的有效状态管理

news2025/4/18 9:15:25

Compose 中 TextField 的有效状态管理

compose TextField

为了防止同步问题和意外行为:

  • 避免在输入和更新TextField状态之间出现延迟/异步行为。
  • 避免使用响应式流收集StateFlow的数据来保存TextField状态,例如使用默认调度程序。
  • 使用Compose API,例如MutableState<String>,定义TextField状态变量。 需要时,将TextField状态提升到ViewModel,例如将业务验证应用于TextField内容。

假设我们必须在Jetpack Compose应用中实现注册页面,并收到以下设计:
使用两个文本输入框实现注册界面

我们有两个文本输入框和一个按钮。
让我们从顶部的文本输入框开始,它是用户名字段。
为了在Compose中实现一个文本输入框,我们需要定义一个状态变量:

  1. 存储当前显示的值,并将其传递给TextField的值参数。
  2. 每当用户在TextField的onValueChange回调中输入新文本时,就会更新它。
/* Copyright 2022 Google LLC.   
   SPDX-License-Identifier: Apache-2.0 */

var myValue = ...

OutlinedTextField(
   value = myValue, // #1
   onValueChange = { newValue -> myValue = newValue } // #2
   ...
  )
}

在处理状态时,重要的事情是决定将状态变量放在何处。在我们的例子中,我们希望对用户名进行一些业务逻辑校验,因此我们将状态提升到ViewModel中,而不是将其保留在组成函数中。如需更多关于此及如何组织应用架构的信息,可以阅读我们的架构指南。
通过将状态放在ViewModel中,TextField值将在配置更改时免费持久化。

基于这些要求,我们创建一个包含类似于此的OutlinedTextField组件的组合注册屏幕:

/* Copyright 2022 Google LLC.   
   SPDX-License-Identifier: Apache-2.0 */

// SignUpScreen.kt

@Composable
fun SignUpScreen(...) {

    OutlinedTextField(
       ...
       value = viewModel.username.collectAsStateWithLifecycle(),
       onValueChange = { viewModel.updateUsername(it) }
       ...
      )
    }
}

接下来,在 ViewModel 中,我们将定义状态变量并执行业务逻辑。

目前,不建议使用响应式流来定义 TextField 的状态变量。我们将在接下来的章节中探讨为什么以及其他陷阱,但是现在假设我们犯了这个错误。我们错误地定义了一个类型为 MutableStateFlow_username 变量来存储 TextField 状态,并通过定义不可变的 backed 变量 username 来公开它。

异步方法 updateUsername 将在用户在 TextField 上键入新字符时,每次调用服务来验证用户名是否可用(例如以前是否已使用)。如果验证失败,它将显示一个错误消息,要求选择不同的用户名。

/* Copyright 2022 Google LLC.   
   SPDX-License-Identifier: Apache-2.0 */

// SignUpViewModel.kt

class SignUpViewModel(private val userRepository: UserRepository) : ViewModel() {
   
   // DO NOT DO THIS. ANTI-PATTERN - using a reactive stream for TextField state
   private val _username = MutableStateFlow("")
   val username = _username.asStateFlow()

   fun updateUsername(input: String) {
       viewModelScope.launch {
           // async operation
           val isUsernameAvailable = userRepository.isUsernameAvailable(input)
           // ...
           
           if (!isUsernameAvailable) {
               // modify error state
           }
           // DO NOT DO THIS. ANTI-PATTERN - updating after an async op
           _username.value = input
       }
   }
}

问题

我们已经完成了用户名字段的实现。如果现在运行应用程序,我们应该能够进行测试:
当我们尝试将用户名jane@mail.com更改为jane.surname@mail.com时,TextField出现不正确的行为
当我们输入时,我们很快发现不正确的行为:在我们输入时,有些字母会被跳过,有些字母按错误的顺序添加到输入中,整个位被重复,光标来回跳动。所有编辑操作均失败,包括删除和选择要替换的文本。显然存在错误。

发生了什么,我们该如何解决呢?

TextField的内部实现

在撰写本文(使用Compose UI 1.3.0-beta01)时,TextField的实现包括持有3个状态的副本:

  1. 输入法编辑器(IME):为了能够执行智能操作,例如建议替换一个单词的下一个词或表情符号,键盘需要拥有当前显示的文本的副本。
  2. 由用户定义并更新的状态持有者,在上面的示例中,它是一个MutableStateFlow变量。
  3. 内部状态充当控制器,使其他两个状态保持同步,因此您无需手动与IME交互。

即使在每个TextField的全部时间内都有3个状态的副本在发挥作用,开发人员只管理其中一个(状态持有者),而其他副本则是内部的。

这三种状态如何在幕后相互作用?为了简化,从键盘键入或添加的每个字符执行一系列步骤,构成一个反馈循环,如下所示:

TextField状态之间的交互

  1. 从键盘输入事件(输入单词“hello”)并被转发到内部控制器。
  2. 内部控制器接收到此更新“hello”,并将其转发给状态持有者。
  3. 状态持有者更新为“hello”内容,这将更新UI并通知内部控制器已接收到更新。
  4. 内部控制器通知键盘。
  5. 键盘被通知,因此它可以为下一个键入事件做准备,例如建议下一个单词。

只要这些状态的副本保持同步,TextField就能按预期运行。

然而,通过引入异步行为和竞态条件到打字的过程中,这些拷贝就可能不同步,且无法恢复。这些错误的严重程度取决于各种因素,如引入的延迟量、键盘语言、文本内容和长度以及输入法实现。

即使只是使用响应式流来表示状态(例如StateFlow)而没有延迟,也可能会出现问题,因为如果您使用默认调度程序,则更新事件的分派不是立即的。

让我们尝试看一下在这种情况下会发生什么,当您开始输入时。来自键盘的新事件“hello”到来,然后在我们更新状态和UI之前,我们生成一个异步调用。然后另一个事件“world”从键盘上来了。

第一个异步事件恢复,循环完成。当TextField内部状态接收到异步“hello”时,它会丢弃之前收到的最新的“hello world”。
TextField 的内部状态被覆盖为 'hello',而不是 'hello world'
但是在某个时候,“hello world” 异步事件也将恢复。此时 TextField 保持无效状态,其中 3 个状态不匹配。

TextField 的内部状态被覆盖为 ‘hello’,而不是 ‘hello world’。
但是在某个时候,“hello world” 异步事件也将恢复。此时 TextField 保持无效状态,其中 3 个状态不匹配。
每次异步处理恢复后,TextField存在不一致性
TextField存在不一致性。这些意外的异步调用与IME的处理、快速输入、时序条件以及替换整个文本块的删除等操作相结合,缺陷变得更加明显。

既然我们对其中的动态有了一些了解,让我们看看如何修复和避免这些问题。

处理TextField状态的最佳实践

避免延迟更新状态
当onValueChange被调用时,立即同步更新您的TextField。

/* Copyright 2022 Google LLC.   
   SPDX-License-Identifier: Apache-2.0 */

// SignUpViewModel.kt

class SignUpViewModel(private val userRepository: UserRepository) : ViewModel() {
   
   fun updateUsername(input: String) {
-       viewModelScope.launch {
-           // async operation
-           val isUsernameAvailable = userRepository.isUsernameAvailable(input)
-           // ...
-           
-           if (!isUsernameAvailable) {
-               // modify error state
-           }
           username.value = input
       }
   }
}


// SignUpScreen.kt

@Composable
fun SignUpScreen(...) {

   OutlinedTextField(
      value = viewModel.username,
      onValueChange = { username -> viewModel.updateUsername(username) })
}

你可能仍然需要对文本进行过滤或修剪。同步操作可以进行。例如,如果你的同步操作将输入转换为不同的字符集,请考虑使用 visualTransformation。你应该避免使用异步操作,因为这会导致上述问题。

使用 MutableState 表示 TextField 状态
避免使用响应式流(例如 StateFlow)来表示 TextField 状态,因为这些结构引入了异步延迟。而应该使用 MutableState:

/* Copyright 2022 Google LLC.   
   SPDX-License-Identifier: Apache-2.0 */

class SignUpViewModel : ViewModel() {
   var username by mutableStateOf("")
       private set

   // ...
}

如果您仍然更喜欢使用 StateFlow 来存储状态,请确保使用立即调度程序而不是默认调度程序来从流中收集。

这种解决方案需要更深入的协程知识,并可能导致以下问题:

  • 由于收集是同步的,因此当它发生时,UI 可能处于不可操作状态。
  • 会干扰 Compose 的线程和渲染阶段,因为它假设重组发生在主线程上。

在哪里定义状态
如果您的TextField state需要在键入时进行业务逻辑验证,则将状态提升到ViewModel中是正确的。如果不需要,您可以使用Composables或状态持有类作为真正的数据源。

一般的规则是,您应该将状态放在尽可能低的位置,同时仍然被正确地拥有,这通常意味着更接近它被使用的地方。有关Compose中状态的更多信息,请查看我们的指南。

在解决此问题时,重要的不是将TextField state提升到哪里,而是如何存储它。

在您的应用程序中应用最佳实践

考虑到这些最佳实践,让我们同时实现异步和同步验证到我们的TextField state中。

从异步验证开始,如果要使用的用户名无效,则我们想要在TextField下方显示错误消息,并在服务器端执行此验证。在我们的UI中,它将如下所示:

显示错误,因为“username1”已经被使用了

当调用onValueChange时,我们将立即调用更新方法来更新TextField,然后,ViewModel将根据刚刚更改的值安排异步检查。

在ViewModel中,我们定义了两个状态变量:一个用于TextField状态的username变量作为MutableState,一个userNameHasError作为StateFlow,它会在用户名更新时进行反应性计算。

snapshotFlow API将Compose State转换为flow,以便我们可以对每个值执行异步(挂起)操作。

因为输入速度可能比获取异步调用结果更快,所以我们按顺序处理事件,并使用mapLatest(实验性)在出现新事件时取消未完成的调用,以避免浪费资源或显示不正确的状态。出于同样的原因,我们还可以添加一个防抖方法(异步调用之间的延迟)。

/* Copyright 2022 Google LLC.   
   SPDX-License-Identifier: Apache-2.0 */

// SignUpViewModel.kt

@OptIn(ExperimentalCoroutinesApi::class)
class SignUpViewModel(private val signUpRepository: SignUpRepository) : ViewModel() {
    var username by mutableStateOf("")
        private set

    val userNameHasError: StateFlow<Boolean> =
        snapshotFlow { username }
            .mapLatest { signUpRepository.isUsernameAvailable(it) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = false
            )

    fun updateUsername(input: String) {
        username = input
    }
}


// SignUpScreen.kt

@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun SignUpScreen(...)

    OutlinedTextField(
        value = viewModel.username,
        onValueChange = { newValue ->
            viewModel.updateUsername(newValue)
        }
    )

    val userNameHasError by viewModel.userNameHasError.collectAsStateWithLifecycle()

    if (userNameHasError) {
        Text(
            text = "Username not available. Please choose a different one.",
            color = Color(ColorError)
        )
    }
    ...
}

请注意,我们正在使用实验性的collectAsStateWithLifecycle API收集错误验证流,这是在Android中收集流的推荐方式。要了解有关此API的更多信息,您可以查看Jetpack Compose博客文章中的“安全地消费流”部分。

https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3

现在,我们想添加同步验证以检查输入是否包含无效字符。我们可以使用synchronous的derivedStateOf() API,每当用户名更改时将触发lambda验证。

/* Copyright 2022 Google LLC.   
   SPDX-License-Identifier: Apache-2.0 */

// SignUpViewModel.kt

class SignUpViewModel(private val signUpRepository: SignUpRepository) : ViewModel() {
    
    var username by mutableStateOf("")
        private set    

    val userNameHasLocalError by derivedStateOf { 
        // synchronous call
        signUpRepository.isUsernameCorrect(username) 
    }

    ...
}

derivedStateOf()创建新的State,读取userNameHasLocalError的组件将在该值在true和false之间更改时重新组合。
我们完整的带验证的用户名实现如下:
实现具有同步和异步错误的用户名字段

考虑 TextField 的实现

目前,我们正在改进 TextField API,并将其视为我们的优先事项之一。

Compose 路线图反映了团队在多个方面开展的工作,这种情况下文本编辑和键盘输入的改进都与这些 API 相关。因此,请注意未来的 Compose 发布版本以及发布说明。

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

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

相关文章

C++练习

整理思维导图有以下定义&#xff0c;说明哪些量可以改变哪些不可以改变&#xff1f; const char *p; const (char *) p; char *const p; const char* const p; char const *p; (char *) const p; char const* const p; 3.总结命名空间使用时的方式和注意事项 1. 2. const cha…

Spring IOC - FactoryBean源码解析

​​​​​1. 介绍 FactoryBean是Spring框架中的一个接口&#xff0c;它允许我们自定义一个工厂类&#xff0c;用于创建和管理Spring容器中的Bean实例。FactoryBean接口定义了两个方法&#xff1a; getObject()&#xff1a;用于返回一个Bean实例&#xff0c;这个方法可以自定义…

创建型设计模式04-建造者模式

✨作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 建造者模式 1、建造者模式介绍 建造者模式是一种创建型设计模式&#xff0c;用于将一个复杂对象的构造过程与其表示分离开来&…

七面蚂蚁金服,超硬核面经,已拿Offer!!

刚面试了蚂蚁金服&#xff0c;给大家分享下这些硬核的面试经验 一面&#xff1a;50分钟 1.自我介绍 2.之前的实习经历提问 3.实习做过什么项目&#xff0c;负责什么 4.进程线程 5.java的垃圾回收 6.数据库简单sql语句 7.事务隔离 8.测试微信点赞 9.对测试的了解 10.编程&…

【LeetCode每日一题】——2269.找到一个数字的 K 美丽值

文章目录 一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【题目提示】七【解题思路】八【时间频度】九【代码实现】十【提交结果】 一【题目类别】 滑动窗口 二【题目难度】 简单 三【题目编号】 2269.找到一个数字的 K 美丽值 四【题目描述…

第二十二篇、基于Arduino uno,控制五线四相步进电机实现正转和反转——结果导向

0、结果 说明&#xff1a;步进电机可以旋转指定角度&#xff0c;例如转了九十度就停止&#xff0c;如果想一直转也是可以的&#xff0c;程序里面已写&#xff0c;而且也有正反转。如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;五线四相步进电机如下…

自然语言处理从入门到应用——文本的表示方法

分类目录&#xff1a;《自然语言处理从入门到应用》总目录 若要利用计算机对自然语言进行处理&#xff0c;首先需要解决语言在计算机内部的存储和计算问题。字符串&#xff08;String&#xff09;是文本最自然&#xff0c;也是最常用的机内存储形式。所谓字符串&#xff0c;即字…

rtty移植到MTK OpenWRT平台

准备工具链 使用MTK Openwrt 相应平台工具链&#xff0c;不能使用buildroot的工具链&#xff0c;否则程序无法运行&#xff0c;本文MTK7621为例工具链官网下载地址 构建libev rtty依赖libev库&#xff0c;因此需要先构建libev git clone https://github.com/enki/libev.git …

C语言-外部关键字extern

extern 关键字 extern 用在全局变量或函数的声明前&#xff0c;用来说明“此变量/函数是在别处定义的&#xff0c;要在此处引用。 什么是定义&#xff0c;什么是声明? 什么是定义:所谓的定义就是为这个变量分配一块内存并给它取上一个名字&#xff0c;这个名字就是我们经…

《多传感器融合感知》一、构建自动驾驶多传感器系统

来自 &#xff1a; https://www.bilibili.com/video/BV1JY4y1R7Be/ 多传感器融合的需求多传感器硬件系统构建多传感器硬件系统的时序闭环 1. 多传感器融合的需求 自动驾驶用来解决的问题&#xff1a; 建图定位、感知问题&#xff08;路上有什么、堵不堵、好不好走…&#xff…

数据可视化系列指南之分布类图表大全

导语 随着数据在各行业中的应用越来越广泛&#xff0c;大家也逐渐认识到数据可视化在企业生产经营中的重要作用&#xff0c;在数据可视化过程中&#xff0c;图表是处理数据的重要组成部分&#xff0c;因为它们是一种将大量数据压缩为易于理解的格式的方法。数据可视化可以让受…

生态系统服务评估方法 | 人类活动影响、重大工程实施的生态成效评估(InVEST模型)

以InVEST模型结合实际项目进行由浅入深的实战技术&#xff0c;针对特点及需求进行分析&#xff0c;融合课程体系&#xff0c;对接工作实际项目及论文写作&#xff0c;解决参会者关注的重点及实际项目过程问题&#xff0c;采取逐步延伸的逻辑&#xff0c;不论您是小白亦或是已经…

数据库磁盘文件格式的设计原理内幕

引言 访问磁盘需通过系统调用来实现&#xff0c;因此通常我们需要指定目标文件的偏移量&#xff0c;然后把数据从磁盘上的形式解析成合适主存的形式。这意味着要想设计一个高效的磁盘数据结构&#xff0c;必须构造一种易于修改和解析的文件格式。在本文中&#xff0c;我们将讨论…

unicms 使用thinkphp8 重构版

unicms 有你存在 一切安好 2023年6月使用thinkphp8&#xff0c;重构了cms。php最低使用php8版。参考了多个cms结合10年的开发经验&#xff0c;打造了开箱机用&#xff0c;简单的cms系统。当然由于你能力和开发时间限制&#xff0c;肯定有bug。欢迎大家指正。我也会不定期的修改…

【线性规划模型】

线性规划模型&#xff1a;原理介绍和预测应用 引言 线性规划是运筹学中一种重要的数学优化方法&#xff0c;被广泛应用于各个领域&#xff0c;包括工业、经济、物流等。 线性规划模型的原理 线性规划模型的目标是在一组线性约束条件下&#xff0c;寻找一组变量的最优解&…

Emm_V4.2步进闭环驱动器说明书Rev1.1

一、产品介绍 1.1 产品简介&#xff1a; Emm42_V4.x步进闭环驱动器是张大头智控为满足广大用户需求而自主研发的一款稳定可靠的产品&#xff0c;它是基于上一代Emm42_V3.6版本升级而来&#xff0c;不仅延续了其优秀的FOC矢量闭环控制算法&#xff0c;更在其传统的Dir/Step控制模…

Java003——编写和运行第一个Java程序HelloWorld

一、使用记事本创建Java并运行 1.1、设置文件显示后缀名 目的是为了方便查看文件类型 1.2、创建一个HelloWorld.java文件 java程序文件都是以.java后缀结尾的 1.3、编写Java程序 编写一下程序&#xff0c;并保存 public class HelloWorld {public static void main(Strin…

100天精通Golang(基础入门篇)——第0天: 安装 Go 语言开发环境的基础教程,带你学习Golang之Hello Go !

文章目录 1. 下载 Go 语言的安装包&#xff1a;1. 安装 Go 编译器&#xff1a;- 双击 安装下一步:切换安装路径:下一步:安装等待安装完成安装完成 - 2. 测试安装是否成功&#xff1a;1. 开始学习 Go 语言&#xff1a;1. 创建项目2. 创建 类文件3. 输入类名4. 键入 下方代码块5.…

【C语言】让你不再害怕“指针”【c】【知识点整理】

目录 一.什么是指针&&为什么需要指针&#xff1f; 1.什么是指针&#xff1f; 2.为什么需要指针&#xff1f; 以一个代码为例观察地址&#xff1a;&#xff08;这里我们可以通过调试和打印两种方式观察&#xff09; 1.调试观察&#xff1a; 2.打印观察&#xff08;…

Vue.js 中的 v-if 和 v-show 有什么区别?

Vue.js 中的 v-if 和 v-show 有什么区别&#xff1f; 在 Vue.js 中&#xff0c;v-if 和 v-show 都是用来控制元素的显示和隐藏的指令。但是&#xff0c;它们之间有一些区别。本文将深入探讨 v-if 和 v-show 的区别&#xff0c;并给出一些相关的代码示例。 v-if v-if 是一种条…