Compose学习笔记1-compose、state、flow、remember

新建一个 compose 项目

开始前,请下载最新版本的 Android Studio Arctic Fox,然后使用 Empty Compose Activity 模板创建应用。

我们先看看在 app/build.gradle 中是如何配置使用 compose 的。

android{
    buildFeatures {
        // viewbinding 之类的功能也需要在此开启
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
}
dependencies {
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}

观察第一个不同之处

默认项目创建的这个 Activity 继承自 ComponentActivity 这个类,而不是我们熟悉的 AppcompatActivity ,这两个类的层级关系如下:

请添加图片描述

普通的用法:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_webview);
}

compose的用法:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    WindowCompat.setDecorFitsSystemWindows(window, false)
    setContent {
    }
}

activity 里的 setContent{} 函数来自androidx.activity:activity-compose

在这个函数中还调用了另一个 setContent{}函数来自androidx.compose.ui:ui

在 compose 中使用 viewmodel

viewmodel很好用,这毋庸置疑,在 compose 中 我们使用 viewmodel 更一步的简化了,只需要在需要时调用 viewModel<>() 函数即可

这一扩展来自于:androidx.lifecycle:lifecycle-viewmodel-compose

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    WindowCompat.setDecorFitsSystemWindows(window, false)
    setContent {
        val appViewModel = viewModel<AppViewModel>()
    }
}

插播一个Flow的玩法

flow 是 kotlin 推出的用于在协程中处理多个异步数据返回的工具(异步数据流),地位等同于 Rxjava 但是比 RX 容易很多也简洁很多。

看一段示例代码:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*


//sampleStart               
fun simple(): Flow<Int> = flow { // 流构建器
    for (i in 1..3) {
        delay(100) // 假装我们在这里做了一些有用的事情
        emit(i) // 发送下一个值
    }
}


fun main() = runBlocking<Unit> {
    // 启动并发的协程以验证主线程并未阻塞
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(100)
        }
    }
    // 收集这个流
    simple().collect { value -> println(value) }
}
//sampleEnd

flow函数可以构建一个 Flow,注意 flow 函数是一个 suspend 挂起函数;

  • 名为 flow 的 Flow 类型构建器函数。
  • flow { ... } 构建块中的代码可以挂起。
  • 函数 simple 不再标有 suspend 修饰符。
  • 流使用 emit 函数 发射 值。
  • 流使用 collect 函数 收集 值。

流是冷的

冷流只是一个概念,等同于 rx 中,创建的 Observable,他在被subscribe之前是不会执行的
Flow 也一样,调用构建函数 flow{} 并不会执行异步流,lambda中的发射函数 emit 也不会执行
直到 flow 函数被 collect() 收集,此时流才开始工作,这也就是所谓冷流的概念。

看一段我以前项目的示例:

private fun syncToDb() {
    lifecycleScope.launch {
        //sync to db
        flow {
            val map = mapOf("userObjId" to SpUtils.decodeString(Constants.SP_USER_ID))
            val resp = BmobMethods.INSTANCE.getAllNoteByUserId(map.toJson())
            emit(resp)
        }.catch { e ->
            XLog.e(e)
        }.flatMapConcat {
            val allNote = it.toBean<GetAllNoteResp>()
            allNote.results.asFlow()
        }.onEach { note ->
            //云端的每一条笔记
            val objId = note.objectId
            //根据bmob的objid查表
            val dbBean = NoteUtils.queryNoteById(objId)
            if (dbBean == null) {
                //本地没有次数据 新建本地数据并保存数据库
                val entity = note.toEntity()
                entity.save()
                XLog.d("本地没有该数据 新建\n$entity")
            }
            //存在本地对象 对比是否跟新 or 删除
            dbBean?.let {
                if (it.isLocalDel) {
                    //本地删除
                    val resp = BmobMethods.INSTANCE.delNoteById(it.objId)
                    if (resp.contains("ok")) {
                        //远端删除成功 本地删除
                        it.delete()
                        XLog.d("远端删除成功 本地删除\n$resp")
                    }
                    return@let
                }
                //未本地删除对比数据相互更新
                when {
                    note.updatedTime > it.updatedTime -> {
                        //云端内容更新更新本地数据
                        it.update(note)
                        XLog.d("使用云端数据更新本地数据库\n$it\n$note")
                    }
                    note.updatedTime < it.updatedTime -> {
                        //云端数据小于本地 更新云端
                        note.update(it)
                        val resp = BmobMethods.INSTANCE.putNoteById(
                            objId,
                            note.toJson(excludeFields = Constants.DEFAULT_EXCLUDE_FIELDS)
                                .createJsonRequestBody()
                        )
                        val putResp = resp.toBean<PutResp>()
                        XLog.d("使用本地数据更新云端数据 \n$it\n$note")
                    }
                    else -> {
                        //数据相同 do nothing
                        XLog.d("本地数据与云端数据相同\n$it\n$note")
                        return@let
                    }
                }
                it.isSync = true
                it.save()
            }
        }.flowOn(Dispatchers.IO).onCompletion {
            //完成时调用与末端流操作符处于同一个协程上下文范围
            XLog.d("Bmob☁️同步执行完毕,开始同步本地数据到云端")
            syncToBmob()
        }.collect {
        }
    }
}
//同步本地其他数据到云端
private suspend fun syncToBmob() {
    //未同步的即本地有而云端无
    NoteUtils.listNotSync().asFlow()
        .onEach {
            XLog.d(it)
            if (it.objId.isEmpty()) {
                //本地有云端无 本地无objId  直接上传
                val note = it.toBmob()
                val resp = BmobMethods.INSTANCE.postNote(
                    note.toJson(excludeFields = Constants.DEFAULT_EXCLUDE_FIELDS)
                        .createJsonRequestBody()
                )
                val postResp = resp.toBean<PostResp>()
                //保存objectId
                it.objId = postResp.objectId
                XLog.d("本地有云端无 新建数据")
            } else {
                //云端同步后 本地不可能出现本地有记录,且存在云端objid,但是没有和云端同步
                // 这种情况只可能是云端手动删除了记录,但是本地没有同步,
                // 即一个账号登录了两个客户端,但是在一个客户端中对该记录进行了删除,在另一个客户端中还存在本地记录
                //此情况可以加入特殊标记 isCloudDel
                it.isCloudDel = true
                XLog.d("不太可能出现的一种情况\n$it")
            }
            it.isSync = true
            it.save()
        }.flowOn(Dispatchers.IO).onCompletion {
            //完成时调用与末端流操作符处于同一个协程上下文范围
            listAllFromDb()
        }.collect {
            XLog.d(it)
        }
}

这是我以前写的一个示例demo,其中用到了Flow,感兴趣的可以去看一下源码:junerver/CloudNote
注意上面使用到的关键函数:

flow 之前我们说过了,用于构造Flow流,通过 emit 发射数据

catch 捕获异常没啥好说的

flatMapConcat 等同于 RxJava中的 flatMap,展平,他的最后一行是返回值,因为我的返回值是一个List需要通过 asFlow 转换成 flow 流,完成展平

onEach 对上游发射过来的每一条数据进行处理,该函数的返回值是被我门处理后的数据,不需要我们特别处理

flowOn 约等于 subscribeOn(Schedulers.io()) 表示流执行的协程

collect 之前说过 约等于 subscribe() 函数,此事流开始执行,注意 collect 执行在哪个协程就在哪个协程,不像RxJava需要使用 observeOn(AndroidSchedulers.mainThread()) 来切换到主线程这样的操作

最简单的流操作就大概如此了,除此以外还有比如 SharedFlowStateFlow 等进阶内容,以后用到再说

可以参考的文章:
协程进阶技巧 - StateFlow和SharedFlow

Kotlin Flow】 一眼看全——Flow操作符大全
不做跟风党,LiveData,StateFlow,SharedFlow 使用场景对比

为什么要在 Compose 中说 Flow

Flow 很好用,比如 StateFlow 甚至可以代替 LiveData,在 Compose 中这一点被放大了

在包 androidx.compose.ui:ui-tooling 中依赖了 androidx.compose.runtime:runtime
它为 StateFlow 提供了一个扩展函数 collectAsState()

@Suppress("StateFlowValueCalledInComposition")
@Composable
fun <T> StateFlow<T>.collectAsState(
    context: CoroutineContext = EmptyCoroutineContext
): State<T> = collectAsState(value, context)

这里看起来函数返回了 一个State类型的封装对象,其实不然

@Stable
interface State<out T> {
    val value: T
}

//注意这是by关键字实现
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value

这是一个非常屌的包装
返回的这个 State 我们使用的时候无需任何解包装,直接就可以使用,而且他似乎还是 Stateful 的。

当然使用的时候不能直接使用这个对象,需要通过 by 来实现获取到value的效果,这样就不需要调用 state.value 这样的写法了。

var count = 0
val testStateFlow = MutableStateFlow(count)
@Composable
fun Greeting(name: String) {
    val state by testStateFlow.collectAsState()
    Column {
        Text(text = "Hello $name!${state}")
        Button(onClick = { testStateFlow.value = ++count},
            modifier = Modifier
                .height(50.dp)
                .width(100.dp)
        ){
            Text(text = "点一下")
        }
    }
}

这里其实我们可以还可以有别的写法:

var count = 0
val testStateFlow = mutableStateOf(count)
@Composable
fun Greeting(name: String) {
//    val state by testStateFlow.collectAsState()
    Column {
        Text(text = "Hello $name!${testStateFlow.value}")
        Button(onClick = { testStateFlow.value = ++count},
            modifier = Modifier
                .height(50.dp)
                .width(100.dp)
        ){
            Text(text = "点一下")
        }
    }
}

就是不使用 Flow 的写法,直接使用 compose 提供的 State。

但这些都不是正确的写法!

为什么说他是错误的写法,因为 compose 是函数式的UI构建方式,每一个composable 函数自身应该是不依赖外部变量的,这种写法其实严重的违背了这一原则。
这时候就要介绍我们的 remember 函数了!

/**
* Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition.
* Recomposition will always return the value produced by composition.
*/
@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

机翻:记住由calculation产生的value。calculation只会在组成过程中进行评估。compose 重构时将始终返回由 composition 产生的值。
大致意思就是:该函数可以将块中的值保存起来,当compose视图重构时,可以读取到这个保存的值。

这种用法类似于在 Flutter 中使用StatefulWidget 类来包装控件(或者使用GetX),而在 Kotlin Compose 中,将 FP 风格贯彻的很好,没有那么多类,而是一个个的函数。需要实现这种 Stateful 时也更为简单。

这种写法乍一看来很奇怪,把变量保存在一个函数里,但其实有很大的优势,比如可以 UI 与逻辑混合使用,这一点比起 Flutter 具有巨大的优势。

例如下面这样的写法:

@Composable
fun Greeting(name: String) {
    val counter = remember {
        mutableStateOf(0)
    }
    Column {
        if (counter.value % 2 == 1) {
            Text(text = "Hello $name!${counter.value}")
        }
        Button(
            onClick = {
                counter.value += 1
            },
            modifier = Modifier
                .height(50.dp)
                .width(100.dp)
        ) {
            Text(text = "点一下")
        }
    }
}

你可以试一下上面的例子,就能看出在 compose 中 UI 配合逻辑是多么简单。

小tips:如果你需要频繁的用到这个state ,翻来覆去的写 state.value 确实让人很烦,这时通过 by 关键字来获取确实很轻松。

在 MutableState 与 MutableStateFlow中如何选择?

上面我们这两个用于保存状态的类型我们都使用了一编,仅他俩作为 State 使用时看起来区别不大,如何选择其实完全看使用场景:
个人认为,在一些简单的场景,我们只是想要这个值用于 UI 的响应式变更刷新,那么直接使用 State 就可以了;如果数据来源是一个流,并且需要做一系列处理(比如 mapflatMap 等),最终结果以 State 形式反应在 UI 上,那就用 Flow 流更方便。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值