《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)。

第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

相关文章
|
29天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
53 19
|
29天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
58 14
|
30天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
在数字时代,掌握安卓应用开发技能是进入IT行业的关键。本文将引导读者从零基础开始,逐步深入安卓开发的世界,通过实际案例和代码示例,展示如何构建自己的第一个安卓应用。我们将探讨基本概念、开发工具设置、用户界面设计、数据处理以及发布应用的全过程。无论你是编程新手还是有一定基础的开发者,这篇文章都将为你提供宝贵的知识和技能,帮助你在安卓开发的道路上迈出坚实的步伐。
33 5
|
29天前
|
开发框架 Android开发 iOS开发
安卓与iOS开发中的跨平台策略:一次编码,多平台部署
在移动应用开发的广阔天地中,安卓和iOS两大阵营各占一方。随着技术的发展,跨平台开发框架应运而生,它们承诺着“一次编码,到处运行”的便捷。本文将深入探讨跨平台开发的现状、挑战以及未来趋势,同时通过代码示例揭示跨平台工具的实际运用。
|
30天前
|
XML 搜索推荐 前端开发
安卓开发中的自定义视图:打造个性化UI组件
在安卓应用开发中,自定义视图是一种强大的工具,它允许开发者创造独一无二的用户界面元素,从而提升应用的外观和用户体验。本文将通过一个简单的自定义视图示例,引导你了解如何在安卓项目中实现自定义组件,并探讨其背后的技术原理。我们将从基础的View类讲起,逐步深入到绘图、事件处理以及性能优化等方面。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。
|
30天前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
30天前
|
Java Android开发 开发者
探索安卓开发:构建你的第一个“Hello World”应用
在安卓开发的浩瀚海洋中,每个新手都渴望扬帆起航。本文将作为你的指南针,引领你通过创建一个简单的“Hello World”应用,迈出安卓开发的第一步。我们将一起搭建开发环境、了解基本概念,并编写第一行代码。就像印度圣雄甘地所说:“你必须成为你希望在世界上看到的改变。”让我们一起开始这段旅程,成为我们想要见到的开发者吧!
36 0
|
8月前
|
存储 Java 开发工具
Android开发的技术与开发流程
Android开发的技术与开发流程
414 1
|
5月前
|
安全 Android开发 Swift
安卓与iOS开发:平台差异与技术选择
【8月更文挑战第26天】 在移动应用开发的广阔天地中,安卓和iOS两大平台各占一方。本文旨在探索这两个系统在开发过程中的不同之处,并分析开发者如何根据项目需求选择合适的技术栈。通过深入浅出的对比,我们将揭示各自平台的优势与挑战,帮助开发者做出更明智的决策。
81 5
|
5月前
|
移动开发 搜索推荐 Android开发
安卓与iOS开发:一场跨平台的技术角逐
在移动开发的广阔舞台上,两大主角——安卓和iOS,持续上演着激烈的技术角逐。本文将深入浅出地探讨这两个平台的开发环境、工具和未来趋势,旨在为开发者揭示跨平台开发的秘密,同时激发读者对技术进步的思考和对未来的期待。