Jetpack Compose和View的互操作性

2022-07-22,,,

jetpack compose interoperability

compose风这么大, 对于已有项目使用新技术, 难免会担心兼容性.
对于compose来说, 至少和view的结合是无缝的.
(目前来讲, 已有项目要采用compose, 可能初期要解决的就是升级gradle plugin, gradle, android studio, kotlin之类的问题.)

构建ui的灵活性还是有保证的:

  • 新界面想用compose, 可以.
  • compose支持不了的, 用view.
  • 已有界面不想动, 可以不动.
  • 已有界面的一部分想用compose, 可以.
  • 有的ui效果想复用之前的, 好的, 可以直接拿来内嵌.

本文就是一些互相调用的简单小demo, 初期用的时候可以复制粘贴一下很趁手.

官方文档:

在activity或者fragment中全部使用compose来搭建ui

use compose in activity

class exampleactivity : appcompatactivity() {
    override fun oncreate(savedinstancestate: bundle?) {
        super.oncreate(savedinstancestate)

        setcontent { // in here, we can call composables!
            materialtheme {
                greeting(name = "compose")
            }
        }
    }
}

@composable
fun greeting(name: string) {
    text(text = "hello $name!")
}

use compose in fragment

class purecomposefragment : fragment() {
    override fun oncreateview(
        inflater: layoutinflater,
        container: viewgroup?,
        savedinstancestate: bundle?
    ): view {
        return composeview(requirecontext()).apply {
            setcontent {
                materialtheme {
                    text("hello compose!")
                }
            }
        }
    }
}

在view中使用compose

composeview内嵌在xml中:

一个平平无奇的xml布局文件中加入composeview:

<?xml version="1.0" encoding="utf-8"?>
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <textview
        android:id="@+id/hello_world"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="hello from xml layout" />

    <androidx.compose.ui.platform.composeview
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</linearlayout>

使用的时候, 先根据id查找出来, 再setcontent:

class composeviewinxmlactivity : appcompatactivity() {
    override fun oncreate(savedinstancestate: bundle?) {
        super.oncreate(savedinstancestate)
        setcontentview(r.layout.activity_compose_view_in_xml)

        findviewbyid<composeview>(r.id.compose_view).setcontent {
            // in compose world
            materialtheme {
                text("hello compose!")
            }
        }
    }
}

动态添加composeview

在代码中使用addview()来添加view对于composeview来说也同样适用:

class composeviewinviewactivity : appcompatactivity() {
    override fun oncreate(savedinstancestate: bundle?) {
        super.oncreate(savedinstancestate)

        setcontentview(linearlayout(this).apply {
            orientation = vertical
            addview(composeview(this@composeviewinviewactivity).apply {
                id = r.id.compose_view_x
                setcontent {
                    materialtheme {
                        text("hello compose view 1")
                    }
                }
            })
            addview(textview(context).apply {
                text = "i'm am old textview"
            })
            addview(composeview(context).apply {
                id = r.id.compose_view_y
                setcontent {
                    materialtheme {
                        text("hello compose view 2")
                    }
                }
            })
        })
    }
}

这里在linearlayout中添加了三个child: 两个composeview中间还有一个textview.

起到桥梁作用的composeview是一个viewgroup, 它本身是一个view, 所以可以混进view的hierarchy tree里占位,
它的setcontent()方法开启了compose世界的大门, 在这里可以传入composable的方法, 绘制ui.

在compose中使用view

都用compose搭建ui了, 什么时候会需要在其中内嵌view呢?

  • 要用的view还没有compose版本, 比如adview, mapview, webview.
  • 有一块之前写好的ui, (暂时或者永远)不想动, 想直接用.
  • 用compose实现不了想要的效果, 就得用view.

在compose中加入android view

例子:

@composable
fun customview() {
    val state = remember { mutablestateof(0) }

    //widget.button
    androidview(
        factory = { ctx ->
            //here you can construct your view
            android.widget.button(ctx).apply {
                text = "my button"
                layoutparams = linearlayout.layoutparams(match_parent, wrap_content)
                setonclicklistener {
                    state.value++
                }
            }
        },
        modifier = modifier.padding(8.dp)
    )
    //widget.textview
    androidview(factory = { ctx ->
        //here you can construct your view
        textview(ctx).apply {
            layoutparams = linearlayout.layoutparams(match_parent, wrap_content)
        }
    }, update = {
        it.text = "you have clicked the buttons: " + state.value.tostring() + " times"
    })
}

这里的桥梁是androidview, 它是一个composable方法:

@composable
fun <t : view> androidview(
    factory: (context) -> t,
    modifier: modifier = modifier,
    update: (t) -> unit = noopupdate
)

factory接收一个context参数, 用来构建一个view.
update方法是一个callback, inflate之后会执行, 读取的状态state值变化后也会被执行.

在compose中使用xml布局

上面提到的在compose中使用androidview的方法, 对于少量的ui还行.
如果需要复用一个已经存在的xml布局怎么办?
不用怕, view binding登场了.

使用起来也很简单:

  • 首先你需要开启view binding.
buildfeatures {
    compose true
    viewbinding true
}
  • 其次你需要一个xml的布局, 比如叫complex_layout.
  • 然后添加一个compose view binding的依赖: androidx.compose.ui:ui-viewbinding.

然后build一下, 生成binding类,
这样就好了, 哒哒:

@composable
private fun composablefromlayout() {
    androidviewbinding(complexlayoutbinding::inflate) {
        samplebutton.setbackgroundcolor(color.gray)
    }
}

其中complexlayoutbinding是根据布局名字生成的类.

androidviewbinding内部还是调用了androidview这个composable方法.

番外篇: 在compose中显示fragment

这个场景听上去有点奇葩, 因为compose的设计理念, 貌似就是为了跟fragment说再见.
在compose构建的ui中, 再找地方显示一个fragment, 有点新瓶装旧酒的意思.

但是遇到的场景多了, 你没准真能遇上呢.

fragment通过fragmentmanager添加, 需要一个布局容器.
把上面viewbinding的例子改改, 布局里加入一个fragmentcontainer, 点击显示fragment:

column(modifier.fillmaxsize()) {
    text("i'm a compose text!")
    button(
        onclick = {
            showfragment()
        }
    ) {
        text(text = "show fragment")
    }
    composablefromlayout()
}

@composable
private fun composablefromlayout() {
    androidviewbinding(
        fragmentcontrainerbinding::inflate,
        modifier = modifier.fillmaxsize()
    ) {

    }
}

private fun showfragment() {
    supportfragmentmanager
        .begintransaction()
        .add(r.id.fragmentcontainer, purecomposefragment())
        .commit()
}

这里没有考虑时机的问题, 因为点击按钮展示fragment, 将时机拖后了.
如果直接在初始化的时候想显示fragment, 可能会抛出异常:

java.lang.illegalargumentexception: no view found for id

解决办法:

@composable
private fun composablefromlayout() {
    androidviewbinding(
        fragmentcontrainerbinding::inflate,
        modifier = modifier.fillmaxsize()
    ) {
        // here is safe
        showfragment()
    }
}

所以show的时机至少要保证container view已经inflated了.

theme & style

迁移view的app到compose, 你可能会需要theme adapter:

关于在现有的view app中使用compose:

总结

compose和view的结合, 主要是靠两个桥梁.
还挺有趣的:

  • composeview其实是个android view.
  • androidview其实是个composable方法.

compose和view可以互相兼容的特点保证了项目可以逐步迁移, 并且也给够了安全感, 像极了当年java项目迁移kotlin.
至于什么学习曲线, 经验不足, 反正早晚都要学的, 整点新鲜的也挺好, 亦可赛艇.

《Jetpack Compose和View的互操作性.doc》

下载本文的Word格式文档,以方便收藏与打印。