2019 年 12 月更新:就在几天前,Anko 已经正式宣布停止维护,官方宣称以后将推荐使用 Android Jetpack 中的 KTX 与 Compose,Anko 这个承载了 Kotlin 初心的项目正式与大家告别了,官方的最后声明:Goodbye。
2017年Google正式确定Kotlin为Android开发一级语言后,有大量的团队和项目开始尝试使用Kotlin,而Anko作为一款和Kotlin联系极为紧密的开源库,也成为了吸引开发者尝试Kotlin的魅力之一;Anko的使用教程可以参照官方文档,这里不再赘述,本文力求从以下五个方面来介绍Anko在各方面的表现以及从个人的角度来论证其实用价值。
Part 1:简介
Part 2:优势
Part 3:兼容性
Part 4:辅助工具
Part 5:风险及缺点
Part 1:简介
Anko是一款JetBrains推出的,利用了Kotlin众多语法特性的Android开发函数库;而JetBrain正是Kotlin以及Intellij IDEA的开发商。虽然Google目前没有为其背书,但是也算是一款半官方的产品;Anko一共分为以下四个部分:
- Anko Commons (Intent,Dialog,Toast等函数式便捷封装)
- Anko Layouts (使用DSL构建速度大幅提高的UI【重点】)
- Anko SQLite (便捷的SQLite操作)
- Anko Coroutines (Kotlin协程辅助)
这四部分分别对应四个Maven库,此外官方还提供了数个针对Android各个support包的封装,需要另外单独集成。
在这四部分中Commons,SQLite,Coroutines三项均是对已有API的封装,使调用者可以使用函数式的方式调用一些系统API;而Layouts正是Anko的一大亮点与特色,它另辟蹊径,使用Kotlin的DSL的方式动态构建Android的UI,从而取代传统的使用XML的方式,这使得Android的UI的生成过程从此更加节省CPU的使用以及降低电池的消耗,是一种值得尝试的新方式。也是本文介绍的重点。
我们通过几个小例子来直观的感受一下Anko构建UI时的代码;
当我们在Activity中创建布局的时候:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
verticalLayout {
padding = dip(30)
editText {
hint = "Name"
textSize = 24f
}
editText {
hint = "Password"
textSize = 24f
}
button("Login") {
textSize = 26f
}
}
}
可以看到,我们无需再调用setContentView()方法去加载布局文件,而是直接在Activity的onCreate()方法中进行UI代码的编写。verticalLayout是官方为了简便使用,特别封装的一个竖直方向的LinearLayout,而editText,Button,等都是我们常见的Android UI控件;而hint,textSize等属性都是它们上级控件的属性,这和我们在XML中给控件使用一些属性是一样的;而dip()函数则是官方提供的dp和px两种单位之间转换的函数,方便我们使用我们习惯的dp的方式来设置一些控件大小的值。
如果你觉的将UI的代码和Activity写在一起不够软件工程,官方也提供了如下的方式把它们分离:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
MyActivityUI().setContentView(this)
}
}
class MyActivityUI : AnkoComponent<MyActivity> {
override fun createView(ui: AnkoContext<MyActivity>) = with(ui) {
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { ctx.toast("Hello, ${name.text}!") }
}
}
}
}
当然,不仅仅是Activity需要UI,Fragment,以及一些Dialog中也需要,我们来看看在Fragment中如何创建UI:
class BlankFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
UI {
verticalLayout {
padding = dip(30)
editText {
hint = "Name"
textSize = 24f
}
editText {
hint = "Password"
textSize = 24f
}
button("Login") {
textSize = 26f
}
}
}.view
}
调用UI {}.view函数即可。
再来看看如何在RecyclerView的Adapter中创建item的UI以及在给Dialog中设置UI的方式:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OSLViewHolder {
val view = AnkoContext.create(mContext).apply {
verticalLayout {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
isClickable = true
foreground = createTouchFeedbackBorderless(mContext)
}
textView {
id = TITLE_ID
textSize = 22f
textColor = black
}.lparams(wrapContent, wrapContent) {
topMargin = dip(16)
bottomMargin = dip(16)
}
textView {
id = CONTENT_ID
textColor = black
}.lparams(wrapContent, wrapContent) {
marginStart = dip(16)
bottomMargin = dip(16)
}
}
}.view
return OSLViewHolder(view)
}
通过AnkoContext.create(context).apply{}.view 的方式就可以轻松创建。
如果你想在DSL中使用你的自定义控件,可以像下面这样写:
//CircleImageView
inline fun ViewManager.circleImageView(init: CircleImageView.() -> Unit): CircleImageView =
ankoView({ CircleImageView(it) }, theme = 0, init = init)
看起来结构挺复杂,其实就是定义一个ViewManager的内联扩展函数,这个扩展函数的参数是一个带你想要创建的View的类型的接受者的lambda表达式,并且通过调用Anko的ankoView函数来返回我们需要的对象。当然,这不都是千篇一律的,如果你要定义一个可以在Activity中直接调用的DSL API,则需要把它定义成Activity的内联扩展函数。如果你的自定义控件是一个ViewGroup,还需要通过DSL的方式定义它的LayoutParams等,这里不再展开。
如果是一个Android的老手,可能看过示例以后就能大体掌握使用DSL构建UI的语法,但是其中的一些细节还是会增加一些学习成本;如果质疑这样的学习成本是否值得,那我们就要来分析分析这种新方式构建UI到底有什么好处,让我们来看看Part 2。
Part 2:优势
前面我们提到过,使用Anko创建的布局的优势在于节约CPU资源和节省电池电量;直观的体现就是省时和省电;当然,这些优势的描述源自官方文档,现在我们来具体分析一下它为什么省时和省电。
先来对比一下XML和DSL的执行流程,先看看我们熟知的XML:
再来对比一下DSL:
这样就很直观,XML作为一种标记型语言是直接打包进APK的,每次要加载布局的时候都要系统去IO读取,并解析XML格式,之后才能在动态的代码中生成各种UI元素,而DSL编译后直接和代码一起(因为DSL本身就是Kotlin代码)被编译成.dex文件或者机器码(ART运行环境),这样在运行时我们就大量的节省了IO读取和解析这样的耗时操作,从而达到了更加快速和节省电池用量的目的。
这样我们算是从理论的角度证明了DSL的优点,但是实际上的表现到底如何?我们可以做个实验,我们就从时间这个角度来具体测验一下,DSL到底快多少。
实验之前,我找到两篇博客文章,分别是两位开发者对XML和DSL的对比测试,我们一个一个来看,首先是第一篇;
《Kotlin Android UI利器之Anko Layouts》
根据他的描述,这位博主的测试是比较完善的,8款机型,30次测试,并且分别测量了Measure,Layout,Draw三大过程,可以看到差距非常明显,速度差距达到了300%,在低端机型上甚至达到了500%之多。
再来看看第二篇博客:
这本来是一篇国外博主写的文章,上面的地址是中文译文。我们也直接从文章中找到测试结果的图片:
作者使用了DevMetrics作为测试工具,测试的结论仍然是性能差距达到了惊人的350% — 600%。
看完了他人的测试结论,我想自己测试一下,我使用三星S8作为测试机型,准备了一个含有6个控件(只含有LineadLayout,Button,TextView),深度为两层的布局(XML和DSL)各一份,然后,使用DevMetrics作为测试工具,发现测试结果并没有什么差异,几乎看不出来哪个是XML编写的,哪个是DSL编写的,于是我准备了一张稍微复杂一点的布局,大概含有30个控件,深度为6层的布局,测试结果发生会出现细微变化,DSL大概会比XML快12%左右。
虽然我的测试结果与两位博主的测试结果相差甚远,但我分析实验条件以后的结论是:两位博主大都采用较为老旧的低端机型,而我采用的是旗舰级别的高端机型,高端机型的优异性能在一定程度上会抹消XML和DSL的性能差距;其次,准备的测试布局不够复杂,从我个人的两次实验大概可以看出,布局变复杂以后,XML和DSL的性能会逐渐拉开差距,但是即使是第二次实验时用的布局,控件种类不够多,设置的属性也不够丰富,嵌套的方式也比较单一,但在实际情况中,一个复杂的页面的布局远比我的更复杂;如果布局中控件的总数太少,IO的时间就会很短,如果XML嵌套的层级不够复杂,XML解析的时间也会很短,这就类似如果要测试JsonObject和Gson的性能,一般都会采用结构比较复杂,总长度足够长的Json数据,否则基本很难看出差异。
总而言之,DSL和XML的确存在性能差距,但是具体差多少,这个依赖于实际的手机机型,以及实际的布局复杂度;如果要在项目中引入Anko,可以先从复杂布局的优化入手,这样对于用户来说体验上会更加明显。
Part 3:兼容性
在上面的论述中,我们可以看到Anko的DSL的API支持绝大部分Android UI控件,但是我们在用XML做开发的时候,我们还会用到其它的标签,比如ViewStub,Merge,Include等等,根据我的测试,Anko支持ViewStub和Include标签,不支持Merge标签;也就是说,DSL可以支持我们将XML定义的布局Include进来,这就极大的增加了兼容性,如果我们要在DSL中include在其它地方定义的UI也很容易,我们直接把其它地方构建的UI定义在一个函数内,在DSL内直接调用函数即可。
近些年来,Google官方推出了很多和XML布局有直接关联的库或框架,便于我们实现许多功能;例如ConstraintLayout,一款帮助我们降低UI层级从而优化布局的工具;我们在Anko的github主页上也找到了ConstraintLayout的支持包,需要单独集成:
// ConstraintLayout
implementation "org.jetbrains.anko:anko-constraint-layout:$anko_version"
可见ConstraintLayout官方已经做了支持。
我们另外关注的框架的就是DataBinding,官方提供的用于便捷实现MVVM架构模式的组件,它的数据绑定基于XML,但是我没在Anko的主页找到JetBrain对DataBinding的支持,可见目前JetBrain官方还没有支持的计划。
目前在网上流传较多的是开发者们个人为Anko开发的DataBinding,也有些文章通过举例来讲解如何编写Anko的DataBinding,如果要编写一个这样的框架也不是难事,在DataBinding中,我们需要在XML中定义data,而根据Anko的API设计哲学,我们可以将原先定义data的方式也包装成DSL的形式,这样做甚至比DataBinding更有优势:比如,我们可以让data的定义在编译期就知道其行为是否正确,而如果在XML中定义,不到运行时我们是没法检验data的定义是否正确的。
Part 4:辅助工具
XML有一个优点在于,由于它是静态的,所以我们即使不运行程序,Android Studio也能给我们提供方便的预览功能——可以在运行前提前看到布局的样子,从而进行动态的调整。而Android Studio肯定不支持DSL的预览,因此我们就需要寻找一些辅助工具。Anko的官方文档中提到,他们推出了一款可以运行在Android Studio和intellij IDEA的插件——Anko Support,我们可以方便的在Android Studio中点击Prefercences -> Plugins -> Install JetBrains Plugins然后列表中的第三个就是Anko Support,安装它就行了。
据官方文档介绍,Anko Support只能预览写在AnkoComponent类中的布局......这其实很无语,上面介绍了那么多种花式写法,全都因为不能预览而只能用AnkoComponent这种写法来写了......
既然如此,我们就看看预览AnkoComponent的效果,我写了一个非常简单的布局:
<?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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="hello"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="I'm"
android:textSize="16sp"
android:textColor="@color/yellow"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Anko"
android:textSize="18sp"
android:textColor="@color/blue" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="DSL"
android:textSize="20sp"
android:textColor="@color/green"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="yes"
android:background="@color/blue"/>
</LinearLayout>
然后我发现,预览根本啥都没有......
我build了一下工程,预览终于出来了:
需要先build才能预览......这太反人类了,这和编译运行到手机直接看效果相比简直没区别.....我在Anko的github的issues里找到了有人问相同的问题,提问的时间是2018年6月15日,在6月30日的时候有人回复说当前版本有bug,正在等待更新,本文的写作日期是2018年7月17日,使用的Anko版本是0.10.5,使用的Anko Support版本是0.10.5-2,然而,还未有相关更新。
此外,Anko Support提供了一个XML TO DSL的工具,可以方便的实现代码转化操作。我们来试试把刚才测试预览的XML布局转化:
转化得到:
class SomeActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super<Activity>.onCreate(savedInstanceState)
linearLayout {
orientation = LinearLayout.VERTICAL
textView("hello") {
textSize = 14f
}.lparams(width = wrapContent, height = wrapContent) {
gravity = Gravity.CENTER_HORIZONTAL
}
textView("I'm") {
textColor = R.color.yellow
textSize = 16f
}.lparams(width = wrapContent, height = wrapContent) {
gravity = Gravity.CENTER_HORIZONTAL
}
textView("Anko") {
textColor = R.color.blue
textSize = 18f
}.lparams(width = wrapContent, height = wrapContent) {
gravity = Gravity.CENTER_HORIZONTAL
}
textView("DSL") {
textColor = R.color.green
textSize = 20f
}.lparams(width = wrapContent, height = wrapContent) {
gravity = Gravity.CENTER_HORIZONTAL
}
button("yes") {
backgroundResource = R.color.blue
}.lparams(width = matchParent, height = wrapContent)
}
}
}
它直接转换成了一个Activity,实际上我们并不想要Activity,而是想要一个AnkoComponent。但实际上我们并不指望这种自动化工具来帮我们写代码,就像Android Studio提供的自动将Java转化为Kotlin的功能,转化出来的代码肯定可以正确工作,但是并不符合我们的编程习惯。我们细看转换出来的DSL布局,大体上没有问题,但是还是有bug,它把颜色ID直接设置给了textColor,但实际上在代码中需要将颜色ID解析成颜色的具体值,或是将ID赋值给textColorResource属性,所以这个转换生成的代码的行为也并不完全符合预期,所以这个XML TO DSL的功能只能作为一种参考,想通过直接转换来得到结果,还是不行的。
总结一下Part 4,预览功能目前仍然处于不完善的状态,也许后续会有改善,但对于很多依赖预览功能来写布局的开发者来说,这项缺点也许足以让他们放弃Anko。代码转化功能大体能达到预期效果,但是还是存在bug,而且这并非是杀手级功能,对于已经掌握DSL的开发者来说,基本可有可无,对于初学者来说,如果事先不知道什么样的代码是bug,很可能会陷入困扰。
Part 5:风险以及缺点
上面说了很多Anko的优点,但是任何一种技术都有缺点和风险,要引入一项新技术,我们就必须权衡优势以及缺点,才能得到最终结论。我认为Anko DSL目前还存在以下问题:
代码可读性要略差于XML
预览功能有很大限制
会增加包大小
不能绝对保证它的行为完全符合预期
没有Google的背书
可读性要略差于XML
XML的可读性是非常高的,它使用的标签化有很高的辨识度。而如果使用DSL构建有多层嵌套的布局时,你可能常常看不清楚,代码块结尾的“}”是属于谁的,加上DSL的LayoutParams独特的设置方式————在代码块结尾调用lParams().{}函数,会更加降低代码层级辨识度。
预览功能有很大限制
这一点在Part 4中已经阐述了,Anko Support插件也只能预览AnkoComponent的子类,而且当前版本的Bug导致它只能先build再预览。
会增加包大小
这一点算是对我们所有App开发者都有较大影响的一项缺点,我们来测试一下引入Anko后APK文件增加的尺寸。我创建了一个全新的Android Project,里面只有一个自动生成的Activity。在我们不引入Anko的时候,我们的项目依赖如下:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
implementation 'com.android.support:support-v4:28.0.0-alpha3'
implementation 'com.android.support:design:28.0.0-alpha3'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation "com.android.support:recyclerview-v7:28.0.0-alpha3"
implementation "com.android.support:cardview-v7:28.0.0-alpha3"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
基本只有一些Android官方提供的支持包,我们build一下,来看看debug版APK的包大小:
可以看到大小是2.7MB。
再来看看加上Anko后的项目依赖:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
implementation 'com.android.support:support-v4:28.0.0-alpha3'
implementation 'com.android.support:design:28.0.0-alpha3'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation "com.android.support:recyclerview-v7:28.0.0-alpha3"
implementation "com.android.support:cardview-v7:28.0.0-alpha3"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// Anko Commons
implementation "org.jetbrains.anko:anko-commons:$anko_version"
// Anko Layouts
implementation "org.jetbrains.anko:anko-sdk25:$anko_version"
// Appcompat-v7 (only Anko Commons)
implementation "org.jetbrains.anko:anko-appcompat-v7-commons:$anko_version"
// Appcompat-v7 (Anko Layouts)
implementation "org.jetbrains.anko:anko-appcompat-v7:$anko_version"
// CardView-v7
implementation "org.jetbrains.anko:anko-cardview-v7:$anko_version"
// Design
implementation "org.jetbrains.anko:anko-design:$anko_version"
implementation "org.jetbrains.anko:anko-design-coroutines:$anko_version"
// GridLayout-v7
implementation "org.jetbrains.anko:anko-gridlayout-v7:$anko_version"
// RecyclerView-v7
implementation "org.jetbrains.anko:anko-recyclerview-v7:$anko_version"
implementation "org.jetbrains.anko:anko-recyclerview-v7-coroutines:$anko_version"
// Support-v4 (only Anko Commons)
implementation "org.jetbrains.anko:anko-support-v4-commons:$anko_version"
// Support-v4 (Anko Layouts)
implementation "org.jetbrains.anko:anko-support-v4:$anko_version"
// ConstraintLayout
implementation "org.jetbrains.anko:anko-constraint-layout:$anko_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
这样的项目依赖包含Anko的Commons和Layouts这两部分以及其它针对Support包UI的支持库,由于SQLite和Coroutines并不是每个项目都会用得到,所以这里没有包含,我们再来看看包的大小:
3.2MB,相比之前也就增加了0.5MB,我觉得可以接受,特别是对于大项目来说,几乎不算什么;就算是对于用户来说刷微博看一张大图都不止0.5MB;所以能否接受这样的包大小增加,见仁见智。
不能绝对保证它的行为完全符合预期
我不否认,Anko中存在bug;简述一个我自己之前遇到的例子,在5.0以上的系统中,只要在style中设置statusBarColor为透明,然后在Activity中使用DrawerLayout就可以实现沉浸式的侧滑菜单,但是行为完全一样的代码使用Anko DSL来编写,状态栏的颜色就变成了诡异的白色,且DrawerLayout的侧滑菜单无法正常延伸到状态栏。这个问题我在Google中搜索后,发现StackOverFlow上有人遇到了同样的问题,但是所有答案给出的解法均无法正常解决。这个问题的详细描述可以参照下面这个链接:
https://stackoverflow.com/questions/39296436/statusbar-is-not-transparent-but-white
当然这样的Bug我也只遇到过这一个,虽然Anko存在这样的问题,但不代表类似的问题比比皆是。
没有Google的背书
Google在2017年确定Kotlin为Android的一级语言后并没有顺带大力推行Anko。而在2017年底Google推出了官方的Kotlin辅助函数库————Android KTX,KTX和Anko在很多功能上都有重叠,而KTX中唯一没有的就是Anko的DSL动态构建UI,可见Google要么是还在观望态度,要么就是也不打算将DSL构建UI的方式纳入主流体系,加上年初开始,特别是2018年的I/O大会,Google开始强推跨平台框架Flutter,可见Google官方的重心以后都会放在这里,DSL的转正可谓是遥遥无期。
但这一切不代表以后都没有任何转机;参考之前的例子,最在在Android上使用的HTTP底层封装的API一款是Apache提供的HttpClient,一款是JDK中自带的HttpURLConnection,HttpClient自带严重BUG,直接在Android 6.0以后被彻底移除,而HttpURLConnection的API太过复杂,开发者都觉得很麻烦,这时,Android之神Jake Wartton的团队开发的开源库OkHttp受到了广泛认可,并最终被Google加入了官方API中,而Jake Wartton本人也被Google招安,所以如果以后Anko DSL发展的更为成熟,也许也会受到Google官方认可。
结论
结合上面的所有讨论,个人认为,如果你正要开始一个新项目,或是准备彻底重写一个当前的项目,都可以试着使用Anko。引入Anko不代表你一定要彻底放弃XML布局,你可以尝试使用Anko重写一些UI特别复杂的布局以在一定程度上降低布局的生成时间,或是其它开销本来就不大的布局里仍使用XML编写的方式;或是你也可以使用Anko来构建绝大部分的UI,只有当你遇到Part 5中介绍的bug时再退守XML的方式;总而言之,如何使用都取决于开发者本人。虽然Anko到目前为止仍未成为主流,但是我仍然认为这是一项值得一试的亮点技术。