简体   繁体   English

Jetpack Compose - 在配置更改时保留 AndroidView 的 state

[英]Jetpack Compose - preserve state of AndroidView on configuration change

Most likely a newbie question, as i'm fairly new to Android dev - I am having troubles preserving the state of AndroidView in my @Composable on configuration change/navigation, as factory block is called (as expected) and my chart gets reinstantiated.很可能是一个新手问题,因为我对 Android 开发人员还很陌生 - 我在配置更改/导航时在 @Composable 中保留 AndroidView 的 state 时遇到了麻烦,因为调用了工厂块(如预期的那样)并且我的图表被重新实例化。

@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")
        })
}

The DataChart is a 3rd party component with a complex chart, i would like to preserve the zoom/scrolling state. DataChart是具有复杂图表的第 3 方组件,我想保留缩放/滚动 state。 I know i can use ViewModel to preserve UI state across conf.我知道我可以使用 ViewModel 在 conf 中保留 UI state。 changes, but given the complexity of saving zoom/scrolling state, i'd like to ask if there is any other easier approach to achieve this?更改,但考虑到保存缩放/滚动 state 的复杂性,我想问是否还有其他更简单的方法来实现这一点?

I tried to move the whole chart instance to viewModel, but as it's using context i get a warning about context object leaks.我试图将整个图表实例移动到 viewModel,但是当它使用上下文时,我收到有关上下文 object 泄漏的警告。

Any help would be appreciated!任何帮助,将不胜感激!

If you want to preserve state of AndroidView on configuration change, Use rememberSaveable instead remember .如果要在配置更改时保留 AndroidView 的 state,请使用rememberSaveable代替remember

While remember helps you retain state across recompositions, the state is not retained across configuration changes.虽然记住可以帮助您在重新组合时保留 state,但不会在配置更改时保留 state。 For this, you must use rememberSaveable .为此,您必须使用rememberSaveable rememberSaveable automatically saves any value that can be saved in a Bundle. rememberSaveable自动保存任何可以保存在 Bundle 中的值。 For other values, you can pass in a custom saver object.对于其他值,您可以传入自定义保护程序 object。

If the DataChart is a type of Parcelable, Serializable or other data types that can be stored in bundle:如果 DataChart 是 Parcelable、Serializable 或其他可以存储在 bundle 中的数据类型:

val chart = rememberSaveable { DataChart(context) }

If the above way not working then create a MapSave to save data, zoom/scrolling states... I assume the DataChart has zoomIndex, scrollingIndex, values properties which your need to save:如果上述方式不起作用,则创建一个 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]
            } 
        }
    )
}

Use:利用:

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

See more Ways to store state查看更多存储方式 state

I'd say your instincts were correct to move the chart instance into the view model, but, as you noted, context dependencies can become a hassle when they are required for objects other than views.我想说你的直觉是正确地将图表实例移动到视图 model,但是,正如你所指出的,当视图以外的对象需要上下文依赖关系时,它们可能会变得很麻烦。 To me, this becomes a question of dependency injection where the dependency is the context or, in a broader sense, the entire data chart.对我来说,这变成了依赖注入的问题,其中依赖是上下文,或者更广泛地说,是整个数据图表。 I'd be interested in knowing how you source your view model, but I'll assume it relies on an Android view model provider (via by viewModels() or some sort of ViewModelProvider.Factory ).我很想知道您如何获取您的视图 model,但我假设它依赖于 Android 视图 model 提供程序(通过ViewModelProvider.Factory by viewModels()或某种排序提供程序。

An immediate solution to this issue is to convert the view model into a subclass of an AndroidViewModel which provides reference to the application context via the view model's constructor.此问题的直接解决方案是将视图 model 转换为AndroidViewModel的子类,该子类通过视图模型的构造函数提供对应用程序上下文的引用。 While it remains an anti-pattern and should used sparingly, the Android team has recognized certain use cases to be valid.虽然它仍然是一种反模式并且应该谨慎使用,但 Android 团队已经认识到某些用例是有效的。 I personally do not use the AndroidViewModel because I believe it to be a crude solution to a problem which could otherwise be solved with refinements to the dependency graph.我个人不使用AndroidViewModel ,因为我认为它是一个粗略的解决方案,否则可以通过改进依赖图来解决。 However, it's sanctioned by the official documentation, and this is only my personal opinion.但是,它是由官方文档批准的,这只是我的个人意见。 From experience, I must say its use makes testing a view model quite a nightmare after-the-fact.根据经验,我必须说它的使用使得测试视图 model 成为事后的噩梦。 If you are interested in a dependency injection library, I'd highly recommend the new Hilt implementation which recently launched a stable 1.0.0 release just this past month.如果你对依赖注入库感兴趣,我强烈推荐新的Hilt实现,它最近在上个月发布了一个稳定的1.0.0版本。

With this aside, I'll now provide two possible solutions to your predicament: one which utilizes the AndroidViewModel and another which does not.除此之外,我现在将为您的困境提供两种可能的解决方案:一种使用AndroidViewModel ,另一种不使用。 If your view model already has other dependencies outside of the context, the AndroidViewModel solution won't save you much overhead as you'd likely already be instantiating a ViewModelProvider.Factory at some point.如果您的视图 model 已经具有上下文之外的其他依赖项,则AndroidViewModel解决方案不会为您节省太多开销,因为您可能已经在某个时候实例化了ViewModelProvider.Factory These solutions will be considering the scope of an Android Fragment but could easily be implemented in an Activity or DialogFragment as well with some tweaks to lifecycle hooks and whatnot.这些解决方案将考虑 Android Fragment的 scope,但也可以很容易地在ActivityDialogFragment中实现,并对生命周期挂钩等进行一些调整。

With AndroidViewModel使用AndroidViewModel

import android.app.Application
import androidx.lifecycle.AndroidViewModel

class MyViewModel(application: Application) : AndroidViewModel(application) {

    val dataChart: DataChart

    init {
        dataChart = DataChart(application.applicationContext)
    }
}

where the fragment could be片段可能在哪里

class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View { ... }
}

Without AndroidViewModel没有AndroidViewModel

import androidx.lifecycle.ViewModel

class MyViewModel(args: Args) : ViewModel() {

    data class Args(
        val dataChart: DataChart
    )

    val dataChart: DataChart = args.dataChart
}

where the fragment could be片段可能在哪里

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

        ...
    }
}

and where ArgsViewModelFactory is a creation of my own as shown below其中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,
    )
}

edit (via Hilt module):编辑(通过 Hilt 模块):

@Module
@InstallIn(...)
object DataChartModule {

    @Provides
    fun provideDataChart(
        @ApplicationContext context: Context,
    ): DataChart = DataChart(context)
}

Here's the simplest way I know of.这是我所知道的最简单的方法。 This keeps the state and doesn't trigger a reload on the WebView when I rotate my phone.这会保留 state 并且不会在我旋转手机时触发 WebView 的重新加载。 It should work with every View.它应该适用于每个视图。

First create an application class:首先创建一个应用程序 class:

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!!
    }
}

Then add the application class to manifest.xml然后将应用程序 class 添加到manifest.xml

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

Finally add this to the composable最后将其添加到可组合

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

That's it.而已。 I'm not sure if this can lead to memory leaks but I haven't had problems yet.我不确定这是否会导致 memory 泄漏,但我还没有遇到问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM