【译】将 Android 项目迁移到 Kotlin 语言

不久前咱们开源了 Topeka,一个 Android 小测试程序。
这个程序是用 integration testsunit tests 进行测试的, 并且自己所有是用 Java 写的。至少之前是这样的...html

圣彼得堡岸边的那个岛屿叫什么?

2017年谷歌在开发者大会上官方宣布 支持 Kotlin 编程语言。从那时起,我便开始移植 Java 代码,同时在过程当中学习 Kotlin。前端

从技术角度上来说,此次的移植并非必须的,程序自己是十分稳定的,而(此次移植)主要是为了知足个人好奇心。Topeka 成为了我学习一门新语言的媒介。java

若是你好奇的话能够直接来看 GitHub 上的源代码
目前 Kotlin 代码在一个独立的分支上,但咱们计划在将来某个时刻将其合并到主代码中。
react

这篇文章涵盖了我在迁移代码过程当中发现的一些关键点,以及 Android 开发新语言时有用的小窍门。android


看上去依旧同样ios

🔑 关键的几点

  • Kotlin 是一门有趣而强大的语言
  • 多测试才能心安
  • 平台受限的状况不多

移植到 Kotlin 的第一步

虽然不可能像 Bad Android Advice 所说的那么简单,但至少是个不错的出发点。git

第一步和第二步对于学好 Kotlin 来讲确实颇有用。github

然而第三步就要看我我的的造化了。数据库

对于 Topeka 来讲实际步骤以下:

  1. 学好 Kotlin 的基础语法
  2. 经过使用 Koan 来逐步熟悉这门语言
  3. 使用 “⌥⇧⌘K” 保证(转化后的文件)仍然能一个个经过测试
  4. 修改 Kotlin 文件使其更加符合语言习惯
  5. 重复第四步直到你和审核你代码的人都满意
  6. 完工并上交

互通性

一步步去作是很明智的作法。
Kotlin 编译为 Java 字节码后两种语言能够互相通用。并且同一个项目中两种语言能够共存,因此并不须要把所有代码都移植成为另外一种语言。
但若是你原本就想这么作,那么重复的改写就是有意义的,这样你在迁移代码时能够尽可能地维持项目的稳定性,并在此过程当中有所收获。编程

多作测试才能更加安心

搭配使用单元和集成测试的好处不少。在绝大多数状况下,这些测试是用来确保当前修改没有损坏现有的功能。

我选择在一开始使用一个不是很复杂的数据类。在整个项目中我一直在用这些类,它们的复杂性相比来讲很低。这样来看在学习新语言的过程当中这些类就成为了最理想的出发点。

在经过使用 Android Studio 自带的 Kotlin 代码转换器移植一部分代码后,我开始执行并经过测试,直到最终将测试自己也移植为 Kotlin 代码。

若是没有测试的话,我在每次改写后都须要对可能受影响的功能手动进行测试。自动化的测试在我移植代码的过程当中显得更加快捷方便。

因此,若是你尚未对你的应用进行正确测试的话,以上就是你须要这么作的又一个缘由。 👆

生成的代码并非每一次都看起来很棒!!

在完成最开始几乎自动化的移植代码以后,我开始学习 Kotlin 代码风格指南。 这使我发现还有一条很长的路要走。

整体来说,代码生成器用起来很不错。尽管有不少语言特征和风格在转换过程当中没有被使用,但翻译语言原本就是件很棘手的事,这么作可能更好一些,尤为是当这门语言所包含不少的特征或者能够经过不一样方式进行表达的时候。

若是想要了解更多有关 Kotlin 转换器的内容, Benjamin Baxter 写过一些他本身的经历:

‼️ ⁉

我发现自动转换会生成不少的 ?!!
这些符号是用来定义可为空的数值和保证其不为空值的。他们反而会致使 空指针异常
我不由想到一条很恰当的名言

“过多使用感叹号,” 他一边摇头一边说道, ”是心理不正常的表现。” — Terry Pratchett

在大部分状况下它不会成为空值,因此咱们不须要使用空值的检查。同时也不必经过构造器来直接初始全部的数值,可使用 lateinit 或者委托来代替初始的流程。

然而这些方法也不是万能的:

有时候变量会成为空值。

看来我得从新把 view 定义为可为空值。

在其余状况下你仍是得检查是否 null 存在。若是存在 supportActionBar 的话, *supportActionBar*?.setDisplayShowTitleEnabled(false) 才会执行问号之后的代码。
这意味着更少的基于 null 检查的 if 条件声明。 🔥

直接在非空数值上使用 stdlib 函数很是方便:

toolbarBack?.let {
    it.scaleX = 0f
    it.scaleY = 0f
}复制代码

大规模地使用它...


变得愈来愈符合语言习惯

由于咱们能够经过审核者的反馈不断地改写生成的代码来使其变得更加符合语言的习惯。这使代码更加简洁而且提高了可读性。以上特色能够证实 Kotlin 是门很强大的语言,

来看看我曾经遇到过的几个例子吧。

少读点儿并不必定是件坏事

咱们拿 adapter 里面的 getView 来举例:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
        if (null == convertView) {
           convertView = createView(parent);
        }
        bindView(convertView);
        return convertView;
}复制代码

Java 中的 getView

override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
    (convertView ?: createView(parent)).also { bindView(it) }复制代码

Kotlin 的 getView

这两段代码在作同一件事:

先检查 convertView 是否为 null ,而后在 createView(...) 里面建立一个新的 view ,或者返回 convertView。同时在最后调用 bindView(...).

两端代码都很清晰,不过能从八行代码减到只有两行确实让我很惊讶。

数据类很神奇 🦄

为了进一步展示 Kotlin 的精简所在,使用数据类能够轻松避免冗长的代码:

public class Player {

    private final String mFirstName;
    private final String mLastInitial;
    private final Avatar mAvatar;

    public Player(String firstName, String lastInitial, Avatar avatar) {
        mFirstName = firstName;
        mLastInitial = lastInitial;
        mAvatar = avatar;
    }

    public String getFirstName() {
        return mFirstName;
    }

    public String getLastInitial() {
        return mLastInitial;
    }

    public Avatar getAvatar() {
        return mAvatar;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Player player = (Player) o;

        if (mAvatar != player.mAvatar) {
            return false;
        }
        if (!mFirstName.equals(player.mFirstName)) {
            return false;
        }
        if (!mLastInitial.equals(player.mLastInitial)) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = mFirstName.hashCode();
        result = 31 * result + mLastInitial.hashCode();
        result = 31 * result + mAvatar.hashCode();
        return result;
    }
}复制代码

下面咱们来看怎么用 Kotlin 写这段代码:

data class Player( val firstName: String?, val lastInitial: String?, val avatar: Avatar?)复制代码

是的,在保证功能的状况下少了整整五十五行代码。这就是数据类的神奇之处

扩展功能性

下面可能就是传统 Android 开发者以为奇怪的地方了。Kotlin 容许在一个给定范围内建立你本身的 DSL。

来看看它是如何运做的

有时咱们会在 Topeka 里经过
Parcel 传递 boolean。Android 框架的 API 没法直接支持这项功能。在一开始实现这项功能的时候必须调用一个功能类函数例如ParcelableHelper.writeBoolean(parcel, value)
若是使用 Kotlin,扩展函数能够解决以前的难题:

import android.os.Parcel

/**
 * 将一个 boolean 值写入[Parcel]。
 * @param toWrite 是即将写入的值。
 */
fun Parcel.writeBoolean(toWrite: Boolean) = writeByte(if (toWrite) 1 else 0)

/**
 * 从[Parcel]中获得 boolean 值。
 */
fun Parcel.readBoolean() = 1 == this.readByte()复制代码

当写好以上代码以后,咱们能够把
parcel.writeBoolean(value)parcel.readBoolean() 当成框架的一部分直接调用。要不是由于 Android Studio 使用不一样的高亮方式区分扩展函数,很难看出它们之间的区别。

扩展函数能够提高代码的可读性。 来看看另外一个例子:在 view 的层次结构中替换 Fragment。

若是使用 Java 的话代码以下:

getSupportFragmentManager().beginTransaction()
        .replace(R.id.quiz_fragment_container, myFragment)
        .commit();复制代码

这几行代码其实写的还不错。但每次当 Fragment 被替换的时候你都要把这几行代码再写一遍,或者在其余的 Utils 类中建立一个函数。

若是使用 Kotlin,当咱们在 FragmentActivity 中须要替换 Fragment 的时候,只须要使用以下代码调用 replaceFragment(R.id.container, MyFragment()) 便可:

fun FragmentActivity.replaceFragment(@IdRes id: Int, fragment: Fragment) {
    supportFragmentManager.beginTransaction().replace(id, fragment).commit()
}复制代码

替换 Fragment 只需一行代码

少一些形式,多一点儿功能

高阶函数太令我震撼了。是的,我知道这不是什么新的概念,但对于部分传统 Android 开发者来讲多是。我以前有据说过这类函数,也见有人写过,但我从未在我本身的代码中使用过它们。

在 Topeka 里我有好几回都是依靠 OnLayoutChangeListener 来实现注入行为。若是没有 Kotlin ,这样作会生成一个包含重复代码的匿名类。

迁移代码以后,只须要调用如下代码:
view.onLayoutChange { myAction() }

这其中的代码被封装到以下扩展函数中了:

/**
 * 当布局改变时执行对应代码
 */
inline fun View.onLayoutChange(crosssinline action: () -> Unit) {
    addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
        override fun onLayoutChange(v: View, left: Int, top: Int,
                                    right: Int, bottom: Int,
                                    oldLeft: Int, oldTop: Int,
                                    oldRight: Int, oldBottom: Int) {
            removeOnLayoutChangeListener(this)
            action()
        }
    })
}复制代码

使用高阶函数减小样板代码

另外一个例子能证实以上的功能一样能够被应用于数据库的操做中:

inline fun SQLiteDatabase.transact(operation: SQLiteDatabase.() -> Unit) {
    try {
        beginTransaction()
        operation()
        setTransactionSuccessful()
    } finally {
        endTransaction()
    }
}复制代码

少一些形式,多一些功能

这样写完后,API 使用者只须要调用 db.transact { operation() } 就能够完成以上全部操做。

经过 Twitter 进行更新: 经过使用 SQLiteDatabase.() 而不是 () 能够在 operation() 中传递函数并实现直接使用数据库。🔥

不用我多说你应该已经懂了。

使用高阶和扩展函数可以提高项目的可读性,同时能去除冗长的代码,提高性能并省略细节。


有待探索

目前为止我一直在讲代码规范以及一些开发的惯例,都没有提到有关 Android 开发的实践经验。

这主要是由于我对这门语言还不是很熟,或者说我尚未花太大精力去收集并发表这方面的内容。也许是由于我尚未碰到这类状况,但彷佛还有至关多的平台特定的语言风格。若是你知道这种状况,请在评论区补充。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索