web-dev-qa-db-fra.com

Android composant de navigation avec des modèles de vue partagée

Un modèle de vue vit et meurt avec une activité ou le fragment auquel il est attaché. Cela a certaines ramifications que je ne comprends pas pourquoi personne ne demande (si nous intégrons l'architecture de navigation dans l'image).

Selon les derniers Android et le fonctionnement du framework de navigation, il est recommandé d'aller dans le verset Single Fragments Multiple Activity.

Soi-disant j'ai la conception d'application suivante.

Activity A (Application Entry Point)
----------
Fragment (A) (Uses ViewModel AB)
Fragment (B) (Uses ViewModel AB)
Fragment (C) (Uses ViewModel CDE)
Fragment (D) (Uses ViewModel CDE)
Fragment (E) (Uses ViewModel CDE)

Maintenant que j'utilise des modèles de vue partagés, cela signifie que mes modèles de vue seraient attachés à l'activité. Cependant, cela semble fuir. Comme si j'avais parcouru tout le chemin de A à E et que je revenais maintenant en éclatant des fragments vers le fragment B, le viewmodel CDE devrait être détruit, mais ce ne sera pas le cas car il est connecté à l'activité.

De plus, nous ne pouvons pas connecter nos modèles de vue au fragment car nous allons partager leurs données.

Le seul fait que je soulève cette question me fait croire que je me trompe sur ma compréhension. Serait ravi si je pouvais avoir un bon aperçu de la situation.

10

Chaque LifecycleOwner (c'est-à-dire un fragment ou une activité) conserve ses modèles dans un ViewModelStore qui a une fonction clear(). Cependant, l'effacement balaye tous les modèles du ViewModelStore qui, dans votre cas, ne sont pas souhaitables (ViewModel AB et ViewModel CDE seront effacés du ViewModelStore de l'activité). Une solution possible à ce problème consiste à disposer d'un magasin par ViewModel qui peut être effacé en toute sécurité si nécessaire:

class MainActivity : AppCompatActivity() {

val individualModelStores = HashMap<KClass<out ViewModel>, ViewModelStore>()

inline fun <reified VIEWMODEL : ViewModel> getSharedViewModel(): VIEWMODEL {
    val factory = object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            //Put your existing ViewModel instantiation code here,
            //e.g., dependency injection or a factory you're using
            //For the simplicity of example let's assume
            //that your ViewModel doesn't take any arguments
            return modelClass.newInstance()
        }
    }

    val viewModelStore = [email protected]<VIEWMODEL>()
    return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.Java)
}

    val viewModelStore = [email protected]<VIEWMODEL>()
    return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.Java)
}

inline fun <reified VIEWMODEL : ViewModel> getIndividualViewModelStore(): ViewModelStore {
    val viewModelKey = VIEWMODEL::class
    var viewModelStore = individualModelStores[viewModelKey]
    return if (viewModelStore != null) {
        viewModelStore
    } else {
        viewModelStore = ViewModelStore()
        individualModelStores[viewModelKey] = viewModelStore
        return viewModelStore
    }
}

inline fun <reified VIEWMODEL : ViewModel> clearIndividualViewModelStore() {
    val viewModelKey = VIEWMODEL::class
    individualModelStores[viewModelKey]?.clear()
    individualModelStores.remove(viewModelKey)
}

}

Utilisez getSharedViewModel() pour obtenir une instance de ViewModel qui est liée au cycle de vie de l'activité:

val viewModelCDE : ViewModelCDE = (requireActivity() as MainActivity).getSharedViewModel(/*There could be some arguments in case of a more complex ViewModelProvider.Factory implementation*/)

Plus tard, quand il est temps de disposer du ViewModel partagé, utilisez clearIndividualViewModelStore<>():

(requireActivity() as MainActivity).clearIndividualViewModelStore<ViewModelCDE>()

Dans certains cas, vous voudrez effacer le ViewModel dès que possible s'il n'est plus nécessaire (par exemple, s'il contient des données utilisateur sensibles comme le nom d'utilisateur ou le mot de passe). Voici un moyen de consigner l'état de individualModelStores à chaque changement de fragment pour vous aider à garder une trace des ViewModels partagés:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    if (BuildConfig.DEBUG) {
        navController.addOnDestinationChangedListener { _, _, _ ->
            if (individualModelStores.isNotEmpty()) {
                val tag = [email protected]
                Log.w(
                        tag,
                        "Don't forget to clear the shared ViewModelStores if they are not needed anymore."
                )
                Log.w(
                        tag,
                        "Currently there are ${individualModelStores.keys.size} ViewModelStores bound to ${[email protected]}:"
                )
                for ((index, viewModelClass) in individualModelStores.keys.withIndex()) {
                    Log.w(
                            tag,
                            "${index + 1}) $viewModelClass\n"
                    )
                }
            }
        }
    }
}
0
Alex Kuzmin

Sinon, si vous ne parvenez pas à exécuter les solutions suggérées, vous pouvez simplement effacer le ViewModel au niveau Activity où le ViewModel partagé est délimité.

Vous pouvez le faire en procédant comme suit:

getActivity().getViewModelStore().clear();

Cela garantira que le modèle de vue partagé verra ses données effacées.

0
Sakiboy

J'ai supposé que c'était votre problème:

Comme si j'avais parcouru tout le chemin de A à E et que je revenais maintenant en éclatant des fragments vers le fragment B, le viewmodel CDE devrait être détruit, mais ce ne sera pas le cas car il est connecté à l'activité.

Vous vouliez partager des données entre plusieurs fragments à l'aide de ViewModel, mais vous voulez vous assurer que les données de ViewModel seraient Détruire lorsque le fragment naviguera vers un certain écran.

Ma solution de suggestion pour cela est:

  1. Créez un Destroy Data Function dans la classe ViewModel qui détruira les données du ViewModel en écrasant sa valeur en valeur vide comme ""

    class CDEViewModel : ViewModel() {  
       var dataString: String = ""
    
       fun destroyViewModelData() { // Function that will Destroythe Data
           dataString= ""
       }
    }
    
  2. Vous pouvez maintenant appeler la fonction destroyViewModelData dans votre fragment chaque fois que vous devez vous assurer que les données ViewModel sont Clear/Destroy

    class FragmentE {
    
    private lateinit var cdeViewModel : CDEViewModel 
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Initialize your ViewModel
        cdeViewModel = ViewModelProviders.of(this).get(CDEViewModel ::class.Java)
    }
    
    override fun onStart() {
        super.onStart()
    
        // Set your Value Here
        cdeViewModel.dataString = "String 1"
    }
    
    override fun onStop() {
        super.onStop()
    
        // Reset/Destroy Data when Screen is Being Close/Navigate to other Screen
        // After Call this function, in Whatever Screen, the ViewModel previous Set ""String 1"" Data is Clear/Destroy and become "" empty value.
        cdeViewModel.destroyViewModelData()
    }
    }
    

Dans votre cas, vous pouvez appeler fonction destroyViewModelData à onStop () de FragmentE, donc quand vous avez naviguez de FragmentE à FragmentB, les données du CDEViewModel deviennent toutes "" Chaîne vide ce qui signifie que cela a été Reset/Destroy.

J'espère que cette solution simple pourrait vous aider. Je vous remercie.

0
I am a Student

Depuis Navigation 2.1.0-alpha02 (Stable dans 2.1.0), Vous pouvez créer des ViewModels avec une portée à un niveau de graphique de navigation via by navGraphViewModels().

Pour qu'un ViewModel ne soit pas attaché à une activité ou à un seul fragment, vous devez créer un graphique de navigation imbriqué et demander des instances du ViewModel dans la portée de ce graphique. Cela entraînera que pendant que vous êtes à l'intérieur du graphique de navigation imbriqué, ViewModel vivra et que les fragments à l'intérieur du graphique imbriqué réutiliseront la même instance de ViewModel.

De cette façon, vous pouvez avoir plusieurs graphiques de navigation imbriqués, chacun avec une seule instance de ViewModel qui sera partagée entre les fragments qui composent ce graphique.

Je vais suivre votre même distribution de fragments et ViewModels:

MainActivity (Application Entry Point)
----------
Fragment (A) (Uses SharedViewModelOne) -> navGraphOne
Fragment (B) (Uses SharedViewModelOne) -> navGraphOne
Fragment (C) (Uses SharedViewModelTwo) -> navGraphTwo
Fragment (D) (Uses SharedViewModelTwo) -> navGraphTwo

Pour ce faire, vous devez suivre ces étapes:

  1. Votre build.gradle (Module) devrait ressembler à ceci

    ...
    apply plugin: 'kotlin-kapt'
    
    Android {
        ...
        kotlinOptions {
            jvmTarget = "1.8"
        }
    }
    
    dependencies{
        ...
        implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
        kapt 'androidx.lifecycle:lifecycle-compiler:2.2.0'
        implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1'
        implementation 'androidx.navigation:navigation-ui-ktx:2.2.1'
    }
    
  2. Sélectionnez les fragments qui partageront le même ViewModel et ajoutez-les à un graphique de navigation imbriqué. Pour ce faire, sélectionnez les fragments dans votre concepteur de graphiques de navigation, puis faites un clic droit dessus et choisissez Move to Nested Graph

    Dans cet exemple, j'ai ajouté FragmentA et Fragment B à navGraphOne et FragmentC et
    Fragment D à navGraphTwo.

    Trouvez plus d'informations sur le graphique de navigation imbriqué ici

  3. Dans le fragment A et le fragment B, demandez une instance de SharedViewModelOne.

    private val modelOne: SharedViewModelOne by navGraphViewModels(R.id.navGraphOne) {
        //defaultViewModelProviderFactory or the ViewModelProvider.Factory you are using.
        defaultViewModelProviderFactory
    }
    
    override fun onCreateView(
        ..
    ): View? {
        ...
        //use binding.lifecycleOwner = viewLifecycleOwner
        //to make sure the observer disappears when the fragment is destroyed
        modelOne.item.observe(viewLifecycleOwner, Observer {
            //do Something
        })
        ...
    }
    
  4. Dans le fragment C et le fragment D, demandez une instance de SharedViewModelTwo.

    private val modelTwo: SharedViewModelTwo by navGraphViewModels(R.id.navGraphTwo) {
        //defaultViewModelProviderFactory or the ViewModelProvider.Factory you are using.
        defaultViewModelProviderFactory
    }
    
    override fun onCreateView(
        ..
    ): View? {
        ...
        //use binding.lifecycleOwner = viewLifecycleOwner
        //to make sure the observer disappears when the fragment is destroyed
        modelTwo.item.observe(viewLifecycleOwner, Observer {
            //do Something
        })
        ...
    }
    
  5. Ensuite, pour vérifier qu'une seule instance des ViewModels est créée et qu'elle est partagée entre les fragments, remplacez la méthode onCleared() et ajoutez un point de contrôle dans le init{} Du ViewModel.

    Par exemple:

    class SharedViewModelOne : ViewModel() {
    
        private val _item = MutableLiveData<String>()
        val item : LiveData<String>
            get() = _item
    
        init {
            Log.d(TAG, "SharedViewModelOne has created!")
        }
    
        override fun onCleared() {
            super.onCleared()
            Log.d(TAG, "SharedViewModelOne has removed!")
        }
    }
    

Après avoir suivi les étapes précédentes, vous devriez pouvoir créer un ViewModel qui sera partagé entre les fragments qui appartiennent au même graphique de navigation imbriqué, dit que ViewModel ne vivra que pendant que vous êtes à l'intérieur du graphique, si vous le quittez, il sera détruit.

Si vous sentez que quelque chose n'est pas très clair pour vous, vous pouvez revoir ceci repo et clarifier vos doutes.

0
Alejandro Ramirez