Android开发Compose框架使用开篇

目录
  • Compose的诞生
  • Compose好处
  • Compose 架构
  • @Composable的背后
  • 智能重组真的那么智能吗
  • 最后

Compose的诞生

在2019年的谷歌IO大会上,Compose作为Android新一代UI开发亮相,因为声明式开发越来越流行了,对标IOS开发SwiftUi,Compose的立项也为Android开发新加了声明式ui的开发选项,在2021年7月1.0正式版本的诞生,也意味着Compose即将进入生产环节,国际app巨头Twitter就首当其冲,在新页面上用上了Compose

Compose好处

与传统的xml相比,Compose不仅吸收了其优点,摒弃了糟粕,还具有以下几个优点

声明式 兼容性 跨平台 布局效率
不同于传统的命令式,ui的刷新需要调用者主动调用刷新方法,比如TextView需要特定的setText进行文本变化,而compose在定义好声明状态后,由框架自主调用刷新,减少状态不一致 compose最低兼容到android api 21,不但可以在原来View体系中嫁接使用,也可以在compose中使用原来View体系的xml 跨平台,目前支持macos等多个平台,跨平台由Jetbrain团队在做,compose未来会实现ui多跨平台,同时也搭配逻辑跨平台KMM项目(有关kmm的以后有机会可以再说说,比起说跨平台,更不如说是多平台,因为编译出来的代码是直接符合原平台开发规范的,比如ios编译出来的就是framework),未来实现ui跟逻辑都跨平台也不在遥远 compose 是严格遵循LayoutNode的单次测量,不会出现View的多次测量导致的问题,在ui卡顿或者ui规范上,是非常重要的改进

因为Compose是Android团队与JetBrain在推,国内外的学习热情都挺好,目前国内也有不少大厂进行了尝试阶段,比如字节。

Compose 架构

说了这么多,那么Compose是怎么做到在原有View体系做到兼容并改善的呢?

我们从一个例子出发:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
             xxx组件
        }
    }
}

我们可以看到,Compose在Activity中,用了setContent方法代替了原有的setContentView方法,那么setContent做了什么呢?

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView
    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

可以看到,android.R.id.content的第一个孩子被替换成了ComposeView,而这个ComposeView,就是Compose环境的提供者,在这里面,Compose将剔除原本View体系的测量逻辑,从而采用自己的测量架构。如下图包饺子架构所示:

值得注意的是ComposeView是继承于AbstractComposeView(他定义了compose环境的规范),还有就是android中是多window架构的,所以针对Dialog这种,也有特别的环境实现类。针对PopupWindow这种共用window的组件,也同样提供了compose环境实现类

可以看到架构图中,AbstractComposeView其实也是继承于ViewGroup的。所以准确来说,Compose并没有完全脱离AndroidView体系,而是在这之上建立起了中间层,这就印证了一句老话,没有什么架构是不能解决的,如果有,那就加个中间层!而这个中间层,提供了全新的设计,从而让android得以脱胎换骨到一个新架构!

@Composable的背后

说到Compose,那么肯定离不开Compsoable的介绍,我们肯定会有一个疑问,为什么一个函数加上了Composable注解,就变成了一个可见的视图了呢?比如

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

我们进行反编译后

    public static final void Greeting(String name, Composer $composer, int $changed) {
        Intrinsics.checkNotNullParameter(name, "name");
        Composer $composer2 = $composer.startRestartGroup(-154424256);
        ComposerKt.sourceInformation($composer2, "C(Greeting)46@1589L27:MainActivity.kt#8m9ksz");
        int $dirty = $changed;
        if (($changed & 14) == 0) {
            $dirty |= $composer2.changed(name) ? 4 : 2;
        }
        if ((($dirty & 11) ^ 2) != 0 || !$composer2.getSkipping()) {
            TextKt.m1219TextfLXpl1I(LiveLiterals$MainActivityKt.INSTANCE.m4567String$0$str$arg0$callText$funGreeting() + name + LiveLiterals$MainActivityKt.INSTANCE.m4584String$2$str$arg0$callText$funGreeting(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
        } else {
            $composer2.skipToGroupEnd();
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup == null) {
            return;
        }
        endRestartGroup.updateScope(new MainActivityKt$Greeting$1(name, $changed));
    }

可以看到,实际上,编译器为我们的带有@Composable的Greeting方法添加了两个参数,

Composer $composer, int $changed

Composer就是我们真的compose执行时的操作者,changed就是一个参与是否重组的标识之一,这种通过注解在编译时生成对应参数的方案,在Coroutine里面也用到,当然我们也可以通过ASM等方法去编译时判断注解并更改相应的函数方法,只不过这部分由compose编译器帮我们做了。

我们重点关注一下composer.startRestartGroup这个方法

override fun startRestartGroup(key: Int): Composer {
    start(key, null, false, null)
    addRecomposeScope()
    return this
}

可以看到传入了一个key为Int类型的标识,所有Composable函数区域会通过特定key去判断重组的范围以及当前范围是否进行更新(范围就是startRestartGroup - endRestartGroup 之间的状态),当然一个Composable函数里面可能存在多个重组范围,我们还是拿上面的Greeting函数做个例子,不过这次有点变化

@Composable
fun Greeting(name: String) {
    val  state by remember {
        mutableStateOf(true)
    }
    if (state){
        Text(text = "true")
    }else{
        Text(text = "false")
    }
}

此时反编译后就会多一些代码

    public static final void Greeting(String name, Composer $composer, int $changed) {
        Object value$iv$iv;
        Intrinsics.checkNotNullParameter(name, "name");
        Composer $composer2 = $composer.startRestartGroup(-154424404);
        ComposerKt.sourceInformation($composer2, "C(Greeting)43@1455L45:MainActivity.kt#8m9ksz");
        if (($changed & 1) == 0 && $composer2.getSkipping()) {
            $composer2.skipToGroupEnd();
        } else {
            $composer2.startReplaceableGroup(-492369756);
            ComposerKt.sourceInformation($composer2, "C(remember):Composables.kt#9igjgp");
            Object it$iv$iv = $composer2.rememberedValue();
            if (it$iv$iv == Composer.Companion.getEmpty()) {
                value$iv$iv = SnapshotStateKt__SnapshotStateKt.mutableStateOf$default(Boolean.valueOf(LiveLiterals$MainActivityKt.INSTANCE.m4554x2b38c863()), null, 2, null);
                $composer2.updateRememberedValue(value$iv$iv);
            } else {
                value$iv$iv = it$iv$iv;
            }
            $composer2.endReplaceableGroup();
            MutableState state$delegate = (MutableState) value$iv$iv;
            // 关注这里
            if (m4607Greeting$lambda1(state$delegate)) {
                $composer2.startReplaceableGroup(-154424297);
                ComposerKt.sourceInformation($composer2, "47@1525L19");
                TextKt.m1219TextfLXpl1I(LiveLiterals$MainActivityKt.INSTANCE.m4597String$arg0$callText$branch$if$funGreeting(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
                $composer2.endReplaceableGroup();
            } else {
                $composer2.startReplaceableGroup(-154424258);
                ComposerKt.sourceInformation($composer2, "49@1564L20");
                TextKt.m1219TextfLXpl1I(LiveLiterals$MainActivityKt.INSTANCE.m4598String$arg0$callText$else$if$funGreeting(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
                $composer2.endReplaceableGroup();
            }
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup == null) {
            return;
        }
        endRestartGroup.updateScope(new MainActivityKt$Greeting$1(name, $changed));
    }

可以看到,我们状态state变量改变的时候,compose会在if里面生成先startReplaceableGroup,代表着这个范围是可以被替换的,也就是说当if成立的时候会生成一个composition节点,此时如果state变成了false,那么这个if里面的范围group就会被移除,此时父group(即在之前调用startgroup的范围)以替换的方式载入新的else,子group,这就是startReplaceableGroup的含义。

总之,我们可以注意到,key就是一个非常关键的点,就是让我们compose识别到哪些范围能够进行重组,哪些不能!在if else语句中,如果我们依赖一个state,就会在相应的if语句里面插入一个新的子范围group,这就是compose架构中的的智能重组

智能重组真的那么智能吗

上面我们提到了智能重组的这个概念,那么智能重组真的如字面所说,那么“智能”吗?我们很容易想到,既然compose能在if else这种有作用域的关键字上加上子group,如果是list这种呢?存在着循环语句的条件呢?就算在循环语句里面加上子group,并不能满足我们想要的需求呀!举个

@Composable
fun CustomList(list: List<CustromData>) {
   Column {
       for (item in list){
           CustromView(item)
       }
   }
}

我们想要for循环里面的list,如果list改变了数据,我们希望compose只重组改变部分的数据,而不是全部list里面的CustromView,那么compose能做到吗?compose:老子不干了! 是的!做不到!因为已经生成的CustromView的key只能依靠着当前的数据决定了,比如list的index,如果下次index更改,那么key就会因为index的不一致,导致了list中每一个生成的CustromView再次重组

CustomList部分反编译代码

  ColumnScopeInstance columnScopeInstance = ColumnScopeInstance.INSTANCE;
            int $changed2 = ((0 >> 6) & 112) | 6;
            // 只加了一个startReplaceableGroup
            $composer2.startReplaceableGroup(-1487261693);
            ComposerKt.sourceInformation($composer2, "C*52@1739L17:MainActivity.kt#8m9ksz");
            if ((($changed2 & 81) ^ 16) != 0 || !$composer2.getSkipping()) {
                Iterator<CustromData> it = list.iterator();
                // 循环没办法加入子group
                while (it.hasNext()) {
                    Iterator<CustromData> it2 = it;
                    CustromData item = it.next();
                    CustromView(item, $composer2, 0);
                    $changed$iv = $changed$iv;
                    it = it2;
                }
            } else {
                $composer2.skipToGroupEnd();
            }
            $composer2.endReplaceableGroup();
    public static final void CustromView(CustromData data, Composer $composer, int $changed) {
        Intrinsics.checkNotNullParameter(data, "data");
        Composer $composer2 = $composer.startRestartGroup(-201540877);
        ComposerKt.sourceInformation($composer2, "C(CustromView)59@1825L23:MainActivity.kt#8m9ksz");
        int $dirty = $changed;
        if (($changed &amp; 14) == 0) {
            $dirty |= $composer2.changed(data) ? 4 : 2;
        }
        if ((($dirty &amp; 11) ^ 2) != 0 || !$composer2.getSkipping()) {
            TextKt.m1219TextfLXpl1I(data.getTest2(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
        } else {
            $composer2.skipToGroupEnd();
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup == null) {
            return;
        }
        endRestartGroup.updateScope(new MainActivityKt$CustromView$1(data, $changed));
    }

所以一旦出现增删改查list的情况,那么不好意思,compose也就只能把group里面的list再重组一次,如图:

这也是为什么list系列的compose函数,比如LazyRow等会被诟病为有性能问题。那么compose就没有相关解决方法吗?嗯!官方肯定准备了,就是我们可以主动设置当前composable函数的key!从而避免额外的重组!

@Composable
inline fun <T> key(
    @Suppress("UNUSED_PARAMETER")
    vararg keys: Any?,
    block: @Composable () -> T
) = block()

回到我们自定义的list,就可以这样使用

for (item in list){
    key(keys = arrayOf(item.test) ) {
        CustromView(item)
    }
}

当然,我们的LazyList系列也可以直接指定,在item函数及其他items函数中直接使用

interface LazyListScope {
fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
...

最后

好的!我们的神奇的compose第一篇架构片就到此结束啦!接下来也会不定期更新其他的compose篇章,让我们拥抱全新的compose吧!更多关于Android开发Compose框架的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android Compose 属性动画使用探索详解

    目录 前言 使用探索 ObjectAnimator 使用探索 ValueAnimator 使用探索 Compose 函数中使用属性动画 实战 上传开始动画 上传进度动画 上传完成动画 最后 前言 Jetpack Compose(简称 Compose )是 Google 官方推出的基于 Kotlin 语言的 Android 新一代 UI 开发框架,其采用声明式的 UI 编程特性使得 Android 应用界面的编写和维护变得更加简单. 本专栏将详细介绍在使用 Compose 进行 UI 开发中如何实

  • Android开发Compose remember原理解析

    目录 正文 随机色文本 原因分析 正确实现 remember的原理剖析 小结 正文 看过Compose案例或者源码的你,相信肯定是见过 remember 了的.顾名思义,Compose是要让我们的代码“记住”东西,那到底是记住什么呢?要是不 remember,相关功能就实现不了了吗? 带着这些问题,来一探究竟吧 随机色文本 假设有这么一个“随机底色文本”的需求:实现一个 Text,其背景色每次启动都随机产生,且生命周期内不变 用Compose可以实现如下: private val items =

  • Android开发Compose集成高德地图实例

    目录 正文 高德地图官网开发者建议 初始化MapView并添加到AndroidView里面 MapView增加一个管理地图生命周期的扩展 给MapView添加生命周期观察者 添加MapView的生命周期控制 正文 Compose中我们应该怎么使用地图呢?像之前我们在xml里面创建MapView,都是在Activity里面,管理MapView生命周期,和其他的监听器,Compose里面怎么搞? 下面我们以高德地图为例,在Compose中创建地图MapView,然后用AndroidView添加Map

  • Android动效Compose贝塞尔曲线动画规格详解

    目录 正文 贝塞尔曲线 解析动画曲线 曲线源码分析 总结 正文 写Compose动画的时候使用animateXAsState的时候会注意到一个参数——animationSpec,如下: val borderRadius by animateIntAsState( targetValue = if (isRound) 100 else 0, animationSpec = tween( durationMillis = 3000, easing = LinearEasing ) ) 此处就不深入探

  • Android Compose实现底部按钮以及首页内容详细过程第1/2页

    目录 前言 Column.Row.ConstraintLayout布局先知 Column纵向排列布局 Row横向排列布局 ConstraintLayout 约束布局 Modifier的简单使用 底部导航栏的实现 首页内容的实现 Banner的实现 首页ViewModel 前言 compose作为Android现在主推的UI框架,各种文章铺天盖地的席卷而来,作为一名Android开发人员也是很有必要的学习一下了,这里就使用wanandroid的开放api来编写一个compose版本的玩安卓客户端,

  • Android开发Jetpack Compose元素Modifier特性详解

    目录 正文 有序性 不可变性 正文 本文将会介绍Jetpack Compose中的Modifier.在谷歌官方文档中它的描述是这么一句话:Modifier元素是一个有序.不可变的集合,它可以往Jetpack Compose UI元素中添加修饰或者各种行为.例如,背景.填充和单击事件监听器装饰或添加行为到文本或按钮.本文将会从修饰符的两个特性有序和不可变入手来探究修饰符的应用,以下是本文目录: 有序性 不可变性 有序性 官方对修饰符定义的这个特性包含两个层面的意思,一是修饰符的使用是链式的它是有先

  • Android开发Compose框架使用开篇

    目录 Compose的诞生 Compose好处 Compose 架构 @Composable的背后 智能重组真的那么智能吗 最后 Compose的诞生 在2019年的谷歌IO大会上,Compose作为Android新一代UI开发亮相,因为声明式开发越来越流行了,对标IOS开发SwiftUi,Compose的立项也为Android开发新加了声明式ui的开发选项,在2021年7月1.0正式版本的诞生,也意味着Compose即将进入生产环节,国际app巨头Twitter就首当其冲,在新页面上用上了Co

  • Android Jetpack Compose开发实用小技巧

    目录 前言 实用小技巧 如何移除View点击阴影 Text文本如何垂直居中 如何移除Button的点击阴影 Dialog宽度如何全屏 如何提升编码效率 前言 在Compose开发的过程中,我们会经常遇到一些看起来很简单却不知道如何处理的小问题,比如去除点击阴影.Dialog全屏等问题,本文记录了这些常见小问题的处理方式.如有更好方案欢迎大佬们交流探讨- 实用小技巧 如何移除View点击阴影 这里的View指的是除了Button系列的之外,如Button.TextButton等,也就是自身没有on

  • Android车载多媒体开发MediaSession框架示例详解

    目录 一.多媒体应用架构 1.1 音视频传统应用架构 1.2 MediaSession 框架 媒体会话 媒体控制器 二.MediaSession 2.1 概述 2.2 MediaBrowser 2.2.1 MediaBrowser.ConnectionCallback 2.2.2 MediaBrowser.ItemCallback 2.2.3 MediaBrowser.MediaItem 2.2.4 MediaBrowser.SubscriptionCallback 2.3 MediaContr

  • Android开发实现模仿360二维码扫描功能实例详解

    本文实例讲述了Android开发实现模仿360二维码扫描功能的方法.分享给大家供大家参考,具体如下: 一.效果图: 二.框架搭建 1.首先,下载最新zxing开源项目. 下载地址:http://code.google.com/p/zxing/ 或 点击此处本站下载. 2.分析项目结构,明确扫描框架需求.在zxing中,有很多其他的功能,项目结构比较复杂:针对二维码QRCode扫描,我们需要几个包: (1)com.google.zxing.client.android.Camera 基于Camer

  • 详解Android控件状态依赖框架

    在生产型Android客户端软件(企业级应用)开发中,界面可能存在多个输入(EditText)和多个操作(MotionEvent和KeyEvent),且操作依赖于输入的状态.如下图所示的场景: 设定图中 确认操作依赖于商品编码和储位的状态 跳过操作不依赖于输入状态 登记差异操作依赖于储位和数量的状态 输入框有三种状态: 待输入: 待校验: 校验成功. 操作需要当其依赖的输入数据校验成功,才能执行. 如果在Activity中去判断输入框状态,那么实际需要调用(3个输入框)*(3种状态)*(3个按钮

  • 这些小工具让你的Android开发更高效

    在做Android 开发过程中,会遇到一些小的问题,虽然自己动手也能解决,但是有了一些小工具,解决这些问题就得心应手了,今天就为大家推荐一下Android 开发遇到的小工具,来让你的开发更高效. Vysor Vysor 是一个可以将手机的屏幕投影到电脑上,当然也可以操作,当我们做分享或者演示的时候,这个工具起到了作用. Vector Asset Android Studio 在1.4 支持了VectorAsset,所谓VectorAsset:它可以帮助你在Android 项目中添加Materia

  • Android的搜索框架实例详解

    基础知识 Android的搜索框架将代您管理的搜索对话框,您不需要自己去开发一个搜索框,不需要担心要把搜索框放什么位置,也不需要担心搜索框影响您当前的界面.所有的这些工作都由SearchManager类来为您处理(以下简称"搜索管理器"),它管理的Android搜索对话框的整个生命周期,并执行您的应用程序将发送的搜索请求,返回相应的搜索关键字. 当用户执行一个搜索,搜索管理器将使用一个专门的Intent把搜索查询的关键字传给您在配置文件中配置的处理搜索结果的Activity.从本质上讲

随机推荐