バックグラウンド
「GoogleAndroidアーキテクチャの設計の分解と改善の提案」の前回の号では、公式のアーキテクチャ「ドメインレイヤー」の設計上の誤解を解体することに焦点を当て、改善の提案を行いました。MVI-Dispatcherを介してイベントハンドラーを実行します。
ただし、一部の友人は、MVI-Dispatcherだけでなく、KotlinバージョンのMVIプラクティスも見たいと言っています。
したがって、この号では、MVI-Dispatcher-KTXサンプルプロジェクトに行き着きました。
プロジェクトの説明
「一方向データフロー」は、近年「グラフィカルクライアント開発」の分野でのベストプラクティスとして認識されています。
MVI-Dispatcherは、「一方向データフロー」の学習コストを排除するため、可変およびMVIに精通していない開発者は、シンプルでわかりやすい入出力に基づく「一方向データフロー」開発を自動的に実現できます。デザイン。
KTXバージョンのインターフェースはMVI-Dispatcherと一貫性があり、可変ボイラープレートコードを完全に排除し、setValue / Emitの誤用や乱用を排除し、JetpackMVVMプロジェクトにシームレスに統合できます。
次のピットテストのヒント:
SharedFlowもイベントを「失います」?
MVI-Dispatcherと同じように、KTXバージョンは、サンプルモジュールで一連の定期的なブルートフォーステストを提供します。
ComplexRequesterにイベントオプションの4つのグループを配置し、イベント1はポーリングしてイベント4に通知してUIをプッシュバックでき、イベント2は200ミリ秒の遅延後にUIをプッシュバックでき、イベント3はUIを直接プッシュバックできます。
class ComplexRequester : MviDispatcherKTX<ComplexEvent>() {
override suspend fun onHandle(event: ComplexEvent) {
when (event) {
is ComplexEvent.ResultTest1 -> interval(100).collect { input(ComplexEvent.ResultTest4(it)) }
is ComplexEvent.ResultTest2 -> timer(1000).collect { sendResult(event) }
is ComplexEvent.ResultTest3 -> sendResult(event)
is ComplexEvent.ResultTest4 -> sendResult(event)
}
}
...
}
同時に、MainActivityの出力関数を介してMVI-Dispatcher-KTXを登録および監視し、入力関数を介してイベント1、2、および3をMVI-Dispatcher-KTXに送信します。
class MainActivity : BaseActivity() {
...
override fun onOutput() {
complexRequester.output(this) { complexEvent ->
when (complexEvent) {
is ComplexEvent.ResultTest1 -> Log.d("e", "---1")
is ComplexEvent.ResultTest2 -> Log.d("e", "---2")
is ComplexEvent.ResultTest3 -> Log.d("e", "---3")
is ComplexEvent.ResultTest4 -> Log.d("e", "---4 " + complexEvent.count)
}
}
}
override fun onInput() {
super.onInput()
complexRequester.input(ComplexEvent.ResultTest1())
complexRequester.input(ComplexEvent.ResultTest2())
complexRequester.input(ComplexEvent.ResultTest2())
complexRequester.input(ComplexEvent.ResultTest2())
complexRequester.input(ComplexEvent.ResultTest2())
complexRequester.input(ComplexEvent.ResultTest3())
complexRequester.input(ComplexEvent.ResultTest3())
complexRequester.input(ComplexEvent.ResultTest3())
}
}
結果は予想外でした:
com.kunminx.purenote_ktx D/e: ---4 0
com.kunminx.purenote_ktx D/e: ---4 1
com.kunminx.purenote_ktx D/e: ---4 2
com.kunminx.purenote_ktx D/e: ---4 3
com.kunminx.purenote_ktx D/e: ---4 4
com.kunminx.purenote_ktx D/e: ---4 5
com.kunminx.purenote_ktx D/e: ---4 6
com.kunminx.purenote_ktx D/e: ---4 7
com.kunminx.purenote_ktx D/e: ---2
com.kunminx.purenote_ktx D/e: ---2
com.kunminx.purenote_ktx D/e: ---2
com.kunminx.purenote_ktx D/e: ---2
com.kunminx.purenote_ktx D/e: ---4 8
com.kunminx.purenote_ktx D/e: ---4 9
com.kunminx.purenote_ktx D/e: ---4 10
com.kunminx.purenote_ktx D/e: ---4 11
com.kunminx.purenote_ktx D/e: ---4 12
イベント3のプッシュバックの結果はどうですか?
MVI-ディスパッチャーテストにはこの問題はありませんが、KTXバージョンに問題があるのはなぜですか?
したがって、引き続きログを再生して観察します。
class ComplexRequester : MviDispatcherKTX<ComplexEvent>() {
override suspend fun onHandle(event: ComplexEvent) {
when (event) {
...
is ComplexEvent.ResultTest3 -> {
Log.d("---", "ResultTest3-sendResult")
sendResult(event)
}
}
}
}
KTX 版基类中观察 SharedFlow 收集时机:
open class MviDispatcherKTX<E> : ViewModel() {
fun output(activity: AppCompatActivity?, observer: (E) -> Unit) {
initQueue()
activity?.lifecycleScope?.launch {
Log.d("---", "activity.lifecycleScope.launch")
activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
Log.d("---", "activity.repeatOnLifecycle")
_sharedFlow?.collect { observer.invoke(it) }
}
}
}
...
}
继续输出结果:
com.kunminx.purenote_ktx D/---: activity.lifecycleScope.launch
com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
com.kunminx.purenote_ktx D/---: activity.repeatOnLifecycle
com.kunminx.purenote_ktx D/e: ---4 0
com.kunminx.purenote_ktx D/e: ---4 1
com.kunminx.purenote_ktx D/e: ---4 2
com.kunminx.purenote_ktx D/e: ---4 3
com.kunminx.purenote_ktx D/e: ---4 4
发现端倪 —— sharedFlow.emit 事件 3 时机早于 activity.repeatOnLifecycle 时机,错过 sharedFlow 收集时,
故此处将 sharedFlow replay 值改为 1 验证下:
open class MviDispatcherKTX<E> : ViewModel() {
private var _sharedFlow: MutableSharedFlow<E>? = null
private fun initQueue() {
if (_sharedFlow == null) _sharedFlow = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
extraBufferCapacity = initQueueMaxLength()
)
}
...
}
这下收到,确实是时机问题,也即 sharedFlow 并非人眼感知到的 “丢事件”,而是其默认 replay = 0,不自动回推缓存数据给订阅者,该设计符合 “事件” 场景,
且此处如设置为 replay = 1,发射 3 次也仅能收到 1 次,故修改 replay 方案 pass。
com.kunminx.purenote_ktx D/---: activity.lifecycleScope.launch
com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
com.kunminx.purenote_ktx D/---: activity.repeatOnLifecycle
com.kunminx.purenote_ktx D/e: ---3
com.kunminx.purenote_ktx D/e: ---4 0
com.kunminx.purenote_ktx D/e: ---4 1
com.kunminx.purenote_ktx D/e: ---4 2
com.kunminx.purenote_ktx D/e: ---4 3
com.kunminx.purenote_ktx D/e: ---4 4
那怎办,repeatOnLifecycle STARTED 回调相对 emit 存在时延,其实在 Activity 中易解决,即通过 View.post 时机,让 emit 处于 MessageQueue 中顺序执行,如此便能确保时机正确,
但发射一事件还要 View.post,显然易忘记、造成一致性问题,且 MVI-Dispatcher-KTX 采用内聚设计,故此处不妨往 input 方法注入 Activity,再于内部拿取 decorView 自动完成 post …
open class MviDispatcherKTX<E> : ViewModel() {
...
fun input(event: E, activity: AppCompatActivity?) {
activity?.window?.decorView?.post {
viewModelScope.launch { onHandle(event) }
}
}
}
倒也行,不过每次 input 都额外注入个 Activity,这写法是不有点莫名其妙?
且如我想在 KTX 版子类内部 input “side effect” 怎办?故该方案暂且 pass。
… 还有无别的办法?
有,
考虑到 “错过事件” 情形较极端,常规 “从数据层取数据” 等操作,由于操作有其耗时,不易遇见;
如是页面 onCreate 环节末尾发送某 sealed.object 事件,由于毫不费时,则易先于 activity.repeatOnLifecycle(Lifecycle.State.STARTED)
,错过时机,
故此处可于每次 input 时自动延迟 1 毫秒 ——
默认设置为 1 毫秒,且通过维护一 delayMap 自动判断时机取消延迟:
open class MviDispatcherKTX<E> : ViewModel() {
...
fun input(event: E) {
viewModelScope.launch {
if (needDelayForLifecycleState) delayForLifecycleState().collect { onHandle(event) }
else onHandle(event)
}
}
private val needDelayForLifecycleState
get() = delayMap.isNotEmpty()
}
输出 Log 看看:
com.kunminx.purenote_ktx D/---: activity.lifecycleScope.launch
com.kunminx.purenote_ktx D/---: activity.repeatOnLifecycle
com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
com.kunminx.purenote_ktx D/e: ---3
com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
com.kunminx.purenote_ktx D/e: ---3
com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
com.kunminx.purenote_ktx D/e: ---3
com.kunminx.purenote_ktx D/e: ---4 0
com.kunminx.purenote_ktx D/e: ---4 1
com.kunminx.purenote_ktx D/e: ---4 2
com.kunminx.purenote_ktx D/e: ---4 3
com.kunminx.purenote_ktx D/e: ---4 4
至此,3 个事件 3 皆收到。
UnPeek-LiveDataはどうですか?
UnPeek-LiveDataのパフォーマンスを見てみましょう:
public class MainActivity extends BaseActivity {
private UnPeekLiveData<String> unpeek = new UnPeekLiveData<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
unpeek.observe(this,s -> {
Log.d("----",s);
});
unpeek.setValue("hahaha");
}
}
表示する出力ログ:
com.kunminx.unpeeklivedata D/----: hahaha
UnPeek-LiveDataのパフォーマンスと、MVI-Dispatcherによる固定長キューの内部保守を考慮すると、デフォルトでは、ネイティブLiveDataで「順次失われるイベント」の問題はありません。
やっと
MVI-Dispatcher-KTX +ピット隊列走行の話はこれまで共有されてきました。現在、MVI-DispatcherとMVI-Dispatcher-KTXはパブリックベータ版です。フィードバックをテストするために皆さんを歓迎します。
ライセンス:この記事の表紙にあるAndroidロボットは、Googleのオリジナルの共有作品を再現したものであり、 Creative CommonsAttribution3.0ライセンスの条件の下で使用されます。