干流移动端新结构都在搞声明式UI

现代的移动使用UI开发结构,如Compose,Flutter,iOS的SwiftUI等都不谋而合的使用了声明式UI的编程范式,这一类结构往往经过状况来驱动UI改变,UI代码首要描述了布局信息,以及控件与状况数据之间的联系。解决了以上说到的传统UI开发的一系列痛点。

BrickUI,基于Android View体系撸一个声明式UI框架

这些声明式UI结构也都有一个特色,便是UI更新都是经过呼应状况的改变来完成

BrickUI,基于Android View体系撸一个声明式UI框架

那咱们Android原生怎样写UI?

关于Android原生的UI开发,咱们肯定对findVIewById不生疏。

关于指令式UI的编程范式,写一个UI的流程一般是:

  • 在xml上写layout,drawable等
  • 在代码中经过findVIewById或其他结构把View找出来
  • 依据交互稿,写UI操控代码来沟通UI和事务代码,UI操控代码首要指的是调用setText,setColor,setImage这一类设置View样式的办法。
BrickUI,基于Android View体系撸一个声明式UI框架

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到实践项目中,去完成社区广场列表这种杂乱混排长列表。

BrickUI,基于Android View体系撸一个声明式UI框架

项目地址:github.com/robin8yeung…

来2个example来体会下吧

1、经典example-计数器

BrickUI,基于Android View体系撸一个声明式UI框架

首先完成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,基于Android View体系撸一个声明式UI框架

头像完成

头像周围的环形描边,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,基于Android View体系撸一个声明式UI框架

BrickUI,基于Android View体系撸一个声明式UI框架

完成原理

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的部分刷新和状况呼应。

BrickUI,基于Android View体系撸一个声明式UI框架

这使得这样的完成成为可能:

// 每次切换状况后,当image的url不存在时,显现一个文字控件,不然加载图片到一个图片控件
image == null? Text("empty"): Image.network(image);

而View系统天然不支撑这样的动态呼应,更多的,咱们会一起创立TextView和ImageView两个控件,依据实践情况对他们的visibility进行设置。

BrickUI也是依据View系统去完成的,所以往往也只能循序这样的完成。

短少IDE的实时预览机制和方便操作机制

因为不短少Android Studio的插件开发才能,无法完成像xml那样的所见即所得的实时预览,最多只能借助自界说View的预览机制来进行预览。

BrickUI,基于Android View体系撸一个声明式UI框架

一起,也无法完成像Flutter这样,经过方便菜单,快速为Widget嵌套一个父Widget

BrickUI,基于Android View体系撸一个声明式UI框架

其他局限性

  • 无法像xml那样动态创立id资源(View id)
  • 为了提升开发体会,结构了一些包装目标,形成了一些功能开支

总结

总的来说,软件设计没有银弹,任何设计也都是针对某种场景进行取舍。BrickUI也存在着许多不够成熟乃至拙劣的地方,也期望和咱们一起交流来完善它。最后假如你觉得BrickUI或许本文对你有协助,点个star再走吧~⭐️

BrickUI:github.com/robin8yeung…