KMM Android 项目完善业务逻辑

关于UI实现部分可以参考这篇文章
代码下载可以参考GitHub

Home
List
Create
Studio

1. Java 中的 IntDef、StringDef在Kotlin中如何实现???

Kotlin 1.0.3 后 @IntDef 不被支持了
KMM shared module 中,不支持 Java 类
虽然 AndroidMain 模块中支持创建 Java 类,但达不到共用的目的。另外个人觉得纯 Kotlin 项目掺杂点 Java 差点意思。
https://stackoverflow.com/questions/37833395/kotlin-annotation-intdef/37839539#37839539

简而言之,使用枚举会增大包体积
官方推荐使用 @IntDefStringDef这类

但Google 使用 R8 对枚举做了优化,在ART 虚拟机上声称我们不用再担心枚举类的体积问题,而ART是5.0引入的,现在开发也基本都是5.0起步

所以还是写枚举吧。

2. 使用JetPack Compose Navigation 后,一个 composable 方法绘制的UI代码会重复调用多次

测试启动APP,startDestination指向的Test1方法调用两次
跳转到Test2后,Test1方法又调用一次,Test2再连续调用两次

2022-03-06 01:39:04.856 5053-5053/io.agora.live.livegame.android D/lq: Test1
2022-03-06 01:39:05.069 5053-5053/io.agora.live.livegame.android D/lq: Test1
2022-03-06 01:39:07.978 5053-5053/io.agora.live.livegame.android D/lq: Test1
2022-03-06 01:39:07.980 5053-5053/io.agora.live.livegame.android D/lq: Test2
2022-03-06 01:39:08.334 5053-5053/io.agora.live.livegame.android D/lq: Test2

目前看来这是无解的,猜测是当前页面改变后 NavHost 的默认行为。

3. 如何将页面与 ViewModel 关联

官方的推荐做法是仅将 ViewModel 与 Screen 级别的 composable 关联

ViewModel 还是正常写
但是由于上面第二点提到的,composable 方法可能会重复执行,那么这里要如何正确初始化ViewModel 呢?

ViewModel 会跟随 viewModelStoreOwner 的生命周期,并且可以在 Configuration 改变时存活下来(无非就是 onDestroy 时判断了一下是否是由于 configuration 改变导致的)

这个特性,如果只是简单地 new MyViewModel() 肯定不行

这里,官方也提供了一个方法 viewmodel()

其实这与原生的by viewmodels()一样

4. ViewModel 如何传参数

  • 自定义ViewModelFactory

    这种方式每个ViewModel要写一个类

  • 使用saveStateHandler

    目前它对于data class处理不优雅,能想到的只能转json或者序列化。
    听说最近出了个CreationExtra,不知道能不能解决这个问题。

5. 如何通过按钮调起SoftKeyboard软键盘

6. SurfaceView 、WebView 等 Compose 不支持的View如何处理


以下为一些开发中发现的错误及调查、处理过程

1. java.lang.IllegalStateException: Compose Runtime internal error

错误如下

2022-03-07 12:15:37.625 4724-4724/io.agora.live.livegame.android E/AndroidRuntime: FATAL EXCEPTION: main
    Process: io.agora.live.livegame.android, PID: 4724
    java.lang.IllegalStateException: Compose Runtime internal error. Unexpected or incorrect use of the Compose internal runtime API (Start/end imbalance). Please report to Google or use https://goo.gle/compose-feedback
        at androidx.compose.runtime.ComposerKt.composeRuntimeError(Composer.kt:3466)
        at androidx.compose.runtime.ComposerImpl.finalizeCompose(Composer.kt:3550)
        at androidx.compose.runtime.ComposerImpl.endRoot(Composer.kt:1237)
        at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2588)
        at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2547)
        at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:620)
        at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:786)
        at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:105)
        at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:456)
        at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:425)
        at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:34)
        at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109)
        at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
        at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1035)
        at android.view.Choreographer.doCallbacks(Choreographer.java:845)
        at android.view.Choreographer.doFrame(Choreographer.java:775)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1022)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7839)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

业务逻辑是 RoomList-> Create -> Studio
Create完成创建,popUp到RoomList同时导航到Studio

目前做法是,UI 监听 ViewModel 的 State,调用业务逻辑去更新State,同时UI也会重新执行 composable

最初怀疑出错的代码调用

// 1. ViewModel 请求接口
val callback = object : BaseStateCallback {
    override fun onSuccess(data: Unit) {
// 回调在子线程
        createState.value = DataState.Success(data)
    }
    override fun onFailure(exception: Throwable) {
        createState.value = DataState.Failure(exception)
    }
}

// 2. UI部分
@Composable
fun CreateScreen(roomViewModel: CreateRoomViewModel = viewModel(), popBack: () -> Unit, nav2Studio: (createdRoom: RoomInfo) -> Unit) {
    "CreateScreen".log()
    val createState = roomViewModel.createState.value
// 根据 State 判断是否跳转
    if (createState is DataState.Success){
        "nav2Studio".log()
        nav2Studio()
// return 可能打断了 composable的正常流程
        return
    }
    1. 猜测是在子线程设置 value 导致的异常

    改成 KotlinFlow的 callback 后,问题依旧

    callbackFlow {
        val callback = object : BaseStateCallback {
            override fun onSuccess(data: Unit) {
                trySend(DataState.Success(data))
            }
            override fun onFailure(exception: Throwable) {
                trySend(DataState.Failure(exception))
            }
        }
        rtm.createRoom(pendingRoomInfo.value, callback)
        awaitClose { rtm.unregisterCallback(callback) }
    }.onEach {
      createState.value = it
    }.launchIn(viewModelScope)
    
    1. 猜测是 return 打断了 composable 的正常流程

    正常情况下,compose -> layout -> draw

    删除 return 后,果然好了,但是 由于 Navigation 改变,CreateScreen 还会继续调用一次

    而每次调用,又会跳转到新的Studio页面,Navigation又改变,...循环往复

    • 给页面堆栈添加限制

      navController.navigate(RallyPage.Studio){
          // 1. 启动模式
          launchSingleTop = true
          // 2. 移除当前页面
          this.popUpTo(RallyPage.RoomList)
      }
      

      结果:

      逻辑 效果 是否解决问题
      1 CreateStudio页面循环往复执行10次左右停止,结构【CS CS CS CS ...】
      2 CreateStudio页面被无限循环调用,结构【CS CSS CSSS CSSSS ...】
      1+2 CreateStudio页面循环往复执行10次左右停止,结构【CS CS CS CS ...】
    • 跳转逻辑执行前先重置 State 状态

      标准开发流程中,UI是不允许改变viewModel的数据的,后续待优化

      if (createState is DataState.Success){
          roomViewModel.createState.value = DataState.None
          nav2Studio(pendingRoomInfo)
      }
      

    成功,跳转逻辑只被执行了一次。

    虽然这种方法可以成功,但是感觉还是不优雅。期待后续优化...

    1. popUpTo的补充

由于我的 RoomList 页面路由是带参数的,而 popUpTo 是全量匹配
所以我改为下面这种跳转方式,相当于弹出Create自己

this.popUpTo(RallyPage.Create){
    inclusive = true
}

你可能感兴趣的:(KMM Android 项目完善业务逻辑)