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!")
                  }
              }
          }
        }
    }
}
相关推荐
xvch1 小时前
Kotlin 2.1.0 入门教程(二十五)类型擦除
android·kotlin
l软件定制开发工作室10 小时前
Jetpack Architecture系列教程之(一)——Jetpack介绍
android jetpack
有点感觉1 天前
Android级联选择器,下拉菜单
kotlin
zhangphil2 天前
Android Coil3缩略图、默认占位图placeholder、error加载错误显示,Kotlin(1)
android·kotlin
xvch2 天前
Kotlin 2.1.0 入门教程(二十三)泛型、泛型约束、协变、逆变、不变
android·kotlin
xvch4 天前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
zhangphil4 天前
Android Coil ImageLoader MemoryCache设置Key与复用内存缓存,Kotlin
android·kotlin
mmsx4 天前
kotlin Java 使用ArrayList.add() ,set()前面所有值被 覆盖 的问题
android·开发语言·kotlin
lavins4 天前
android studio kotlin项目build时候提示错误 Unknown Kotlin JVM target: 21
jvm·kotlin·android studio
面向未来_4 天前
JAVA Kotlin Androd 使用String.format()格式化日期
java·开发语言·kotlin