简谈Compose中的Composable参数传递与Android权限请求

3,265 阅读7分钟

一、Composable传参

  1. 直接传参

最普通的参数传递,与函数传参无异,即一级一级往下传递:

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

22222.jpg

结论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儿子大儿子告诉了2儿子

    结论2:CompositionLocalProvider的传参是就近父节点向子节点传参,且只能向下传递

文档说:

在Composable树的某个地方,使用CompositionLocalProvider可以在树的“根”处为CompositionLocal提供一个值。 ,但也可以在任何地方,也可以在多个地方使用,以覆盖为父为子树提供的值。

公共属性current,返回最近的compontionlocalprovider组件所提供的值,该组件直接或间接地调用使用此属性的可组合函数。
https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal#current()
  1. 使用场景:

    除了页面之间传参,最先想到的场景就是导航组件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提供的一些标准活动调用契约的集合,用它可以便捷选取本地文件,权限请求,拍照等操作,此处出于好奇只做简单尝试。

ezgif-5-6f50d19cb4.gif

文档地址: androidx/activity/result/contract/ActivityResultContracts.kt

提供了一些类,包括以下还有ContentResolver,可以根据自己的需要扩展

  • RequestMultiplePermissions.class //多权限请求
  • StartActivityForResult.class
  • RequestPermission.class //单一权限请求
  • TakePicture.class //你需要提供一个存储Uri
  • TakeVideo.class // 录视频
  • GetContent // 内容选取,这个下面会举个例子
  1. 单次权限请求的使用:

    将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 = "检查并申请单一权限")
            }
        }
    }
    
  2. 多权限请求使用

    多权限这只是个实现没有考虑性能问题,多权限通过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 = "选取照片")
    }
}

ezgif-3-2d3df0d80b.gif

这个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是对常用功能的封装,将入参和出参封装,我们只需要监听拍照,请求权限,内容选取的结果,方便业务处理。

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