做自由与创造的先行者

Android状态容器和界面状态

Android开发手册

单向数据流 (UDF) 可作为为界面层提供和管理界面状态的方式,界面层指南介绍了这种方式。

此外,该指南还重点介绍了将 UDF 管理委托给名为状态容器的特殊类的好处。您可以通过 ViewModel 或普通类实现状态容器。本文档详细介绍了状态容器及其在界面层中的作用。

学完本文档后,您应了解如何在界面层中管理应用状态;这就是界面状态生成流水线。您应该能够了解和掌握以下内容:

了解界面层中存在的界面状态类型。

了解在界面层中对这些界面状态执行的逻辑类型。

知道如何选择合适的状态容器实现方式,例如 ViewModel 或简单类。

界面状态生成流水线的元素

界面状态以及生成该状态的逻辑定义了界面层。

界面状态

界面状态是描述界面的属性。界面状态有两种类型:

屏幕界面状态是需要在屏幕上显示的内容。例如,NewsUiState 类可以包含呈现界面所需的新闻报道和其他信息。由于该状态包含应用数据,因此通常会与层次结构中的其他层相关联。

界面元素状态是指界面元素的固有属性,这些属性会影响界面元素的呈现方式。界面元素可能处于显示或隐藏状态,并且可能具有特定的字体、字体大小或字体颜色。在 Android View 中,View 会自行管理此状态(因为它本身是有状态的),并公开用于修改或查询其状态的方法。例如,TextView 类的 get 和 set 方法用于显示该类的文本。在 Jetpack Compose 中,状态在可组合项之外,您甚至可以将状态从可组合项附近提升到执行调用的可组合函数或状态容器中。例如,Scaffold 可组合项的 ScaffoldState。

逻辑

界面状态不是静态属性,因为应用数据和用户事件会导致界面状态随时间而变化。逻辑决定了变化的具体细节,包括界面状态的哪些部分发生了变化、为什么发生变化以及应该在何时发生变化。

应用中的逻辑可以是业务逻辑或界面逻辑:

业务逻辑决定着应用数据的产品要求的实现。例如,在新闻阅读器应用中,当用户点按相应按钮时,就会为报道添加书签。这种用于将书签保存到文件或数据库的逻辑通常放置在领域或数据层中。状态容器通常通过调用这类层公开的方法,将此逻辑委托给相应的层。

界面逻辑决定着如何在屏幕上显示界面状态。例如,在用户选择了某个类别时获取正确的搜索栏提示、滚动至列表中的特定项,或者在用户点击某按钮时便进入特定屏幕的导航逻辑。

Android 生命周期以及界面状态和逻辑的类型

界面层包含两个部分:一部分依赖于界面生命周期,另一部分不依赖于界面生命周期。这种分离决定了每个部分可用的数据源,因此需要不同类型的界面状态和逻辑。

不依赖于界面生命周期:界面层的这一部分用于处理应用的数据生成层(数据层或网域层),由业务逻辑定义。界面中的生命周期、配置更改和 Activity 重新创建可能会影响界面状态生成流水线是否处于活动状态,但不会影响生成的数据的有效性。

依赖于界面生命周期:界面层的这一部分用于处理界面逻辑,受生命周期或配置更改的直接影响。这些更改会直接影响从中读取数据的来源的有效性,因此其状态只会在其生命周期处于活动状态时发生变化。例如运行时权限,以及获取依赖于配置的资源(例如本地化字符串)。

界面状态生成流水线

界面状态生成流水线是指为生成界面状态而执行的步骤。相关步骤包括应用上文定义的各类逻辑,并且完全取决于界面的需求。有些界面可能会受益于流水线中不依赖于界面生命周期的部分和/或依赖于界面生命周期的部分,也可能不会受益于其中任一部分。

也就是说,界面层流水线的以下排列是有效的:

由界面本身生成和管理的界面状态。例如,一个简单且可重复使用的基本计数器:

@Composable

fun Counter() {

// The UI state is managed by the UI itself

var count by remember { mutableStateOf(0) }

Row {

Button(onClick = { ++count }) {

Text(text = "Increment")

}

Button(onClick = { --count }) {

Text(text = "Decrement")

}

}

}

界面逻辑 → 界面。例如,显示或隐藏允许用户跳转到列表顶部的按钮。

@Composable

fun ContactsList(contacts: List) {

val listState = rememberLazyListState()

val isAtTopOfList by remember {

derivedStateOf {

listState.firstVisibleItemIndex < 3

}

}

// Create the LazyColumn with the lazyListState

...

// Show or hide the button (UI logic) based on the list scroll position

AnimatedVisibility(visible = !isAtTopOfList) {

ScrollToTopButton()

}

}

业务逻辑 → 界面。在屏幕上展示当前用户的照片的界面元素。

@Composable

fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {

// Read screen UI state from the business logic state holder

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

// Call on the UserAvatar Composable to display the photo

UserAvatar(picture = uiState.profilePicture)

}

业务逻辑 → 界面逻辑 → 界面。会针对给定界面状态在屏幕上滚动以显示正确信息的界面元素。

@Composable

fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {

// Read screen UI state from the business logic state holder

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

val contacts = uiState.contacts

val deepLinkedContact = uiState.deepLinkedContact

val listState = rememberLazyListState()

// Create the LazyColumn with the lazyListState

...

// Perform UI logic that depends on information from business logic

if (deepLinkedContact != null && contacts.isNotEmpty()) {

LaunchedEffect(listState, deepLinkedContact, contacts) {

val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)

if (deepLinkedContactIndex >= 0) {

// Scroll to deep linked item

listState.animateScrollToItem(deepLinkedContactIndex)

}

}

}

}

如果将这两种逻辑都应用于界面状态生成流水线,则必须始终先应用业务逻辑,然后再应用界面逻辑。如果尝试先应用界面逻辑,再应用业务逻辑,则意味着业务逻辑依赖于界面逻辑。

状态容器及其责任

状态容器的责任是存储状态,以便应用读取状态。 在需要逻辑时,它会充当中介,并提供对托管所需逻辑的数据源的访问权限。这样,状态容器就会将逻辑委托给相应的数据源。

这会带来以下好处:

简单的界面:界面仅绑定了其状态。

可维护性:可以对状态容器中定义的逻辑进行迭代,而无需更改界面本身。

可测试性:界面及其状态生成逻辑可独立进行测试。

可读性:代码读者可以清楚地看出界面呈现代码与界面状态生成代码之间的差异。

无论大小或作用域如何,每个界面元素都与其对应的状态容器具有 1 对 1 关系。此外,状态容器必须能够接受和处理任何可能导致界面状态发生变化的用户操作,并且必须生成随后的状态变化。

注意:状态容器并非绝对必要。简单的界面可能会托管内嵌到其呈现代码中的逻辑。

状态容器的类型

与界面状态和逻辑的类型类似,界面层中有两种类型的状态容器,它们根据自身与界面生命周期的关系而定义:

业务逻辑状态容器。

界面逻辑状态容器。

以下几个部分更详细地介绍了状态容器的类型,首先讲的就是业务逻辑状态容器。

注意:如果界面逻辑状态容器依赖于数据层或网域层中的信息,您应从业务逻辑状态容器向界面逻辑状态容器传递该信息。这是因为与界面逻辑状态容器相比,业务逻辑状态容器的存在期更长,原因是后者不依赖于界面生命周期。

业务逻辑及其状态容器

业务逻辑状态容器会处理用户事件,并将数据从数据层或网域层转换为屏幕界面状态。

将 ViewModel 用作业务逻辑状态容器

ViewModel 在 Android 开发中的优势使其适用于提供对业务逻辑的访问权限以及准备要在屏幕上呈现的应用数据。这些优势包括如下各项:

ViewModel 触发的操作在配置发生变化后仍然有效。

与 Navigation 集成:

当屏幕位于返回堆栈中时,Navigation 会缓存 ViewModel。这对在返回目标位置时即时提供之前加载的数据非常重要。使用遵循可组合项屏幕的生命周期的状态容器时,这种情况会更难处理。

当目标位置从返回堆栈弹出后,ViewModel 也会被一并清除,以确保自动清理状态。这不同于监听可组合项的处理,监听的原因可能有多种,例如转到新屏幕、配置发生变化等。

与其他 Jetpack 库(如 Hilt)集成。

注意:如果 ViewModel 的优势不适用于您的用例,或者您以其他方式执行操作,则可以将 ViewModel 的责任转移到对普通状态容器类中。

界面逻辑及其状态容器

界面逻辑是对界面本身提供的数据执行操作的逻辑。它可能依赖于界面元素的状态或界面数据源(如权限 API 或 Resources)。利用界面逻辑的状态容器通常具有以下属性:

生成界面状态并管理界面元素状态。

在 Activity 重新创建后不再有效:托管在界面逻辑中的状态容器通常依赖于界面本身的数据源,并且在很多情况下,尝试在配置发生变化后保留此信息会导致内存泄漏。如果状态容器需要数据在配置发生变化后保持不变,则需要将其委托给更适合在 Activity 重新创建后继续留存的其他组件。例如,在 Jetpack Compose 中,使用 remembered 函数创建的可组合界面元素状态通常会委托给 rememberSaveable,以便在 Activity 重新创建后保留状态。此类函数的示例包括 rememberScaffoldState() 和 rememberLazyListState()。

引用了界面范围的数据源:生命周期 API 和资源等数据源可以安全地引用和读取,因为界面逻辑状态容器与界面具有相同的生命周期。

可在多个不同的界面中重复使用:同一界面逻辑状态容器的不同实例可以在应用的不同部分中重复使用。例如,用于管理条状标签组的用户输入事件的状态容器可用在过滤条件块的搜索页上,也可以用于表示电子邮件接收者的“收件人”字段。

界面逻辑状态容器通常使用普通类实现。这是因为界面本身负责创建界面逻辑状态容器,而界面逻辑状态容器与界面本身具有相同的生命周期。例如,在 Jetpack Compose 中,状态容器是组合的一部分,并遵循组合的生命周期。

注意:当界面逻辑足够复杂,可以移出界面时,会使用普通类状态容器。否则,界面逻辑可以在界面中以内嵌方式实现。

Now in Android 示例会根据设备的屏幕大小来显示用于导航的底部应用栏或导航栏。较小的屏幕使用底部应用栏,较大的屏幕则使用导航栏。

由于决定 NiaApp 可组合函数中使用的适当导航界面元素的逻辑不依赖于业务逻辑,因此可以通过名称为 NiaAppState 的普通类状态容器来管理:

@Stable

class NiaAppState(

val navController: NavHostController,

val windowSizeClass: WindowSizeClass

) {

// UI logic

val shouldShowBottomBar: Boolean

get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||

windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

// UI logic

val shouldShowNavRail: Boolean

get() = !shouldShowBottomBar

// UI State

val currentDestination: NavDestination?

@Composable get() = navController

.currentBackStackEntryAsState().value?.destination

// UI logic

fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

/* ... */

}

在上面的示例中,关于 NiaAppState 的以下详细信息值得注意:

在 Activity 重新创建后不再有效:通过使用遵循 Compose 命名惯例的可组合函数 rememberNiaAppState 创建 NiaAppState,在组合中 remembered 了该容器。重新创建 Activity 后,之前的实例会丢失,并会使用传入的所有依赖项(适用于重新创建的 Activity 的新配置)创建一个新实例。这些依赖项可能是新的,也可能是根据以前的配置恢复的。例如,NiaAppState 构造函数中使用了 rememberNavController(),后者会委托给 rememberSaveable 以在重新创建 Activity 的过程中保留状态。

引用了界面范围的数据源:对 navigationController、Resources 和其他类似生命周期范围的类型的引用可以安全地保存在 NiaAppState 中,因为它们具有相同的生命周期作用域。

注意:建议为可重用的界面部分(如搜索栏或条状标签组)使用普通状态容器类。在这种情况下,您不应使用 ViewModel,因为 ViewModel 最适合用于管理导航目的地的状态和对业务逻辑的访问权限。

为状态容器选择 ViewModel 和普通类

在上面几部分中,选择 ViewModel 还是普通类状态容器取决于对界面状态应用的逻辑以及执行该逻辑的数据源。

注意:大多数应用会选择执行内嵌在界面本身中的界面逻辑,而这些逻辑原本可以放在普通类状态容器中。这适用于简单的情况,但在其他情况下,您可以通过将逻辑拉取到普通类状态容器中来提高可读性

状态容器可组合

状态容器可以依赖于另一个状态容器,前提是依赖项的生命周期与状态容器相同或更短。示例如下:

界面逻辑状态容器可以依赖于另一个界面逻辑状态容器。

屏幕级状态容器可以依赖于界面逻辑状态容器。

以下代码段展示了 Compose 的 DrawerState 如何依赖于另一个内部状态容器,即 SwipeableState;还展示了应用的界面逻辑状态容器可以如何依赖于 DrawerState:

@Stable

class DrawerState(/* ... */) {

internal val swipeableState = SwipeableState(/* ... */)

// ...

}

@Stable

class MyAppState(

private val drawerState: DrawerState,

private val navController: NavHostController

) { /* ... */ }

@Composable

fun rememberMyAppState(

drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),

navController: NavHostController = rememberNavController()

): MyAppState = remember(drawerState, navController) {

MyAppState(drawerState, navController)

}

注意:鉴于屏幕级状态容器管理部分或整个屏幕的业务逻辑复杂性,因此让屏幕级状态容器依赖于另一个屏幕级状态容器的做法并不合理。如果您遇到这种情况,请重新考虑相关屏幕和状态容器,确定您是否真的需要这样做。

举例来说,如果界面逻辑状态容器依赖于屏幕级状态容器,那么依赖项的生命周期就比状态容器更长。这会降低生命周期较短的状态容器的可重用性,并使其能够访问超出实际需要的逻辑和状态。

如果生命周期较短的状态容器需要来自较高层级范围的状态容器的某些信息,请仅将它需要的信息作为参数传递,而不是传递状态容器实例。例如,在以下代码段中,界面逻辑状态容器类仅从 ViewModel 接收所需信息,而不是将整个 ViewModel 实例作为依赖项传递。

网站建设开发|APP设计开发|小程序建设开发
下一篇:Android界面状态生成
上一篇:Android界面事件