干流移动端新结构都在搞声明式UI
现代的移动使用UI开发结构,如Compose,Flutter,iOS的SwiftUI等都不谋而合的使用了声明式UI的编程范式,这一类结构往往经过状况来驱动UI改变,UI代码首要描述了布局信息,以及控件与状况数据之间的联系。解决了以上说到的传统UI开发的一系列痛点。
这些声明式UI结构也都有一个特色,便是UI更新都是经过呼应状况的改变来完成
那咱们Android原生怎样写UI?
关于Android原生的UI开发,咱们肯定对findVIewById不生疏。
关于指令式UI的编程范式,写一个UI的流程一般是:
- 在xml上写layout,drawable等
- 在代码中经过findVIewById或其他结构把View找出来
- 依据交互稿,写UI操控代码来沟通UI和事务代码,UI操控代码首要指的是调用setText,setColor,setImage这一类设置View样式的办法。
Android指令式UI的一些痛点:
- 冗余:需求编写冗余代码来处理UI改变
- 耦合:有交互关联的View常常存在耦合,相互影响
- xml:layout,drawable,代码之间的切换,对开发来说形成必定打断
DataBinding
MVVM架构兴起的时代,Jetpack推出了lifecycle组件和DataBinding来完善UI开发的架构。
DataBinding的确解决了传统指令式UI的一些痛点,但又引出了新的问题,导致依然并不受开发者所待见:
- xml上写代码逻辑
- 对编译功能的影响
- 常常导致编译异常的log难以定位
能不能依据View系统撸一个声明式UI结构
就Android而言,不论Compose仍是Flutter,都依据自有的烘托系统,虽然在宣传上都声称其具有和原生一样的功能表现,但就现阶段而言,我在实践项目或官方demo中体会,在一些高频烘托场景,如不抬手的翻滚长列表时,卡顿或顿挫感依然是肉眼可见的,在低端机器,旧机器上的表现尤甚。
此外,当需求接入成熟项目时,往往需求采用混合开发的接入模式,因为这些结构的系统独立于Android的原生View系统之外,往往需求引进一些“桥”,这就导致了开发、功能和保护成本的增长。
关于Compose,现在的生态也是有待完善的,特别是Compose的热度现在可能还达不到Flutter的水平,只能说未来可期。
鉴于以上考虑到的一些痛点,我就考虑,能否依据Android原生View系统撸一个声明式UI结构?
生态位
咱们知道,无论是Flutter仍是Compose,其关键卖点在于跨渠道。BrickUI不触及跨渠道,它旨在提升原生UI开发者的开发效率,对标的是DataBinding,ViewBinding这一类传统的原生UI开发结构,期望它的生态位是:
- 比原生开发有更高的开发效率
- 能真正具有和原生相仿的功能表现
- 不会像Compose/Flutter那样,引进过多的混合开发的接入成本
BrickUI诞生了,期望有了它,你能够和xml说一句:
“xml吗?别再给我打电话了,我怕BrickUI误解”
现在已接入BrickUI到实践项目中,去完成社区广场列表这种杂乱混排长列表。
项目地址:github.com/robin8yeung…
来2个example来体会下吧
1、经典example-计数器
首先完成TopBar
fun ViewGroup.TopBar() {
// 行布局,经过dp扩展特点快速使用dp单位
row(
MATCH_PARENT, 44.dp,
) {
imageView(
44.dp, 44.dp,
// 经过drawable扩展特点快速使用Drawable资源
drawable = R.drawable.ic_back_dark.drawable,
scaleType = ImageView.ScaleType.CENTER_INSIDE,
) {
(context as? Activity)?.onBackPressed()
}
// 在线性布局中填充剩余区域
expand()
}
}
封装带暗影的按钮
原生UI开发时,并没有相似前端css中这么详细的界说box-shadow的办法,而BrickUI则供给了相似的办法来界说外暗影
private fun ViewGroup.button(
text: String,
onClick: View.OnClickListener
) = shadowBox(
// shadowBox允许界说带圆角的外暗影,还能够界说暗影的blur,颜色,和x,y的offset
radius = 14.dp, shadow = Shadow(blur = 8.dp),
onClick = onClick
) {
textView(
width = 100.dp, height = 100.dp,
text = text,
textSize = 28.dp,
textStyle = Typeface.BOLD,
gravity = Gravity.CENTER
)
}
把控件拼装起来,放到Activity中
fun Context.CounterPage() = column(
MATCH_PARENT, MATCH_PARENT,
// 适配状况栏
fitsSystemWindows = true,
) {
// 界说计数值,即经过扩展特点live快速界说LiveData<Int>,初始值为0
val count = 0.live
TopBar()
divider(background = ColorDrawable(Color.GRAY))
row(
MATCH_PARENT, 160.dp,
gravity = Gravity.CENTER,
fitsSystemWindows = true,
) {
// 减号按钮
button("-") {
count.value = count.value - 1
}
// 计数数值,可绑定LiveData的控件由 brick-ui-live 供给
liveText(
84.dp,
style = R.style.BigNumber,
// 计数值显现
text = count.map { it.toString() }
// 依据计数值是偶数显现文字为红色,基数显现为黑色
textColor = count.map { if (it % 2 == 0) Color.RED else Color.BLACK },
// 借鉴了Flutter的EdgeInsets界说
padding = EdgeInsets.all(12.dp),
)
// 加号按钮
button("+") {
count.value = count.value + 1
}
}
expend()
}
以上的CounterPage函数,返回的即为一个View,能够经过Activity的setContentView来直接展现。
class CounterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(CounterPage())
}
}
至此,完成了计数器的完成。关于更杂乱的事务则推荐使用ViewModel,假如ViewModel是绑定Activity的,那经过UI内的恣意一个Context强转为Activity,均可拿到其所绑定的ViewModel,然后便利去完成UI和ViewModel的交互。假如不喜欢这种强转的办法,也能够借助其他依靠注入结构来取得期望的ViewModel目标。
需求特别指出的是:现在阶段只供给了依据LiveData的控件呼应更新机制,下阶段会考虑经过Flow来操控控件的呼应更新,为开发者供给便当。
2、社区动态的九宫格example
限于篇幅,这儿仅举例一条社区动态的UI完成,且不包括数据交互。这儿首要展现BrickUI中列表的完成和Drawable的快速完成。
前排叠甲:以下牛杂师傅的图片均来源于网络
头像完成
头像周围的环形描边,BrickUI能够直接经过代码去创立,而不需求另外去写Drawable的xml。
private fun ViewGroup.Photo() {
// BrickUI对Glide加载独自封装了一个lib,防止耦合
glideImage(
urlOrPath = "头像图片实践url",
// 图片圆形裁剪
request0ptions = RequestOptions().transform(CircleCrop())
) {
ImageView(
60.dp, 60.dp,
scaleType = ImageView.ScaleType.CENTER_CROP,
padding = EdgeInsets.all(4dp),
// 快速结构圆形描边Drawable,防止为Drawble又跑去写一个xml
background = ovalDrawable(
strokecolor = Color.parseColor("#331088"),
strockWidth = 2.dp,
),
)
}
}
图片九宫格完成
BrickUI供给了简略快速构建RecyclerView的办法,能够快速完成九宫格UI。本例子仅针对静态列表,假如需求呼应动态数据,能够在BrickUI的demo中去看完成举例。
private fun ViewGroup.ImageList() {
// 结构RecyclerView
simpleStatelessRecyclerView(
WRAP_CONTENT, WRAP_CONTENT,
//列表数据
data = listof(
"图片1 url",
"图片2 url",
"图片3 url",
"图片4 url",
"图片5 url",
),
// 设置GridLayoutManager,每行3列
layoutManager = GridLayoutManager (context, 3),
padding = EdgeInsets.symmetric(vertical = 8.dp),
) { data, index ->
// 为每一个position的图片url创立UI
glideImage(data[index]) {
imageView(
88.dp, 88.dp,
scaleType = ImageView.ScaleType.CENTER_CROP,
padding = EdgeInsets.all(2.dp),
) {
// 此处回调点击事件
}
}
}
}
拼装控件到页面中,完成社区动态展现
fun Context.Moments() = row(
MATCH_PARENT,
gravity = Gravity.TOP,
padding = EdgeInsets.symmetric(horizontal = 16.dp)
) {
Photo()
// 填满右侧空间
expand(margins = EdgeInsets.only(start = 8.dp)) {
Contents()
}
}
// 右侧内容
private fun ViewGroup.Contents () = column(width: 0) {
textView(
text = "刻睛",
textColor = Color.parseColor("#331088"),
textSize = 18.dp,
textStyle = Typeface.BOLD,
)
textView(
text = "耽误太多时间,工作可就做不完了!",
textSize = 18.dp,
padding = EdgeInsets.only(top = 4.dp),
)
ImageList()
}
至此,单条动态的UI就完成完了,关于RecyclerView,BrickUI不再需求开发者自己去自界说Adapter和ViewHolder。相比原生UI开发,是不是节省了许多代码呢?
BrickUI完整的才能体会能够经过demo去体会~
demo能够经过扫码下载。除了简略易用的行列布局,也支撑相对布局、束缚布局、协调布局等,更有许多使用功能~
完成原理
BrickUI的完成原理并不杂乱,无非也是经过Kotlin的扩展函数的特性,依照DSL的写法,把整个ViewTree的树状结构建立起来,感兴趣的同学能够直接检查源码。
此外,为了既能把View系统中RecyclerView这个神器使用起来,又能让开发者具有相似Flutter的ListView那样的方便列表开发体会,BrickUI对功能作了必定取舍,即不再把每一个ItemView都交给ViewHolder来进行回收再使用,但从profile看,假如图片这种大目标交给Glide之类的缓存结构来进行缓存了,那也并没有形成显着的内存颤动。
原生View的嵌入
既然BrickUI是依据原生View系统开发的,那么嵌入原生完成的View是否也是十分简单的?
fun ViewGroup.Markdown(
text: String
): View {
// 经过view函数能够很便利的把原生View嵌入到BrickUI的声明式UI中
return view {
// 第三方控件:MarkdownWebView
MarkdownWebView(context).apply {
// 初始化宽高
init(MATCH_PARENT, MATCH_PARENT)
setText(text)
}
}
}
BrickUI的局限性
1、BrickUI实践是一种”伪“声明式UI
如前面所述,常见的声明式UI结构,往往是经过状况驱动的,得益于这些结构都具有自己的烘托系统,它们的烘托原理往往都是经过3棵树来完成。当状况改变时,对比前后两个状况的Element树,能够得到二者差分的补丁,最后把补丁使用到RenderObject树上,即可完成UI的部分刷新和状况呼应。
这使得这样的完成成为可能:
// 每次切换状况后,当image的url不存在时,显现一个文字控件,不然加载图片到一个图片控件
image == null? Text("empty"): Image.network(image);
而View系统天然不支撑这样的动态呼应,更多的,咱们会一起创立TextView和ImageView两个控件,依据实践情况对他们的visibility进行设置。
BrickUI也是依据View系统去完成的,所以往往也只能循序这样的完成。
短少IDE的实时预览机制和方便操作机制
因为不短少Android Studio的插件开发才能,无法完成像xml那样的所见即所得的实时预览,最多只能借助自界说View的预览机制来进行预览。
一起,也无法完成像Flutter这样,经过方便菜单,快速为Widget嵌套一个父Widget
其他局限性
- 无法像xml那样动态创立id资源(View id)
- 为了提升开发体会,结构了一些包装目标,形成了一些功能开支
总结
总的来说,软件设计没有银弹,任何设计也都是针对某种场景进行取舍。BrickUI也存在着许多不够成熟乃至拙劣的地方,也期望和咱们一起交流来完善它。最后假如你觉得BrickUI或许本文对你有协助,点个star再走吧~⭐️
BrickUI:github.com/robin8yeung…