Jetpack Compose和View的互操作性
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.
至于什么学习曲线, 经验不足, 反正早晚都要学的, 整点新鲜的也挺好, 亦可赛艇.