ViewModel обновляет данные при воссоздании фрагмента

Я использую Нижняя навигация с Компонент архитектуры навигации. Когда пользователь переходит от одного элемента к другому (через нижнюю навигацию) и обратно, просмотрите функцию репозитория вызова модели, чтобы снова получить данные. Таким образом, если пользователь переходит туда и обратно 10 раз, одни и те же данные будут получены 10 раз. Как избежать повторной выборки, когда фрагмент пересоздается, данные уже есть?

Фрагмент

class HomeFragment : Fragment() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    private lateinit var productsViewModel: ProductsViewModel
    private lateinit var productsAdapter: ProductsAdapter

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_home, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        initViewModel()
        initAdapters()
        initLayouts()
        getData()
    }

    private fun initViewModel() {
        (activity!!.application as App).component.inject(this)

        productsViewModel = activity?.run {
            ViewModelProviders.of(this, viewModelFactory).get(ProductsViewModel::class.java)
        }!!
    }

    private fun initAdapters() {
        productsAdapter = ProductsAdapter(this.context!!, From.HOME_FRAGMENT)
    }

    private fun initLayouts() {
        productsRecyclerView.layoutManager = LinearLayoutManager(this.activity)
        productsRecyclerView.adapter = productsAdapter
    }

    private fun getData() {
        val productsFilters = ProductsFilters.builder().sortBy(SortProductsBy.NEWEST).build()

        //Products filters
        productsViewModel.setInput(productsFilters, 2)

        //Observing products data
        productsViewModel.products.observe(viewLifecycleOwner, Observer {
            it.products()?.let { products -> productsAdapter.setData(products) }
        })

        //Observing loading
        productsViewModel.networkState.observe(viewLifecycleOwner, Observer {
            //Todo showing progress bar
        })
    }
}

ViewModel

class ProductsViewModel
@Inject constructor(private val repository: ProductsRepository) : ViewModel() {

    private val _input = MutableLiveData<PInput>()

    fun setInput(filters: ProductsFilters, limit: Int) {
        _input.value = PInput(filters, limit)
    }

    private val getProducts = map(_input) {
        repository.getProducts(it.filters, it.limit)
    }

    val products = switchMap(getProducts) { it.data }
    val networkState = switchMap(getProducts) { it.networkState }
}

data class PInput(val filters: ProductsFilters, val limit: Int)

Репозиторий

@Singleton
class ProductsRepository @Inject constructor(private val api: ApolloClient) {

    val networkState = MutableLiveData<NetworkState>()

    fun getProducts(filters: ProductsFilters, limit: Int): ApiResponse<ProductsQuery.Data> {
        val products = MutableLiveData<ProductsQuery.Data>()

        networkState.postValue(NetworkState.LOADING)

        val request = api.query(ProductsQuery
                .builder()
                .filters(filters)
                .limit(limit)
                .build())

        request.enqueue(object : ApolloCall.Callback<ProductsQuery.Data>() {
            override fun onFailure(e: ApolloException) {
                networkState.postValue(NetworkState.error(e.localizedMessage))
            }

            override fun onResponse(response: Response<ProductsQuery.Data>) = when {
                response.hasErrors() -> networkState.postValue(NetworkState.error(response.errors()[0].message()))
                else -> {
                    networkState.postValue(NetworkState.LOADED)
                    products.postValue(response.data())
                }
            }
        })

        return ApiResponse(data = products, networkState = networkState)
    }
}

Навигация main.xml

<?xml version = "1.0" encoding = "utf-8"?>
<navigation xmlns:android = "http://schemas.android.com/apk/res/android"
    xmlns:app = "http://schemas.android.com/apk/res-auto"
    xmlns:tools = "http://schemas.android.com/tools"
    android:id = "@+id/mobile_navigation.xml"
    app:startDestination = "@id/home">

    <fragment
        android:id = "@+id/home"
        android:name = "com.nux.ui.home.HomeFragment"
        android:label = "@string/title_home"
        tools:layout = "@layout/fragment_home"/>
    <fragment
        android:id = "@+id/search"
        android:name = "com.nux.ui.search.SearchFragment"
        android:label = "@string/title_search"
        tools:layout = "@layout/fragment_search" />
    <fragment
        android:id = "@+id/my_profile"
        android:name = "com.nux.ui.user.MyProfileFragment"
        android:label = "@string/title_profile"
        tools:layout = "@layout/fragment_profile" />
</navigation>

ViewModelFactory

@Singleton
class ViewModelFactory @Inject
constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val creator = viewModels[modelClass]
                ?: viewModels.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
                ?: throw IllegalArgumentException("unknown model class $modelClass")
        return try {
            creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

ViewModel обновляет данные при воссоздании фрагмента

Можете ли вы рассказать, как вы ViewModelProvider.Factory создаете экземпляры?

Jeel Vankhede 30.05.2019 14:46

@JeelVankhede Я добавил это.

Nux 30.05.2019 14:49

Поставьте точку останова на repository.getProducts(it.filters, it.limit) и посмотрите, когда она будет вызвана. Я предполагаю, что компонент навигации создает 10 экземпляров HomeFragment в вашем сценарии, и каждый из них получит свою собственную модель представления.

CommonsWare 30.05.2019 14:50

Хорошо, одним из решений было бы избегать получение данных из фрагмента и извлекать его при действии один раз вместо (Поскольку вы уже используете контекст деятельности для ViewModelProviders), а затем наблюдать за ним, используя LiveData внутри фрагмента.

Jeel Vankhede 30.05.2019 14:58

@JeelVankhede У меня есть много фрагментов, каждому из которых в какой-то момент понадобится определенный объем данных. Поэтому я избегаю получения дополнительных данных, которые пользователю никогда не понадобятся. И я использую GraphQL на сервере, я думаю, что одна из причин, по которой он был создан, — это предотвращение избыточной выборки данных.

Nux 30.05.2019 15:06

Эта строка мне кажется разнообразный: попробуйте ViewModelProviders.of(this@run, viewModelFactory).get(ProductsViewModel::class.java)

Jeel Vankhede 30.05.2019 15:14

@CommonsWare это repository.getProducts(it.filters, it.limit) вызывается каждый раз, когда я вижу фрагмент. Это мой первый раз, когда я использую точку останова, поэтому я не знаю, использую ли я ее коллективно, но я могу просто сказать, что она вызывается, потому что при запуске приложения поток приостанавливается, пока я не нажму кнопку воспроизведения (этот фрагмент находится дома). Когда я перехожу к другому фрагменту и возвращаюсь, поток снова приостанавливается. надеюсь правильно сделал! Так что это значит? заранее спасибо

Nux 30.05.2019 15:29

«Когда я перехожу к другому фрагменту и возвращаюсь, поток снова приостанавливается» — в отладчике вы увидите стек вызовов, когда ваша точка останова сработает (по умолчанию она находится в левой части инструмента «Отладка»). Посмотрите, какие строки вашего кода вызывают срабатывание точки останова. Если это связано с тем, что каждый раз создается новый экземпляр ProductsViewModel, то я думаю, что моя предыдущая догадка верна: компонент навигации создает 10 экземпляров HomeFragment в вашем сценарии, и каждый получит свою собственную модель представления.

CommonsWare 30.05.2019 15:39

@JeelVankhede Спасибо за ответ. Но это не помогает. Тем не менее данные обновляются.

Nux 30.05.2019 15:39

@CommonsWare Я добавил скриншот отладчика, пожалуйста, посмотрите, если я что-то упустил. Я перемещаюсь 4 раза и получил 8 обращений. Мое наблюдение: setInput в productsViewModel, getData в HomeFragment вызывает его.

Nux 30.05.2019 15:56
13
10
9 798
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

В onActivityCreated() вы звоните getData(). Там у вас есть:

productsViewModel.setInput(productsFilters, 2)

Это, в свою очередь, изменяет значение _input в вашем ProductsViewModel. И каждый раз, когда _input изменяется, лямбда-выражение getProducts будет оцениваться, вызывая ваш репозиторий.

Таким образом, каждый вызов onActivityCreated() вызывает вызов вашего репозитория.

Я недостаточно знаю о вашем приложении, чтобы сказать вам, что вам нужно изменить. Вот некоторые возможности:

  • Переключитесь с onActivityCreated() на другие методы жизненного цикла. initViewModel() можно вызвать в onCreate(), а остальные должны быть в onViewCreated().

  • Пересмотрите свою getData() реализацию. Вам действительно нужно вызывать setInput() каждый раз, когда мы переходим к этому фрагменту? Или это должно быть частью initViewModel() и делаться один раз в onCreate()? Или, поскольку productsFilters, похоже, вообще не привязан к фрагменту, должны ли productsFilters и вызов setInput() быть частью init блока ProductsViewModel, чтобы это произошло только один раз?

Что мне использовать в этой строке кода productsViewModel.products.observe(--WHAT-HERE--,....** this** или this.activity или **viewLifecycleOwner?

Nux 30.05.2019 16:11

@Nux: это останется там, где оно есть. Однако первые два оператора в getData() (объявление productsFilters и вызов setInput() не зависят от фрагмента, поэтому они могут стать частью блока init модели представления.

CommonsWare 30.05.2019 16:15

Я сделал, как вы упомянули. Спасибо, теперь нет повторной выборки. Единственная оставшаяся проблема заключается в том, что ViewLifecycleOwner, когда он упоминается в onCreate, вызывает сбой приложения. Я изменил его на это, и когда я перехожу от одного элемента к другому в навигационном меню, все в порядке. Но когда я нажимаю название продукта, чтобы перейти к фрагменту сведений о продукте (через указания, реализованные в редакторе навигации), и возвращаюсь, данные теряются. Что может быть причиной этого?

Nux 30.05.2019 16:38

@Nux: понятия не имею, извини. Вы можете задать отдельный вопрос о переполнении стека, показать текущий код и объяснить там свои симптомы.

CommonsWare 30.05.2019 16:41

@Nux Разве onCreate не вызывается каждый раз, когда вы возвращаетесь к своему фрагменту ?? Я сталкиваюсь с похожей ситуацией.

Alok Bharti 28.05.2020 09:32

@AlokBharti Я уже давно закончил приложение, в котором была эта проблема, я думаю, что сейчас у меня нет точного ответа на ваш вопрос.

Nux 28.05.2020 17:59

Если возможно, вы можете помочь мне здесь? Как вам удается предотвратить вызов функции модели представления, которая содержит вызов API, всякий раз, когда вы возвращаетесь к фрагменту??

Alok Bharti 28.05.2020 18:09

Когда вы выбираете другие страницы с помощью нижней навигации и возвращаетесь, фрагмент уничтожается и создается заново. Итак, onCreate, onViewCreated и onActivityCreate снова побежит. Но viewModel все еще жив.

Таким образом, вы можете вызвать свою функцию (получитьПродукты) внутри "в этом" в модели представления, чтобы запустить ее один раз.

init {
        getProducts()
    }

Это практически возможно во всех случаях, предположим, что вы передали параметр фрагменту, множественный сетевой вызов :(. Код становится более грязным.

Code_Life 22.06.2020 20:28

Одним из простых решений было бы изменить владельца ViewModelProvider с это на требуется активность () в этой строке кода:

ViewModelProviders.of(this, viewModelFactory).get(ProductsViewModel::class.java)

Поэтому, поскольку действие является владельцем модели представления, а жизненный цикл модели представления привязан к действию, а не к фрагменту, навигация между фрагментами внутри действия не приведет к воссозданию модели представления.

определите свою ProductsViewModel с помощью статики в mainActivity и инициализируйте в методе onCreate. Теперь просто используйте это во фрагменте:

MainActivity.productsViewModel.products.observe(viewLifecycleOwner, Observer {
            it.products()?.let { products -> productsAdapter.setData(products) }
        })

Другие вопросы по теме