关于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
简而言之,使用枚举会增大包体积
官方推荐使用 @IntDef、StringDef这类
但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<Unit> {
    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
    }
- 
- 猜测🤔是在子线程设置 value 导致的异常
 改成 KotlinFlow的 callback 后,问题依旧😭 callbackFlow { val callback = object : BaseStateCallback<Unit> { 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)
- 
- 猜测🤔是 return 打断了 composable 的正常流程
 正常情况下,compose -> layout -> draw 删除 return 后,果然好了,但是 由于 Navigation 改变,CreateScreen 还会继续调用一次 而每次调用,又会跳转到新的 Studio页面,Navigation又改变,...循环往复🙄- 
🤤给页面堆栈添加限制 navController.navigate(RallyPage.Studio){ // 1. 启动模式 launchSingleTop = true // 2. 移除当前页面 this.popUpTo(RallyPage.RoomList) }结果: 逻辑 效果 是否解决问题 1 Create、Studio页面循环往复执行10次左右停止,结构【CS CS CS CS ...】❌ 2 Create、Studio页面被无限循环调用,结构【CS CSS CSSS CSSSS ...】❌ 1+2 Create、Studio页面循环往复执行10次左右停止,结构【CS CS CS CS ...】❌ 
- 
🤤跳转逻辑执行前先重置 State 状态 标准开发流程中,UI是不允许改变viewModel的数据的,后续待优化 if (createState is DataState.Success){ roomViewModel.createState.value = DataState.None nav2Studio(pendingRoomInfo) }
 成功,跳转逻辑只被执行了一次。 虽然这种方法可以成功,但是感觉还是不优雅。期待后续优化... 
- popUpTo的补充
 
由于我的 RoomList 页面路由是带参数的,而 popUpTo 是全量匹配
所以我改为👇下面这种跳转方式,相当于弹出Create自己
this.popUpTo(RallyPage.Create){
    inclusive = true
}