《Kotin 极简教程》第13章 使用 Kotlin 和 Anko 的Android 开发

第13章 使用 Kotlin 和 Anko 的Android 开发


《Kotlin极简教程》正式上架:

点击这里 > 去京东商城购买阅读

点击这里 > 去天猫商城购买阅读

非常感谢您亲爱的读者,大家请多支持!!!有任何问题,欢迎随时与我交流~


13.1 什么是 Anko?

Anko (https://github.com/Kotlin/anko) 是一个用 Kotlin 写的Android DSL (Domain-Specific Language)。长久以来,Android视图都是用 XML 来完成布局的。这些 XML可重用性比较差。同时在运行的时候,XML 要转换成 Java 表述,这在一定程度上占用了 CPU 和耗费了电量。

Anko是一个 Kotlin 库, 它使 android 应用程序的开发变得更快、更容易。它使您的代码更加简单干净, 易于阅读。

Anko由几个部分组成:

模块 功能说明
Anko Commons 使得对 intents, dialogs, logging等操作更加简单的轻量级库
Anko Layouts 快速和类型安全的动态的 android 布局库
Anko SQLite 用于 android sqlite 的查询 dsl 和分析库
Anko Coroutines 基于 kotlinx 协程库

有了Anko 我们就能直接用 Kotlin 在任何的 Activity 、 Fragment 或者 AnkoComponent里来编写视图。

13.2 一个简单Anko视图

这里是一个转换成 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" />
    <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 -> {
                // 可以在这里添加一些处理逻辑
                title.text = "Foo"
            }
        }
    }
}

可以看到在button布局中的onClick监听函数中,因为我们是使用 Kotlin代码来设计视图,所以可以直接使用title变量(editText视图对象)。

13.3 快速入门实例

下面我们通过一个“我的日程”待办事项应用,来详细介绍使用 Kotlin 混合 Java,使用 Anko 开发的Android 应用的方法。移动端数据库引擎我们使用 Realm,视图绑定使用Butter Knife。

这个应用程序界面如下所示:

Screenshot_1500661334.png
Screenshot_1500661325.png
Screenshot_1500661320.png

13.4 使用 Android Studio 新建工程

我们首先在 Android Studio 中新建工程,步骤如下:

第一步,新建项目

螢幕快照 2017-07-21 00.03.49.png

第二步,配置项目基本信息

螢幕快照 2017-07-20 23.38.01.png

第三步,设置支持设备以及 SDK 版本

螢幕快照 2017-07-20 23.38.13.png

第四步,选择 Basic Activity

螢幕快照 2017-07-20 23.38.29.png

第五步,使用默认的Activity命名

螢幕快照 2017-07-20 23.39.04.png

我们将得到一个标准的 Gradle Android 工程:

螢幕快照 2017-07-21 00.08.01.png

其中,app 工程 src 目录如下:

.
├── androidTest
│   └── java
│       └── com
│           └── easy
│               └── kotlin
│                   └── mytodoapplication
│                       └── ExampleInstrumentedTest.java
├── main
│   ├── AndroidManifest.xml
│   ├── java
│   │   └── com
│   │       └── easy
│   │           └── kotlin
│   │               └── mytodoapplication
│   │                   └── MainActivity.java
│   └── res
│       ├── drawable
│       ├── layout
│       │   ├── activity_main.xml
│       │   └── content_main.xml
│       ├── menu
│       │   └── menu_main.xml
│       ├── mipmap-hdpi
│       │   ├── ic_launcher.png
│       │   └── ic_launcher_round.png
│       ├── mipmap-mdpi
│       │   ├── ic_launcher.png
│       │   └── ic_launcher_round.png
│       ├── mipmap-xhdpi
│       │   ├── ic_launcher.png
│       │   └── ic_launcher_round.png
│       ├── mipmap-xxhdpi
│       │   ├── ic_launcher.png
│       │   └── ic_launcher_round.png
│       ├── mipmap-xxxhdpi
│       │   ├── ic_launcher.png
│       │   └── ic_launcher_round.png
│       └── values
│           ├── colors.xml
│           ├── dimens.xml
│           ├── strings.xml
│           └── styles.xml
└── test
    └── java
        └── com
            └── easy
                └── kotlin
                    └── mytodoapplication
                        └── ExampleUnitTest.java

28 directories, 21 files

我们直接在Android 模拟器中(也可以选择用真机)运行它,可以看到如下效果:

Screenshot_1500567437.png

13.5 设计UI 界面主题颜色

我们首先把应用名称改成“我的日程”。在文件MyTodoApplication/app/src/main/res/values/strings.xml中:

<resources>
    <string name="app_name">MyTodoApplication</string>
    <string name="action_settings">Settings</string>
</resources>

改写成

<resources>
    <string name="app_name">我的日程</string>
    <string name="action_settings">设置</string>
</resources>

再去colors.xml中,设计主题颜色为:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#f2fced</color>
    <color name="colorPrimaryDark">#456a7c</color>
    <color name="colorAccent">#8fb3c4</color>
</resources>

然后到文件MyTodoApplication/app/src/main/res/layout/activity_main.xml中,设置android.support.v7.widget.Toolbar的背景色为

android:background="?attr/colorPrimaryDark"

配置android.support.design.widget.FloatingActionButton的图标为:

app:srcCompat="drawable/ic_content_add"

其中,ic_content_add.png图片是我们添加按钮中间的加号 icon。

13.6 配置 Kotlin 与 Anko 依赖

我们默认生成的 app 项目的 Gradle 配置文件build.gradle如下:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "com.easy.kotlin.mytodoapplication"
        minSdkVersion 21
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.android.support:design:25.3.1'
    testCompile 'junit:junit:4.12'
}

下面我们在 app 项目的build.gradle里面加上Kotlin 、Anko 、Realm、Butter Knife 等依赖。

13.6.1 Kotlin依赖

首先,启用插件kotlin-android :

apply plugin: 'kotlin-android'

然后,添加构建脚本

buildscript {

}

我们使用 Kotlin 1.1.3版本。在构建脚本中添加kotlin-gradle-plugin依赖,使用 Kotlin 对应的版本号。

buildscript {
    ext.kotlin_version = '1.1.3'
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

在项目依赖里添加 Kotlin 标准库:

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

13.6.2 添加 Kotlin 源代码目录

首先,我们在 src/main/下面新建一个 kotlin 目录,来存放 Kotlin源码。然后在 build.gradle 文件里的 android {} 配置里面添加Java的编译路径:

android {
    ...
    sourceSets {
        // += , 在main中创建kotlin文件夹, 用于存放kotlin代码
        main.java.srcDirs += 'src/main/kotlin'
    }
}

刚添加完毕,src/main/kotlin 还没有变成源码目录的蓝色,这个时候点击下图右上角的 Sync Now :

螢幕快照 2017-07-21 15.27.57.png

Gradle 同步完毕,即可看到kotlin 目录已经变成蓝色的源码目录了:

螢幕快照 2017-07-21 16.01.41.png

13.6.3 Anko依赖

在项目依赖里添加

    // 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

13.6.4 Realm依赖

    compile 'io.realm:realm-android:0.87.1'
    compile 'com.github.thorbenprimke:realm-recyclerview:0.9.12' // 在jitpack.io上

其中,Realm是一个轻量级的跨平台移动数据库引。Realm 简单易用,model 设计在代码中,更加易于维护,同时其性能也不错。在Android开发中,它可以替代 SQLite 和 ORM 框架。相比SQLite,Realm更快并且具有很多现代数据库的特性,比如支持JSON,流式api,数据变更通知,以及加密支持。

RecyclerView用于在有限的窗口展现大量的数据,相比ListView、GridView,RecyclerView标准化了ViewHolder,而且更加灵活,可以轻松实现ListView实现不了的样式和功能。我们使用的com.github.thorbenprimke:realm-recyclerview 依赖包在在jitpack.io上, 所以我们还需要配置一下仓库地址:

repositories {
    mavenCentral()
    maven { url "https://jitpack.io" }
}

提示:realm-recyclerview的 Github 地址是 https://github.com/thorbenprimke/realm-recyclerview

另外, Kotlin使用 Realm 还要加上注解处理的依赖库:

    // kotlin使用realm的注解处理依赖库
    kapt "io.realm:realm-annotations:0.87.1"
    kapt "io.realm:realm-annotations-processor:0.87.1"

13.6.5 Butter Knife依赖

Butter Knife是基于注解处理方式工作:通过对代码注解自动生成模板代码。我们添加其依赖如下:

    // Butter Knife,专门为Android View设计的绑定注解,专业解决各种findViewById
    compile 'com.jakewharton:butterknife:8.7.0'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.7.0'

Butter Knife主要是用来做Android视图的成员变量和属性的数据绑定。在开发过程中,我们通常要写大量的findViewById和点击事件,像初始view、设置view监听这样简单而重复的操作会显得比较繁琐。而我们有了 Butter Knife,就可以通过使用注解直接生成样板代码。例如,在 Java 中我们可以通过在字段上使用 @BindView 来替代 findViewById 的调用。上面的配置中的annotationProcessor 'com.jakewharton:butterknife-compiler:8.7.0'就是来处理这些注解从而生成样板代码的。

@Bind(R.id.todo_item_todo_title)
public TextView todoTitle;

@Bind(R.id.todo_item_todo_content)
public TextView todoContent;

而在 Kotlin 中使用Butter Knife情况有些不同,需要作额外的配置。

如果在Kotlin中直接使用ButterKnife的注解方式的话,会出现空指针的异常,导致绑定失败。例如

@Bind(R.id.todos_recycler_view)
var realmRecyclerView: RealmRecyclerView? = null

运行会报错:

Caused by: kotlin.KotlinNullPointerException 
at com.easy.kotlin.mytodoapplication.TodoListFragment.onResume(TodoListFragment.kt:43)

一般情况下,我们使用Kotlin集成 Java 生态的一些框架的时候,像 Spring Boot,JPA,Butter Knife,Realm等,都需要一些额外的插件或者依赖来“填充缝隙”(例如:all-open, kotterknife,realm-annotations等), 所谓Kotlin 与 Java 的无缝集成,很多时候并非Java 中怎么用,Kotlin就直接拿过来就怎么用,往往是要再添加一些插件或者额外的配置等。

那么要如何才能在Kotlin的环境中使用ButterKnife呢?

在早些时候,ButterKnife的作者已经帮我们想好解决方案了,那就是——KotterKnife,见名知意。KotterKnife的GitHub地址是:https://github.com/JakeWharton/kotterknife 。这个插件是建立在ButterKnife 7的基础上的。

下面我们配置一下在 Kotlin 中使用 Butter Knife 的依赖库 KotterKnife。

首先在repositories中添加KotterKnife的仓库地址(KotterKnife不在 Maven Center 仓库中,而是在oss.sonatype.org仓库中。这么多仓库,要是哪天能统一用一个就方便多了)。

repositories {
    ...
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}

然后在dependencies里面添加依赖

dependencies {
    ...
    compile 'com.jakewharton:butterknife:7.0.1'
    compile 'com.jakewharton:kotterknife:0.1.0-SNAPSHOT'
}

采用这种方式的配置,我们的视图注入代码如下

val todoTitle: TextView by bindView(R.id.todo_item_todo_title)
val todoContent: TextView by bindView(R.id.todo_item_todo_content)

这样的代码看起来不是那么的优雅,还没有在 Java 中直接使用注解来的简单好看。同时要注意的是,如果使用 kotterknife 0.1.0 + butterknife:7.0.1 ,同时使用 Java 跟 Kotlin 混合编程的场景中使用 Butter Knife,发现配了KotterKnife 之后的 Java 的注解式写法就失效了。也就是说,如果我们上面添加了KotterKnife的依赖,那么 Java 代码中同时使用 Butter Knife 注解的地方会绑定失败。不过这个问题,在后面的新版本中已经解决。例如在butterknife 8.7.0中,我们可以直接添加下面的依赖项:

    compile 'com.jakewharton:butterknife:8.7.0'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.7.0'
    kapt 'com.jakewharton:butterknife-compiler:8.7.0'

其中,annotationProcessor 'com.jakewharton:butterknife-compiler:8.7.0' 是 Java 的butterknife注解处理器。kapt 'com.jakewharton:butterknife-compiler:8.7.0' 是 Kotlin 的butterknife注解处理器(Kotlin Annotation processing tool,kapt)。

这样我们的代码就继续优雅简洁下去了:

@BindView(R.id.todo_item_todo_title)
lateinit var todoTitle: TextView
@BindView(R.id.todo_item_todo_content)
lateinit var todoContent: TextView

其中,lateinit 修饰符允许声明非空类型,并在对象创建后(构造函数调用后)初始化。 不使用 lateinit 则需要声明可空类型并且有额外的空安全检测操作。

当然,我们使用 Butter Knife 的同时,仍然可以使用原生的 findViewById :

class MainActivity : AppCompatActivity() {
    var fab: FloatingActionButton? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val toolbar = findViewById(R.id.toolbar) as Toolbar
        setSupportActionBar(toolbar)

        fab = findViewById(R.id.fab) as FloatingActionButton


        // 添加日程事件
        fab?.setOnClickListener { _ ->
            ...
            hideFab()
        }
    }

    fun hideFab() {
        fab?.visibility = View.GONE
    }

    fun showFab() {
        fab?.visibility = View.VISIBLE
    }

}

13.7 将MainActivity.java 转成 Kotlin 代码

选中默认生成的MainActivity.java, 我们使用 IDEA 的 Code > Convert Java File to Kotlin File :

螢幕快照 2017-07-21 16.19.35.png

点击转换,即可看到转换成 Kotlin 的代码:

package com.easy.kotlin.mytodoapplication

import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val toolbar = findViewById(R.id.toolbar) as Toolbar
        setSupportActionBar(toolbar)

        val fab = findViewById(R.id.fab) as FloatingActionButton
        fab.setOnClickListener { view ->
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        val id = item.itemId


        if (id == R.id.action_settings) {
            return true
        }

        return super.onOptionsItemSelected(item)
    }
}

看,这就是Android 开发者,从 Java无缝转到 Kotlin 的过程。

我们把这个MainActivity.kt放到对应的 src/main/kotlin 目录下。首先新建package com.easy.kotlin.mytodoapplication , 直接在 IDEA 中把这个MainActivity.kt 拖到这个package 下面即可。现在我们的工程目录是下面这个样子

螢幕快照 2017-07-21 16.26.14.png

13.8 在 Kotlin 中使用 Realm

我们需要添加针对 Kotlin 的realm注解处理的库:

    kapt "io.realm:realm-annotations:0.87.1"
    kapt "io.realm:realm-annotations-processor:0.87.1"

13.9 添加日程实体类

我们先从领域模型的建立开始。首先我们需要设计一个极简的待办事项的实体类 Todo, 它有主键 id、标题、内容三个字段。

@RealmClass
open class Todo : RealmObject() {
    @PrimaryKey
    open var id: String = "-1"
    open var title: String = "日程"
    open var content: String = "事项"
}

然后,我们写一个应用程序入口类MyTodoApplication继承android.app.Application, 在 onCreate() 里面初始化 Realm 数据库的配置。代码如下:

class MyTodoApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        val config = RealmConfiguration.Builder(this)
                .name("realm.my_todos")// 库文件名
                .encryptionKey(getKey())  // 加密
                .schemaVersion(1)  // 版本号
                .deleteRealmIfMigrationNeeded()
                .build()

        Realm.setDefaultConfiguration(config)// 设置默认 RealmConfiguration
        
    }

    /**
     * 64 bits
     * @return
     */
    private fun getKey(): ByteArray {
        return byteArrayOf(0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1)
    }
}

RealmConfiguration.Builder里面如果没有deleteRealmIfMigrationNeeded()的话,会如下报错误:

Caused by: io.realm.exceptions.RealmMigrationNeededException: 
RealmMigration must be provided ...
at com.easy.kotlin.mytodoapplication.TodoListFragment.onActivityCreated(TodoListFragment.kt:36)

提示: 更多关于 realm 数据库的相关内容可参考 https://realm.io/docs/

13.10 添加日程事件

现在我们点击添加日程的浮层按钮中,添加切换到 “日程添加编辑” TodoEditFragment的逻辑。

// 添加日程事件
fab?.setOnClickListener { _ ->
    // Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show()
    val todoEditFragment = TodoEditFragment()
    getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.content_main, todoEditFragment, todoEditFragment.javaClass.getSimpleName())
            .addToBackStack(todoEditFragment.javaClass.getSimpleName())
            .commit()

    hideFab()
}

13.11 添加日程界面

下面我们来完成这个添加日程的界面。

Screenshot_1500666104.png

我们采用Fragment来实现。首先新建一个TodoEditFragment继承Fragment() :

class TodoEditFragment : Fragment() {
    val realm: Realm = Realm.getDefaultInstance()
    var todo: Todo? = null

    companion object {
        val TODO_ID_KEY: String = "todo_id_key"

        fun newInstance(id: String): TodoEditFragment {
            var args: Bundle = Bundle()
            args.putString(TODO_ID_KEY, id)
            var todoEditFragment: TodoEditFragment = newInstance()
            todoEditFragment.arguments = args
            return todoEditFragment
        }

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

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return UI {
            // AnkoContext

            verticalLayout {
                padding = dip(30)
                var title = editText {
                    // editText 视图
                    id = R.id.todo_title
                    hintResource = R.string.title_hint
                }

                var content = editText {
                    id = R.id.todo_content
                    height = 400
                    hintResource = R.string.content_hint
                }
                button {
                    // button 视图
                    id = R.id.todo_add
                    textResource = R.string.add_todo
                    textColor = Color.WHITE
                    setBackgroundColor(Color.DKGRAY)
                    onClick { _ -> createTodoFrom(title, content) }
                }
            }
        }.view
    }

    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 todoContent = find<EditText>(R.id.todo_content)
            todoContent.setText(todo?.content)

            val add = find<Button>(R.id.todo_add)
            add.setText(R.string.save)
        }
    }

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


    /**
     *  新增待办事项,存入Realm数据库
     *
     *  @param title the title edit text.
     *  @param todoContent the content edit text.
     */
    private fun createTodoFrom(title: EditText, todoContent: 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.content = todoContent.text.toString()

        realm.commitTransaction()
        activity.supportFragmentManager.popBackStack()
    }


}

其中,我们重点讲下 Anko 的 UI 布局部分的代码。

return UI {
    // AnkoContext

    verticalLayout {
        padding = dip(30)
        var title = editText {
            // editText 视图
            id = R.id.todo_title
            hintResource = R.string.title_hint
        }

        var content = editText {
            id = R.id.todo_content
            height = 400
            hintResource = R.string.content_hint
        }
        button {
            // button 视图
            id = R.id.todo_add
            textResource = R.string.add_todo
            textColor = Color.WHITE
            setBackgroundColor(Color.DKGRAY)
            onClick { _ -> createTodoFrom(title, content) }
        }
    }
}.view

我们使用 Kotlin 的代码 Anko DSL 创建了一个垂直方向的线性布局(用代码写配置写布局要比 XML 灵活方便多了)。 其中 UI 函数

fun Fragment.UI(init: AnkoContext<Fragment>.() -> Unit) = createAnkoContext(activity, init)

是Fragment的扩展函数,它接收一个函数

init: AnkoContext<Fragment>.() -> Unit

init 的入参是AnkoContext类型。

而verticalLayout函数则是ViewManager的内联扩展函数。

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

从这些例子我们可以看出 Kotlin 的函数扩展功能相当实用,尤其在 DSL 中用的非常广泛。

在 verticalLayout 代码段内部,创建了三个Android的控件 - 两个 editText 视图和一个 button 视图。这里视图的属性都在一行里面设置好了。

padding = dip(30)
var title = editText {
    // editText 视图
    id = R.id.todo_title
    hintResource = R.string.title_hint
}

var content = editText {
    id = R.id.todo_content
    height = 400
    hintResource = R.string.content_hint
}
button {
    // button 视图
    id = R.id.todo_add
    textResource = R.string.add_todo
    textColor = Color.WHITE
    setBackgroundColor(Color.DKGRAY)
    onClick { _ -> createTodoFrom(title, content) }
}

这样的视图文件要比 XML 优雅许多了,XML 的配置有时候让人看了就心生烦恼。

我们可以看下按钮控件定义的地方。按钮有一个点击监听函数是定义在视图定义文件里面的。在定义按钮之前,有两个参数 title 和 content 的方法 createTodoFrom 已经被调用了。最后,通过在 AnkoContext (UI 类)上调用 view 属性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_content" type="id" />
    <item name="todo_add" type="id" />
</resources>

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

13.12 保存到 Realm 中

新增待办事项,存入Realm数据库:

    private fun createTodoFrom(title: EditText, todoContent: 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.content = todoContent.text.toString()

        realm.commitTransaction()

        activity.supportFragmentManager.popBackStack()
    }

13.13 用RecyclerView 来展示待办事项

下面我们来实现这个页面。

Screenshot_1500667248.png

首先,这个是主页面,对应 activity_main.xml 视图, 文件内容如下:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.easy.kotlin.mytodoapplication.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimaryDark"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_main" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@drawable/ic_content_add" />

</android.support.design.widget.CoordinatorLayout>

我们的待办事项列表视图是fragment_todos.xml, 文件内容如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".TodosFragment"
    tools:showIn="@layout/activity_main">

    <co.moonmonkeylabs.realmrecyclerview.RealmRecyclerView
        android:id="@+id/todos_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:rrvEmptyLayoutId="@layout/empty_view"
        app:rrvIsRefreshable="false"
        app:rrvLayoutType="LinearLayout" />

</RelativeLayout>

我们看下RealmRecyclerView的配置:

配置项 功能说明
app:rrvEmptyLayoutId 当列表为空的时候的显示页面
app:rrvIsRefreshable 是否支持下拉刷新,通过setOnRefreshListener 或 setRefreshing来进行事件处理
app:rrvLayoutType 配置LayoutManager,可选项是:LinearLayout,Grid,LinearLayoutWithHeaders等

下面我们来实现这个TodosFragment 。

首先新建TodosFragment类,继承如下面代码所示:

class TodosFragment : Fragment(), TodoAdapter.TodoItemClickListener {
    @BindView(R.id.todos_recycler_view)
    lateinit var realmRecyclerView: RealmRecyclerView

    private var realm: Realm? = null
    ...
}

其中,TodoAdapter是继承了RealmBasedRecyclerViewAdapter的适配器类。我们在 TodoAdapter 里面定义了一个视图持有类:

    inner class ViewHolder(view: View, private val clickListener: TodoItemClickListener?) :
            RealmViewHolder(view), View.OnClickListener {

        // Bind a field to the view for the specified ID. The view will automatically be cast to the field type
        @BindView(R.id.todo_item_todo_title)
        lateinit var todoTitle: TextView
        // val todoTitle: TextView by bindView(R.id.todo_item_todo_title)
        @BindView(R.id.todo_item_todo_content)
        lateinit var todoContent: TextView
        // val todoContent: TextView by bindView(R.id.todo_item_todo_content)

        init {
            // Bind annotated fields and methods
            ButterKnife.bind(this, view)
            view.setOnClickListener(this)
        }

        override fun onClick(v: View) {
            clickListener?.onClick(v, realmResults[adapterPosition])
        }
    }

在ViewHolder初始化 View 的时候,我们使用ButterKnife进行了绑定

init {
            // Bind annotated fields and methods
            ButterKnife.bind(this, view)
            view.setOnClickListener(this)
        }

待办事项监听器类:

    interface TodoItemClickListener {
        fun onClick(caller: View, todo: Todo)
    }

我们在TodosFragment中实现这个方法:

    override fun onClick(caller: View, todo: Todo) {
        (activity as MainActivity).hideFab()

        val todoEditFragment = TodoEditFragment.newInstance(todo.id)
        activity.supportFragmentManager
                .beginTransaction()
                .replace(R.id.content_main, todoEditFragment, todoEditFragment.javaClass.getSimpleName())
                .addToBackStack(todoEditFragment.javaClass.getSimpleName())
                .commit()
    }

点击待办事项,把当前的content_main切换成编辑事项 EditFragment的视图。

然后我们在TodoAdapter中重写RealmBasedRecyclerViewAdapter的onCreateRealmViewHolder和onBindRealmViewHolder方法。

    override fun onCreateRealmViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        val v = inflater.inflate(R.layout.todo_item, viewGroup, false)
        return ViewHolder(v, clickListener)
    }

    override fun onBindRealmViewHolder(viewHolder: ViewHolder, position: Int) {
        val todo = realmResults[position]

        viewHolder.todoTitle.setText(todo.title)
        viewHolder.todoTitle.fontFeatureSettings = "font-size:12px"
        viewHolder.todoTitle.setTextColor(Color.argb(255, 69, 106, 124))

        viewHolder.todoContent.setText(todo.content)
    }

我们在添加(保存)完事项的时候,回到之前的列表页面:

private fun createTodoFrom(title: EditText, todoContent: 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.content = todoContent.text.toString()

        realm.commitTransaction()

        activity.supportFragmentManager.popBackStack()
    }

当回退到待办事项列表的时候,我们在TodosFragment中的 onResume() 函数中来实现数据的更新展示:

override fun onResume() {
        super.onResume()
        val todos = realm!!.where(Todo::class.java).findAll()
        Log.i(MY_TAG, "onResume: ${todos}")
        Log.i(MY_TAG, "onResume: realmRecyclerView = ${realmRecyclerView} ")
        val adapter = TodoAdapter(activity, todos, true, true, this)
        realmRecyclerView.setAdapter(adapter)
    }

其中,val todos = realm!!.where(Todo::class.java).findAll() 是去 Realm 数据库中查询出所有Todo对应的实体记录。

然后,通过适配器val adapter = TodoAdapter(activity, todos, true, true, this)把数据装配到RecyclerView中 realmRecyclerView.setAdapter(adapter)

13.14 运行测试

编译安装应用,我们就可以看到如下的界面了,我们可以在里面添加编辑我们的待办事项。

Screenshot_1500669269.png
Screenshot_1500669264.png
Screenshot_1500669255.png

本章小结

Android 中经常出现的空引用、API的冗余样板式代码等都是是驱动我们转向 Kotlin 语言的动力。另外,Kotlin 的 Android 视图 DSL Anko帮我们从繁杂的 XML 视图配置文件中解放出来。我们可以像在 Java 中一样方便的使用 Android 开发的流行的库诸如 Butter Knife、Realm、RecyclerView等。当然,我们使用 Kotlin 集成这些库来进行 Andorid 开发,既能够直接使用我们之前的开发库,又能够从 Java 语言、Android API 的限制中出来。这不得不说是一件好事。

下一章我们介绍使用 Kotlin 创建 DSL。

本章工程源码:

https://github.com/EasyKotlin/chapter13_kotlin_android


Kotlin 开发者社区

国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容