使用 Kotlin 和 Anko 的安卓开发入门

使用 Kotlin 和 Anko 的安卓开发入门

我将会说这会像这个样子……

安卓困在了 Java 6 的炼狱中。


ADB Hell

当我刚开始开发安卓的时候,我是一个多年的 C# 工程师。我发现安卓缺少泛型支持(和 Java 的泛型比较而言),没有 lambda 表达式, 我认为在一个语言中应该有的东西这里却有着非常笨拙的语法。8 年后,我仍然在写着非常繁琐的 Java 6。Java 8 已经出来一段时间了,在不改变二进制代码的情况下,如果能使用它的一些功能就太棒了!(这就是说,我非常感谢 retrolambda。)但是不幸的是,谁知道 Java 8 什么时候会到来呢。

谢天谢地,安卓开发看起来有希望了:Kotlin

Kotlin Logo

Kotlin 是 JetBrains 公司提供的安卓应用开发的全新 JVM-compatible 的语言。如果你从来没有听说过这个语言,我强烈推荐你看看 Michael Pardo’s 关于 Kotlin 的演讲 来自 Droidcon NYC。而且,应用局部布局视图可用通过 Anko 来创建,这是一个用 Kotlin 写的安卓开发的 DSL (Domain-Specific Language) 组件。你可以对 Anko 有一个基本的了解 这里

Kotlin,作为一个开发语言相对于 Java 6 来说是全新的。当你习惯了它的语法以后,你会注意到它比 Java 6 要简洁很多。因为 Kotlin 是 JVM-compatible 的,它编译成的 JVM 二进制代码能被安卓理解。

重要提示:这篇文章假设你已经有了对于基本的 Kotlin 和 Anko 的理解。

在安卓项目里面使用 Kotlin

作为一个新的知识,你应该想试试 Kotlin,但是你还不想深入理解整个项目。在 Kotlin 和 Android Studio 的帮助下,你可以在你的应用里面同时使用 Java 和 Kotlin。我推荐从一个屏幕,一个功能,或者一个简单的定制的空间开始,使用 Kotlin 看看如何编写它们。慢慢地往你的代码中集成 Kotlin,这样可以让你对这个语言有一个测试,然后允许你保持应用的代码不被改动。

幸运的是集成 Kotlin 到现有的安卓应用里面是十分容易的,只需要使用免费的 Android Studio 的 Kotlin 插件。一开始,你需要安装插件。打开 Android Studio 然后选择 Configure > Plugins。如果你的屏幕下方看不见了,关闭你所有的项目然后 Welcome to Android Studio 窗口会自动出现。

Configure Plugin

然后选择 Install JetBrains Plugin 如下。

Install Plugin

现在搜索 Kotlin 然后按如下步骤安装 Kotlin 插件。Kotlin 主插件就会出现在你的安卓扩展里面。

你现在已经可以开始创建你的第一个用 Kotlin 实现的功能了!

应用

我打算创建一个简单的 to-do 列表的应用。主屏幕有一个 to-do 的待办事项如下:

Example Screenshot

用户通过点击 FAB (Floating Action Button) 来增加一个 to-do。编辑 to-do, 你需要点击 to-do 本身。这会加载一个编辑界面。编辑界面是我用 Kotlin 和 Anko 来编写的。

什么是 Anko?

Anko 是一个 DSL (Domain-Specific Language), 它是用 Kotlin 写的安卓插件。长久以来,安卓视图都是用 XML 来表述布局的。这个 XML 常常在你的应用里面有多个复制的版本,而且不能重用(有时候能,通过 includes)。在运行的时候,XML 被转换成 Java 表述,这很浪费 CPU 和电池。Anko 允许你能用 Kotlin 来编写视图,在任何的 Activity 或者 Fragment 里(或者一个 [AnkoComponent] (https://github.com/Kotlin/anko#ankocomponent) 里,这是一个表述视图的扩展 Kotlin 文件)。

Receive news and updates from Realm straight to your inbox

这里是一个转换成 Anko 的简单 XML 文件。

XML

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent">

    <EditText
        android:id="@+id/todo_title"
        android:layout_width="match_parent"
        android:layout_heigh="wrap_content"
        android:hint="@string/title_hint" />

    <!-- Cannot directly add an inline click listener as onClick delegates implementation to the activity -->
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/add_todo" />

</LinearLayout>

用 Anko 描述的同样的视图

verticalLayout {
    var title = editText {
        id = R.id.todo_title
        hintResource = R.string.title_hint
    }
    button {
        textResource = R.string.add_todo
        onClick { view -> {
                // do something here
                title.text = "Foo"
            }
        }
    }
}

注意上面第一个的布局中的点击监听函数。因为这是 Kotlin,你可以访问其他的视图成员,比如 title 然后在点击监听函数里面使用它。

上手

使用这个 初学者的应用 你可以以一个空白的面板开始。(最终的代码在 这里)。 这个应用有以下组件:

  • 一个 Activity (MainActivity) 作为应用的简单控制器。
  • 一个 RecyclerView 来展示第一屏上的 to-dos(TodosFragment)
  • 一个 Realm 数据库来存储 to-dos
  • 一个 Todo.java 表述 Realm 模型
  • 一个 RecyclerView 的 adapter

你现在用 Kotlin 和 Anko 来创建编���屏。

创建 Kotlin 和 Anko 的应用

现在你已经安装了 Kotlin 扩展,你想用 Configure Kotlin in Project 来配置你的应用。在 Android Studio 里面,按下 CMD+SHIFT+A 来打开 action 查找对话框。输入 Kotlin 然后选择 Configure Kotlin in Project 如下:

Actions

在这之后,你的 build.gradle 文件会变成 kotlin-android,并且作为最上层的配置文件,一个 Kotlin sourceSet 被添加了,然后 Kotlin 被加做了你的依赖。

然后你也想把 Anko 加做依赖。你的 build.gradle 文件看起来像这样:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.donnfelker.kotlinmix"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
        }
    }
    packagingOptions {
        exclude 'META-INF/services/javax.annotation.processing.Processor'
    }
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'

    // Kotlin
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

    // Anko
    compile 'org.jetbrains.anko:anko-sdk15:0.8.2' // sdk19, sdk21, sdk23 are also available
    compile 'org.jetbrains.anko:anko-support-v4:0.8.2' // In case you need support-v4 bindings
    compile 'org.jetbrains.anko:anko-appcompat-v7:0.8.2' // For appcompat-v7 bindings

    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:design:23.1.1'
    compile 'io.realm:realm-android:0.87.1'
    compile 'com.github.thorbenprimke:realm-recyclerview:0.9.12'
    compile 'com.jakewharton:butterknife:7.0.1'
    compile 'com.android.support:support-v4:23.1.1'
}
buildscript {
    ext.kotlin_version = '1.0.0'
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
repositories {
    mavenCentral()
}

现在你可以增加编辑屏幕了。

用 Kotlin 增加一个 Fragment

如果 src/main/kotlin/com.donnfelker.kotlinmix/ 目录不存在,创建它。你会注意到 kotlin 文件夹变蓝了,这意味着它是一个源文件目录了。

右键点击 /src/main/kotlin/com.donnfelker.kotlinmix/ 目录,选择 New > Kotlin File/Class,然后重命名为 EditFragment。新文件被创建了,并且只包含包声明。

拷贝下面的代码到 EditFragment 文件里。

package com.donnfelker.kotlinmix

import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import com.donnfelker.kotlinmix.models.Todo
import io.realm.Realm
import org.jetbrains.anko.*
import org.jetbrains.anko.support.v4.UI
import org.jetbrains.anko.support.v4.find
import java.util.*

class EditFragment : Fragment() {

    val TODO_ID_KEY: String = "todo_id_key"

    val realm: Realm = Realm.getDefaultInstance()

    var todo: Todo? = null

    companion object {
        fun newInstance(id: String): EditFragment {
            var args: Bundle = Bundle()
            args.putString("todo_id_key", id)
            var editFragment: EditFragment = newInstance()
            editFragment.arguments = args
            return editFragment
        }

        fun newInstance(): EditFragment {
            return EditFragment()
        }
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        if(arguments != null && arguments.containsKey(TODO_ID_KEY)) {
            val todoId = arguments.getString(TODO_ID_KEY)
            todo = realm.where(Todo::class.java).equalTo("id", todoId).findFirst()
            val todoTitle = find<EditText>(R.id.todo_title)
            todoTitle.setText(todo?.title)
            val todoDesc = find<EditText>(R.id.todo_desc)
            todoDesc.setText(todo?.description)
            val add = find<Button>(R.id.todo_add)
            add.setText(R.string.save)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        realm.close()
    }

    /**
     *  A private function to create a TODO item in the database (Realm).
     *
     *  @param title the title edit text.
     *  @param desc the description edit text.
     */
    private fun createTodoFrom(title: EditText, desc: EditText) {
        realm.beginTransaction()

        // Either update the edited object or create a new one.
        var t = todo?: realm.createObject(Todo::class.java)
        t.id = todo?.id?: UUID.randomUUID().toString()
        t.title = title.text.toString()
        t.description = desc.text.toString()
        realm.commitTransaction()

        // Go back to previous activity
        activity.supportFragmentManager.popBackStack();
    }

}

上面的例子有一些方法: newInstanceonActivityCreatedonDestroy,和 createTodoFromcreateTodoFrom 接收两个 EditText 组件作为参数,或者用作创建新的 Todo 或者用作更新一个已存在的条目,所有的事情用一行代码搞定。

var t = todo?: realm.createObject(Todo::class.java)

这会检查看看当前的 todo 值是不是为 null。如果是的,它会创建一个新的 Todo 实例。如果不为 null,它会使用一个本地的作用域的实例。这个本地作用域的实例会在文件开始的 onActivityCreated 方法中实例化。

onActivityCreated 里面,fragment 的参数会被检查。如果他们不是 null,Todo 的 id 会从 intent extras 中解析出来,然后从 Realm 中得到 Todo 对象。todo 条目已经实例化了,标示着 Todo 对象可以被编辑。这时候,视图会被相应的值更新。

用 Anko 增加视图

你现在可能意识到我们的 Fragment 里面还没有视图。为了增加一个视图,拷贝粘贴下面的代码到 fragment:

override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return UI {
        verticalLayout {
            padding = dip(30)
            var title = editText {
                id = R.id.todo_title
                hintResource = R.string.title_hint
            }

            var desc = editText {
                id = R.id.todo_desc
                hintResource = R.string.description_hint
            }
            button {
                id = R.id.todo_add
                textResource = R.string.add_todo
                onClick { view -> createTodoFrom(title, desc) }
            }
        }
    }.view
}

这里的 Anko 代码创建了一个垂直方向的线性布局(verticalLayout)。在 verticalLayout 代码段内部,创建了三个安卓的控件 - 两个 editText 视图和一个 button 视图。这里视图的属性都在一行里面设置好了。按钮控件的定义有些有意思的地方。按钮有一个点击监听函数是定义在视图定义文件里面的。在定义按钮之前,有两个参数 titledesc 的方法 createTodoFrom 已经被调用了。最后,通过在 AnkoContext (UI 类)上调用 view 属性来返回视图。

这里的 ids 被设置为 R.id.<id_name>。这些 ids 需要手工在一个加做 ids.xml 的文件里创建,这个文件放在 app/src/main/res/values/ids.xml。如果这个文件不存在就创建它。文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="todo_title" type="id" />
    <item name="todo_desc" type="id" />
    <item name="todo_add" type="id" />
</resources>

这个 ids.xml 文件定义了所有能够被安卓应用引用到的各种视图的 ids。

Java 和 Kotlin 编译

现在,视图能在屏幕上显示了。唯一剩下的事情就是在用户点击一个条目的时候显示 Fragment。

打开 TodosFragment 然后在 onTodoClick 方法中增加下面的代码:

EditFragment editFragment = EditFragment.Companion.newInstance(task.getId());
        getActivity().getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.content_main, editFragment, editFragment.getClass().getSimpleName())
                .addToBackStack(editFragment.getClass().getSimpleName())
                .commit();

这个 EditFragment 是使用纯 Kotlin 写的,你可以把它当作一个正常的 Java 对象在安卓代码的任何地方非常容易地调用它。

注意 EditFragment.Companion.newInstance 调用?这是必须的,因为 Kotlin 没有静态方法。所以,一个 伴随对象 是完成 Kotlin 中相似功能的必须品。

最后,你需要连接 FAB 来启动 fragment。在 FAB’s 的点击监听函数里面,在 MainActivity 里面,你需要增加下面的代码:

EditFragment editFragment = EditFragment.Companion.newInstance();
getSupportFragmentManager()
    .beginTransaction()
    .replace(R.id.content_main, editFragment, editFragment.getClass().getSimpleName())
    .addToBackStack(editFragment.getClass().getSimpleName())
    .commit();

编译和安装你的应用,然后点击 FAB。这会启动你的应用中的 Kotlin 部分。添加一个 to-do 然后点击 Add。退回到 to-dos 的列表,点击一个 to-do 然后你可以编辑它。在 Kotlin 里面的按钮文字 EditFagment 会变成 ‘save’。更新 to-do 然后点击 save。

Example

恭喜你,你现在有混合的 Java 和 Kotlin 了!👏

你现在用 Kotlin 创建了一个功能,而你剩下的应用依旧是用安卓中典型的 Java 来工作的。你可以继续在你的 Kotlin 开发的道路上前行或者在你需要它的时候再使用它。

你现在能够使用 Anko 作为你的 Kotlin 的视图机制了。如果你更喜欢 XML,你可以继续使用 XML 布局。例如,你可以把上面的 onCreateView 方法用下面的内容替换:

override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return inflater?.inflate(R.layout.your_layout, container, false)
}

这给你提供了使用 Kotlin 或者 Anko 的灵活性。

祝你的 Kotlin 旅程好运!

例子代码


Donn Felker

Donn Felker is the co-host of the Fragmented Podcast and the founder of Caster.IO, a bite-sized Android developer video training site. He’s the curator of the #AndroidDev Digest, a best selling mobile author, international speaker, and consultant on all things Android. He also loves Chipotle.