Jetpack Compose项目学习

实战项目

视频地址:https://www.bilibili.com/video/BV1aS4y1D7dv/?vd_source=3065178ab11a8e498478d23c269ac3a0

知识点

  • 状态栏颜色配置:res --> values --> themes.xml

    <style name="Theme.Worldlet" parent="android:Theme.Material.Light.NoActionBar">
        <item name="android:statusBarColor">@color/purple_700</item>
    </style>
    
  • 获取系统的AppBar高度可以通过查看TopAppBar源码,即56dp

  • 设置系统状态栏透明

    • 方案一

      1. 引入依赖

        // 可以设置System UI,例如系统状态栏的颜色和显示隐藏
        dependencies {
            implementation "com.google.accompanist:accompanist-systemuicontroller:<version>"
        }
        
      2. 在Activity中

        // 让内容显示在状态栏和系统导航栏后面,状态栏和导航栏会遮盖部分内容(在不同机型上)
        WindowCompat.setDecorFitsSystemWindows(window,false)
        
      3. 在themes.xml中

        <style name="Theme.Worldlet" parent="android:Theme.Material.Light.NoActionBar">
            <item name="android:statusBarColor">@color/purple_700</item>
            <item name="android:windowTranslucentStatus">true</item>
        </style>
        
    • 方案二

      // 处理不同机型,状态栏不透明问题
      window.statusBarColor = Color.Transparent.value.toInt()
      // 处理不同机型,导航栏遮盖内容问题(已过时)
      // systemUiVisibility只能设置一个值,如果想要设置多个值,则需要这些值进行或运算(Java中是|,Kotlin中是or)
      window.decorView.systemUiVisibility =
          View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
      
    • 方案三

      1. 引入依赖

        dependencies {
            implementation "com.google.accompanist:accompanist-systemuicontroller:<version>"
        }
        
      2. 设置状态栏透明

        val systemUiController = rememberSystemUiController()
        LaunchedEffect(key1 = Unit) {
            systemUiController.setStatusBarColor(Color.Transparent)
        }
        
      3. 设置导航栏,防止遮挡:将用 Modifier.navigationBarsPadding() 修饰

        注意:BottomNavigation 外围要有 ProvideWindowInsets 包裹

  • 获取系统状态栏高度

    • 方案一

      // 此方案只能在Activity中获取,如果其他组件需要,只能一步步向下传递
      var statusHeight = 0
      val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
      if (resourceId) {
          // 获取到的为Int,利用下一步转为dp
          statusHeight = resources.getDimensionPixelSize(resourceId)
      }
      
    • 方案二

      1. 引入依赖

        dependencies {
            implementation "com.google.accompanist:accompanist-insets:<version>"
        }
        
      2. 在需要的组件中

        val statusBarHeightDp = with(LocalDensity.current) {
            LocalWindowInsets.current.statusBars.top.toDp()
        }
        
  • 转换 pxdp

    val statusBarHeightDp = with(LocalDensity.current) {
        // statusBarHeight为Int,density:屏幕密度
        statusBarHeight.toDp()
    }
    
  • Modifier.then() 函数可以将两个Modifier合并

  • 依赖文件中,如果想使用定义的变量,则依赖项要用双引号包裹

    dependencies {
    	// 正确
        implementation "com.google.accompanist:accompanist-insets:<version>"
        // 错误    
        implementation 'com.google.accompanist:accompanist-insets:<version>'
    }
    
  • 获取屏幕宽度或高度

    var otherSize: Int
    with(LocalConfiguration.current) {
        otherSize = screenWidthDp / 2
    }
    
  • Modifier.offset() 可以为负值,用于在 padding 不合适时进行负偏移( padding 不能为负值),但只能用在父组件上,用于控制子组件

  • 在使用Row时,horizontalArrangement生效的前提是要有宽度限定,同理,在使用Colum时,verticalArrangement要有高度限定

  • Modifier.weight(weight: Float, fill: Boolean)

    如果 fill 为true,元素将占据分配的整个宽度,反之,包裹内容即可

  • dppx 的转换关系:dpValue = valueInPixel / density

  • 在 Compose 中使用 View ,使用 AndroidView 可组合项,可以直接调用相应的View组件,也可以从XML加载

    // 直接调用
    AndroidView(factory = { context ->
        TXCloudVideoView(context).apply {
            vodPlayer.setPlayerView(this)
        }
    })
    // XML加载
    AndroidView(factory = { context ->
        (LayoutInflater.from(context).inflate(R.layout.video, null, false)
            .findViewById(R.id.videoView) as TXCloudVideoView).apply {
            vodPlayer.setPlayerView(this)
        }
    })
    
  • Compose 中横竖屏切换

    // 定义扩展函数
    fun Context.findActivity(): Activity? = when (this) {
        is Activity -> this
        is ContextWrapper -> baseContext.findActivity()
        else -> null
    }
    
    // 控制横竖屏
    val configuration = LocalConfiguration.current
    val context = LocalContext.current
    if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
        context.findActivity()?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
    } else {
        context.findActivity()?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
    }
    
    <!-- 在 AndroidManifest.xml 中配置启动时强制竖屏 -->
    <activity
        android:name=".MainActivity"
        android:exported="true"
        android:label="@string/app_name"
        android:screenOrientation="portrait"
        android:theme="@style/Theme.App">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
    
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    
  • rememberSaveable 会自动保存可保存在 Bundle 中的任何值,对于其他值,可以将其传入自定义 Saver 对象,实现 saverestore 方法,对于 save 方法的返回值类型,要可序列化。通过以下方式使类可序列化:

    // 在 build.gradle(Project:App) 中
    plugins {
        id 'org.jetbrains.kotlin.plugin.parcelize' version '1.6.10' apply false
    }
    
    // 在 build.gradle(Project:App.app) 中
    plugins {
        id 'org.jetbrains.kotlin.plugin.parcelize'
    }
    
    import android.os.Parcelable
    import kotlinx.parcelize.Parcelize
    
    @Parcelize
    class MyClass : Parcelable {
        ...
    }
    
  • 监听生命周期

    val lifecycleOwner = LocalLifecycleOwner.current
    
    DisposableEffect(vodController) {
        val lifecycleEventObserver = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_RESUME -> vodController.resume()
                Lifecycle.Event.ON_PAUSE -> vodController.pause()
                else -> {}
            }
        }
        lifecycleOwner.lifecycle.addObserver(lifecycleEventObserver)
    
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleEventObserver)
            vodController.stopPlay()
        }
    }
    
  • 监听返回键

    BackHandler(enable = true) {
        // 所要进行的操作
    }
    

    注意:使用上述组件会拦截返回键,也就是说点击返回键只会执行上述操作,默认的操作将丢失,需要在上述操作中添加对默认操作的处理,也可以使用 enabled 参数使 BackHandler 只在特定情况下启用

  • ViewModel 中调用挂起函数可以使用 viewModelScope.launch { } 来提供协程作用域

  • 将 Compose 的 State 作为 LaunchedEffectkey 时,需要使用 snapshotFlow 将 State 转换为 Flow ,否则监听不到变化

    参考文档:https://developer.android.google.cn/jetpack/compose/side-effects#snapshotFlow

Modifier

Compose 修饰符列表

函数描述
clickable添加点击事件,默认会有水波纹效果,可以通过传递参数 interactionSource = remember { MutableInteractionSource() } , indication = null 取消
placeholder添加占位符,需要使用第三方库 Placeholder

组件

名字描述使用
BottomSheetScaffold脚手架类似于 Scaffold ,不过自带一个BottomSheet,默认高度 56.dp ,可以设为 0.dp ,然后设置底部弹窗,需要设置 scaffoldStaterememberBottomSheetScaffoldState() 获取),然后可以控制其状态设置弹窗是否显示
Canvas画布画弧形(drawArc)等,Canvas上单位为 pxInt.dp.toPx() 可以转换单位(也可以画其他的比如折线图,但没有相应的方法,需要自己计算怎么画)
CircularProgressIndicator环形加载指示器
Divider分割线在组件之间显示分割线
LazyColumn懒加载列内部可以使用item添加单个项,也可以使用items循环添加
LazyHorizontalGrid懒加载网格先排列列,使用 GridCells.Fixed 设置固定行数,使用 GridCells.Adaptive 设置动态行数
LazyVerticalGrid懒加载网格先排列行,使用 GridCells.Fixed 设置固定列数,使用 GridCells.Adaptive 设置动态列数
LeadingIconTab带图标的单个分类TabRow子项
Scaffold脚手架页面框架,在设置bottomBar等参数后,Scaffold会算出一个剩余空间传入content
Slider滑块实现可拖动进度条、档次选择(例如字体小、大、超大)等功能,注意实现档次选择功能时其 steps 参数为中间的档次数,例如上例应为1
Spacer空白空间可以用来控制组件间距
Tab单个分类TabRow子项
TabRow分类标签显示多个分类
TextField输入框visualTransformation 可以配置视觉效果,如密码显示为点
keyboardOptions 可以配置键盘选项
colors 可以配置输入框的各种颜色,包括聚焦状态、指示器

实战经验

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SeqPVdEr-1670167793715)(https://api.onedrive.com/v1.0/shares/s!AoF7mN_tKB-G0B5VxMtOoyIqXgEQ/root/content)]

    需要引入依赖

    dependencies {
     implementation "android.lifecycle:lifecycle-runtime-ktx:<version>"
     implementation "android.lifecycle:lifecycle-viewmodel-compose:<version>"
    }
    
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mEeeRaZk-1670167793716)(https://api.onedrive.com/v1.0/shares/s!AoF7mN_tKB-G0B0dhekJjjumYZjg/root/content)]

  • 一般ViewModel和Entity是一对多的关系,一个页面可能会用到几个Entity,但一般只有一个ViewModel

  • 图片的纵横比(aspectRatio)一般为7/3或16/9

  • 设置轮播图无限滑动

    @OptIn(ExperimentalPagerApi::class)
    @Composable
    fun SwiperContent(vm: MainViewModel) {
    
        //虚拟页数
        val virtualCount = Int.MAX_VALUE
    
        //实际页数
        val actualCount = vm.swiperData.size
    
        //初始图片下标
        val initialIndex = virtualCount / 2
    
        val pagerState = rememberPagerState(initialPage = initialIndex)
    
        val coroutineScope = rememberCoroutineScope()
    
        //自动滚动
        DisposableEffect(Unit) {
    
            coroutineScope.launch {
                vm.swiperData()
            }
    
            val timer = Timer()
    
            timer.schedule(object : TimerTask() {
                override fun run() {
                    if (vm.swiperLoaded) {
                        coroutineScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
                    }
                }
            }, 3000, 3000)
    
            onDispose {
                timer.cancel()
            }
        }
    
        HorizontalPager(
            count = virtualCount,
            state = pagerState,
            modifier = Modifier
                .padding(horizontal = 8.dp)
                .clip(RoundedCornerShape(8.dp)),
            userScrollEnabled = vm.swiperLoaded
        ) { index ->
            val actualIndex =
                (index - initialIndex).floorMod(actualCount) //index - (index.floorDiv(actualCount)) * actualCount
    
            AsyncImage(
                model = vm.swiperData[actualIndex].imageUrl,
                contentDescription = null,
                modifier = Modifier
                    .fillMaxWidth()
                    .aspectRatio(7 / 3f)
                    .placeholder(
                        visible = !vm.swiperLoaded,
                        highlight = PlaceholderHighlight.shimmer()
                    ),
                contentScale = ContentScale.Crop
            )
        }
    }
    
    fun Int.floorMod(other: Int): Int = when (other) {
        0 -> this
        //将虚拟数据按照实际数据总数分为 N 组
        //当前虚拟下标是在这虚拟数据中的哪一组:虚拟下标floorDiv实际数据总数(虚拟下标/实际数据总数)。向下取整
        //虚拟下标 - (虚拟下标/实际数据总数) * 实际数据总数
        else -> this - floorDiv(other) * other
    }
    
  • WebView

  • 作用域与模块化

  • 当以函数做参数且可以为空时,不可以直接调用(例如 content() ),这时可以使用 content?.invoke() 调用,其中 content()content.invoke() 效果一样

  • 因为 Jetpack Compose 有很多实验性API,项目开始之前,先确定好使用的版本,否则将来开发过程中再改变版本,可能会出现实验性API被改变,导致不必要的重构。如果项目开发过程中确实要改变版本,且遇到了实验性API的改变,可以去官方文档查看实验性API的改变情况

  • 视频播放器可以用 Box 包裹,因为视频播放器会有一些控制按钮(暂停、倍速等),而 Box 可以将组件堆叠起来,也就是说一层视频播放层,一层视频控制层,视频控制层在视频播放层上面

    参考实现:https://gitee.com/RandyWei/jetpack-compose-case/blob/main/App/app/src/main/java/icu/bughub/app/app/ui/components/video/VideoPlayer.kt

  • 复杂渐变实现

    // 实现思路:在右上角向左下角渐变,从黄色到透明,再从左下角向右上角渐变,从蓝色到透明,即堆叠两个
    // 也可以将最外层的 BoxWithConstraints 换成 Box ,但要手动获取屏幕的宽高,并转换为 px
    BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
        //背景图层
        Image(
            painter = painterResource(id = R.drawable.bg),
            contentDescription = null,
            modifier = Modifier.fillMaxSize(),
            contentScale = ContentScale.Crop
        )
    
        //右上往左下渐变层
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(
                    // 可以实现从指定位置到指定位置的渐变
                    Brush.linearGradient(
                        listOf(Color(0xffbb8378), Color.Transparent),
                        // BoxWithConstraints 中可以直接获取屏幕的宽高( BoxWithConstraints 铺满屏幕的前提下)
                        start = Offset(x = constraints.maxWidth.toFloat(), y = 0f),
                        end = Offset(x = 0f, y = constraints.maxHeight.toFloat())
                    )
                )
        )
        //左下往右上渐变层
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(
                    Brush.linearGradient(
                        listOf(Color(0xFF149EE7), Color.Transparent),
                        start = Offset(x = 0f, y = constraints.maxHeight.toFloat()),
                        end = Offset(x = constraints.maxWidth.toFloat(), y = 0f)
                    )
                )
        )
    }
    
  • 到达底部后加载更多

    参考实现:https://www.bilibili.com/video/BV1aS4y1D7dv/?p=47&vd_source=3065178ab11a8e498478d23c269ac3a0

第三方库

  • Accompanist:Jetpack Compose 的工具包

    https://google.github.io/accompanist/

  • 异步加载图片

    Coil https://coil-kt.github.io/coil/

  • 视频SDK

    腾讯视频SDK集成文档 https://cloud.tencent.com/document/product/881

  • 网络请求框架

    Retrofit https://square.github.io/retrofit/

  • JSON解析

    GSON https://github.com/google/gson

    Moshi https://github.com/square/moshi/
    vigation-animation/) 可以设置动画的 Navigation

  • 异步加载图片

    Coil https://coil-kt.github.io/coil/

  • 视频SDK

    腾讯视频SDK集成文档 https://cloud.tencent.com/document/product/881

  • 网络请求框架

    Retrofit https://square.github.io/retrofit/

  • JSON解析

    GSON https://github.com/google/gson

    Moshi https://github.com/square/moshi/

### Jetpack Compose 项目架构设计 #### 架构概述 Jetpack Compose 并不是一个单一的整体框架,而是由多个模块构成的生态系统。这种分层结构允许开发者根据不同需求灵活选择所需的组件和服务[^5]。 #### 主要层次划分 1. **UI 层** - 负责界面渲染逻辑。 - 提供声明式的 UI 编写方式,使得视图更新更加直观简单。 2. **业务逻辑层 (ViewModel 或其他状态管理方案)** - 处理应用程序的核心功能实现。 - 可采用 MVI(Model-View-Intent)[^4] 等先进架构模式来增强代码可维护性和测试友好度。 3. **数据访问对象(DAO)/Repository 层** - 封装了与持久化存储交互的方法。 - 实现本地数据库操作、网络请求等功能。 4. **基础工具类/扩展函数集** - 收录常用辅助方法及自定义修饰符等实用程序。 - 方便快速集成第三方库或复用已有解决方案。 ```kotlin // ViewModel 示例 class MyViewModel : ViewModel() { private val _uiState = MutableStateFlow(Data()) val uiState: StateFlow<Data> get() = _uiState fun fetchData(){ viewModelScope.launch{ try { // 模拟异步加载过程... delay(2000L) _uiState.value = Data("Loaded!") } catch(e: Exception){ Log.e("MyViewModel", "Error fetching data.", e) } } } } ``` #### 最佳实践建议 为了充分利用 Jetpack Compose 的特性并保持良好的编码习惯: - 遵循单向数据流原则,在 View 和 Business Logic 之间建立清晰界限; - 利用 CompositionLocal 进行依赖注入,简化参数传递流程; - 积极参与社区交流和技术分享活动,及时掌握最新动态和发展趋势[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值