一、Composable传参
- 直接传参
最普通的参数传递,与函数传参无异,即一级一级往下传递:
@Composable
fun UserInfo(user:User){
UserName(user.name)
}
@Composable
fun UserName(name:String){
Text(text=name)
}
但是如果有多个层级需要同一个参数呢? 那这种模式会很繁琐,对于这些情况,文档建议[compostionlocal]可以作为一种隐式的方式来使用数据 贯穿整个父级上下文。
简单来说就是,就是创建一个 CompositionLocal ,然后它的值可以通过CompositionLocalProvider 提供,然后在调用处用' CompositionLocal.current '来读取。 比如Composable中常用的获取context,就是通过这种方式实现的:
val context = LOcalContext.current
- CompositionLocal简单使用
假设我有几个Composable,他们之间需要参数传递,传递一本书的作者和封面:
- 假设有一本书,父亲和儿子都想看,一个一个读
data class Book(val name: String, val photoUrl: String)
// 页面入口
@Composable
fun TestEnterScreen() {
val advert = "https://img0.baidu.com/it/u=2586663849,2136659347&fm=26&fmt=auto"
val name = "孙子兵法"
val book = Book(name = name, photoUrl = advert)
// 父给大儿子
TestComposable1(book=book)
}
// 大儿子
@Composable
fun TestComposable1(book: Book) {
ReadBook(book)
// 老大读完给老2
TestComposable2(book=book)
}
// 二儿子
@Composable
fun TestComposable2(book: Book) {
ReadBook(book)
// 老2读完给老3
TestComposable3(book=book)
}
// 最小
@Composable
fun TestComposable3(book:Book){
// 老小读
ReadBook(book)
}
@Composable
fun ReadBook(book:Book){
CoilCircleImage(url = book.photoUrl, modifier = Modifier.size(120.dp))
}
- 后来有一天,他爹说,想做个书架,书就放上面,谁想读就去读:
首先,做个书架:
// bookshelf.kt
val ActiveBook = compositionLocalOf<Book> { error("No active book found!") }
接下来:
// 页面入口
@Composable
fun TestEnterScreen() {
val advert = "https://img0.baidu.com/it/u=2586663849,2136659347&fm=26&fmt=auto"
val name = "孙子兵法"
val book = Book(name = name, photoUrl = advert)
// 提供参数者
ReadBook(book)
CompositionLocalProvider(ActiveBook provides book) {
// 爹告诉大儿子,书我上架了,自己想读就去拿
TestComposable1()
}
}
// 大儿子
@Composable
fun TestComposable1() {
Text("大儿子没读",Modifier.padding(vertical = 16.dp, horizontal = 8.dp))
TestComposable2()
}
// 二儿子
@Composable
fun TestComposable2() {
Text("2儿子也没读",Modifier.padding(vertical = 16.dp, horizontal = 8.dp))
TestComposable3()
}
// 最小
@Composable
fun TestComposable3() {
val book = ActiveBook.current
ReadBook(book)
}
@Composable
fun ReadBook(book: Book) {
CoilCircleImage(url = book.photoUrl, modifier = Modifier.size(120.dp))
}
结论1:CompositionLocal可以保存传参,CompositionLocalProvider可以用来初始化或改变传参值
-
再后来大儿子听说金瓶梅好,自己读,两种情况:
- 读完就悄悄上架了,别人不知道。
- 不仅自己读了,还告诉了2儿子,这下好了,只有老爹蒙在鼓里。
// 页面入口 @Composable fun TestEnterScreen() { val advert = "https://img0.baidu.com/it/u=2586663849,2136659347&fm=26&fmt=auto" val name = "孙子兵法" val book = Book(name = name, photoUrl = advert) // 提供参数者 ReadBook(book) CompositionLocalProvider(ActiveBook provides book) { // 爹告诉大儿子,书我上架了,自己想读就去拿 TestComposable1() } } // 大儿子 @Composable fun TestComposable1() { // 大儿子找来一本瓶梅自己看 val newBook = Book( name = "金瓶梅", photoUrl = "https://bkimg.cdn.bcebos.com/pic/0eb30f2442a7d9333a74267fad4bd11372f001da?x-bce-process=image/resize,m_lfit,w_268,limit_1/format,f_auto" ) ReadBook(newBook) CompositionLocalProvider(ActiveBook provides newBook) { // 读完上架了,还告诉了2儿子 TestComposable2() } // 情况2:读完上架了:没有告诉2儿子 //TestComposable2() } // 二儿子 @Composable fun TestComposable2() { val book = ActiveBook.current ReadBook(book) TestComposable3() } // 最小 @Composable fun TestComposable3() { val book = ActiveBook.current ReadBook(book) } @Composable fun ReadBook(book: Book) { CoilCircleImage(url = book.photoUrl, modifier = Modifier.size(120.dp)) }结论2:CompositionLocalProvider的传参是就近父节点向子节点传参,且只能向下传递。
文档说:
在Composable树的某个地方,使用CompositionLocalProvider可以在树的“根”处为CompositionLocal提供一个值。 ,但也可以在任何地方,也可以在多个地方使用,以覆盖为父为子树提供的值。
公共属性current,返回最近的compontionlocalprovider组件所提供的值,该组件直接或间接地调用使用此属性的可组合函数。 https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal#current()
-
使用场景:
除了页面之间传参,最先想到的场景就是导航组件Navigation,可以把navController用CompositionLocal处理,这样就不用处处传参:
//in MainActivity setContent { val navController = rememberAnimatedNavController() CompositionLocalProvider( LocalNavController provides navController ){ xxx_Theme{ } } } // 定义“书架” // in Contanct.kt val LocalNavController = compositionLocalOf<NavController> { error("Not Init") } // 使用 LocalNavController.current
在官方文档androidx.compose.runtime目录提供了一些说明,但是说明很多地方我还是没看懂->官方文档
二、Compose中的权限请求
在读取文档的过程中发现了一个文件,它是一个为android提供的一些标准活动调用契约的集合,用它可以便捷选取本地文件,权限请求,拍照等操作,此处出于好奇只做简单尝试。
文档地址: androidx/activity/result/contract/ActivityResultContracts.kt
提供了一些类,包括以下还有ContentResolver,可以根据自己的需要扩展
RequestMultiplePermissions.class//多权限请求StartActivityForResult.classRequestPermission.class//单一权限请求TakePicture.class//你需要提供一个存储UriTakeVideo.class// 录视频GetContent// 内容选取,这个下面会举个例子
-
单次权限请求的使用:
将LauncherActivityResult定义为一个可观察状态值,点击按钮先检查权限,如果权限没给再做权限请求。
得到的值是一个boolean值,就几行代码,是不是非常简洁。
@Composable fun SinglePermissionTest() { val context = LocalContext.current val launcher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> if (isGranted) { oLog("request permission success.") showToast(context, "权限申请成功") } else { oLog("request permission failed") showToast(context, "权限申请被拒绝") } } Column( modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) { Button( onClick = { // 检查权限 when (PackageManager.PERMISSION_GRANTED) { ContextCompat.checkSelfPermission( context, Manifest.permission.READ_EXTERNAL_STORAGE ) -> { showToast(context, "权限访问正常哦") oLog("permission is ok") } else -> { // 请求权限 launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } } } ) { Text(text = "检查并申请单一权限") } } } -
多权限请求使用
多权限这只是个实现没有考虑性能问题,多权限通过ActivityResultContracts.RequestMultiplePermissions()的launch方法发起权限请求,返回值是个Map<String,Boolean>,即权限名称和是否通过的集合
@Composable
fun MutilPermissionTest() {
var remembers = remember {
mutableStateOf(
arrayListOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA
)
)
}
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) {
it.entries.filter { entry ->
oLog("请求结果:权限${entry.key}${if (entry.value) "通过" else "被拒绝"}")
entry.value
}.map { successEntry ->
if (remembers.value.contains(successEntry.key)) {
remembers.value.remove(successEntry.key)
}
}
if (remembers.value.isEmpty()) {
showToast(context, "权限全部通过")
}
}
Button(
onClick = {
if (remembers.value.isNullOrEmpty()) {
showToast(context, "权限全部通过")
} else {
// 检查所有权限
val allFailed = remembers.value.map { it ->
mutableListOf(
it to (ContextCompat.checkSelfPermission(
context,
it
) == PackageManager.PERMISSION_GRANTED)
).filter { entry ->
// 过滤未通过权限
!entry.second
}.map { entry ->
if (entry.second) {
remembers.value.remove(entry.first)
}
}
}
if (allFailed.isNotEmpty()) {
launcher.launch(remembers.value.toTypedArray())
}
}
},
) {
Text(text = "检查并请求多个权限")
}
}
在androidx.activity.compose 包下还提供了一个类ManagedActivityResultLauncher,可以用来管理前面提到的ActivityResultContract。
它的成员是ActivityResultContract:
/**
* A launcher for a previously-[prepared call][ActivityResultCaller.registerForActivityResult]
* to start the process of executing an [ActivityResultContract].
*
* This launcher does not support the [unregister] function. Attempting to use [unregister] will
* result in an [IllegalStateException].
*
* @param I type of the input required to launch
*/
public class ManagedActivityResultLauncher<I, O> internal constructor(
private val launcher: ActivityResultLauncherHolder<I>,
private val contract: State<ActivityResultContract<I, O>>
) : ActivityResultLauncher<I>() {
// ....
}
前面说ActivityResultContract它能提供内容选取,因为它定义了这个类:
/**
* 内部会通过android.content.ContentResolver.openInputStream给你选取的内容
* 入参需要指定一个文件类型, The input is the mime type to filter by, e.g. `image/*`
* 出参是Uri
*/
open class GetContent : ActivityResultContract<String, Uri?>() {
@CallSuper
override fun createIntent(context: Context, input: String): Intent {
return Intent(Intent.ACTION_GET_CONTENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(input)
}
final override fun getSynchronousResult(
context: Context,
input: String
): SynchronousResult<Uri?>? = null
final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
如果用ManagedActivityResultLauncher 来管理,就可以这么用:
/**
* 定义一个发射器结果管理类,
* 成员包含结果Uri和发射器launcher
* 调用发射器的launch,将得到结果uri
* 相当于把二者打包了
*/
class GetContentActivityResult(
// 入参是String,结果是Uri
private val launcher: ManagedActivityResultLauncher<String, Uri?>,
val uri: Uri?
) {
fun launch(mimeType: String) {
launcher.launch(mimeType)
}
}
/**
* 提供发射器实例
*/
@Composable
fun rememberGetContentActivityResult(): GetContentActivityResult {
var uri by remember{mutableStateOf<Uri?>(null)}
// rememberLauncherForActivityResult 是ManagedActivityResultLauncher的子类
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent(), onResult = {
oLog("rememberGetContentActivityResult, it:$it")
uri = it
})
return remember(launcher, uri) {
// 构造出GetContentActivityResult实例
GetContentActivityResult(launcher, uri)
}
}
/*********************************test :**************************************/
@ExperimentalCoilApi
@Composable
fun ContactPagePhoto() {
// 拿到构造好的发射器管理对象,点击click的时候调用它的发射方法,结果会回调在第二个成员变量uri中
val getContent = rememberGetContentActivityResult()
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.layoutId("userImageLayout")
.fillMaxWidth(1f)
) {
oLog("--ContactPagePhoto, uri: ${getContent.uri}")
// 拿到结果
getContent.uri?.let {uri->
Image(
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxSize(1f),
contentDescription = "selected image",
contentScale = ContentScale.Crop,
painter = rememberImagePainter(data = uri)
)
}
}
Button(
onClick = { getContent.launch("image/*") },
modifier = Modifier.layoutId("getImageBtn")
) {
Text(text = "选取照片")
}
}
这个launch方法,最后调用的是ComponentActivity.class的onLaunch()方法:
public <I, O> void onLaunch(final int requestCode, @NonNull ActivityResultContract<I, O> contract, I input, @Nullable ActivityOptionsCompat options) {
ComponentActivity activity = ComponentActivity.this;
final SynchronousResult<O> synchronousResult = contract.getSynchronousResult(activity, input);
if (synchronousResult != null) {
(new Handler(Looper.getMainLooper())).post(new Runnable() {
public void run() {
dispatchResult(requestCode, synchronousResult.getValue());
}
});
} else {
Intent intent = contract.createIntent(activity, input);
Bundle optionsBundle = null;
if (intent.getExtras() != null && intent.getExtras().getClassLoader() == null) {
intent.setExtrasClassLoader(activity.getClassLoader());
}
//.....
// .....
} else {
ActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle);
}
}
}
可见最终是startActivityForResult().
结论:ActivityResultContracts是对常用功能的封装,将入参和出参封装,我们只需要监听拍照,请求权限,内容选取的结果,方便业务处理。
-
Compose中系统返回键处理:
Compose底层创建了一个OnBackPressedCallback回调,并在可组合函数成功重组和后启用,也就是供当前Composable调用,在dispose生命周期中会被移除掉。上层提供了BackHandler拦截系统返回键,如果自定义Button想像系统返回键一样的效果,可以拿到系统返回键的分发调度器,用在自定义按钮中:
@Composable fun TestBackPressed() { // 返回按键 var backPressedCount by remember { mutableStateOf(0) } BackHandler { backPressedCount++ } // 拿到当前可以分发按键的Activity val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher Button(onClick = { dispatcher.onBackPressed() }) { Text("返回按键测试: $backPressedCount") } } // 或者 @Composable fun NewsDetail( state: DetailState, onBack: () -> Unit, ) { BackHandler(onBack = onBack) Scaffold( topBar = { CustomTopAppBar(onBack) } ) { // content } }