Artikelverzeichnis
vorne geschrieben
Im vorigen Artikel wird die häufig genutzte Architektur und deren Kapselung und Verwendung MVVM
vorgestellt 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、MVVM
MVVM
MVVM
Repository
ViewModel
UI层
LiveData
UI层
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 MVI
was ist es? Nach dem Lesen einiger MVI
Artikel darüber sagten alle, MVI是(Model-View-Intent)
dass es Absicht heißt (beachten Sie, dass es nicht verwendet wird, wenn die Seite Intent
hierher springt ), und MVI ist im Wesentlichen eine einheitliche Integration der Datenübertragung zwischen und auf der Grundlage von .Intent
Intent
MVVM
View
ViewModel
google
Es gibt keine Aussage im offiziellen Dokument MVI
, aber ein Upgrade auf der Grundlage der vorherigen MVVM
Architektur. Inhalt und Bedeutung sind sehr ähnlich. Um die Konsistenz zu wahren, wird die MVI
später eingeführte MVVM
aktualisierte Architektur zusammenfassend als MVI
Architektur bezeichnet.
MVI gegen MVVM
Vergleich alter und neuer Architektur
-
Legacy-
MVVM
Schema:
-
Die neue Version
MVVM
oder wie sie heißtMVI
:
Unterschied 1. LiveData<T> wird in Flow<UIState> geändert
Nachteile über LiveData
:
LiveData
Der Empfang kann nur im Hauptthread erfolgen;LiveData
Das Senden von Daten ist eine einmalige Transaktion und kann nicht mehrmals gesendet werden;LiveData
Der Thread zum Senden von Daten ist festgelegt, und der Thread kann nicht gewechselt werden.ImsetValue/postValue
Wesentlichen werden alle Daten auf dem Haupt-Thread gesendet. Wenn Sie Threads hin und her wechseln müssen,LiveData
wird es machtlos.
Flow
Es 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 Flow
detaillierten 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 LiveData
zum Senden von Daten verwenden; wenn Sie darauf umgestellt haben Kotlin
, wird es eher empfohlen, es Flow
zum Senden von Daten zu verwenden.
Es gibt auch einen Unterschied.In LiveData
der alten Version der Architektur werden Daten einer einzigen Entität übertragen,d.h. alle Daten entsprechen einer.Wenn LiveData
die Seitenlogik sehr kompliziert ist, führt dies natürlichzu einer ViewModel
Erweiterung LiveData
;in der neuen Version der Architektur, Flow
die vereinheitlichte UIState
Bestellung 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 UIState
wird View
, sodass nur eine für die vereinheitlichte Interaktion in ViewModel
benö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 Events
nach oben und Zustände UiState
nach unten . Der Gesamtprozess ist wie folgt:
ViewModel
Speichert und macht den Zustand verfügbar, der von der Schnittstelle verwendet werden soll. UI-Status sindViewModel
transformierte Anwendungsdaten.- Die Benutzeroberfläche
ViewModel
sendet . ViewModel
Benutzeraktionen 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:
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:
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
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()
}
MviState
Was in definiert ist , ist UIState
die View
schichtbezogene Datenklasse, und MviSingleUiState
was in definiert ist, ist ein einmaliges Verbrauchsereignis, wie Toast
z Channel
Artikel, 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()
}
LoadUiState
Mehrere Zustände des Seitenladens sind definiert: LadenLoading
, Laden erfolgreichShowMainView
und Laden fehlgeschlagenError
. Die Verwendung und das Umschalten mehrerer ZuständeBaseViewModel
sind in der mittleren Datenanforderung gekapselt. Für eine spezifische Verwendung siehe Beispielcode.- Wenn in der Seitenanforderung kein einmaliges Verbrauchsereignis vorhanden ist,
ViewModel
kann es direkt während der Initialisierung übergeben werdenEmptySingleState
.
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))
}
}
}
}
}
StateFlow
Standardwerte 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. requestDataWithFlow
Die 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->ViewModel
Beim Anfordern von Daten events
werden diese durchgereicht und können ViewModel
gekapselt 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 View
kann der Layer wie folgt aufgerufen werden:
mViewModel.dispatchEvent(EVENT.Banner)
mViewModel.dispatchEvent(EVENT.Detail)
In dem Beispiel, View
wenn die Datenanforderung auf der Schicht gesendet wird, wird die Anforderung nicht ViewModel
in der Schicht gekapselt, sondern mViewModel.loadBannerData()
die Anforderung wird direkt weitergeleitet.Ich persönlich finde die Event
Methode der Kapselung etwas überflüssig.
Zusammenfassen
MVI
Im Vergleich zur alten Version MVVM
ist die aktualisierte Version der Struktur standardisierter und restriktiver. Speziell:
Flow
Im Vergleich dazuLiveData
ist 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, sodassViewModel
nur eine für die Verwaltung definiert werden mussStateFlow
, wodurch der Vorlagencode reduziert wird. GleichzeitigUIState
bringt die Definition auch Nebenwirkungen mit sich, das heißt,View
die Schicht hat keinediff
Fähigkeit und führt für jedes Ereignis eine vollständige Aktualisierung durch, aber der InhaltView
in der Schicht kannUIState
im Detail überwacht werden, umUI
den 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