译者简介:ASCE1885, 《Android 高级进阶》作者。小密圈:Android高级进阶
原文链接:https://savvyapps.com/blog/kotlin-tips-android-development。
Savvy Apps 在 2016 年底开始在新的 Android 项目中使用 Kotlin,就在 Kotlin 1.0.4 发布之际。最初,我们得到了在规模较小的项目中尝试 Kotlin 的机会,当尝试过后,我们发现了它的易用性,使用扩展函数可以很容易的将功能和业务逻辑分离开,而且它为我们节省了开发时间,因此,我们觉得它将是一门先进的语言选型。从那时开始,我们使用 Kotlin 创建了多个 Android App,同时也开发了一些内部的 Kotlin 函数库。
为了建立我们的 Kotlin 开发经验,我们决定汇总编辑 Savvy 旗下所有 Android 开发团队中最有用和大家最喜欢的开发建议。阅读这些建议之前,你应该先看看 Kotlin 官方文档1,并在 try.kotlinlang.org2 这个网站上亲自探索一下这门语言。由于这些建议专门针对 Kotlin 在 Android 平台上面的开发 ,因此你也应该有基于 Android SDK 开发的经验。同时,你也应该熟悉由 Kotlin 的创建者 JetBrains 提供的 Kotlin 插件和在 Android Studio 中使用 Kotlin3。
注意,这些建议是基于你对 Kotlin 的熟悉程度进行排序的,所以你可以根据自身的技术水平很容易的跳过一些觉得没必要看的建议。
初级建议
延迟加载
延迟加载有几个好处。首先由于加载时机推迟到了变量被访问时,因此它可以提高应用的启动速度。相比于使用 Kotlin 开发服务端应用,这一特性对开发 Android 应用而言特别有用。对于 Android 应用来说,我们想要减少应用启动时间,这样用户可以更快看到应用的内容,而不是干等着看启动加载页面。
其次,这样的延迟加载也有更高的内存效率,因为我们只在它被调用时才将资源加载进内存。在 Android 这样的移动平台上,内存的使用是非常重要的。因为手机资源是共享的且有限的。例如,如果你创建一个购物应用程序,而用户可能只会浏览你的物品,那么你可以延迟加载购买相关的 API:
val purchasingApi: PurchasingApi by lazy {
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
retrofit.create(PurchasingApi::class.java)
}
通过使用像这样的延迟加载,如果用户根本没有想要在应用中发生购买行为,你的应用将不会加载 PurchasingApi,因此不会消耗它可能会占用的资源。
延迟加载也是封装初始化逻辑的好方法:
// bounds is created as soon as the first call to bounds is made
val bounds: RectF by lazy {
RectF(0f, 0f, width.toFloat(), height.toFloat())
}
只有当 bounds 变量第一次被引用时,将会使用 view 的当前宽和高的值来创建 RectF,这样我们就不需要一开始显式的创建 RectF,然后把它设置给 bounds。
自定义 Getters/Setters
Kotlin 的自定义 getters 和 setters 使用 model 类的结构,但指定自定义的行为来获取和设置字段值。当为某些框架例如 Parse SDK 使用自定义 model 类时,我们获取的值不是存放在类实例的局部变量中,而是通过某些自定义方式存储和检索到的值,例如从 JSON 中。通过使用自定义的 getters 和 setters,我们可以简化存取方法的定义:
@ParseClassName("Book")
class Book : ParseObject() {
// getString() and put() are methods that come from ParseObject
var name: String
get() = getString("name")
set(value) = put("name", value)
var author: String
get() = getString("author")
set(value) = put("author", value)
}
存取上面定义的字段的方式看起来和传统的访问 model 类的方式类似:
val book = api.getBook()
textAuthor.text = book.author
现在如果你的 model 类的数据来源需要从 Parse SDK 改为其他的数据源,那么你的代码可能只需要修改一个地方即可。
Lambdas 表达式
Lambdas 表达式在减少源文件中代码的总行数的同时,也支持函数式编程。虽然目前在 Android 开发中(译者注:使用 Java 语言)可以使用 lambdas 表达式,但要么需要在工程中引入 Retrolambda4,要么需要改变工程构建时的配置,但 Kotlin 更进一步,完全不需要这些额外的操作即可支持 lambdas。
例如,使用 lambdas 表示式时,onClickListener 的用法如下:
button.setOnClickListener { view ->
startDetailActivity()
}
它甚至可以支持返回值:
toolbar.setOnLongClickListener {
showContextMenu()
true
}
Android SDK 中有很多设置 listener 或者需要实现单个方法的场景,在这些场景下 lambdas 表示式可以发挥很大的作用。
数据类
数据类简化了类的定义,自动为类添加equals()
,hashCode()
,copy()
和toString()
方法。它明确定义了 model 类的意图,以及应该包含什么内容,同时将纯数据与业务逻辑分离开来。
我们以一个例子来看下数据类的定义:
data class User(val name: String, val age: Int)
就这么简单,不需要再增加其他的定义,这个类就可以正常工作了。如果将数据类和 Gson 或者类似的 JSON 解析函数库一起使用,你可以像下面代码这样使用默认值创建默认的构建方法:
// Example with Gson's @SerializedName annotation
data class User(
@SerializedName("name") val name: String = "",
@SerializedName("age") val age: Int = 0
)
集合过滤
使用 API 时我们经常需要和集合打交道。在大多数情况下你想要过滤或者修改集合的内容。通过使用 Kotlin 的集合过滤功能,我们可以使代码变得更清晰简洁。通过下面的集合过滤代码我们可以更容易的表达结果列表中想要包含的内容:
val users = api.getUsers()
// we only want to show the active users in one list
val activeUsersNames = items.filter {
it.active // the "it" variable is the parameter for single parameter lamdba functions
}
adapter.setUsers(activeUsers)
使用 Kotlin 内置方法实现集合过滤的功能和其他函数式编程语言非常类似,例如 Java 8 的 streams 或者 Swift 的集合类型。能够以统一的方式实现集合过滤,有助于我们与团队成员讨论时更好更快的达成共识,例如如何将正确的元素显示到列表中。
对象表达式
对象表达式允许严格的单例定义,所以对于一个可以实例化的类来说它不会有问题。对象表达式也确保你不需要将单例存放在类似 Application 类或者作为一个静态类的变量。
例如,如果我有一个工具类,它拥有一个静态的线程相关方法,我想在应用的任何地方中访问它:
package com.savvyapps.example.util
import android.os.Handler
import android.os.Looper
// notice that this is object instead of class
object ThreadUtil {
fun onMainThread(runnable: Runnable) {
val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post(runnable)
}
}
然后 ThreadUtil 就可以像调用静态类方法那样进行调用了:
ThreadUtil.onMainThread(runnable)
这意味着再也不需要声明私有的构造方法,或者不得不指定在哪里存放静态实例。对象表达式本质上是 Kotlin 语言的第一公民。类似的,我们可以使用对象表达式来代替匿名内部类,如下所示:
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
bindUser(position)
}
});
这两种方法本质上是一样的,它们通过声明一个对象来创建一个类的单例。
Companion 对象
咋看之下,Kotlin 似乎缺少静态变量和方法。从某种意义上说,Kotlin 中是没有这些概念的,但它有 companion 对象的概念。这些 companion 对象是类里面的单例对象,其中包含了你可能希望以静态的方式访问的方法和变量。companion 对象中可以定义常量和方法,类似 Java 中的静态变量和方法。有了它,你可以采用 fragments 中的 newInstance 模式。
下面让我们来看一下 companion 对象的最简单形式:
class User {
companion object {
const val DEFAULT_USER_AGE = 30
}
}
// later, accessed like you would a static variable:
user.age = User.DEFAULT_USER_AGE
在 Android 中,我们通常使用静态方法和变量来为 fragments 和 activity intents 创建静态工厂,如下所示:
class ViewUserActivity : AppCompatActivity() {
companion object {
const val KEY_USER = "user"
fun intent(context: Context, user: User): Intent {
val intent = Intent(context, ViewUserActivity::class.java)
intent.putExtra(KEY_USER, user)
return intent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cooking)
val user = intent.getParcelableExtra<User>(KEY_USER)
//...
}
}
创建这个 Intent 的调用方式跟你在 Java 中看到的类似:
val intent = ViewUserActivity.intent(context, user)
startActivity(intent)
这种模式非常好,因为它减少了 Intent 或者 Fragment 丢失需要显示的用户信息或者其他所需要内容的可能性。Companion 对象是在 Kotlin 中保持某种形式的静态访问的一种方式,因此应该被相应。
全局常量
Kotlin 允许开发者定义能在整个应用程序的所有地方都能够访问的常量。通常情况下,常量应该尽可能减小它们的作用域,但当需要有全局作用域的常量时,这是一种很好的方法,你不需要实现一个常量类:
package com.savvyapps.example
import android.support.annotation.StringDef
// Note that this is not a class, or an object
const val PRESENTATION_MODE_PRESENTING = "presenting"
const val PRESENTATION_MODE_EDITING = "editing"
这些全局常量可以在工程中任何地方访问:
import com.savvyapps.example.PRESENTATION_MODE_EDITING
val currentPresentationMode = PRESENTATION_MODE_EDITING
需要记住的是,为了减小代码复杂性,常量应该尽可能的缩小它的作用域。如果有一个常量值只和 user 类相关,那么应该将这个常量定义在 user 类的 companion 对象中,而不是通过全局常量的方式。
Optional Parameters
可选参数使得方法调用更灵活,因为我们无需传递 null 或者默认值。在定义动画时尤其有用。
例如,如果你希望在整个应用程序中为 views 的淡出动画定义一个方法,但只在特殊的场景下需要指定动画持续的时间,那么你可以如下定义这个方法:
fun View.fadeOut(duration: Long = 500): ViewPropertyAnimator {
return animate()
.alpha(0.0f)
.setDuration(duration)
}
使用方法如下:
icon.fadeOut() // fade out with default time (500)
icon.fadeOut(1000) // fade out with custom time
中级建议
扩展
扩展的好处在于它允许我们为一个类添加功能同时无需继承它。例如,你是否曾经希望 Activity 有某些方法,比如 hideKeyboard()?使用扩展,你可以很容易实现这个功能:
fun Activity.hideKeyboard(): Boolean {
val view = currentFocus
view?.let {
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE)
as InputMethodManager
return inputMethodManager.hideSoftInputFromWindow(view.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS)
}
return false
}
有了扩展,你可以很容易消除使用工具类或者工具方法的需要,并且可以真正的提高代码的可读性。我们想要更进一步,使用扩展来改进代码的组织结构。例如,假设你有一个基础的 model 类,比如一篇文章。这篇文章可能被看作一个数据类,它从一个 API 数据源中获取:
class Article(val title: String, val numberOfViews: Int, val topic: String)
假设你想要根据某种公式来计算文章跟用户的相关性。你是应该直接把相关性作为变量放在这个数据类中吗?有人会说 Article 这个 model 类应该只保存从 API 中获取到的数据,仅此而已。在这种情况下,扩展可以再次为你所用:
// In another Kotlin file, possibly named ArticleLogic.kt or something similar
fun Article.isArticleRelevant(user: User): Boolean {
return user.favoriteTopics.contains(topic)
}
现阶段,上面这段代码的作用只是简单的检查指定的文章是否在用户最喜欢的主题列表中。但是,从逻辑上讲,这个业务逻辑也可以修改成检查用户其他的属性。由于这个检查逻辑与 Article model 类是独立的,你可以修改它,并对方法的目的和它可以更改的能力充满信心。
lateinit
Kotlin 的一个主要特性是它对空指针安全特性的支持。而 lateinit 提供一种简单的方式,同时实现空指针安全和 Android 平台要求的变量初始化。这是一个伟大的语言特性,但如果你之前做过较长时间 Java 开发的话,你仍然需要一些时间来适应。在 Kotlin 中,一个字段要么立即被赋值,要么声明可为空:
var total = 0 // declared right away, no possibility of null
var toolbar: Toolbar? = null // could be a toolbar, could be null
在处理 Android 布局时,这种语言特性会令人感到沮丧。我们知道,views 是存在于 Activity 或者 Fragment 中的,在声明的时候我们不能给 views 赋值,因为它们必须在布局被 inflate 之后才能在 onCreate/onCreateView 中被赋值。你可以在 Activity 中需要用到 view 的地方进行检查来处理这类问题,但从一个空检查的处理角度看,这些操作是令人沮丧同时没有必要的。与之对应的,在 Kotlin 中你可以使用 lateinit 修饰符:
lateinit var toolbar: Toolbar
现在,开发者可以不引用 toolbar,直到它被初始化为止。当和 Butter Knife5 之类的函数库一起使用时,这个效果非常好:
@BindView(R.id.toolbar) lateinit var toolbar: Toolbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ButterKnife.bind(this)
// you can now reference toolbar with no problems!
toolbar.setTitle("Hello There")
}
安全的类型转换
某些 Android 编程规范要求进行安全的类型转换,因为普通的类型转换可能会引起异常。例如,在 Activity 中创建 Fragment 的一个典型做法是首先使用 FragmentManager 检查这个 Fragment 实例是否已经存在。如果不存在就创建它并添加到当前 Activity 中。当第一次看到 Kotlin 的类型转换时,你可能会这样实现这个功能:
var feedFragment: FeedFragment? = supportFragmentManager
.findFragmentByTag(TAG_FEED_FRAGMENT) as FeedFragment
这实际上会导致一个 crash。当你使用 as
时,它会尝试对对象进行类型转换,在这个例子中,转换后可能为 nul,同时可能引起空指针异常。你需要使用 as?
来代替 as
,意思是对对象进行类型转换,如果转换失败,则返回 null。因此,Fragment 的正确初始化应该如下所示:
var feedFragment: FeedFragment? = supportFragmentManager
.findFragmentByTag(TAG_FEED_FRAGMENT) as? FeedFragment
if (feedFragment == null) {
feedFragment = FeedFragment.newInstance()
supportFragmentManager.beginTransaction()
.replace(R.id.root_fragment, feedFragment, TAG_FEED_FRAGMENT)
.commit()
}
使用 let
let 修饰的对象的值如果不为 null 时,允许执行对应的一个代码块。这使得开发者可以避免进行空类型的检查,使得代码可读性更强。例如在 Java 中代码如下:
if (currentUser != null) {
text.setText(currentUser.name)
}
在 Kotlin 中使用 let 的方式如下:
user?.let {
println(it.name)
}
在这个例子中,let 除了让代码对开发者更友好,还自动为 user 实例分配一个不为空的变量 it,我们可以继续使用它而不用担心它会被置空。
isNullOrEmpty | isNullOrBlank
在开发一个 Android 应用的过程中,我们通常需要进行很多的校验。如果你曾经在没有 Kotlin 的情况下处理过这种情况,你可能已经发现并使用过 Android 中的 TextUtils 类。TextUtils 类的用法看下来如下所示:
if (TextUtils.isEmpty(name)) {
// alert the user!
}
在这个例子中,你会发现如果用户将它们的用户名 name 设置为空白,它将通过上面的检验。isNullOrEmpty 和 isNullOrBlank 是内置在 Kotlin 语言中的,它们消除了使用 TextUtisl.isEmpty(someString) 的需要,同时提供检查空白的额外功能。你可以在适当的时候使用这两个方法:
// If we do not care about the possibility of only spaces...
if (number.isNullOrEmpty()) {
// alert the user to fill in their number!
}
// when we need to block the user from inputting only spaces
if (name.isNullOrBlank()) {
// alert the user to fill in their name!
}
字段校验的应用程序注册时常见的一种情况。这些内置的方法对于字段的校验和提醒用户输入有误是很好用的。你甚至可以利用扩展方法来实现一些自定义校验,例如校验电子邮件地址:
fun TextInputLayout.isValidForEmail(): Boolean {
val input = editText?.text.toString()
if (input.isNullOrBlank()) {
error = resources.getString(R.string.required)
return false
} else if (emailPattern.matcher(input).matches()) {
error = resources.getString(R.string.invalid_email)
return false
} else {
error = null
return true
}
}
高级建议
避免为 Kotlin 类定义唯一的抽象方法
这个建议可以让你使用 lambdas 表达式,它使得代码更清晰更简洁。例如,当使用 Java 编写代码时,经常会遇到编写监听器类的情况,如下所示:
public interface OnClickListener {
void onClick(View v);
}
Kotlin 的一大特性是它为 Java 类执行SAM(Single Abstract Method6,单一抽象方法)的转换。在 Java 中 ClickListener 的用法如下:
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// do something
}
});
使用 Kotlin 代码可以精简为:
textView.setOnClickListener { view ->
// do something
}
但同样的用法不能用在 Kotlin 中创建的 SAM 接口。这个是语言的设计7决定的,对于 Kotlin 的初学者而言他们会感到吃惊和沮丧。如果相同的监听器接口是定义在 Kotlin 中,它看起来应该像这样:
view.setOnClickListener(object : OnClickListener {
override fun onClick(v: View?) {
// do things
}
})
为了减少代码量,你会如下所示这样来编写类中的监听器:
private var onClickListener: ((View) -> Unit)? = null
fun setOnClickListener(listener: (view: View) -> Unit) {
onClickListener = listener
}
// later, to invoke
onClickListener?.invoke(this)
这将使你可以重新使用 SAM 自动转换实习的简单的 lambda 语法。
使用协程代替 AnsyncTask
由于 AsyncTask 很笨重,而且往往会导致内存泄漏,因此我们更喜欢使用协程,因为它能够改善代码的可读性同时不会导致内存泄露。这个资源8可以学习协程的基本概念和用法。需要注意的是,由于协程目前还是 Kotlin 1.1 版本中的实验特性,在大多数的异步场景中我们仍然推荐使用 RxJava9。
在本文写作时,协程的使用需要引入额外的依赖库:
compile "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.13"
并在 gradle.properties 文件中增加配置:
kotlin.coroutines=enable
通过使用协程,你可以写出简单的,内联的异步代码,同时可以直接的方式修改 UI 界面。
fun bindUser() = launch(UI) {
// Call to API or some other things that takes time
val user = Api.getUser().execute()
// continue doing things with the ui
text.text = user.name
}
结论
以上是自从我们开始使用 Kotlin 进行开发以来,我们总结收集到的认为最有用的一些建议。在此特别感谢我的同事 Nathan Hillyer 的贡献。我们希望这些建议能够在你使用 Kotlin 开发 Android 时有一个好的开端。你也可以看看 Kotlin 的官方文档10。Jake Wharton,来自 Square 公司的一名 Android 开发者,他也提供了一些关于 Kotlin 的有用的资源,其中包括他关于 Android 开发中使用 Kotlin 的演讲11和笔记12。
期待随着 Kotlin 作为一门编程语言不断进化过程中产生更多的开发建议。
[1] https://kotlinlang.org/docs/reference/ ↩
[2] https://try.kotlinlang.org/ ↩
[3] https://kotlinlang.org/docs/reference/using-gradle.html#targeting-android ↩
[4] https://github.com/orfjackal/retrolambda ↩
[5] https://github.com/JakeWharton/butterknife ↩
[6] https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions ↩
[7] https://youtrack.jetbrains.com/oauth?state=%2Fissue%2FKT-7770 ↩
[8] https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md↩
[9] https://github.com/ReactiveX/RxJava ↩
[10] https://kotlinlang.org/docs/reference/ ↩
[11] https://www.youtube.com/watch?v=A2LukgT2mKc ↩
[12] https://docs.google.com/document/d/1ReS3ep-hjxWA8kZi0YqDbEhCqTt29hG8P44aA9W0DM8/preview ↩