web-dev-qa-db-fra.com

Effacement manuel d'un Android ViewModel?

Edit: Cette question est un peu dépassée maintenant que Google nous a donné la possibilité d'étendre ViewModel aux graphiques de navigation. La meilleure approche (plutôt que d'essayer d'effacer les modèles de portée d'activité) serait de créer des graphiques de navigation spécifiques pour la bonne quantité d'écrans, et la portée de ceux-ci.


En référence au Android.Arch.lifecycle.ViewModel classe.

ViewModel est limité au cycle de vie du composant d'interface utilisateur auquel il se rapporte, donc dans une application basée sur Fragment, ce sera le cycle de vie du fragment. C'est une bonne chose.


Dans certains cas, on veut partager une instance ViewModel entre plusieurs fragments. Plus précisément, je m'intéresse au cas où de nombreux écrans se rapportent aux mêmes données sous-jacentes .

(Les documents suggèrent une approche similaire lorsque plusieurs fragments liés sont affichés sur le même écran mais cela peut être contourné en utilisant un seul fragment hôte comme indiqué ci-dessous .)

Ceci est discuté dans la documentation officielle de ViewModel :

Les ViewModels peuvent également être utilisés comme couche de communication entre différents fragments d'une activité. Chaque Fragment peut acquérir le ViewModel en utilisant la même clé via son Activité. Cela permet la communication entre les fragments d'une manière découplée de sorte qu'ils n'ont jamais besoin de parler directement à l'autre fragment.

En d'autres termes, pour partager des informations entre des fragments qui représentent différents écrans, le ViewModel doit être limité au cycle de vie Activity (et selon Android docs cela peut également être utilisé dans d'autres instances partagées).


Maintenant, dans le nouveau modèle de navigation Jetpack, il est recommandé d'utiliser une architecture "une activité/plusieurs fragments". Cela signifie que l'activité vit pendant toute la durée d'utilisation de l'application.

c'est-à-dire que toutes les instances partagées ViewModel qui sont étendues au cycle de vie Activity ne seront jamais effacées - la mémoire reste en utilisation constante.

Dans le but de préserver la mémoire et d'utiliser le moins possible à tout moment, il serait bien de pouvoir effacer les instances partagées ViewModel lorsqu'elles ne sont plus nécessaires.


Comment effacer manuellement un ViewModel de son ViewModelStore ou fragment de support?

46

Si vous vérifiez le code ici vous découvrirez que vous pouvez obtenir le ViewModelStore à partir d'un ViewModelStoreOwner et Fragment, FragmentActivity par exemple implémente cette interface.

Soo à partir de là, vous pouvez simplement appeler viewModelStore.clear(), qui, comme le dit la documentation:

 /**
 *  Clears internal storage and notifies ViewModels that they are no longer used.
 */
public final void clear() {
    for (ViewModel vm : mMap.values()) {
        vm.clear();
    }
    mMap.clear();
}

N.B.: Cela effacera tous les ViewModels disponibles pour le LifeCycleOwner spécifique, cela ne vous permettra pas d'effacer un ViewModel spécifique.

12
Nagy Robi

Si vous ne souhaitez pas que ViewModel soit limité au cycle de vie Activity, vous pouvez l'étendre au cycle de vie du fragment parent. Donc, si vous souhaitez partager une instance de ViewModel avec plusieurs fragments dans un écran, vous pouvez disposer les fragments de telle sorte qu'ils partagent tous un fragment parent commun. De cette façon, lorsque vous instanciez le ViewModel, vous pouvez simplement faire ceci:

CommonViewModel viewModel = ViewModelProviders.of(getParentFragment()).class(CommonViewModel.class);

J'espère que cela aide!

5
AvidRP

Je pense que j'ai une meilleure solution.

Comme indiqué par @Nagy Robi, vous pouvez effacer le ViewModel en appelant viewModelStore.clear(). Le problème avec cela est qu'il effacera TOUS les modèles de vues dans ce ViewModelStore. En d'autres termes, vous n'aurez aucun contrôle sur lequel ViewModel à effacer.

Mais selon @mikehc ici . Nous pourrions en fait créer notre propre ViewModelStore à la place. Cela nous permettra un contrôle granulaire de l'étendue du ViewModel.

Remarque: Je n'ai vu personne faire cette approche, mais j'espère que celle-ci est valide. Ce sera un très bon moyen de contrôler les étendues dans une application à activité unique.

Veuillez donner quelques commentaires sur cette approche. Tout sera apprécié.

Mettre à jour:

Depuis composant de navigation v2.1.0-alpha02 , ViewModels peut maintenant être limité à un flux. L'inconvénient est que vous devez implémenter Navigation Component à votre projet et vous n'avez aucun contrôle granulaire sur l'étendue de votre ViewModel. Mais cela semble être une meilleure chose.

4
Archie G. Quiñones

Solution rapide sans avoir à utiliser Navigation Component bibliothèque:

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

Cela résoudra ce problème sans incorporer le Navigation Component bibliothèque. C'est aussi une simple ligne de code. Il effacera les ViewModels qui sont partagés entre Fragments via le Activity

4
Sakiboy

Je suis juste en train d'écrire une bibliothèque pour résoudre ce problème: scoped-vm , n'hésitez pas à le vérifier et j'apprécierai grandement tout commentaire. Sous le capot, il utilise l'approche @ Archie mentionnée - il maintient ViewModelStore séparé par portée. Mais cela va encore plus loin et efface ViewModelStore lui-même dès que le dernier fragment qui a demandé le viewmodel de cette étendue est détruit.

Je dois dire que la gestion actuelle de tout le viewmodel (et cette lib en particulier) est affectée par un bug grave avec le backstack, j'espère qu'il sera corrigé.

Résumé:

  • Si vous vous souciez de ne pas appeler ViewModel.onCleared(), la meilleure façon (pour l'instant) est de l'effacer vous-même. En raison de ce bogue, vous n'avez aucune garantie que le modèle d'affichage d'un fragment sera jamais effacé.
  • Si vous vous inquiétez simplement des fuites ViewModel - ne vous inquiétez pas, elles seront récupérées comme tout autre objet non référencé. N'hésitez pas à utiliser ma bibliothèque pour un cadrage à grain fin, si cela convient à vos besoins.
1
dhabensky

Comme il a été souligné, il n'est pas possible d'effacer un ViewModel individuel d'un ViewModelStore à l'aide de l'API des composants d'architecture. 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 yourViewModel : YourViewModel = (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<YourViewModel>()

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"
                    )
                }
            }
        }
    }
}
1
Alex Kuzmin

Dans mon cas, la plupart des choses que j'observe sont liées aux Views, donc je n'ai pas besoin de l'effacer au cas où le View serait détruit (mais pas le Fragment).

Dans le cas où j'ai besoin de choses comme un LiveData qui m'emmène dans un autre Fragment (ou qui ne fait la chose qu'une seule fois), je crée un "observateur consommateur".

Cela peut être fait en étendant MutableLiveData<T>:

fun <T> MutableLiveData<T>.observeConsuming(viewLifecycleOwner: LifecycleOwner, function: (T) -> Unit) {
    observe(viewLifecycleOwner, Observer<T> {
        function(it ?: return@Observer)
        value = null
    })
}

et dès qu'il est observé, il disparaîtra du LiveData.

Maintenant, vous pouvez l'appeler comme:

viewModel.navigation.observeConsuming(viewLifecycleOwner) { 
    startActivity(Intent(this, LoginActivity::class.Java))
}
1
Rafael Ruiz Muñoz

J'ai trouvé un moyen simple et assez élégant de traiter ce problème. L'astuce consiste à utiliser un DummyViewModel et une clé de modèle.

Le code fonctionne car AndroidX vérifie le type de classe du modèle sur get (). S'il ne correspond pas, il crée un nouveau ViewModel en utilisant le ViewModelProvider.Factory actuel.

public class MyActivity extends AppCompatActivity {
    private static final String KEY_MY_MODEL = "model";

    void clearMyViewModel() {
        new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).
            .get(KEY_MY_MODEL, DummyViewModel.class);
    }

    MyViewModel getMyViewModel() {
        return new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication()).
            .get(KEY_MY_MODEL, MyViewModel.class);
    }

    static class DummyViewModel extends ViewModel {
        //Intentionally blank
    }
}   
1
Dustin

Comme je sais que vous ne pouvez pas supprimer manuellement l'objet ViewModel par programme, mais vous pouvez effacer les données qui y sont stockées, dans ce cas, vous devez appeler la méthode Oncleared() manuellement pour ce faire:

  1. Substitue la méthode Oncleared() dans cette classe qui est étendue à partir de la classe ViewModel
  2. Dans cette méthode, vous pouvez nettoyer les données en annulant le champ dans lequel vous stockez les données
  3. Appelez cette méthode lorsque vous souhaitez effacer complètement les données.
0
Amir Hossein