【问题标题】:Jetpack Compose - preserve state of AndroidView on configuration changeJetpack Compose - 在配置更改时保留 AndroidView 的状态
【发布时间】:2021-10-27 19:48:19
【问题描述】:

很可能是一个新手问题,因为我对 Android 开发人员还很陌生 - 我在配置更改/导航时在 @Composable 中保留 AndroidView 的状态时遇到了麻烦,因为工厂块被调用(如预期的那样)和我的图表被重新实例化。

@Composable
fun ChartView(viewModel:ViewModel, modifier:Modifier){
    val context = LocalContext.current
    val chart = remember { DataChart(context) }

    AndroidView(
        modifier = modifier,
        factory = { context ->
            Log.d("DEBUGLOG", "chart init")
            chart
        },
        update = { chart ->
            Log.d("DEBUGLOG", "chart update")
        })
}

DataChart 是具有复杂图表的第 3 方组件,我想保留缩放/滚动状态。我知道我可以使用 ViewModel 在 conf 中保留 UI 状态。变化,但考虑到保存缩放/滚动状态的复杂性,我想问一下是否有其他更简单的方法来实现这一点?

我试图将整个图表实例移动到 viewModel,但由于它使用上下文,我收到有关上下文对象泄漏的警告。

任何帮助将不胜感激!

【问题讨论】:

  • 我刚刚更新了我的答案,请检查并给我反馈:)

标签: android android-jetpack-compose


【解决方案1】:

如果您想在配置更改时保留 AndroidView 的状态,请使用 rememberSaveable 而不是 remember

虽然 remember 可以帮助您在重新组合时保留状态,但状态 不会在配置更改中保留。为此,您必须使用 rememberSaveablerememberSaveable 自动保存任何值 可以保存在一个Bundle中。对于其他值,您可以传入自定义 保护对象。

如果DataChart是Parcelable、Serializable或其他可以存储在bundle中的数据类型:

val chart = rememberSaveable { DataChart(context) }

如果上述方法不起作用,则创建一个 MapSave 来保存数据、缩放/滚动状态...我假设 DataChart 具有您需要保存的 zoomIndex、scrollingIndex、values 属性:

fun getDataChartSaver(context: Context) = run {
    val zoomKey = "ZoomState"
    val scrollingKey = "ScrollingState"
    val dataKey = "DataState"

    mapSaver(
        save = { mapOf(zoomKey to it.zoomIndex, scrollingKey to it.scrollingIndex, dataKey to it.values) },
        restore = { 
            DataChart(context).apply{
               zoomIndex = it[zoomKey]
               scrollingIndex = it[scrollingKey]
               values = it[dataKey]
            } 
        }
    )
}

用途:

val chart = rememberSaveable(stateSaver = getDataChartSaver(context)){ DataChart(context) }

查看更多Ways to store state

【讨论】:

  • 感谢您的建议。 DataChart 可以根据数据变得相当大(支持多个数据集等),所以我不确定通过 rememberSaveable 将它保存在 Bundle() 中是否是一种好方法。我将研究自定义保护程序,这可能是一个更好的选择就我而言。
  • 几乎没有提供高级功能的第三方组件是可序列化的,所以这个解决方案有点没用。
  • 我收到一个编译时错误Unresolved reference: mapSaver。有没有必要的依赖?
  • 您签出此文档developer.android.com/jetpack/compose/state#ways-to-store 了吗?我认为没有必要添加任何依赖项。
【解决方案2】:

这是我所知道的最简单的方法。当我旋转手机时,这会保持状态并且不会触发 WebView 上的重新加载。它应该适用于每个视图。

首先创建一个应用类

class MainApplication : Application() {

    private var view: WebView? = null

    override fun onCreate() {
        super.onCreate()
        LOG("started application")
    }

    fun loadView(context: Context): WebView {
        if (view == null) {
            view = WebView(context)
            //init your view here
        }
        return view!!
    }
}

然后将应用类添加到manifest.xml

<manifest>
    ...
    <application
        android:name=".MainApplication"
    ...

最后将这个添加到可组合中

AndroidView(modifier = Modifier.fillMaxSize(), factory = { context ->
    val application = context.applicationContext as MainApplication
    application.loadView(context = context)
})

就是这样。我不确定这是否会导致内存泄漏,但我还没有遇到问题。

【讨论】:

    【解决方案3】:

    我想说你的直觉是正确地将图表实例移动到视图模型中,但是,正如你所指出的,当视图以外的对象需要上下文依赖关系时,它们可能会变得很麻烦。对我来说,这变成了依赖注入的问题,其中依赖是上下文,或者更广泛地说,是整个数据图表。我很想知道您如何获取视图模型,但我假设它依赖于 Android 视图模型提供程序(通过by viewModels() 或某种ViewModelProvider.Factory)。

    此问题的直接解决方案是将视图模型转换为AndroidViewModel 的子类,它通过视图模型的构造函数提供对应用程序上下文的引用。虽然它仍然是一种反模式并且应该谨慎使用,但 Android 团队已经认识到某些用例是有效的。我个人不使用AndroidViewModel,因为我相信它是一个粗略的解决方案,否则可以通过改进依赖图来解决。但是,它是由官方文档批准的,这只是我个人的看法。根据经验,我必须说它的使用使测试视图模型成为事后的噩梦。如果您对依赖注入库感兴趣,我强烈推荐新的Hilt 实现,它最近在上个月发布了稳定的1.0.0 版本。

    除此之外,我现在将为您的困境提供两种可能的解决方案:一种使用AndroidViewModel,另一种不使用。如果您的视图模型在上下文之外已经有其他依赖项,那么AndroidViewModel 解决方案不会为您节省太多开销,因为您可能已经在某个时候实例化了ViewModelProvider.Factory。这些解决方案将考虑 Android Fragment 的范围,但可以轻松地在 ActivityDialogFragment 中实现,以及对生命周期挂钩等进行一些调整。

    AndroidViewModel

    import android.app.Application
    import androidx.lifecycle.AndroidViewModel
    
    class MyViewModel(application: Application) : AndroidViewModel(application) {
    
        val dataChart: DataChart
    
        init {
            dataChart = DataChart(application.applicationContext)
        }
    }
    

    片段可能在哪里

    class MyFragment : Fragment() {
    
        private val viewModel: MyViewModel by viewModels()
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View { ... }
    }
    

    没有AndroidViewModel

    import androidx.lifecycle.ViewModel
    
    class MyViewModel(args: Args) : ViewModel() {
    
        data class Args(
            val dataChart: DataChart
        )
    
        val dataChart: DataChart = args.dataChart
    }
    

    片段可能在哪里

    class MyFragment : Fragment() {
    
        private lateinit var viewModel: MyViewModel
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            val applicationContext: Context = requireContext().applicationContext
            val dataChart = DataChart(applicationContext)
    
            val viewModel: MyViewModel by viewModels {
                ArgsViewModelFactory(
                    args = MyViewModel.Args(
                        dataChart = dataChart,
                    ),
                    argsClass = MyViewModel.Args::class.java,
                )
            }
    
            this.viewModel = viewModel
    
            ...
        }
    }
    

    ArgsViewModelFactory 是我自己的创作,如下所示

    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.ViewModelProvider
    
    class ArgsViewModelFactory<T>(
        private val args: T,
        private val argsClass: Class<T>,
    ) : ViewModelProvider.Factory {
    
        override fun <T : ViewModel?> create(modelClass: Class<T>): T = modelClass.getConstructor(
            argsClass,
        ).newInstance(
            args,
        )
    }
    

    编辑(通过 Hilt 模块):

    @Module
    @InstallIn(...)
    object DataChartModule {
    
        @Provides
        fun provideDataChart(
            @ApplicationContext context: Context,
        ): DataChart = DataChart(context)
    }
    

    【讨论】:

    • 感谢您的广泛回复和宝贵的见解。我将 Hilt(Dagger) 用于 DI 并且没有片段(纯一个带有嵌套导航的活动组合应用程序),我将尝试应用您建议的一些概念。
    • 太酷了!如果您使用 Hilt,请查看 @ApplicationContext 注释文档。您应该能够直接注入 DataChart 的实例,因为 Hilt 提供了一种简单的方法来引用应用程序上下文。我将编辑我的答案以表明它可能是什么样子。
    • 非常感谢。我一般担心这种方法,因为通常不建议在视图模型中注入上下文,因为它可能导致(如 android studio 本身所示)内存泄漏。
    • 当 DataChart 引用上下文时,它引用的是应用程序上下文,它是一个生命周期与应用程序相同的单例。这与传递活动或某种形式的活动上下文不同。据我所知,这永远不会导致内存泄漏,这就是 Android 团队自己引入 AndroidViewModel 的原因。
    • 在功能上,使用 Hilt 的 @ApplicationContext 注释与使用 AndroidViewModel 相同。虽然检查泄漏是一个好主意,但我相信这是一个预期的用例,而且我过去曾采用这种方法将 AppUpdateManager 注入 Fragment 而不发生泄漏。
    猜你喜欢
    • 1970-01-01
    • 2022-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多