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

新的 UI 体系

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

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

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

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

既然 AnkoContext 与 android.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 的子类,上面的类关系图已经展示出了它们之间的关系。

AnkoContextImpl 是 AnkoContext 接口的具体实现,覆写了 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,两者唯一的不同点在于 ReusableAnkoContext 的 alreadHasView 是一个空实现,而 AnkoContextImpl 会抛出一个异常,它不支持 view 复用。

DelegatingAnkoContext 是 AnkoContext 的另一个实现:

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>,也就是说 DelegatingAnkoContext 的 owner 必须是 ViewGroup 或者它的子类。

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

override val view: View = owner

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

  • AnkoContext 继承自 ViewManager
  • AnkoContextImpl 和 DelegatingAnkoContext 实现了 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。

首先,relativeLayout 是 ViewManager 的扩展函数,返回值是一个 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。

再回到 AnkoComponent 的 createView:

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

with(ui) 会把 ui: AnkoContext<MainActivity> 作为第二个参数(用大括号表示的高阶函数)的 receiver,也就是说 高阶函数内的 this 会指向变量 ui。这样的话,AnkoInternals.addView(this, view) 会调用到 AnkoContext 的 addView 方法,具体如何处理取决于接口 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,也就说扩展函数 image、text 所创建的 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)
      }
    }
  }
}

MainActivityUI 是 AnkoComponent<MainActivity> 的子类,类型实参 <MainActivity> 表示 AnkoContext 的 owner 的类型。

创建完 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())

创建组件所使用的 AnkoContext 是 ReusableAnkoContext。

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 的布局方式都在朝着“拖拽”的方向演进,比如 ConstraintLayout、AutoLayout,设计稿直接转换为 XML 布局文件也已经指日可待。除此之外,逻辑代码和布局代码混在一起的方式也不便于代码维护。

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