MVVM-Muster und startActivity

Lesezeit: 6 Minuten

Benutzer-Avatar
NSimon

Ich habe mich kürzlich entschieden, mir die neuen Android-Architekturkomponenten, die Google veröffentlicht hat, genauer anzusehen, insbesondere die lebenszyklusbewusste Klasse ViewModel für eine MVVM-Architektur und LiveData.

Solange ich es mit einer einzelnen Aktivität oder einem einzelnen Fragment zu tun habe, ist alles in Ordnung.

Ich kann jedoch keine nette Lösung finden, um die Aktivitätsumschaltung zu handhaben. Nehmen wir für ein kurzes Beispiel an, dass Aktivität A eine Schaltfläche zum Starten von Aktivität B hat.

Wo würde die startActivity() behandelt werden?

Gemäß dem MVVM-Muster sollte sich die Logik des ClickListeners im ViewModel befinden. Wir möchten jedoch vermeiden, dort Verweise auf die Aktivität zu haben. Das Übergeben des Kontexts an das ViewModel ist also keine Option.

Ich habe ein paar Optionen eingegrenzt, die “OK” erscheinen, konnte aber keine richtige Antwort auf “So geht’s” finden.

Option 1 : Haben Sie eine Aufzählung im ViewModel mit Werten, die einem möglichen Routing (ACTIVITY_B, ACTIVITY_C) zugeordnet sind. Koppeln Sie dies mit einem LiveData. Die Aktivität würde diese LiveData beobachten, und wenn das ViewModel entscheidet, dass ACTIVITY_C gestartet werden soll, würde es einfach Value(ACTIVITY_C) posten. Activity kann dann startActivity() normal aufrufen.

Option 2 : Das reguläre Schnittstellenmuster. Gleiches Prinzip wie Option 1, aber Activity würde die Schnittstelle implementieren. Ich fühle mich jedoch ein bisschen mehr verbunden mit diesem.

Möglichkeit 3 : Messaging-Option, z. B. Otto oder ähnliches. ViewModel sendet einen Broadcast, Activity nimmt ihn auf und startet, was er soll. Das einzige Problem bei dieser Lösung ist, dass Sie die Registrierung/Abmeldung dieser Sendung standardmäßig in das ViewModel einfügen sollten. Hilft also nicht.

Möglichkeit 4 : Irgendwo eine große Routing-Klasse als Singleton oder ähnliches haben, die aufgerufen werden könnte, um relevantes Routing an jede Aktivität zu senden. Eventuell per Schnittstelle? So würde jede Aktivität (oder eine BaseActivity) implementiert werden

IRouting { void requestLaunchActivity(ACTIVITY_B); }

Diese Methode macht mir nur ein bisschen Sorgen, wenn Ihre App anfängt, viele Fragmente/Aktivitäten zu haben (weil die Routing-Klasse riesig werden würde).

Das war’s. Das ist meine Frage. Wie geht ihr damit um? Entscheiden Sie sich für eine Option, an die ich nicht gedacht habe? Welche Option halten Sie für die relevanteste und warum? Was ist der von Google empfohlene Ansatz?

PS: Links, die mich nicht weitergebracht haben 1 – Android ViewModel-Aufruf Aktivitätsmethoden 2 – Wie starte ich eine Aktivität aus einer einfachen Nicht-Aktivitäts-Java-Klasse?

Benutzer-Avatar
Emmanuel S

NSimon, es ist großartig, dass du anfängst, Unterstützte Kommunikation zu verwenden.

Ich schrieb ein Ausgabe im aac’s-github vorher darüber.

Dafür gibt es mehrere Möglichkeiten.

Eine Lösung wäre die Verwendung von a

SchwachReferenz an einen NavigationController, der den Kontext der Aktivität enthält. Dies ist ein häufig verwendetes Muster für die Behandlung kontextgebundener Dinge in einem ViewModel.

Ich lehne dies aus mehreren Gründen stark ab. Erstens: Das bedeutet normalerweise, dass Sie einen Verweis auf Ihren NavigationController beibehalten müssen, der das Kontextleck behebt, aber die Architektur überhaupt nicht löst.

Der beste Weg (meiner Meinung nach) ist die Verwendung von LiveData, das den Lebenszyklus berücksichtigt und alle gewünschten Dinge tun kann.

Beispiel:

class YourVm : ViewModel() { 

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

Danach können Sie in Ihrer Ansicht nach Änderungen lauschen.

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 -> { .. } 
            }

        })
    }
}

Achten Sie darauf, dass ich ein modifiziertes MutableLiveData verwendet habe, da es sonst immer das neueste Ergebnis für neue Beobachter ausgibt, was zu schlechtem Verhalten führt. Wenn Sie beispielsweise die Aktivität ändern und zurückgehen, endet dies in einer Schleife.

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

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

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

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

Warum ist dieser Versuch besser als die Verwendung von WeakReferences, Interfaces oder einer anderen Lösung?

Weil dieses Ereignis die UI-Logik mit der Geschäftslogik geteilt hat. Es ist auch möglich, mehrere Beobachter zu haben. Es kümmert sich um den Lebenszyklus. Es leckt nichts.

Sie können es auch lösen, indem Sie RxJava anstelle von LiveData verwenden, indem Sie ein PublishSubject verwenden. (addTo erfordert RxKotlin)

Achten Sie darauf, dass ein Abonnement nicht verloren geht, indem Sie es in onStop() freigeben.

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

Achten Sie auch darauf, dass ein ViewModel an eine Aktivität ODER ein Fragment gebunden ist. Sie können ein ViewModel nicht zwischen mehreren Aktivitäten teilen, da dies das “Livecycle-Awareness” brechen würde.

Wenn Sie dies benötigen, speichern Sie Ihre Daten mithilfe einer Datenbank wie z Zimmer oder teilen Sie die Daten über Pakete.

  • Danke für die sehr ausführliche Antwort. Ich tendierte auch zum LiveData-Ansatz, dachte jedoch nicht an Ihre LiveData-Optimierungen. Insgesamt wirkt das alles aber sehr “hacky”, und es fühlt sich fast schon schlecht an, es so zu machen. Trotzdem danke ! (Bearbeiten: kann das Kopfgeld nur in 20 Stunden validieren)

    – NSimon

    19. Oktober 2017 um 13:32 Uhr


  • Nein, es ist nicht hacky. So funktioniert das Beobachtermuster. Sie erhalten einen Klick, schieben ihn zu Ihrem ViewModel und wenn es eine Ansicht gibt (dies ist garantiert), verarbeitet es die Daten. Es ist nicht einmal ein Patch 🙂 So funktioniert es. Click ist nur ein Beispiel, es kann jede “Dateneingabe” sein.

    – Emmanuel S

    19. Oktober 2017 um 13:33 Uhr


  • @Suman, die Blaupause für die Android-Architektur beschreibt es sehr detailliert. Ich denke, Sie können die SingleLiveData-Klasse finden, die hier im Link implementiert ist. github.com/googlesamples/android-architecture/blob/…

    – Tejas Sherdiwala

    26. Dezember 2018 um 11:08 Uhr

  • Dies ist genau die Lösung, die ich seit der Einführung der Android-Architekturkomponenten verwende 🙂

    – Derek K

    23. Januar 2019 um 14:32 Uhr

  • Ich werde die Lösung (Java + Kotlin) aktualisieren, wenn ich die Zeit sehr bald finde. Am besten, Emanuel

    – Emmanuel S

    27. Januar 2019 um 11:33 Uhr

Sie sollten startActivity aus der Aktivität heraus aufrufen, nicht aus dem Ansichtsmodell. Wenn Sie es aus Viewmodel öffnen möchten, müssen Sie Livedaten in Viewmodel mit einigen Navigationsparametern erstellen und Livedaten innerhalb der Aktivität beobachten

  • Überprüfen Sie diese Frage, es kann Ihnen helfen. stackoverflow.com/questions/46727276/…

    – Saif

    11. Mai 2019 um 7:57 Uhr

Sie können Ihr ViewModel von erweitern AndroidViewModeldie die Anwendungsreferenz enthält, und starten Sie die Aktivität mit diesem Kontext.

  • WAHR. Allerdings müssten Sie beim UnitTesting den Kontext verspotten, was zusätzliche (unnötige) Arbeit mit sich bringt. Ich landete bei dem oben genannten Ansatz, der aus einem LiveData-Feld nur zur Navigation besteht.

    – NSimon

    18. April 2018 um 10:01 Uhr

  • Beide Wege sind in Ordnung, nur eine weitere Option hinzugefügt.

    – Wadim Gorjainow

    18. April 2018 um 10:34 Uhr

  • Ja, aber wenn Sie Ihre Aktivität mit diesem Kontext starten, müssen Sie das NEW_TASK-Flag auf die Absicht setzen, und das ist einfach chaotisch. Der Anwendungskontext sollte nicht zum Starten von Aktivitäten verwendet werden.

    – 4gus71n

    5. Juni 2018 um 23:04 Uhr

1032730cookie-checkMVVM-Muster und startActivity

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy