web-dev-qa-db-fra.com

Modèle MVVM et startActivity

J'ai récemment décidé d'examiner de plus près les nouveaux Android) composants architecturaux que Google a publiés, notamment en utilisant leur classe ViewModel lifecycle-aware à une architecture MVVM et LiveData.

Tant que je traite d'une seule activité ou d'un seul fragment, tout va bien.

Cependant, je ne trouve pas de solution intéressante pour gérer le changement d’activité. Supposons, pour un court exemple, que l'activité A comporte un bouton permettant de lancer l'activité B.

Où la startActivity () serait-elle gérée?

Suivant le modèle MVVM, la logique de clickListener doit être dans le ViewModel. Cependant, nous voulons éviter d’avoir des références à l’activité ici. Donc, passer le contexte au ViewModel n'est pas une option.

J'ai réduit quelques options qui semblent "OK", mais je n'ai pas pu trouver de réponse appropriée à "voici comment le faire.".

Option 1 : créez une énumération dans le ViewModel avec le mappage des valeurs sur le routage possible (ACTIVITY_B, ACTIVITY_C). Coupler ceci avec un LiveData. L'activité observerait ces données LiveData et, lorsque le ViewModel déciderait d'activer ACTIVITY_C, il ne ferait que postValue (ACTIVITY_C). Activity peut alors appeler startActivity () normalement.

Option 2 : modèle d'interface standard. Même principe que l'option 1, mais Activity implémenterait l'interface. Je me sens un peu plus couplé avec cela cependant.

Option 3 : option de messagerie, telle que Otto ou similaire. ViewModel envoie une diffusion, Activity la récupère et lance ce dont elle a besoin. Le seul problème avec cette solution est que, par défaut, vous devez mettre le registre/désinscrire de cette diffusion dans le ViewModel. Alors n'aide pas.

Option 4 : Avoir une grande classe de routage, quelque part, en singleton ou similaire, pouvant être appelée pour répartir le routage pertinent vers n'importe quelle activité. Finalement, via l'interface? Donc, chaque activité (ou une BaseActivity) implémenterait

IRouting { void requestLaunchActivity(ACTIVITY_B); }

Cette méthode m'inquiète un peu lorsque votre application commence à avoir beaucoup de fragments/activités (car la classe de routage deviendrait énorme)

Alors c'est tout. C'est ma question. Comment gérez-vous cela? Allez-vous avec une option à laquelle je n'ai pas pensé? Quelle option considérez-vous la plus pertinente et pourquoi? Quelle est l'approche recommandée par Google?

PS: Des liens qui ne m’ont pas mené nulle part 1 - Méthodes d’activité des appels Android ViewModel 2 - Comment démarrer une activité à partir d’une non-activité simple Java = classe?

39
NSimon

NSimon, c’est génial que vous commenciez à utiliser AAC.

J'ai écrit un numéro dans l'aac's-github auparavant à ce sujet.

Il y a plusieurs façons de le faire.

Une solution consisterait à utiliser un

WeakReference à un NavigationController qui contient le contexte de l'activité. Il s'agit d'un modèle couramment utilisé pour gérer les éléments liés au contexte dans un ViewModel.

Je décline fortement cela pour plusieurs raisons. Premièrement: cela signifie généralement que vous devez conserver une référence à votre NavigationController, ce qui corrige la fuite de contexte, mais ne résout pas du tout l’architecture.

Le meilleur moyen (à mon avis) consiste à utiliser LiveData, qui prend en compte le cycle de vie et peut effectuer toutes les tâches voulues.

Exemple:

class YourVm : ViewModel() { 

    val uiEventLiveData = SingleLiveData<Pair<YourModel, Int>>()
    fun onClick(item: YourModel) {
        uiEventLiveData.value = item to 3 // can be predefined values
    }
}

Après cela, vous pouvez écouter les changements dans votre vue.

class YourFragmentOrActivity { 
     //assign your vm whatever
     override fun onActivityCreated(savedInstanceState: Bundle?) { 
        var context = this
        yourVm.uiEventLiveData.observe(this, Observer {
            when (it?.second) {
                1 -> { context.startActivity( ... ) }
                2 -> { .. } 
            }

        })
    }
}

Assurez-vous que ive a utilisé un MutableLiveData modifié, car sinon, il émettra toujours le dernier résultat pour les nouveaux observateurs, ce qui conduit à un mauvais comportement. Par exemple, si vous changez d'activité et que vous revenez en arrière, cela se termine en boucle.

class SingleLiveData<T> : MutableLiveData<T>() {

    private val mPending = AtomicBoolean(false)

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<T>) {

        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
        }

        // Observe the internal MutableLiveData
        super.observe(owner, Observer { t ->
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    @MainThread
    override fun setValue(t: T?) {
        mPending.set(true)
        super.setValue(t)
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }

    companion object {
        private val TAG = "SingleLiveData"
    }
}

Pourquoi cette tentative est-elle préférable à l'aide de WeakReferences, Interfaces ou de toute autre solution?

Parce que cet événement divise la logique de l'interface utilisateur avec la logique métier. Il est également possible d'avoir plusieurs observateurs. Il se soucie du cycle de vie. Il ne fuit rien.

Vous pouvez également le résoudre en utilisant RxJava au lieu de LiveData en utilisant un PublishSubject. (addTo requiert RxKotlin )

Veillez à ne pas divulguer un abonnement en le publiant dans onStop ().

class YourVm : ViewModel() { 
   var subject : PublishSubject<YourItem>  = PublishSubject.create();
}

class YourFragmentOrActivityOrWhatever {
    var composite = CompositeDisposable() 
    onStart() { 
         YourVm.subject 
             .subscribe( { Log.d("...", "Event emitted $it") }, { error("Error occured $it") }) 
               .addTo(compositeDisposable)         
       }   
       onStop() {
         compositeDisposable.clear()
       }
    }

Veillez également à ce qu'un ViewModel soit lié à une activité OR un fragment. Vous ne pouvez pas partager un ViewModel entre plusieurs activités car cela romprait le "Livecycle-Awareness".

Si vous en avez besoin, conservez vos données en utilisant une base de données telle que room ou partagez les données en utilisant des colis.

36
Emanuel S

Vous devez appeler startActivity à partir d'activité, et non à partir de viewmodel. Si vous voulez l'ouvrir à partir de viewmodel, vous devez créer des données de vécu dans viewmodel avec un paramètre de navigation et observer les données de données à l'intérieur de l'activité.

0
Majid Sadeghi