Anko 3个月前

编程语言
380
Anko

1. 简化页面操作

我们写 Android 最先做的是什么?当然是设置个 OnClickLisener,这样自然我的按钮听我的,我的地盘我做主了。

hello.onClick {
    startActivity<AnotherActivity>("from" to "MainActivity")
}

哎哟,不错哦。其中 hello 是一个 TextView,我们通过 onClick 为其设置了一个 OnClickListener,这样看上去真是简洁不少。

fun android.view.View.onClick(l: (v: android.view.View?) -> Unit) {
    setOnClickListener(l)
}

也没什么难理解的,onClick 是一个扩展方法,传入的 Lambda 表达式通过 SAM 转换成了 OnClickListener,一切都是这么的自然。如果你对传入的 view 感兴趣,你当然可以直接用 it 召唤它:

hello.onClick {
    Log.d(TAG, it.toString())
    startActivity<AnotherActivity>("from" to "MainActivity")
}

简单吧。

等等!那个 startActivity 是怎么回事?没有 Intent 么?

哈哈,这个嘛,且看源码:

inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any>) {
    AnkoInternals.internalStartActivity(this, T::class.java, params)
}

...

fun internalStartActivity(
        ctx: Context,
        activity: Class<out Activity>,
        params: Array<out Pair<String, Any>>
) {
    ctx.startActivity(createIntent(ctx, activity, params))
}

其实也没什么,就是对我们之前模板式的跳转写法做了简化而已,至于用到的 reified 和 Pair 也不算什么新鲜的东西,Pair 当中的 K-V 实际上就是我们通常放入 Intent 的 extra,所以我们自然可以在 AnotherActivity 当中取到这个值:

class AnotherActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        relativeLayout {
            textView {
                text = if(intent == null) 
                            "from nowhere" 
                        else 
                                intent.extras["from"]?.toString()
            }
        }
    }
}

取到我们传入的值,在 AnotherActivity 当中显示出来。有木有觉得要比我们用 Java 老大哥写出来的代码简洁易懂呢?

2. 聊聊 DSL 布局

再等等!那个 relativeLayout{...}是几个意思??

嗯,这个要多说几句了,Anko 这个框架虽然打着简化开发的旗号,不过野心终归还是不小的。它自己搞出一套用 Kotlin 写布局的 DSL,换句话说,有了 Anko 我们布局甚至可以不需要用 XML 了,也不需要像用 Java 硬编码 View 那么繁琐,只需要通过几句 DSL 就可以搞定。我们来多看几个例子:

2.1 水平布局

下面是是三个按钮水平等分布局的写法,我们用到的实际上就是线性布局,比较简单,button 的参数是按钮的文字(有较多重载的版本,大家可以酌情选择),lparams 的参数有三个,前两个分别是宽、高,最后一个是一个 Lambda 表达式,我们可以在这个Lambda表达式当中详细定义我们需要的布局,比如设置 margin 等等。

linearLayout {
    button("1").lparams(wrapContent, wrapContent){
        weight = 1f
    }
    button("2").lparams(wrapContent, wrapContent){
        weight = 1f
    }
    button("3").lparams(wrapContent, wrapContent){
        weight = 1f
    }
}

效果图如下:

image

2.2 纵向布局

还是线性布局,不过换了个方向,你当然可以这么写:

linearLayout {
    orientation = LinearLayout.VERTICAL
    button("1").lparams(wrapContent, wrapContent){
        weight = 1f
    }
    button("2").lparams(wrapContent, wrapContent){
        weight = 1f
    }
    button("3").lparams(wrapContent, wrapContent){
        weight = 1f
    }
}

不过,Anko 更倾向于让我们用这个:

verticalLayout {
    button("1").lparams(wrapContent, wrapContent){
        weight = 1f
    }
    button("2").lparams(wrapContent, wrapContent){
        weight = 1f
    }
    button("3").lparams(wrapContent, wrapContent){
        weight = 1f
    }
}

我给大家看一下源码,大家就分分钟明白了:

val VERTICAL_LAYOUT_FACTORY = { ctx: Context ->
    val view = _LinearLayout(ctx)
    view.orientation = LinearLayout.VERTICAL
    view
}

其实我们创建的 verticalLayout 最终是从这个方法当中获取的,没啥新鲜的,就是设置了一下 orientation 罢了。效果图我就不贴了,大家很容易猜得到。

2.3 相对布局

relativeLayout {
    relativeLayout {
        textView("周杰伦") {
            id = R.id.extra
            useSecondary()
        }.lparams(wrapContent, wrapContent) {
            alignParentRight()
            centerVertically()
            rightMargin = dip(10)
        }

        imageView {
            id = R.id.avatar
            imageResource = R.drawable.jaychow
            scaleType = ImageView.ScaleType.FIT_XY
        }.lparams(dip(40), dip(40)){
            centerVertically()
            leftMargin = dip(10)
        }

        textView("千里之外") {
            id = R.id.title
            usePrimary()
        }.lparams(matchParent, wrapContent) {
            leftOf(R.id.extra)
            rightOf(R.id.avatar)
            margin = dip(5)
        }

        textView("依然范特西") {
            id = R.id.subtitle
            useSecondary()
        }.lparams(matchParent, wrapContent) {
            leftOf(R.id.extra)
            rightOf(R.id.avatar)
            below(R.id.title)
            leftMargin = dip(5)
        }
    }.lparams(matchParent, dip(50))
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="title" type="id"/>
    <item name="subtitle" type="id"/>
    <item name="extra" type="id"/>
    <item name="avatar" type="id"/>
</resources>

这个布局我们看到其实就是一张图片,三个 TextView,难度也不大,不过这种写法可能要适应一下。

效果如下:

image

注意到我在 TextView 当中用了两个方法:usePrimary() 和 useSecondary(),这其实是我定义的样式:

fun TextView.usePrimary(){
    textSize = 15f //注意这里就是 sp 的值
    textColor = Color.BLACK
    typeface = Typeface.DEFAULT_BOLD
}

fun TextView.useSecondary(){
    textSize = 12f //注意这里就是 sp 的值
    textColor = Color.GRAY
}

这个算是比较复杂的一个布局了,只要 XML 可以搞定的用 Anko DSL 的方式一样可以搞定,而且写出来的东西都可以直接对应到源码,这一点是非常棒的。我们在使用 XML 布局的时候如果想要知道某一个属性对应 View 的什么成员,还得去找这个 View 解析 XML 的代码,显然这一点 DSL 要方便一些。

2.4 独立的 UI

前面我们说到的都是在 Activity 的 onCreate 方法中使用 DSL 的场景。很多时候我们其实还是希望布局和 Activity 分开的,那么我们就可以用官方推荐的这种方式来给 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}!") }
            }
        }
    }
}

2.5 在任意位置用 DSL 定义 View

前面提到的各种 relativeLayout {} 也好,verticalLayout {} 也好,都只能在 Activity、ViewManager(ViewGroup 的接口)、Context 这三个类的作用域范围之内使用,换句话说前面的几个布局的方法都是这几个类的扩展方法。

下面这个写法是没有问题的:

fun createView(context: Context): View{
    return context.relativeLayout{
        ...
    }
}

相应的,我们可以用任意一个 ViewGroup 的子类来调用类似的方法,这与调用 viewRoot.addView(FrameLayout(viewRoot.context)) 是一样的:

fun addViewToParent(viewRoot: ViewGroup){
    viewRoot.frameLayout { 
        ...
    }
}

如果是在 Fragment 当中,Anko 还非常贴心的定义了一个叫 UI 的方法,这个方法同时也存在于 Context 当中,用法也比较简单:

class MainFragment: Fragment(){
    override fun onCreateView(
            inflater: LayoutInflater?, 
            container: ViewGroup?, 
            savedInstanceState: Bundle?): View {
        return UI {
            tableLayout { 
                ...
            }
        }.view
    }
}

2.6 扩展 Anko,支持自定义 View

我们在开发中经常继承一个 View 实现一些自己想要的功能,比如我们继承 _RelativeLayout:

class CustomLayout(context: Context)
    : _RelativeLayout(context) {
     ...   
}

注意,如果我们直接继承 RelativeLayout,那么还需要自己定义 lparams 方法,这个我就不细说了,大家有需求可以自己详细研究~

为了让 Anko DSL 支持下面的写法:

customLayout{
    button("ClickMe"){ ... }
}

我们需要定义下面三组扩展方法:

inline fun ViewManager.customLayout(theme: Int = 0) 
        = customLayout(theme) {}
inline fun ViewManager.customLayout(
        theme: Int = 0, 
        init: CustomLayout.() -> Unit) 
        = ankoView(::CustomLayout, theme, init)

inline fun Activity.customLayout(theme: Int = 0) 
        = customLayout(theme) {}
inline fun Activity.customLayout(
        theme: Int = 0, 
        init: CustomLayout.() -> Unit) 
        = ankoView(::CustomLayout, theme, init)

inline fun Context.customLayout(theme: Int = 0) 
        = customLayout(theme) {}
inline fun Context.customLayout(
        theme: Int = 0, 
        init: CustomLayout.() -> Unit) 
        = ankoView(::CustomLayout, theme, init)

其中,第一组 ViewManager 的是为了在 ViewGroup 当中使用;第二组是为了在 Activity 当中使用,第三组就是为了在所有 Context 当中使用。

扩展也是非常简单的,用起来也丝毫感觉不到这些 View 是自定义的,比起 XML 标签长长的一串确实也要美观得多。

<net.println.kotlinandroiddemo.CustomLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
        ...
</net.println.kotlinandroiddemo.CustomLayout>

2.7 Anko DSL 使用小结

Anko DSL 的方式布局看上去还是比较清爽直观的,而且因为这是 Kotlin 代码,自然所有的view 都是强类型约束的,不需要我们 findViewById 再强转,除此之外由于是代码,可以直接运行,也就省去了运行时解析 XML 的开销,这一点可以说也是相比于 Android 官方的 XML 布局而言 Anko 主打的性能优势。

它的各方面优势我们在前面已经给大家一一点到,可是它存在哪些问题呢?

  • 首先,Anko DSL 布局不能预览。可以说这一点足以让我们放弃它了,不能预览的话很多时候我们只能通过运行结果来判断布局是否准确,这对开发效率的影响是巨大的。当然,这么说可能 Anko 不服,毕竟人家也是发布了一个叫 Anko Preview Plugin 的 IDE 插件的,有了这个插件理论上我们就可以预览 Anko DSL 的布局结果了对吧?可是结果呢,每次做了修改都需要 make 一下才可以看到结果,显然预览速度来看不如 XML 快。而就算这个问题我们可以忍,慢就慢点儿,别慢太多就行了吧,结果呢,人家这个插件存在各种各样的问题,比如对最新版的 Android Studio 2.2 和 IntelliJ 2016.3 不支持(当然其实本质上是对新版本的 Preview 功能不兼容),大家可以参考这个 issue:https://github.com/Kotlin/anko/issues/202。也就是说,这个插件现在是不能用的,所以跟没有也没啥区别。

  • 其次,对于 id 的定义会比较蛋疼。我们知道我们在布局的时候可以通过 @+id/xxx 的方式生成一个 id,并交给 Android 资源管理器统一管理,用 Anko DSL 的话我们就得专门定义一个变量或者在 value 目录下面增加 id 的定义(就像 2.3 的例子那样)去让 view 引用。不用 id 行不行呢?你去问问 RelativeLayout 答应不答应吧。

    val FROM_TEXT = 0
      val CLICK_ME = 1
      relativeLayout {
          textView {
              text = ...
              id = FROM_TEXT
          }
          
          button("clickMe"){
              id = CLICK_ME
          }.lparams { 
              below(FROM_TEXT)
          }
      }
    
  • 再次,我们通常会需要引用一些 view,通过 XML 布局 + kotlin-android-extensions 的方式,我们可以直接引用到这些有 id 的 view,非常方便,不过,如果我们用 Anko DSL 布局的话,我们就享受不到这项福利了(如果你不明白为什么,可以去看下前一篇文章: 用Kotlin写Android 01 难道只有环境搭建这么简单?

    val FROM_TEXT = 0
      val CLICK_ME = 1
      var fromText: TextView? = null
      relativeLayout {
          fromText = textView {
              text = ...
              id = FROM_TEXT
          }
          
          button("clickMe"){
              id = CLICK_ME
          }.lparams { 
              below(FROM_TEXT)
          }
      }
      
      ...
      
      if(shouldHideText) fromText?.visibility = View.GONE
      else fromText?.visibility = View.VISIBLE
    
  • 还有就是,如果我们的布局有多个版本,而且需要动态替换外部资源以达到换肤的效果,那么 XML 显然比 Kotlin 代码要来得容易:前者可以编译成一个只有资源的 apk 供应用加载,后者的话就得搞一下动态类加载了。

总之,Anko DSL 布局这个特性我个人觉得还没有达到可以取代 XML 布局的地步,如果大家习惯用 Java 硬编码 View 结构的话,Anko DSL 是个非常不错的选择;相反,如果大家一直用 XML 的话,那请接着用 XML 吧。当然,如果大家有好的使用方式,无论如何要来我这儿跟我嘚瑟一下哈~

3. 简化异步操作

假如你要在点击按钮之后把一个文件(本地或者服务端,也可能比较大,总之读取耗时)当中的文字显示出来,你用 Java 会怎么写呢?

button.setOnClickListener(new OnClickListener(){
    @Override public void onClick(View view){
        getExecutor().execute(new Runnable(){
            @Override public void run(){
                ...
                MainActivity.this.runOnUiThread(new Runnable(){
                    ...
                });
            }
        })
    }
});

哎呀我去,真是蜜汁缩进啊,我都写晕了。可是有了 Anko 配合,这段代码简直不能更清爽:

button.onClick {
    doAsync {
        val text = File("You raise me up.lrc").readText()
        uiThread {
            hello.text = text
        }
    }
}

doAsync 当中的代码运行在 Anko 配置的线程池当中,执行完之后还可以转入 uiThread 块来操作 UI,简单明了,还不容易出错。你当然也可以处理异常和自定义线程池:

doAsync(
    exceptionHandler = {
        Log.e(TAG, "error happened when read file.", it)
    },
    task = {
        val text = File("You raise me up.lrc").readText()
        uiThread {
            hello.text = text
        }
    },
    executorService = Executors.newSingleThreadExecutor()
)

其实大家肯定想到了这两个方法的实现逻辑:

fun <T> T.doAsync(
        exceptionHandler: ((Throwable) -> Unit)? = null,
        executorService: ExecutorService,
        task: AnkoAsyncContext<T>.() -> Unit
): Future<Unit> {
    val context = AnkoAsyncContext(WeakReference(this))
    return executorService.submit<Unit> {
        try {
            context.task()
        } catch (thr: Throwable) {
            exceptionHandler?.invoke(thr)
        }
    }
}

...

fun <T> AnkoAsyncContext<T>.uiThread(f: (T) -> Unit): Boolean {
    val ref = weakRef.get() ?: return false
    if (ContextHelper.mainThread == Thread.currentThread()) {
        f(ref)
    } else {
        ContextHelper.handler.post { f(ref) }
    }
    return true
}

4. 简化日志打印

不知道大家有没有觉得 Log.d(TAG, ...) 这样的代码写起来麻烦,绝大多数情况下,我们打日志都需要多写个 Log. 除非静态导入方法,以及 TAG 的值通常都是对应的类名,有时候我只是为了临时打印一行日志,还得去定义一个静态常量 TAG,简直了,还有就是如果我只是想要打印一下某一个对象,还得显式得调用 toString 方法,一点儿都不智能。

public class MainActivity extends Activity{
    public static final String TAG = "MainActivity";
    
    ...
    View view = ...
    Log.d(TAG, view.toString());
    ...

}

有了 Anko 就要简单的多了,只要实现 AnkoLogger 这个接口,我们就可以愉快的打印日志了:

class SomeActivity : Activity(), AnkoLogger {
    private fun someMethod() {
        info("London is the capital of Great Britain")
        debug(5) // .toString() method will be executed
        warn(null) // "null" will be printed
    }
}

日志的 TAG 默认就是类名称,如果你需要自定义,那也没关系,直接覆写这个变量就可以了:

override val loggerTag: String = "SomeActivityTag"

5. 小结

Anko 这个框架其实没有什么复杂的地方,它更多的是在想办法简化我们的“八股文”代码,让我们的生活更轻松一些而已。DSL 布局是一个很不错的尝试,不过现在看来还是不太完美的,XML 本身也没有太大的问题,想必后续大家完全转向 DSL 的动力也不会很大。

除了前面提到的特性,Anko 还可以简化对话框、toast、sqlite 等操作,相比之下,toast 的用法还是比较常用的,也比较简单,我就不细说了;至于 sqlite ,通常我们也不建议去直接操作它,用一些 ORM 框架可能会让你的代码更友好。

image
EchoEcho官方
无论前方如何,请不要后悔与我相遇。
1377
发布数
439
关注者
2223355
累计阅读

热门教程文档

Next
43小节
爬虫
6小节
10.x
88小节
Redis
14小节
React Native
40小节