在前两篇文章中,咱们别离运用DSL自界说了一个弹框和drawable控件,大致的现已熟悉了DSL的语法点,下面是前两篇文章的链接
- 运用DSL的办法自界说了一个弹框,代码遽然变的有那么一点点好看
- 运用DSL办法自界说一个Drawable控件,准备给项目来个全套
在这一篇文章里边将会进一步增加难度,在之前的基础上继续运用DSL开发一个登录页面,咱们知道在安卓里边想要开发一个页面,必不行少的便是要写layout文件,然后经过setContentView设置进去,这种传统的布局办法虽说有它的优势之处,比方安稳,布局可预览等,但如今更多的是对这种办法吐槽,乃至是抱有一些抵触情绪,我想原因或许包含以下几点
- 在setContentView里边会对layout文件进行xml解析,将解析出来的节点生成一个个View制作到界面上,这是个极度消耗时刻与内存的进程
- 代码中会存在很多findViewById的模版代码,虽然诸如Butterknife,Viewbinding以及kotlin-android-extension这些库现已处理了很多模版代码的问题,但一直需求与控件id绑定在一同,id如果变了,就会编译报错
- 编写layout文件比较耗时,降低了开发功率
除此之外,实践开发傍边咱们还要留意layout文件里边View树的层级不要过深,谷歌也引荐运用束缚布局来制作界面减少层级到达性能,但这些也仅仅减缓上述说的性能开销,并不能说彻底处理,想要彻底处理这个问题,咱们不得不考虑是否有其他计划能够替代这种传统UI的开发办法
替代传统编写xml文件的UI开发办法
或许到了这儿,咱们脑海傍边肯定立马出来了一个词,便是Jetpack Compose,没错,运用Compose开发UI确实是谷歌近几年鼓励引荐的,咱们也留意到了咱们Android Studio近几回的版本或多或少都在优化Compose相关的功用,确实能够去测验一下,但我想大多数项目想要从传统Ui的开发办法转变到Compose仍是需求很大的人力时刻本钱的,所以还得找其他计划,首先让咱们先看一下setContentView里边
它其实是有重载办法的,除了运用layout文件的id作为参数,它还支撑直接运用View作为入参,也便是说整个页面咱们能够当作是在开发一个自界说View相同,而咱们传统的自界说View办法是新建一个类,继承某一个父View,这样的做法没问题,但不利于扩展,也有必定的保护本钱,想要让一套代码能够支撑搭建任意一种布局款式,咱们就得运用DSL的办法去自界说咱们的布局,所以第一步便是先界说一个顶层函数createLayout,接纳一个函数类型参数,这个参数便是拿来自界说View的,然后这个View就作为返回值,用来当作setContentView的参数,函数界说如下
页面的入口代码就变成这个姿态
然后咱们就能够在createLayout里边一步步地将咱们的视图元素塞进去了
分析页面元素
在开发之前,咱们首先看一下咱们要做的登录页面的作用图
就像写布局文件相同,在运用DSL写布局之前,咱们首先要确认咱们的根布局,从作用图上看,咱们的根视图是一个笔直方向的线性布局,从上到下别离是标题的TextView,副标题的TextView,用户名输入框EditText,暗码输入框EditText,以及登录按钮Button,而EditText里边还有一个清空按钮,两者是在一个水平方向的线性布局里边,用一个树状图表达它们的层级联系
依据层级联系,咱们的根视图便是LinearLayout,所以咱们第一步就先创立最外层的LinearLayout
创立根视图
咱们再界说一个顶层函数linearLayout,表示这个函数便是用来生成线性布局视图的,代码如下
- 第一个参数不必说,创立视图必传的context
- 第二个参数initJob,它是一个带接纳者的lambda,接纳者为Linearlayout,即用来初始化一些LinearLayout特色,比方宽高,内边距,外边距,背景色等等
- 第三个参数childView,由于线性布局底下有多少子视图是不固定的,所以要让这个参数可变,能够是一个子视图,也能够是若干个
线性布局的视图界说好了,咱们顺便在linearlayout的基础上再创立两个函数,别离代表笔直线性布局和水平线性布局,这样在调用线性布局的时分,就不必每次去界说orientation特色了,两种线性布局的代码如下
这儿有个语法点,可变参数childView在当作参数传递到其他函数里边去的时分,变量前有必要要带上*,这样编译器才会把它从头复制到一个新的数组中传递曩昔,不然仅仅传递的是数组自身的Object
要不是编译器提示我带上*,我还真不知道,学到了学到了,言归正传,咱们把界说好的线性布局传到createLayout函数中去
由于整个登录页面的布局是笔直方向的,所以这儿用的是verticalLinearlayout,顺便给它加了点边距,让元素不要贴边,这儿咱们能够简化一下,像上述LayoutParam,根本每个元素都要设置一遍,类似于xml布局里边给每个元素设置layout_width和layout_height特色,所以咱们能够运用几个函数,把常用的宽高设置放在函数里边,调用的时分只需求调用函数就好了,类似于Compose里边的Modifier类的fillMaxSize,fillMaxWidth,size函数,咱们这边的函数界说如下
这样这儿就能够运用fillMaxWidth函数来替代上面的宽高设置,简化后的代码如下
到这儿整个页面的根视图就制作完毕了,作用图就不展现了,反正咱们都知道现在仅仅一个大白板,接下去开端开发里边的子视图
主标题与副标题
主标题与副标题,其实便是两个Textview,能够放在一同讲,老规矩先要界说一个函数来生成TextView,代码如下
然后对TextView进行初始化
接着将设置好的Textview加到咱们布局里边去
很简单,主标题与副标题就设置好了,咱们运转下代码看看作用
用户名与暗码编辑框
编辑框部分稍微杂乱一点了,咱们先分析一下需求留意哪几点
- 从上面的树状图,编辑框是一个水平线性布局里边包着一个EditText和ImageView,ImageView常驻在线性布局右边,笔直居中
- 编辑框布局的款式是一个圆角带灰色边框的drawable,这个能够用上一篇文章中的drawable控件来完结
咱们先完结线性布局部分
一个宽度满屏,高度自适应的并且带圆角与边框的布局就完结了,咱们将这个布局加到页面中去
运转后的作用图如下
嗯?怎么感觉没啥改动?没改动就对了,由于咱们的编辑框布局是高度自适应的,里边没有视图填充自然是看不见的,要把编辑框放进去才能够,所以得先界说一个函数生成EditText
再界说一个函数去初始化EditText的特色
把EditText设置到编辑框布局中去
现在再run一下,编辑框就出来了
编辑框还有个清空按钮,相同咱们先创立个生成ImageView的函数
然后再对这个ImageView做初始化操作
再增加到咱们的编辑框布局里边
再运转一遍,咱们的编辑框布局里边就带上了清空按钮了
用户名的编辑框完结了,暗码的编辑框根本相同,这儿直接上代码
再相同把暗码编辑框加到咱们的布局里边
咱们的暗码编辑框就加好了,运转一下看看作用
登录按钮
还剩余最终一个元素登录按钮,这是一个Button,所以咱们相同先界说一个函数用来生成Button
然后再对按钮做初始化
按钮就这样完结了,咱们把按钮增加到布局里边后看下作用
按钮是出来了,可是咱们想要的款式还没有,所以跟编辑框布局相同,咱们在按钮里边也运用DSL的drawablw控件加上咱们想要的款式,代码如下
咱们再运转一遍看看作用
半途总结
咱们的布局现已制作完毕了,整个进程下来给我的感觉用DSL制作布局有优势也有下风
优势
- 代码简洁,结构明晰,视图树能够明晰的反映在代码上
- 声明式ui的构建办法,只需关怀视图自身的特色,无需关怀视图怎么制作测量排版
- 经过界说各种顶层函数扩展函数,减少重复代码,提升开发功率
下风
- 无法预览作用,有必要经过运转代码才知道布局制作的是否正确
- 遇到一些第一次运用的控件,有必要界说新的函数,才能够在View层运用
怎么开发视图逻辑
现在为止咱们的静态页面完结了,可是作为一个完好的页面,还有必要要对用户的操作作出呼应,这个呼应一般体现在界面元素上的改动,咱们也管这些改动叫视图逻辑,比方咱们的登录页面,它的视图逻辑包含但不限于以下几点
- 编辑框在没有内容输入的时分,清空按钮应该是躲藏状况
- 按钮初始状况应该是置灰不行点击,当满足必定条件才能够变成高亮可点击状况
- 点击清空按钮,对应编辑框内容清空
- 点击登录按钮,建议登录恳求
任何一个登录页面应该都会有以上四点交互逻辑吧,那么咱们该怎么在现在的DSL的结构下去完结这些逻辑呢?这个问题其实在刚开端写页面的时分没怎么去想,揣摩着直接把界面上的元素声明为成员变量,在DSL的布局里边对这些成员变量赋值,想要改动某一个View的状况的时分,改动它的私有特色就好了,代码如下
lateinit var etUserName:EditText
lateinit var etPassword:EditText
lateinit var clearUserName:ImageView
lateinit var clearPassword:ImageView
lateinit var loginBtn:Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(createLayout {
verticalLinearlayout(this, {
...省掉...
}, { 省掉}, { 省掉}, {省掉 }, { 省掉},{
loginBtn = generateButton()
loginBtn
})
})
}
private fun generateUserEditRoot(): LinearLayout {
return horizontalLinearlayout(this, {
...省掉...
}, {
etUserName = generateUserEdit()
etUserName
}, {
clearUserName = generateUserEditClear()
clearUserName
}).also {
...省掉...
}
}
private fun generatePasswordEditRoot(): LinearLayout {
return horizontalLinearlayout(this, {
...省掉...
}, {
etPassword = generatePasswordEdit()
etPassword
}, {
clearPassword = generatePasswordEditClear()
clearPassword
}).also {
...省掉...
}
}
仍是习惯性的把传统UI那套思维带进来了是不,这样的做法确实能够完结上述所描述的视图逻辑,但这样做不光破坏了声明式UI的结构,如果要拜访的View变多了,很多的赋值句子和改动View私有特色的句子也会让代码变得难以保护,降低了代码可读性,所以咱们需求换一种设计思路,咱们从头讲目光回到代码中,咱们发现代码中有个特色,那便是每一个独立的View,都会有一个特色它的初始化函数
这样做的目的当然一个是需求返回一个View然后塞给LinearLayout#addView里边将视图显现出来,另一个原因是咱们能够在这些初始化函数里边做一些视图相关的操作,比方发送作业,或许接纳外界传来的作业改动自身的状况,而接纳作业以及分发这些状况的作业,咱们有一个公共的逻辑层去处理
像图中描述的那样,拥有仅有数据来源,作业由下往上流,状况由上往下流的单向数据流方向,以及View经过接纳状况去完结改写特色,这样的架构形式咱们现在有一个一致的名称–MVI
运用MVI的办法开发
首先咱们需求确认好页面需求有哪几个状况,咱们这儿有创立一个LoginState,这个类便是去管理这些状况
咱们先去完结按钮的高亮逻辑,按钮是监听到用户名与暗码编辑框一起都有内容的时分才会高亮,由于用户名编辑框与暗码编辑框都是在各自的lambda表达式做初始化作业,相互都拜访不了对方,它们发送作业只能发送自己自身输入的内容,无法转化为按钮需求监听的状况,所以需求把它们输入的内容状况上移,保护在一个公共的类里边
页面中保护仅有的仅有的LoginParam,编辑框发送作业的时分,将输入的内容封装在LoginParam里边并将它发送出去,这样逻辑层接纳的作业就一起包含着用户名与暗码编辑框输入的内容了,代码完结如下
loginViewModel是咱们的逻辑层,它担任接纳作业,而处理数据,分发状况的作业,咱们交给SharedFlow,由于每一个View都会去订阅作业,多个订阅者的场景SharedFlow比较合适,代码如下
然后咱们在之前初始化按钮的方位,监听来自LoginViewModel分发过来的状况值,当状况值是true就高亮按钮,按钮可点击,当状况是false的时分,就把按钮置灰,按钮不行点,代码如下
按钮的初始状况也改为了置灰,这样咱们按钮与编辑框之间的视图逻辑就完结了,咱们看下作用
接着是咱们的清空按钮,咱们希望清空按钮在编辑框没有内容的时分是躲藏状况,有东西输入的时分才显现,那么这个也很简单,由于咱们在LoginViewmodel里边现已有接纳编辑框传来的作业,咱们只需求将这些作业别离分发给用户名与暗码编辑框里的两个清空按钮,剩余的便是两个清空按钮各自去监听状况改写ui了,咱们更新下loginViewModel的update函数
之后咱们将两个清空按钮初始状况变成不行见,并监听传来的状况改写ui
咱们再运转一遍代码看看作用怎么
清空按钮的改写逻辑做好了,可是它自身的功用还没完结,清空按钮是点了今后会将输入框里的内容悉数清空,那这儿也便是要将这个清空作业发送给LoginViewModel,然后在里边针对是从哪个清空按钮发来的作业判断需求去更新哪个编辑框的UI状况,将对应状况分发出去,咱们在LoginViewModel里边增加一个函数
咱们看到userEdit为true的时分,表示从用户名编辑框的清空按钮传来的作业,所以咱们将清空用户名编辑框的状况分发出去,同理,当userEdit为false的时分,将清空暗码编辑框的状况分发出去,咱们在编辑框那儿监听这些状况的代码如下
当清空了编辑框里边内容的时分,一起也向LoginViewModel发送了一个update的作业,目的也是去改写按钮与清空按钮自身的ui状况,这个也便是咱们这个架构的特色,元素的作业转化为其他元素的状况,元素的状况发生新的元素的作业,这也帮助咱们在DSL这样的布局里边,虽然元素之间零耦合联系,但咱们仍是能够经过一个逻辑层比方ViewModel,完结元素之间的交互逻辑。话题回到代码上,现在咱们现已完结编辑框监听清空内容的状况,咱们最终一步给清空按钮增加上点击动作并发送清空作业,代码如下
咱们运转下代码看下作用
现在整个页面的交互逻辑只剩余最终一个,那便是点击登录按钮发送登录恳求,依据获取到的不同结果让页面做出不同呼应,咱们这儿省掉登录恳求的进程,直接将暗码长度大于6作为登录成功的判断条件,否则登录失利,现在咱们在LoginViewModel中增加login函数,完结上述逻辑
然后在按钮上增加上点击作业
而咱们这个登录状况的监听,由所以整个页面的监听,所以不必放在任何一个元素里边,能够直接放在布局之外的地方
而doError()跟doSuccess(),别离是针对恳求呼应失利与成功作出的操作,咱们这边直接运用DSL弹出一个框,告知用户登录失利的原因是参数错误,或许告知用户登录成功
作用图如下
总结
整个登录页面现已开发完结了,虽然是一个很简单的登录界面,可是经过这篇文章,咱们仍是能够学到一些东西
- 怎么在不写xml布局的前提下,运用DSL搭建一个页面
- 怎么运用MVI的架构形式,在DSL内部结构中,完结元素呼应式更新状况
希望能够经过这篇文章,能够给一些不太乐意写xml布局,或许想测验声明式UI而项目又不太便利转Compose或许Flutter的小伙伴们提供一个新的思路