web-dev-qa-db-fra.com

ViewModel récupère les données lorsque le fragment est recréé

J'utilise navigation inférieure avec composant d'architecture de navigation . Lorsque l'utilisateur navigue d'un élément à un autre (via la navigation en bas) et vice-versa, affichez la fonction de référentiel d'appels du modèle pour récupérer à nouveau les données. Donc, si l'utilisateur va et vient 10 fois, les mêmes données seront récupérées 10 fois. Comment éviter de récupérer à nouveau lorsque le fragment est recréé des données sont déjà là?.

Fragment

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)

Référentiel

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

Navigation 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)
        }
    }
}

enter image description here

6
Nux

Dans onActivityCreated(), vous appelez getData(). Là-bas, vous avez:

productsViewModel.setInput(productsFilters, 2)

Cela, à son tour, modifie la valeur de _input Dans votre ProductsViewModel. Et, chaque fois que _input Change, l'expression getProducts lambda sera évaluée, appelant votre référentiel.

Ainsi, chaque appel à onActivityCreated() déclenche un appel à votre référentiel.

Je ne connais pas suffisamment votre application pour vous dire ce que vous devez changer. Voici quelques possibilités:

  • Passez de onActivityCreated() à d'autres méthodes de cycle de vie. initViewModel() pourrait être appelée dans onCreate(), tandis que le reste devrait être dans onViewCreated().

  • Reconsidérez votre implémentation de getData(). Avez-vous vraiment besoin d'appeler setInput() chaque fois que nous naviguons vers ce fragment? Ou, cela devrait-il faire partie de initViewModel() et être fait une fois dans onCreate()? Ou, puisque productsFilters ne semble pas du tout lié au fragment, doit-il y avoir productsFilters et l'appel setInput() au bloc init de ProductsViewModel, donc ça n'arrive qu'une seule fois?

1
CommonsWare

J'ai eu le même problème. Mon application utilise également la navigation inférieure avec le composant d'architecture de navigation JetPack. J'utilise une conception d'activité unique avec plusieurs fragments comme destinations. Chaque fragment a son propre ViewModel et MainActivity a également un SharedViewModel.

Le bloc d'initialisation du ViewModel de l'un de mes fragments récupère les données du réseau. Le problème était que lorsque je suis passé à un autre fragment via la barre de navigation inférieure, le fragment s'est détaché de MainActivity et le ViewModel a été effacé. Lorsque je suis revenu, le ViewModel a été recréé et le bloc d'initialisation a de nouveau été exécuté pour récupérer de nouvelles données réseau. J'ai résolu cela en utilisant une préférence partagée et le SharedViewModel. Étant donné que le SharedViewmodel est lié à la MainActivity et non à l'un des fragments, il n'est pas effacé. Mon code est ci-dessous. Je suis nouveau sur Android donc je ne sais pas si cette solution hacky est une bonne pratique, mais cela fonctionne. Assurez-vous d'étendre à partir d'AndroidViewModel au lieu de ViewModel car vous avez besoin du contexte d'application pour obtenir l'accès au SharedPreferenceManager.

Le bloc d'initialisation du ViewModel de mon fragment:

init {
    if (preferenceManager.getBoolean("appfirstlaunched", true)) {
        viewModelScope.launch {
            photoRepository.refreshPhotos() //kotlin coroutine function
            preferenceManager.edit().putBoolean("appfirstlaunched", false).apply()
        }
   }
}

Et puis remplacez la méthode onCleared () dans votre SharedViewModel:

override fun onCleared() {
    super.onCleared()
    PreferenceManager.getDefaultSharedPreferences(getApplication()).edit().putBoolean("appfirstlaunched", true).apply()

Dans votre settings_prefences.xml, créez un SwitchPreference invisible comme ceci:

<SwitchPreferenceCompat
        Android:defaultValue="true"
        app:key="appfirstlaunched"
        app:persistent="true"
        app:isPreferenceVisible="false"
        />

De cette façon, la première fois que l'application lance la valeur est "true" et le bloc init de votre fragment ViewModel s'exécute, la valeur est définie sur "false" et reste "false" jusqu'à ce que le SharedViewModel soit effacé. Cela se produit généralement lorsque l'activité est entièrement détruite et que votre utilisateur attend de nouvelles données lors du retour à l'application.

0
Rvb84