Anko的设计之道

Posted by liangfei on 2018-05-17

Anko 是一个完全基于 Kotlin 设计的 Android 三方库,名字来自于 Android Kotlin 这两个单词的前两个字母。Anko 试图建立一套新的 Android 开发范式, 虽然不会成为主流,但是它的设计思想值得我们借鉴。

新的 UI 体系

先看一下 Anko 用于构建 UI 的几个关键类:

+--------------+
| ViewManaager |
+-------.------+
       /|\
        |
+---------------+
|  AnkoContext  |<--------------+
+-------.-------+               |
       /|\                      |
        |                       |
+-----------------+   +-----------------------+
| AnkoContextImpl |   | DelegatingAnkoContext |
+-------.---------+   +-----------------------+
       /|\
        |
+---------------------+
| ReusableContextImpl |
+---------------------+

这里需要强调的是,AnkoContext 继承自 ViewManager,而不是 android.content.Context。刚开始读源码时,总会觉得 AnkoContext 的命名有点反直觉,但是,就像文章一开始所说的——“Anko 试图建立一套新的 Android 开发范式” ——其实 AnkoContextandroid.content.Context 之间是并列关系。

就像 Android 需要基于 Context 来创建一个 View 一样,Anko 创建 UI 组件也需要基于 AnkoContext

既然 AnkoContextandroid.content.Context 是并列关系,那么大部分为 android.content.Context 定义扩展的地方也定义了 AnkoContext 的扩展。例如 Dimensions.kt 文件:

fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt()
inline fun AnkoContext<*>.dip(value: Int): Int = ctx.dip(value)
inline fun View.dip(value: Int): Int = context.dip(value)
inline fun Fragment.dip(value: Int): Int = activity.dip(value)

Anko 的 UI 组件用一个接口 AnkoComponent 来表示,接口内提供了一个模板方法 createView

interface AnkoComponent<in T> {
fun createView(ui: AnkoContext<T>): View
}

可以看出,AnkoComponent 的类型参数(type parameter)是一个逆变的声明处变型(declaration-site variance),模板方法 createView 的作用是基于 AnkoContext<T> 实例创建一个 View

AnkoContext 继承自 ViewManager,也是一个接口,它“内藏”了 Android 的 android.content.Context 实例,同时还定义了一个类型为 T 的属性 owner 用于表示 AnkoContext 的拥有者。AnkoContext 没有对类型参数 T 加以限定,任何类型都可以。

interface AnkoContext<out T> : ViewManager {
val ctx: Context
val owner: T
val view: View

override fun updateViewLayout(view: View, params: ViewGroup.LayoutParams) {
throw UnsupportedOperationException()
}

override fun removeView(view: View) {
throw UnsupportedOperationException()
}

companion object {
fun create(ctx: Context, setContentView: Boolean = false): AnkoContext<Context>
= AnkoContextImpl(ctx, ctx, setContentView)

fun createReusable(ctx: Context, setContentView: Boolean = false): AnkoContext<Context>
= ReusableAnkoContext(ctx, ctx, setContentView)

fun <T> create(ctx: Context, owner: T, setContentView: Boolean = false): AnkoContext<T>
= AnkoContextImpl(ctx, owner, setContentView)

fun <T> createReusable(ctx: Context, owner: T, setContentView: Boolean = false): AnkoContext<T>
= ReusableAnkoContext(ctx, owner, setContentView)

fun <T: ViewGroup> createDelegate(owner: T): AnkoContext<T> = DelegatingAnkoContext(owner)
}
}

AnkoContext 还通过伴生对象(companion object)提供了五个工厂方法,它们生产出来的对象都是 AnkoContext 的子类,上面的类关系图已经展示出了它们之间的关系。

AnkoContextImplAnkoContext 接口的具体实现,覆写了 ViewManager#addView 方法:

override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
if (view == null) return

if (myView != null) {
alreadyHasView()
}

this.myView = view

if (setContentView) {
doAddView(ctx, view)
}
}

我们来分析一下 addView 的具体实现:

参数 view: View? 是一个可空类型,但是 AnkoContext 的属性 view: View 是一个非可空类型,所以 AnkoContextImpl 重新定义了一个属性 var myView: View? 用于存储参数 view: View?

private var myView: View? = null

override val view: View
get() = myView ?: throw IllegalStateException("View was not set previously")

AnkoContextImpl 的构造方法多了一个属性参数 setContentView: Boolean,它表示是否要把 addView 的参数 view 设置为属性 ctx: Context 的 content view:

private fun doAddView(context: Context, view: View) {
when (context) {
is Activity -> context.setContentView(view)
is ContextWrapper -> doAddView(context.baseContext, view)
else -> throw IllegalStateException("Context is not an Activity, can't set content view")
}
}

ReusableAnkoContext 继承自 AnkoContextImpl,两者唯一的不同点在于 ReusableAnkoContextalreadHasView 是一个空实现,而 AnkoContextImpl 会抛出一个异常,它不支持 view 复用。

DelegatingAnkoContextAnkoContext 的另一个实现:

internal class DelegatingAnkoContext<T: ViewGroup>(override val owner: T): AnkoContext<T> {
override val ctx: Context = owner.context
override val view: View = owner

override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
if (view == null) return

if (params == null) {
owner.addView(view)
} else {
owner.addView(view, params)
}
}
}

DelegatingAnkoContext 的类型参数指定了 upper bound —— <T: ViewGroup>,也就是说 DelegatingAnkoContextowner 必须是 ViewGroup 或者它的子类。

关于 DelegatingAnkoContext 名字中的 Delegating 应该是为了表达属性 view: View 代理了(delegating)owner: T

override val view: View = owner

再来回看一下这几个类的继承关系:

  • AnkoContext 继承自 ViewManager
  • AnkoContextImplDelegatingAnkoContext 实现了 AnkoContext
  • ReusableContextImpl 继承自 AnkoContextImpl
+--------------+
| ViewManaager |
+-------.------+
       /|\
        |
+---------------+
|  AnkoContext  |<--------------+
+-------.-------+               |
       /|\                      |
        |                       |
+-----------------+   +-----------------------+
| AnkoContextImpl |   | DelegatingAnkoContext |
+-------.---------+   +-----------------------+
       /|\
        |
+---------------------+
| ReusableContextImpl |
+---------------------+

基于 DSL 实现 UI 布局

DSL 可以用于取代手写 XML 布局,其优势如下:

  • 类型安全:编译阶段就能够发现类型错误,而且不会出现 NPE
  • 代码复用:可封装 View 的创建逻辑
  • 性能提升:节省了 inflate 的运行开销

先来看一段代码:

verticalLayout {
padding = dip(32)

imageView(android.R.drawable.ic_menu_manage).lparams {
margin = dip(16)
gravity = Gravity.CENTER
}

val name = editText {
hintResource = R.string.name
}
val password = editText {
hintResource = R.string.password
inputType = TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_PASSWORD
}

button("Log in") {
onClick {
ui.owner.tryLogin(ui, name.text, password.text)
}
}

myRichView()
}.applyRecursively(customStyle)

在 Anko 新的 UI 体系中,AnkoComponent 表示一个 UI 组件,模板方法 createView 返回 View 实例,上面的代码即可作为 createView 的函数内容,返回值是一个 vertical 的 LinearLayout

加上 AnkoComponent 把上面的代码补全:

class MainActivityUi : AnkoComponent<MainActivity> {
private val customStyle = { v: Any ->
when (v) {
is Button -> v.textSize = 26f
is EditText -> v.textSize = 24f
}
}
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
relativeLayout {
// ...
}.applyRecursively(customStyle)
}
}

我们来分析一下 relativeLayout 如何成为 ui: AnkoContext<MainActivity> 的 content view。

首先,relativeLayoutViewManager 的扩展函数,返回值是一个 LinearLayout

inline fun ViewManager.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}

然后,它会调用 ViewManager 的另一个扩展函数——ankoView

inline fun <T : View> ViewManager.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
val ctx = AnkoInternals.wrapContextIfNeeded(AnkoInternals.getContext(this), theme)
val view = factory(ctx)
view.init()
AnkoInternals.addView(this, view)
return view
}

ankoView 会调用 AnkoInternals#addView

fun <T : View> addView(manager: ViewManager, view: T) = when (manager) {
is ViewGroup -> manager.addView(view)
is AnkoContext<*> -> manager.addView(view, null)
else -> throw AnkoException("$manager is the wrong parent")
}

也就是说,verticalLayout 函数会把它所创建的 LinearLayout 作为 child view 添加到接收者(receiver)中,这里是 ViewManager

再回到 AnkoComponentcreateView

override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
verticalLayout {
// ...
}
}

with(ui) 会把 ui: AnkoContext<MainActivity> 作为第二个参数(用大括号表示的高阶函数)的 receiver,也就是说 高阶函数内的 this 会指向变量 ui。这样的话,AnkoInternals.addView(this, view) 会调用到 AnkoContextaddView 方法,具体如何处理取决于接口 AnkoContext 的具体实现。

下面举例来展示它们的用法。

自定义 View

class RichView : LinearLayout {
private lateinit var image: ImageView
private lateinit var text: TextView

private fun init() = AnkoContext.createDelegate(this).apply {
gravity = CENTER
padding = dip(24)

image = imageView(imageResource = R.drawable.kotlin) {
onClick { startAnimation() }

padding = dip(8)
layoutParams = LinearLayout.LayoutParams(dip(48), dip(48))
}

text = textView("Anko Rich view") {
textSize = 19f
}

startAnimation()
}

// ...
}

AnkoContext.createDelegate 创建了一个 DelegatingAnkoContext,它的 owner 和 view 都是 this,也就说扩展函数 imagetext 所创建的 view 都会作为 child view 添加给 this

Activity 的 content view

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val adapter = ProverbAdapter(this, proverbs)
MainActivityUI(adapter).setContentView(this)
}
}

class MainActivityUI(private val adapter: ProverbAdapter) : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
relativeLayout {
recyclerView {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
adapter = this@MainActivityUI.adapter
}
}.lparams(width = matchParent, height = matchParent)
}.apply {
layoutParams = FrameLayout.LayoutParams(matchParent, matchParent).apply {
padding = dip(16)
}
}
}
}

MainActivityUIAnkoComponent<MainActivity> 的子类,类型实参 <MainActivity> 表示 AnkoContextowner 的类型。

创建完 MainActivityUI 的实例之后,用法就变得非常简单,直接调用它的扩展函数 setContentView

MainActivityUI(adapter).setContentView(this)

setContentView 会创建一个 AnkoContextImpl 类型的实例,然后把这个实例作为参数调用 createView

fun <T : Activity> AnkoComponent<T>.setContentView(activity: T): View =
createView(AnkoContextImpl(activity, activity, true))

RecyclerView.ViewHolder

首先用 AnkoComponent 定义 item 的 UI 组件。

class ProverbComponent(val ui: AnkoContext<ProverbAdapter>) : AnkoComponent<ProverbAdapter> {
lateinit var category: TextView
lateinit var title: TextView

fun createView() = createView(ui)

override fun createView(ui: AnkoContext<ProverbAdapter>) = with (ui) {
linearLayout {
category = textView {
textColor = Color.RED
}

textView {
text = ": "
}

title = textView {
textColor = Color.BLUE
}
}
}
}

然后用 ViewHolder 来 hold 这个组件。

class ViewHolder(val ankoComponent: ProverbComponent) : RecyclerView.ViewHolder(ankoComponent.createView())

创建组件所使用的 AnkoContextReusableAnkoContext

class ProverbAdapter(context: Context, private val items: List<Proverb>) : RecyclerView.Adapter<ViewHolder>() {
private val ankoContext: AnkoContext<ProverbAdapter> = AnkoContext.createReusable(context, this)

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.ankoComponent.category.text = items[position].category
holder.ankoComponent.title.text = items[position].phrase
}

override fun getItemCount(): Int {
return items.size
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ProverbComponent(ankoContext))
}
}

总结

Anko 这种基于 DSL 的布局方式可以算得上是一股清流,虽然在执行效率上面有所提升,但是从开发效率和分工协作的角度来看,DSL 算不上是一种高效的方式。

我们都知道,用 Sketch 画 UI 肯定比手写代码的方式效率高,无论是 Android 还是 iOS,UI 的布局方式都在朝着“拖拽”的方向演进,比如 ConstraintLayoutAutoLayout,设计稿直接转换为 XML 布局文件也已经指日可待。除此之外,逻辑代码和布局代码混在一起的方式也不便于代码维护。

UI 布局只是 Anko 的功能之一,有时间了再分析一下 Anko Coroutines 的实现。


欢迎扫码关注 老梁写代码 微信公众号