RepeatOnLifecycle + SharedFlow Hidden Pit Solución de problemas y gobernanza

antecedentes

El último número de "Sugerencias para mejorar y desensamblar el diseño de la arquitectura de Android de Google" se centró en desmantelar los malentendidos de diseño de la "capa de dominio" de la arquitectura oficial y ofreció sugerencias para mejorar: llevar a cabo Event-Handler a través de MVI-Dispatcher,

Sin embargo, algunos amigos dijeron que no solo quieren MVI-Dispatcher, sino que también quieren ver la versión Kotlin de MVI en la práctica.

Por lo tanto, en este número hemos llegado al proyecto de ejemplo MVI-Dispatcher-KTX.

Descripción del Proyecto

El "flujo de datos unidireccional" se reconoce como la mejor práctica en el campo del "desarrollo de clientes gráficos" en los últimos años.

MVI-Dispatcher elimina el costo de aprendizaje del "flujo de datos unidireccional", de modo que los desarrolladores que no están familiarizados con mutable y MVI pueden realizar automáticamente un desarrollo de "flujo de datos unidireccional" basado en una entrada-salida simple y fácil de entender. diseño.

La interfaz de la versión KTX es consistente con MVI-Dispatcher , que puede eliminar por completo el código repetitivo mutable, eliminar el uso indebido y el abuso de setValue/emit, y puede integrarse perfectamente en el proyecto Jetpack MVVM.

Los siguientes datos de prueba en boxes:

SharedFlow también "pierde" eventos?

Al igual que MVI-Dispatcher, la versión KTX proporciona una serie de pruebas regulares + de fuerza bruta en el módulo de muestra,

Organizamos 4 grupos de opciones de eventos en ComplexRequester, el evento 1 puede sondear para notificar al evento 4 para hacer retroceder la interfaz de usuario, el evento 2 puede hacer retroceder la interfaz de usuario después de un retraso de 200 ms, el evento 3 puede hacer retroceder la interfaz de usuario directamente,

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

Al mismo tiempo, registramos y observamos MVI-Dispatcher-KTX a través de la función de salida en MainActivity y enviamos los eventos 1, 2 y 3 a MVI-Dispatcher-KTX a través de la función de entrada.

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

El resultado fue inesperado:

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

¿Qué pasa con los resultados del retroceso del evento 3?

La prueba MVI-Dispatcher no tiene este problema, ¿por qué la versión KTX lo tiene?

Así que continúa jugando Log para observar:

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 皆收到。

¿Qué hay de UnPeek-LiveData?

Echemos un vistazo al rendimiento de 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");
  }
}

Registro de salida para ver:

com.kunminx.unpeeklivedata D/----: hahaha

En vista del rendimiento de UnPeek-LiveData y el mantenimiento interno de las colas de longitud fija por parte de MVI-Dispatcher, no hay ningún problema de "eventos perdidos secuencialmente" en LiveData nativo de forma predeterminada.

Al final

La historia de MVI-Dispatcher-KTX + pelotones de boxes se ha compartido hasta ahora. Actualmente, MVI-Dispatcher y MVI-Dispatcher-KTX están en la versión beta pública. Bienvenidos a todos a probar los comentarios:

Github:MVI-Despachador

Github:MVI-Despachador-KTX

Licencia: el robot de Android en la portada de este artículo es una recreación del trabajo original y compartido de Google, y se usa bajo los términos de la licencia Creative Commons Attribution 3.0.

Supongo que te gusta

Origin juejin.im/post/7121377247105122317
Recomendado
Clasificación