Jetpack Compose(第八趴)——Jetpack Compose 中的高级状态和附带效应(下)

五、rememberCoroutineScope

在此步骤中,我们将使抽屉式单行栏正常工作。目前,如果宁尝试点汉堡式菜单,什么都不会发生。

打开home/CraneHome.kt文件,并查看CraneHome可组合项,看看我们需要在何处打开抽屉式导航栏:在openDrawer回调中!

CraneHome中,有一个包含DrawerStatescaffoldStateDrawerState具有以程序化方式打开和关闭抽屉式导航栏的方法。不过,如果您尝试在openDrawer回调中编写scaffoldState.drawerState.open(),您会收到一条错误消息!这是因为,open函数是一个挂起函数。我们再次进入协程的领域。

除了可让你从界面层安全调用协程的API之外,某些Compose API是挂起函数。用于打开抽屉式导航栏的API就是一个这样的例子。挂起函数除了能够运行异步代码之外,还可以帮助表示随着时间的推移出现的概念。由于打开抽屉式导航栏需要一些时间和移动,而且还有可能需要动画,这可以通过挂起函数完美地反映出来,挂起函数将在被调用的地方暂停协程的执行,直到它完成,然后再继续执行。

必须在协程中调用scaffoldState.drawerState.open()。我们能做些什么呢?openDrawer是一个简单的回调函数,因此:

  • 我们不能简单地在其中调用挂起函数,因为openDrawer不在携程的上下文中执行。
  • 我们不能像之前一样使用LaunchedEffect,因为我们不能在openDrawer中调用可组合项。我们并不在组合中。

我们希望启动一个协程;我们应使用哪个作用域呢?理想情况下,我们希望CoroutineScope能够遵循其调用点的生命周期。如果使用rememberCoroutineScopeAPI,则会返回一个CoroutineScope,该CoroutineScope会绑定到它在组合中的调用点。一旦退出组合,作用域将自动取消。有了这个作用域,即使您不在组合中(例如,在openDrawer回调中),也可以启动协程。

ini 复制代码
// home/CraneHome.kt file

import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch

@Composable
fun CraneHome(
    onExploreItemClicked: OnExploreItemClicked,
    modifier: Modifier = Modifier
) {
    val scaffoldState = remeberScaffoldState()
    Scaffold(
        scaffoldState = scaffoldState,
        modifier = Modifier.statusBarsPadding(),
        drawerContent = {
            CraneDrawer()
        }
    ) {
        val scope = rememberCoroutineScope()
        CraneHomeContent(
            modifier = modifier,
            onExploreItemClickec = onExploreItemClicked,
            openDrawer = {
                scope.launch {
                    scaffoldState.drawerState.open()
                }
            }
        )
    }
}

如果您运行应用,您会看到当您点击汉堡菜单图标时,系统会打开抽屉式导航栏。

5.1、LaunchedEffect与rememberCoroutineScope

在这种情况下无法使用LaunchedEffect,因为我们需要触发调用以在组合之外的常规回调中创建协程。

回顾一下使用LaunchedEffect的着陆屏幕步骤,您可以使用rememberCoroutineScope并调用scope.launch { delay(); onTImeout(); }而不使用LaunchedEffect吗?

您本来可以这样做,而且似乎可行,但这样并不正确。如"Compose编程思想"所述,Compose可以随时调用可组合项。LaunchedEffecrt可以保证当对该组合项的调用使其进入组合时将会执行附带效应。如果您在LandingScreen的主体中使用rememberCoroutineScopescope.launch,则每次Compose调用LandingScreen时都会执行协程,而不管该调用是否使用其进入组合。因此,您会浪费资源,而且不会在受控环境中执行此附带效应。

六、创建状态容器

您注意到了吗?如果您点按"Choose Destination",您可以修改该字段,并根据搜索输入过滤城市。此外,您或许还注意到了,每当您修改"Choose Destination"时,文本样式都会发生改变。

打开base/EditableUserInput.kt文件。CraneEditableUserInput有状态可组合项接受一些参数,如hintcaption,后者对应图标旁边的可选文本。例如,当您搜索目的地时,会出现caption"To"。

kotlin 复制代码
// base/EditableUserInput.kt file - code in the main branch

@Composable
fun CraneEditableUserInput(
    hint: String,
    caption: String? = null,
    @DrawableRes vectorImageId: Int? = null,
    onInputChanged: (String) -> Unit
) {
    // TODO :Encapsulate this state in a state holder
    var textState by remember { mutableStateOf(hint) }
    val isHint = { textState == hint }
    ...
}

6.1、为什么?

用于更新textState以及确定显示的内容是否对于提示的逻辑全部都在CraneEditableUserInput可组合项的主体中。这就带来了一些缺点:

  • TextField的值未提升,因而无法从外部进行控制,这使得测试更加困难。
  • 此可组合项的逻辑可能会变得更加复杂,并且内部状态可能会更容易不同步。

通过创建负责此可组合项的内部状态的状态容器,您可以将所有状态变化集中在一个位置。这样,状态不同步就更难了,并且相关的逻辑全部跪在一个类中。此外,此状态很容易向上提升,并且可以从此可组合项的调用方使用。

在这种情况下,提升状态是一种不错的做法,因为这是一个低级界面组件,可能会在应用的其他部分中重复使用。因此,它越灵活越可控,就越好。

6.2、创建状态容器

由于CraneEditableUserInput是一个可重复使用的组件,让我们在同一文件中创建一个名为EditableUserInputState的常规作为状态容器,如下所示:

kotlin 复制代码
//  base/EditableUserInput.kt file

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue

class EditableUserInputState(private val hint: String, initlalText: String) {
    var text by mutableStateOf(initialText)
        private set
    
    fun updateText(newText: String) {
        text = newText
    }
    
    val isHint: Boolean
        get() = text == hint
}

该类应具有以下特征:

  • textString类型的可变状态,就像在CraneEditableUserInput中一样。请务必使用mutableStateOf,以便Compose跟踪值的更改,并在发生更改时重组。
  • text是具有私有setvar,因此无法直接从类外部改变它。您可以公开updateText事件来对此变量进行修改,从而将该类设为单一可信来源,而不是公开此变量。
  • 该类将initialText作为用于初始化text的依赖项。
  • 用于判断text是否为提示的逻辑在按需执行检查的isHint属性中。

如果将来逻辑变得更加复杂,我们只需要对一个类进行更改:EditableUserInputState

6.3、记住状态容器

始终需要记住状态容器,以使其留在组合中,而不是每次都创建一个新的。最好在同一文件中创建一个执行此操作的方法,以移除样板并避免可能发生的任何错误。在base/EditableUserInput.kt文件中,添加以下代码:

kotlin 复制代码
// base/EditableUserInput.kt file

@Composable
fun rememberEditableUserInputState(hint: String): EditableUserInputState = remember(hint) {
    EditableUserInputState(hint, hint)
}

如果我们只是使用remember记住此状态,它在activity重新创建后不会继续存留。为了解决此问题,我们可以改用rememberSaveableAPI,它的行为方式与remember类似,但存储的值在activity和进程重新创建后会继续留存。在内部,它使用保存的实例状态机制。

对于可以存储在Bundle内的对象,rememberSaveable可以做所有这些工作,而无需任何额外的操作。对于我们在项目中创建的EditableUserInputState类,却并非如此。因此,我们需要告知rememberSaveable如何使用Saver保存和恢复此类的实例。

6.4、创建自定义保存器

Saver描述了如何将对象转换为Saveable(可保存)的内容。Saver的实现需要替换两个函数:

  • save-将原始值转换为可保存的值。
  • restore-将恢复的值转换为原始类的实例。

在本例中,我们可以使用一些现有的Compose API,如listSavermapSaver(用于存储要保存在ListMap中的值),以减少我们需要编写的代码量,而不是为EditableUserInputState类创建Saver的自定义实现。

最好将Saver定义放置在与其一起使用的类附近。由于需要被静态访问,因此让我们在companion object中为EditableUserInputState添加Saver。在base/EditableUserInput.kt文件中,添加Saver的实现:

kotlin 复制代码
// base/EditableUserInput.kt file

import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver

class EditableUserInputState(private val hint: String, initialText: String) {
    var text by mutableStateOf(initialText)
    
    val isHint: Boolean
        get() = text == hint
        
    companion object {
        val Saver: Saver<EditableUserInputState, *> = listSaver(
            save = { listOf(it.hint, it.text) },
            restore = {
                EditableUserInputState(
                    hint = it[0],
                    initialText = it[1],
                )
            }
        )
    }
}

在本例子,我们将listSaver用作实现细节,在保存期中存储和恢复EditableUserInputState的实例。

现在,我们可以在之前创建的rememberEditableUserInputState方法的rememberSaveable(而不是remember)中使用此保存器:

kotlin 复制代码
// base/EditableUserInput.kt file
import androidx.compose.runtime.saveable.rememberSaveable

@Composable
fun rememberEdiableUserInputState(hint: String): EditableUserInputState = 
    rememberSaveable(hint, saver = EditableUserInputState.Saver) {
        EditableUserInputState(hint, hint)
    }

这样,EditableUserInput记住的状态就会在进程和activity重新创建后继续留存。

6.5、使用状态容器

我们将要使用EditableUserInputState而不是textisHint,但我们不希望只将其用作CraneEditableUserInput中的内部状态,因为调用方可组合项无法控制状态。箱单,我们希望提升EditableUserInputState,以便调用方可以控制CraneEditableUserInput的状态。如果我们提升状态,那么可组合项就可以在预览中使用,并且更容易进行测试,因为您能够从调用方修改器状态。

为此,我们需要更改可组合函数的参数,并在需要时为其提供默认值。由于我们可能希望允许CraneEditableUserInput带有空提示,因此我们添加一个默认参数:

less 复制代码
@Composable
fun CraneEditableUserInput(
    state: EditableUserInputState = rememberEditableUserInputState(""),
    caption: String? = null,
    @DrawableRes vectorImageId: Int? = null
) { /* ... */ }

您或许已经注意到,onInputChanged参数不存在了!由于状态可以提升,因此如果调用方想要知道输入是否发生了更改,它们可以控制状态并将该状态传入此函数。

接下来,我们需要调整函数主题,以使用提升的状态,而不是之前使用的内部状态。重构后,函数应如下所示:

ini 复制代码
@Composable
fun CraneEditableUserInput(
    state: EditableUserInputState = rememberEditabelUserInputState(""),
    caption: String? = null,
    @DrawableRes vectorImageId: Int? = null
) {
    CraneBaseUserInput(
        caption = caption,
        tintIcon = { !state.isHint },
        showCaption = { !state.isHint },
        vectorImageId = vectorImageId
    ) {
        BasicTextField(
            value = state.text,
            onValueChange = { state.updateText(it) },
            textStyle = if (state.isHint) {
                captionTextStyle.copy(color = LocalContentColor.current)
            } else {
                MaterilTheme.typography.body1.copy(color = LocalContentColor.current)
            },
            cursorBrush = SolidColor(LocalContentColor.current)
        )
    }
}

6.6、状态容器调用方

由于我们更改了CraneEditableUserInput的API,因此需要再调用它的所有位置进行检查,以确保传入适当的参数。

在项目中,我们旨在一个位置调用此API,那就是在home/SearchUserInput.kt文件中。打开该文件并转到ToDestinationUserInput可组合函数;您应该会在该位置看到一个构建错误。由于提示现在是状态容器的一部分,并且我们希望在组合中设置此CraneEditableUserInput实例的自定义提示,因此我们需要记住ToDestinationUserInput级别的状态,并将其传入CraneEditableUserInput:

kotlin 复制代码
// home/SearchUserInput.kt file

import androidx.compose.sample.crane.base.rememberEditableUserInputState

@Composable
fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {
    val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination")
    CraneEditableUserInput(
        state = editableUserInputState,
        caption = "To",
        vectorImageId = R.drawable.ic_plane
    )
}

6.7、snapshotFlow

上面的代码缺少在输入更改时通知ToDestinationUserInput的调用方的功能。由于应用的结构,我们不希望在层次结构中将EditableUserInputState提升到任何更高的界别。而且,我们也不希望将其它可组合项(如FlySearchContent)与此状态项结合。我们如何从ToDestinationUserInput调用onToDestinationChanged lambda并且仍使此可组合项可重复使用呢?

我们可以在每次输入更改时使用LaunchedEffect出发附带效应,并调用onToDestinationChangedlambda:

kotlin 复制代码
// home/SearchUserInput.kt file

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter

@Composable
fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {
    val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination")
    CraneEditableUserInput(
        state = editableUserInputState,
        caption = "To",
        vectorImageId = R.drawable.ic_plane
    )
    
    val currentOnDestinationChanged by rememberUpdatedState(onToDestinationChanged)
    LaunchedEffect(editableUserInputState) {
        snapshotFlow { editableUserInputState.text }
        .filter { !editableUserInputState.isHint }
        .collect {
            currentOnDestinationChanged(editableUserInputState.text)
        }
    }
}

我们之前已经使用LaunchedEffectrememberUpdatedState,但上面的代码还使用了一个新的API!我们使用snapshotFlowAPI将ComposeState<T>对象转换为Flow。当在snapshotFlow内读取的状态发生变化时,Flow会向收集器发出新值。在本例中,我们将状态转换为Flow,以使用Flow运算符的强大功能。这样,我们就可以在text不是hint时使用filter进行过滤,并使用collect收集发出的项,以通知父级当前的目的地发生了比那换。

七、DisposableEffect

当您点按某个目的地,系统会打开详情屏幕,您可以看到相应的城市在地图上的位置。该代码位于details/DetailsActivity.kt文件中。在CityMapView可组合项中,我们调用rememberMapViewWithLifecycle函数。如果您打开此函数,您会看到它未关联到任何生命周期!它只是记住MapView并对其调用onCreate

kotlin 复制代码
// details/MapViewUtils.kt file

@Composable
fun rememberMapViewWithLifecycle(): MapView {
    val context = LocalContext.current
    return remember {
        MapView(context).apply {
            id = R.id.map
            onCreate(Bundle())
        }
    }
}

虽然应用运行良好,但这也是一个问题,因为MapView未遵循正确的生命周期。因此,它不知道应用何时转至后台,View何时应暂停,等等。让我们来解决这一问题!

由于MapView是View而不是可组合项,因此我们希望它遵循使用它的Activity的生命周期,以及组合的生命周期。这意味着,我们需要创建一个LifecycleEventObserver来监听生命周期事件并在MapView上调用正确的方法。然后,我们需要将此观察期添加到当前activity的生命周期。

我们首先创建一个函数,该函数返回LifecycleEventObserver,在给定某个事件的情况下,它会在MapView中调用相应的方法:

kotlin 复制代码
// details/MapViewUtils.kt file

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

private fun getMapLigecycleObserver(mapView: MapView): LifecycleEventObserver = 
    LifecycleEventObserver { _, event ->
        when (evenr) {
            Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle())
            Lifecycle.Event.ON_START ->
            mapView.onStart()
            Lifecycle.Event.ON_RESUME -> mapView.onResume()
            Lifecycle.Event.ON_PAUSE -> mapView.onPause()
            Lifecycle.Event.ON_STOP -> mapView.onStop()
            Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
            else -> throw IllegalStateException()
        }
    }

现在,我们需要将次观察器添加到当前的生命周期,沃恩可以使用当前的LifecycleOwnerLocalLifecycleOwner组合局部函数来获取该生命周期。不过,仅仅添加观察器是不够的;我们还需要能够将其移出!我们需要一种附带效应,可以在效应退出组合时告知我们,以便我们可以执行一些清理代码。我们寻找的附带效应API是DisposableEffect

DisposableEffect适用于在键发生变化或可组合项退出组合后需要清理的附带效应。最终的rememberMapViewWithLifecycle代码正好起到这种作用。在项目中实现以下代码行:

kotlin 复制代码
// details/MapViewUtils.kt file

import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalLifecycleOwner

@Composable
fun rememberMapViewWithLifecycle(): MapView {
    val context = LocalContext.current
    val mapView = remember {
        MapView(context).apply {
            id = R.id.map
        }
    }
    
    val lifecycle = LocalLifecycleOwner.current.lifecycle
    DisposableEffect(key1 = lifecycle, key2 = mapView) {
        // Make MapView follow the current lifecycle
        val lifecycleObserver = getMapLifecycleObserver(mapView)
        lifecycle.addObserver(lifecycleObserver)
        onDispose {
           lifecycle.removeObserver(lifecycleObserver)
        }
    }
    
    return mapView
}

将观察器添加到了当前的lifecycle,只要当前的生命周期发生变化或者此可组合项退出组合,就将其移出。对于DisposableEffect中的key,如果lifecyclemapView发生变化,系统会移出观察器并再次将其添加到正确的lifecycle

通过我们刚刚所做的更改,MapView将始终遵循当前LifecycleOwnerlifecycle,并且其行为就像在View环境中使用它时一样。

您可以随意运行并打开详情屏幕,以确保MapView仍能正确呈现。这一步骤没有任何视觉变化。

八、produceState

在本部分,我们将改进详情屏幕的启动方式。details/DetailsActivity.kt文件中的DetailsScreen可组合项会从ViewModel同步获取cityDetails,并在结果成功时调用DetailsContent

不过,cityDetails在界面线程上的加载成本可能会变得越来越高,它可以使用协程将数据的加载工作移至其他线程。让我们来改进此代码,添加一个加载屏幕,并在数据准备就绪时显示DetailsContent

为屏幕状态建模的一种方法是使用以下类,它涵盖了所有的可能性:要在屏幕上显示的数据,以及加载和错误信号。将DetailsUiState类添加到DetailsActivity.kt文件:

kotlin 复制代码
// details/DetailsActivity.kt file

data class DetailsUiState(
    val cityDetails: ExploreModel? = null,
    val isLoading: Boolean = false,
    val throwError: Boolean = false
)

我们可以使用一个数据流(即DetailsUiState类型的StateFlow)映射屏幕需要显示的内容和ViewModel层中的UiState,ViewModel会在信息准备就绪时更新该数据流,而Compose会使用您已了解的collectAsStateWithLifecycle() API收集该数据流。

如果我们希望uiState映射逻辑移至Compose环境,我们可以使用produceStateAPI.

produceState可让您将非Compose状态转换为Compose状态。它会启动一个作用域限定为组合的协程,该协程可使用value属性将值推送到返回的State。与LaunchedEffect一样,produceState也采用键来取消和重新开始计算。

用于我们的用例,我们可以使用produceState发出初始值为DetailsUiState(isLoading = true)uiState更新,如下所示:

kotlin 复制代码
// details/DetailsActivity.kt file

import androidx.compose.runtime.produceState

@Composable
fun DetailScreen(
    onErrorLoading: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: DetailsViewModel = viewModel()
) {
    val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
        // In a coroutine, this can call suspend functions or move
        // the computation to different Dispatchers
        val cityDetailsResult = viewModel.cityDetails
        value = if (cityDetailsResult is Result.Success<ExploreModel>) {
            DetailsUiState(citiDetailsResult.data)
        } else {
            DetailsUiState(throwError = true)
        }
    }
}

接下来,根据uiState,我们会显示数据、显示加载屏幕或报告错误。下面是DetailsScreen可组合项的完整代码:

kotlin 复制代码
// details/DetailsActivity.kt file

import androidx.compose.foundation.layout.Box
import androidx.compose.material.CircularProgressIndicator

@Composable
fun DetailsScreen(
    onErrorLoading: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: DetailsViewModel = viewModel()
) {
    val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
        val cityDetailsResult = viewModel.cityDetails
        value = if (cityDetailsResult is Result.Success<ExploreModel>) {
            DetailsUiState(cityDetailsResult.data)
        } else {
            DetailsUiState(throwError = true)
        }
    }
    
    when {
        uiState.cityDetails != null -> {
            DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
        }
        uiState.isLoading -> {
            Box(modifier.fillMaxSize()) {
                CircularProgressIndicator(
                    color = MaterialTheme.colors.onSurface,
                    modifier = Modifier.align(Alignment.Center)
                )
            }
        }
        else -> { onErrorLoading() }
    }
}

如果您运行应用,您会看到在现实城市详情之前如何出现了指示"正在加载"的旋转图标。

九、derivedStateOf

我们要对Crane做最后一项改进是,每当您在航班目的地列表中滚动时,经过屏幕的第一个元素之后,就会显示一个用于"滚动至顶部"的按钮。点按该按钮会使您前往列表中的第一个元素。

打开包含此代码的base/ExploreSection.kt文件。ExploreSection可组合项对应于您在Scaffold的背景屏中看到的内容。

为了计算用户是否已经过第一项,我们可以使用LazyColumnLazyListState并检查listState.firstVisibleItemIndex > 0

简单实现如下所示:

vbnet 复制代码
// DO NOT DO THIS - It's executed on every recomposition
val showButton = listState.firstVisibleItemIndex > 0

该解决方案的效率并不高,因为每当fitstVisibleItemIndex发生变化时,读取showButton的可组合函数都会重组,这种情况经常会在滚动时发生。不过,我们希望该函数仅在条件在truefalse之间变化时重组。

有一个API可让我们做到这一点,那就是derivedStateOf API.

listState是一个可观察的ComposeState。我们的计算showButton也需要是一个ComposeState,因为我们希望界面在其值发生变化时重组,并显示或隐藏按钮。

当您想要的某个ComposeState衍生自另一个State时,请使用derivedStateOf。每当内部状态发生变化时,系统都会执行derivedStateOf计算块,但只有当计算结果与上一项不同时,可组合函数才会重组。这样可以最大限度地减少读取showButton的函数的重组次数。

在这种情况下,使用derivedStateOfAPI是一种更好且更高效的替代方案。我们还会使用rememberAPI来封装调用,因此计算得出的值在重组后继续有效。

kotlin 复制代码
// Show the button if the first visible item is past
// the first item. We use a remembered derived state to 
// minimize unnecessary recompositions
val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

您应该已经熟悉ExploreSection可组合项的新代码。我们使用Box将根据条件显示的Button放置在ExploreList的顶部。我们会使用rememberCoroutineScopeButtononClick回调内调用listState.scrollToItem挂起函数。

scss 复制代码
// base/ExploreSection.kt file

import androidx.compose.material.FloatingActionButton
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.foundation.layout.navigationBarsPadding
import kotlinx.coroutine.launch

@Composable
fun ExploreSection(
    modifier: Modifier = Modifier,
    title: String,
    exploreList: List<ExploreModel>,
    onItemClicked: OnExploreItemClicked
) {
    Surface(modifier = modifier.fillMaxSize(), color = Color.White, shape = BottomSheetShape) {
        Column(modifier = Modifier.padding(start = 24.dp, top = 20.dp, end = 24.dp)) {
          Text(
               text = title,
               style = MaterialTheme.typography.caption.copy(color = crane_caption)
          )
          Spacer(Modifier.height(8.dp))
          Box(Modifier.weight(1f)) {
              val listState = rememberLazyListState()
              ExploreList(exploreList, onItemClicked, listState = listState)
              
              // Show the button if the first visible item is past
              // the first item. We use a remembered derived state to 
              // minimize unnecessary compositons
              val showButton by remember {
                  derivedStateOf {
                      listState.firstVisibleItemIndex > 0
                  }
              }
              if (showButton) {
                  val coroutineScope = rememberCoroutineScope()
                  FloatingActionButton(
                      background = MaterialTheme.colors.primary,
                      modifier = Modifier
                          .align(Alignment.BottomEnd)
                          .navigationBarsPadding(),
                          padding(bottom = 8.dp),
                          onClick = {
                              coroutineScope.launch {
                                  listState.scrollToItem(0)
                              }
                          }
                  ) {
                      Text("Up!")
                  }
              }
          }
        }
    }
}
相关推荐
zhangphil12 小时前
Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现“刮刮乐”效果,Kotlin(2)
android·kotlin
开发者阿伟13 小时前
Android Jetpack DataBinding源码解析与实践
android·android jetpack
居居飒1 天前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
刘争Stanley2 天前
如何高效调试复杂布局?Layout Inspector 的 Toggle Deep Inspect 完全解析
android·kotlin·android 15·黑屏闪屏白屏
sickworm陈浩2 天前
Java 转 Kotlin 系列:究竟该不该用 lateinit?
android·kotlin
droidHZ4 天前
Compose Multiplatform 之旅—声明式UI
android·kotlin
zhangphil4 天前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆角矩形实现,Kotlin(1)
android·kotlin
alexhilton6 天前
Android技巧:学习使用GridLayout
android·kotlin·android jetpack
zhangphil7 天前
Android使用PorterDuffXfermode的模式PorterDuff.Mode.SRC_OUT实现橡皮擦,Kotlin(1)
android·kotlin
IH_LZH8 天前
OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用
android·java·okhttp·kotlin