MVI-Architektur der Android Jetpack-Serie

vorne geschrieben

Im vorigen Artikel wird die häufig genutzte Architektur und deren Kapselung und Verwendung MVVMvorgestellt Der Grundgedanke kann als datengetrieben verstanden werden: Daten bereitstellen, Daten mittendrin senden, die genutzten Daten abonnieren und dort aktiv benachrichtigen und aktualisieren ist eine Datenänderung . Wer Interesse hat, kann sich hier umsehen:MVC、MVP、MVVMMVVMMVVMRepositoryViewModelUI层LiveDataUI层

1. Die Verwendung und Verpackung von MVVM in der Android Jetpack-Reihe
2. Die Verwendung und Verpackung von MVVM in der Android Jetpack-Reihe (Fortsetzung)

Also MVIwas ist es? Nach dem Lesen einiger MVIArtikel darüber sagten alle, MVI是(Model-View-Intent)dass es Absicht heißt (beachten Sie, dass es nicht verwendet wird, wenn die Seite Intenthierher springt ), und MVI ist im Wesentlichen eine einheitliche Integration der Datenübertragung zwischen und auf der Grundlage von .IntentIntentMVVMViewViewModel

googleEs gibt keine Aussage im offiziellen Dokument MVI, aber ein Upgrade auf der Grundlage der vorherigen MVVMArchitektur. Inhalt und Bedeutung sind sehr ähnlich. Um die Konsistenz zu wahren, wird die MVIspäter eingeführte MVVMaktualisierte Architektur zusammenfassend als MVIArchitektur bezeichnet.

MVI gegen MVVM

Vergleich alter und neuer Architektur

  • Legacy- MVVMSchema:
    MVVM

  • Die neue Version MVVModer wie sie heißt MVI:
    Die neue Version von MVVM oder MVI genannt

Unterschied 1. LiveData<T> wird in Flow<UIState> geändert

Nachteile über LiveData:

  • LiveDataDer Empfang kann nur im Hauptthread erfolgen;
  • LiveDataDas Senden von Daten ist eine einmalige Transaktion und kann nicht mehrmals gesendet werden;
  • LiveDataDer Thread zum Senden von Daten ist festgelegt, und der Thread kann nicht gewechselt werden.Im setValue/postValueWesentlichen werden alle Daten auf dem Haupt-Thread gesendet. Wenn Sie Threads hin und her wechseln müssen, LiveDatawird es machtlos.

FlowEs kann die Probleme von LiveData perfekt lösen. Es kann nicht nur Daten vom Upstream mehrfach senden, sondern auch Threads flexibel wechseln. Wenn es also darum geht, Threads hin und her zu wechseln, ist es eine bessere Lösung Flow. Informationen zur Flowdetaillierten Verwendung von finden interessierte Schüler unter: Flow-Datenfluss von Android Kotlin

Hinweis: Wenn Sie im Projekt nicht darauf umgestellt haben Kotlin, können Sie es trotzdem LiveDatazum Senden von Daten verwenden; wenn Sie darauf umgestellt haben Kotlin, wird es eher empfohlen, es Flowzum Senden von Daten zu verwenden.

Es gibt auch einen Unterschied.In LiveDatader alten Version der Architektur werden Daten einer einzigen Entität übertragen,d.h. alle Daten entsprechen einer.Wenn LiveDatadie Seitenlogik sehr kompliziert ist, führt dies natürlichzu einer ViewModelErweiterung LiveData;in der neuen Version der Architektur, Flowdie vereinheitlichte UIStateBestellung UIState. Im Wesentlichen ist es auch eine data类, der Unterschied besteht darin , dass der Zustand der Entität in Bezug auf die Schicht einheitlich gesteuert UIStatewird View, sodass nur eine für die vereinheitlichte Interaktion in ViewModelbenötigt wird .Flow

Unterschied 2. Interaktionsspezifikation

In der neuen Version der Architektur wird das Konzept des unidirektionalen Datenflusses zum Verwalten des Seitenstatus vorgeschlagen: Das heißt, die Flussrichtung der Daten ist festgelegt, und die gesamte Datenflussrichtung ist View -> ViewModel -> Model数据层 -> ViewModel获得数据 -> 根据UiState刷新View层. Unter ihnen fließen Ereignisse Eventsnach oben und Zustände UiStatenach unten . Der Gesamtprozess ist wie folgt:

  • ViewModelSpeichert und macht den Zustand verfügbar, der von der Schnittstelle verwendet werden soll. UI-Status sind ViewModeltransformierte Anwendungsdaten.
  • Die Benutzeroberfläche ViewModelsendet .
  • ViewModelBenutzeraktionen werden verarbeitet und der Status wird aktualisiert.
  • Der aktualisierte Zustand wird zum Rendern an die Benutzeroberfläche zurückgemeldet.
  • Dies wird für alle Ereignisse wiederholt, die zu einer Zustandsänderung führen.

Der Beamte gab ein Beispiel für das Klicken auf ein Lesezeichen:
Lesezeichen
Das obige ist UI界面der Vorgang zum Hinzufügen eines Lesezeichens, und das Lesezeichen wird nach dem Klicken erfolgreich hinzugefügt, dann ist der gesamte Datenübertragungsprozess wie folgt:
Beispiel für Datenfluss

Der Datenfluss in eine Richtung verbessert die Lesbarkeit des Codes und erleichtert die Änderung. Der unidirektionale Datenfluss hat die folgenden Vorteile:

  • Datenkonsistenz. Es gibt nur eine Vertrauensquelle für die Schnittstelle.
  • Testbarkeit. Zustandsquellen sind unabhängig, sodass sie unabhängig von der Schnittstelle getestet werden können.
  • Wartbarkeit. Zustandsänderungen folgen einem klar definierten Muster, wobei Zustandsänderungen das Ergebnis einer Kombination aus Benutzerereignissen und ihren Datenabrufquellen sind.

MVI-Kampf

Beispieldiagramm

Bitte fügen Sie eine Bildbeschreibung hinzu

Definieren Sie UIState und schreiben Sie ViewModel

class MViewModel : BaseViewModel<MviState, MviSingleUiState>() {
    //Repository中间层 管理所有数据来源 包括本地的及网络的
    private val mWanRepo = WanRepository()

    override fun initUiState(): MviState {
        return MviState(BannerUiState.INIT, DetailUiState.INIT)
    }

    //请求Banner数据
    fun loadBannerData() {
        requestDataWithFlow(
            showLoading = true,
            request = { mWanRepo.requestWanData("") },
            successCallback = { data ->
                sendUiState {
                    copy(bannerUiState = BannerUiState.SUCCESS(data))
                }
            },
            failCallback = {}
        )
    }

    //请求List数据
    fun loadDetailData() {
        requestDataWithFlow(
            showLoading = false,
            request = { mWanRepo.requestRankData() },
            successCallback = { data ->
                sendUiState {
                    copy(detailUiState = DetailUiState.SUCCESS(data))
                }
            },
        )
    }

    fun showToast() {
        sendSingleUiState(MviSingleUiState("触发了一次性消费事件!"))
    }
}

/**
 * 定义UiState 将View层所有实体类相关的都包括在这里,可以有效避免模板代码(StateFlow只需要定义一个即可)
 */
data class MviState(val bannerUiState: BannerUiState, val detailUiState: DetailUiState?) : IUiState
data class MviSingleUiState(val message: String) : ISingleUiState

sealed class BannerUiState {
    object INIT : BannerUiState()
    data class SUCCESS(val models: List<WanModel>) : BannerUiState()
}

sealed class DetailUiState {
    object INIT : DetailUiState()
    data class SUCCESS(val detail: RankModel) : DetailUiState()
}

MviStateWas in definiert ist , ist UIStatedie Viewschichtbezogene Datenklasse, und MviSingleUiStatewas in definiert ist, ist ein einmaliges Verbrauchsereignis, wie Toastz ChannelArtikel, und wird hier nicht wiederholt.

Verwandte Schnittstellen :

interface IUiState //重复性事件 可以多次消费
interface ISingleUiState //一次性事件,不支持多次消费

object EmptySingleState : ISingleUiState

//一次性事件,不支持多次消费
sealed class LoadUiState {
    data class Loading(var isShow: Boolean) : LoadUiState()
    object ShowMainView : LoadUiState()
    data class Error(val msg: String) : LoadUiState()
}
  • LoadUiStateMehrere Zustände des Seitenladens sind definiert: Laden Loading, Laden erfolgreich ShowMainViewund Laden fehlgeschlagen Error. Die Verwendung und das Umschalten mehrerer Zustände BaseViewModelsind in der mittleren Datenanforderung gekapselt. Für eine spezifische Verwendung siehe Beispielcode.
  • Wenn in der Seitenanforderung kein einmaliges Verbrauchsereignis vorhanden ist, ViewModelkann es direkt während der Initialisierung übergeben werden EmptySingleState.

BasisklasseBaseViewModel

/**
 * ViewModel基类
 *
 * @param UiState 重复性事件,View层可以多次接收并刷新
 * @param SingleUiState 一次性事件,View层不支持多次消费 如弹Toast,导航Activity等
 */
abstract class BaseViewModel<UiState : IUiState, SingleUiState : ISingleUiState> : ViewModel() {
    /**
     * 可以重复消费的事件
     */
    private val _uiStateFlow = MutableStateFlow(initUiState())
    val uiStateFlow: StateFlow<UiState> = _uiStateFlow

    /**
     * 一次性事件 且 一对一的订阅关系
     * 例如:弹Toast、导航Fragment等
     * Channel特点
     * 1.每个消息只有一个订阅者可以收到,用于一对一的通信
     * 2.第一个订阅者可以收到 collect 之前的事件
     */
    private val _sUiStateFlow: Channel<SingleUiState> = Channel()
    val sUiStateFlow: Flow<SingleUiState> = _sUiStateFlow.receiveAsFlow()

    private val _loadUiStateFlow: Channel<LoadUiState> = Channel()
    val loadUiStateFlow: Flow<LoadUiState> = _loadUiStateFlow.receiveAsFlow()

    protected abstract fun initUiState(): UiState

    protected fun sendUiState(copy: UiState.() -> UiState) {
        _uiStateFlow.update { _uiStateFlow.value.copy() }
    }

    protected fun sendSingleUiState(sUiState: SingleUiState) {
        viewModelScope.launch {
            _sUiStateFlow.send(sUiState)
        }
    }

    /**
     * 发送当前加载状态: Loading、Error、Normal
     */
    private fun sendLoadUiState(loadState: LoadUiState) {
        viewModelScope.launch {
            _loadUiStateFlow.send(loadState)
        }
    }

    /**
     * @param showLoading 是否展示Loading
     * @param request 请求数据
     * @param successCallback 请求成功
     * @param failCallback 请求失败,处理异常逻辑
     */
    protected fun <T : Any> requestDataWithFlow(
        showLoading: Boolean = true,
        request: suspend () -> BaseData<T>,
        successCallback: (T) -> Unit,
        failCallback: suspend (String) -> Unit = { errMsg ->
            //默认异常处理,子类可以进行覆写
            sendLoadUiState(LoadUiState.Error(errMsg))
        },
    ) {
        viewModelScope.launch {
            //是否展示Loading
            if (showLoading) {
                sendLoadUiState(LoadUiState.Loading(true))
            }
            val baseData: BaseData<T>
            try {
                baseData = request()
                when (baseData.state) {
                    ReqState.Success -> {
                        sendLoadUiState(LoadUiState.ShowMainView)
                        baseData.data?.let { successCallback(it) }
                    }
                    ReqState.Error -> baseData.msg?.let {
                        error(it)
                    }
                }
            } catch (e: Exception) {
                e.message?.let { failCallback(it) }
            } finally {
                if (showLoading) {
                    sendLoadUiState(LoadUiState.Loading(false))
                }
            }
        }
    }

}

StateFlowStandardwerte in der Basisklasse werden initUiState()über definiert und erzwingen die Notwendigkeit von Unterklassenimplementierungen:

    override fun initUiState(): MviState {
        return MviState(BannerUiState.INIT, DetailUiState.INIT)
    }

Auf diese Weise hören Sie beim Aufrufen der Seite auf diese Initialisierungsereignisse und reagieren darauf.Wenn Sie sich nicht damit befassen müssen, können Sie sie direkt überspringen. requestDataWithFlowDie gesamte Anforderungslogik ist darin gekapselt

Unterstützung von Repository-Daten

Definieren Sie die Datenklasse BaseData:

class BaseData<T> {
    @SerializedName("errorCode")
    var code = -1
    @SerializedName("errorMsg")
    var msg: String? = null
    var data: T? = null
    var state: ReqState = ReqState.Error
}

enum class ReqState {
    Success, Error
}

Basisklasse BaseRepository :

open class BaseRepository {

    suspend fun <T : Any> executeRequest(
        block: suspend () -> BaseData<T>
    ): BaseData<T> {
        val baseData = block.invoke()
        if (baseData.code == 0) {
            //正确
            baseData.state = ReqState.Success
        } else {
            //错误
            baseData.state = ReqState.Error
        }
        return baseData
    }
}

Die Anforderungslogik wird in der Basisklasse definiert und direkt in der Unterklasse verwendet:

class WanRepository : BaseRepository() {
    val service = RetrofitUtil.getService(DrinkService::class.java)

    suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>> {
        return executeRequest { service.getBanner() }
    }

    suspend fun requestRankData(): BaseData<RankModel> {
        return executeRequest { service.getRankList() }
    }
}

Ebene anzeigen

/**
 * MVI示例
 */
class MviExampleActivity : BaseMviActivity() {

    private val mBtnQuest: Button by id(R.id.btn_request)
    private val mToolBar: Toolbar by id(R.id.toolbar)
    private val mContentView: ViewGroup by id(R.id.cl_content_view)
    private val mViewPager2: MVPager2 by id(R.id.mvp_pager2)
    private val mRvRank: RecyclerView by id(R.id.rv_view)

    private val mViewModel: MViewModel by viewModels()

    override fun getLayoutId(): Int {
        return R.layout.activity_wan_android_mvi
    }

    override fun initViews() {
        initToolBar(mToolBar, "Jetpack MVI", true, true, BaseActivity.TYPE_BLOG)
        mRvRank.layoutManager = GridLayoutManager(this, 2)
    }

    override fun initEvents() {
        registerEvent()
        mBtnQuest.setOnClickListener {
            mViewModel.showToast() //一次性消费
            mViewModel.loadBannerData()
            mViewModel.loadDetailData()
        }
    }

    private fun registerEvent() {
        /**
         * Load加载事件 Loading、Error、ShowMainView
         */
        mViewModel.loadUiStateFlow.flowWithLifecycle2(this) { state ->
            when (state) {
                is LoadUiState.Error -> mStatusViewUtil.showErrorView(state.msg)
                is LoadUiState.ShowMainView -> mStatusViewUtil.showMainView()
                is LoadUiState.Loading -> mStatusViewUtil.showLoadingView(state.isShow)
            }
        }
        /**
         * 一次性消费事件
         */
        mViewModel.sUiStateFlow.flowWithLifecycle2(this) { data ->
            showToast(data.message)
        }

        mViewModel.uiStateFlow.flowWithLifecycle2(this, prop1 = MviState::bannerUiState) { state ->
            when (state) {
                is BannerUiState.INIT -> {}
                is BannerUiState.SUCCESS -> {
                    mViewPager2.visibility = View.VISIBLE
                    mBtnQuest.visibility = View.GONE
                    val imgs = mutableListOf<String>()
                    for (model in state.models) {
                        imgs.add(model.imagePath)
                    }
                    mViewPager2.setIndicatorShow(true).setModels(imgs).start()
                }
            }

        }

        mViewModel.uiStateFlow.flowWithLifecycle2(this, Lifecycle.State.STARTED,
            prop1 = MviState::detailUiState) { state ->
            when (state) {
                is DetailUiState.INIT -> {}
                is DetailUiState.SUCCESS -> {
                    mRvRank.visibility = View.VISIBLE
                    val list = state.detail.datas
                    mRvRank.adapter = RankAdapter().apply { setModels(list) }
                }
            }

        }
    }

    override fun retryRequest() {
        //点击屏幕重试
        mViewModel.showToast() //一次性消费
        mViewModel.loadBannerData()
        mViewModel.loadDetailData()
    }

    /**
     * 展示Loading、Empty、Error视图等
     */
    override fun getStatusOwnerView(): View? {
        return mContentView
    }
}

Schauen Sie sich zunächst die neue Version des Architekturdiagramms an. View->ViewModelBeim Anfordern von Daten eventswerden diese durchgereicht und können ViewModelgekapselt werden in:

sealed class EVENT : IEvent {
    object Banner : EVENT()
    object Detail : EVENT()
  }
    
override fun dispatchEvent(event: EVENT) {
  when (event) {
    EVENT.Banner -> { loadBannerData() }
    EVENT.Detail -> {loadDetailData() }
}

Dann Viewkann der Layer wie folgt aufgerufen werden:

mViewModel.dispatchEvent(EVENT.Banner)
mViewModel.dispatchEvent(EVENT.Detail)

In dem Beispiel, Viewwenn die Datenanforderung auf der Schicht gesendet wird, wird die Anforderung nicht ViewModelin der Schicht gekapselt, sondern mViewModel.loadBannerData()die Anforderung wird direkt weitergeleitet.Ich persönlich finde die EventMethode der Kapselung etwas überflüssig.

Zusammenfassen

MVIIm Vergleich zur alten Version MVVMist die aktualisierte Version der Struktur standardisierter und restriktiver. Speziell:

  • FlowIm Vergleich dazu LiveDataist die Fähigkeit stärker, insbesondere beim Hin- und Herwechseln von Threads;
  • Es ist so definiert UIState, dass es den Datenstatus der Seite zentral verwaltet, sodass ViewModelnur eine für die Verwaltung definiert werden muss StateFlow, wodurch der Vorlagencode reduziert wird. Gleichzeitig UIStatebringt die Definition auch Nebenwirkungen mit sich, das heißt, Viewdie Schicht hat keine diffFähigkeit und führt für jedes Ereignis eine vollständige Aktualisierung durch, aber der Inhalt Viewin der Schicht kann UIStateim Detail überwacht werden, um UIden Zweck der inkrementellen Aktualisierung zu erreichen .

Das bedeutet aber nicht, dass die neue Version der Architektur unbedingt für Ihr Projekt geeignet ist, schließlich ist die Architektur eine Spezifikation und die konkrete Nutzung muss anders sein.

Vollständiger Beispielcode

Vollständigen Beispielcode finden Sie unter: MVI-Beispiel

Material

[1] Leitfaden zur Anwendungsarchitektur : https://developer.android.com/jetpack/guide?hl=zh-cn
[2] Architektur der Schnittstellenschicht : https://developer.android.com/jetpack/guide/ui-layer?hl=zh-cn#views
[3] Schnittstellenereignisse :https://developer.android.com/jetpack/guide/ui-layer/events?hl=zh-cn#views

Guess you like

Origin blog.csdn.net/u013700502/article/details/127155843