在2011年下半年,Savvy Apps开始使用Kotlin新的Android项目,当时Kotlin 1.0.4被发布。最初,我们看到有机会在较小规模的项目中尝试Kotlin。一旦我们尝试了,看到易用性,就可以轻松地将功能与使用扩展的业务逻辑分开,一般来说,它为我们节省的开发时间,我们决定这将是选择的语言向前推进。从那时起,我们已经使用Kotlin来创建多个Android应用程序,并且还开发了一些内部的Kotlin库。 为了增强Kotlin的经验,我们决定在Savvy的集体Android开发团队中编制最有用和最喜爱的技巧。在阅读这些提示之前,您应该查看Kotlin文档并探索自己的语言attry.kotlinlang.org。由于这些提示专注于在Android开发的上下文中使用Kotlin,所以您也应该拥有Android SDK的经验。您也应该熟悉Kotlin插件,并使用Kotlin与Android Studio,由JetBrains,Kotlin的创作者提供。
注意:这些提示是根据您对Kotlin的熟悉进行排序,因此您可以轻松跳过适合您技能水平的提示。
Android的初步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,因此不会占用所需的资源。
Lazy加载也是封装初始化逻辑的好方法:
// bounds is created as soon as the first call to bounds is madeval bounds: RectF by lazy { RectF(0f, 0f, width.toFloat(), height.toFloat()) }
一旦创建了对边界的第一个引用,就会使用视图的当前宽度和高度创建RectF,从而节省了我们不必明确创建这个RectF,然后再进行设置。
定制Getters / Setters
Kotlin的定制getter和setter使用模型的结构,但指定自定义行为来获取和设置字段。 当为某些框架(如Parse SDK)使用自定义模型时,您将获取类中实际上不是局部变量的值,但以某些自定义方式(例如从JSON)存储和检索。 通过使用定制的getter和setter,我们可以简化访问:
@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)}
获取这些值将与使用属性访问语法与其他模型类似:
val book = api.getBook()textAuthor.text = book.author
现在,如果您的模型需要从Parse更改为其他数据源,则您的代码可能只需要在一个地方更改。
Lambda表达式
Lambdas减少源文件中的总代码行,并允许功能编程。 虽然目前有可能使用Android的Lambdas,但Kotlin通过确保您不必处理Retrolambda或更改您的构建方式配置,从而进一步提高了它们的进步。
例如,一个点击式监听器将如下所示:button.setOnClickListener { view -> startDetailActivity()}
它甚至与返回值一起使用:
toolbar.setOnLongClickListener { showContextMenu() true}
Android SDK包含大量情况,您正在设置侦听器或实现单个方法。 Lambdas在这种情况下工作得很好。
数据类
数据类简化类,自动添加equals(),hashCode(),copy()和toString()方法。 他们澄清了模型的意图,以及应该如何处理,将纯数据与业务逻辑分开。
以这个数据类为例:
data class User(val name: String, val age: Int)
而已。 没有什么需要使这个类工作。 如果您正在使用类似Gson或其他JSON解析库的数据类,则可以使用如下默认值创建默认构造函数:
// Example with Gson's @SerializedName annotationdata 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 listval activeUsersNames = items.filter { it.active // the "it" variable is the parameter for single parameter lamdba functions}adapter.setUsers(activeUsers)
使用内置的Kotlin方法过滤集合与其他功能编程语言(例如Java 8流或Swift集合类型)非常相似。能够以统一的方式过滤集合有助于在与团队成员交谈时需要完成哪些操作以将列表缩小到要显示的正确元素。
对象表达式
对象表达式允许严格的单例定义,因此对于可以实例化的类,不会有任何错误。他们还确保您不必在应用程序类中的某个位置存储单个或静态类变量。
例如,如果我有一个具有静态线程相关方法的实用程序类,我想在整个应用程序中访问:
package com.savvyapps.example.utilimport android.os.Handlerimport android.os.Looper// notice that this is object instead of classobject ThreadUtil { fun onMainThread(runnable: Runnable) { val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post(runnable) }}
ThreadUtil
稍后以典型的方式调用静态类方法:
ThreadUtil.onMainThread(runnable)
这意味着没有更多的将构造函数声明为私有的,或者必须弄清静态实例的存储位置。对象本质上是该语言的一流公民。以类似的方式,我们创建对象而不是匿名内部类:
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) }});
这两个都基本上是相同的事情 - 创建一个类作为声明对象的单个实例。
伴侣对象
一目了然,Kotlin似乎缺少静态变量和方法。 在某种意义上,它没有这些概念,而是具有伴随对象的想法。 这些配对对象是一个类中的单例对象,其中包含您可能要以静态方式访问的方法和变量。 一个配对对象允许定义的常量和方法,类似于Java中的static。 有了它,你可以按照NewInstance的片段模式。
以下是最简单的配套对象:
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中,我们通常使用静态方法和变量为片段或活动意图创建静态工厂。例如:
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)
这种模式是非常好的,因为它减少了意图或片段丢失所需数据以显示用户或其意图显示的内容的可能性。伴侣对象是在Kotlin中保持某种形式的静态访问的方式,应该相应使用。
全局常数
Kotlin允许您在一个位置定义跨整个应用程序的常量(如果适用)。通常,常量应尽可能减少其范围,但是当范围需要为全局时,这是一个很好的方法,而不必经历一个常量类。
package com.savvyapps.exampleimport android.support.annotation.StringDef// Note that this is not a class, or an objectconst val PRESENTATION_MODE_PRESENTING = "presenting"const val PRESENTATION_MODE_EDITING
= "editing"
这些可以用作项目中的任何位置的常量:
import com.savvyapps.example.PRESENTATION_MODE_EDITINGval currentPresentationMode = PRESENTATION_MODE_EDITING
请记住,常数应保持尽可能小的范围以减少复杂性。如果您有一个仅与用户类相关的值,请将值放在随附对象中。
可选参数
可选参数使得方法调用更加灵活,而不必传递null或默认值。这在定义动画时特别有用。
例如,如果您想要在整个应用程序中定义淡化视图的方法,但只有在特殊情况下,您需要指定持续时间,您可以像这样定义方法:
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
针对Android的中级Kotlin提示
扩展
扩展是有用的,因为它们允许您添加到类的功能,而无需继承它。例如,有没有希望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}
通过扩展,您可以轻松地消除对实用程序类或方法的需求,并可以真正提高代码的可读性。我们喜欢进一步,并使用扩展来改进我们的代码的组织。例如,假设你有一个基本的模型,如文章。这篇文章可能被视为一个数据类,它已经从一个源(如API)中获取:
class Article(val title: String, val numberOfViews: Int, val topic: String)
假设您想根据一些公式确定文章与用户的相关性。你应该直接在文章类吗?有些人会说,该模型只应该保存来自API的数据,没有了。在这种情况下,扩展程序可以再次为您服务:
// 在另一个Kotlin文件中,可能命名为ArticleLogic.kt或类似的文章
Article.isArticleRelevant(user: User): Boolean { return user.favoriteTopics.contains(topic)}
目前,这是一个简单的检查,以查看用户是否在自己喜欢的主题列表中列出了该文章的主题。但是,下线,逻辑可能会改变到您要检查用户的其他属性的位置。由于该逻辑与“文章”模型有些独立,因此您可以对其进行更改,并对该方法的目的及其变更能力有信心。
lateinit
Kotlin的一个主要特点是致力于安全无误。 lateinit提供了一种简单的方式,既可以安全地进行安全,也可以将Android初始化为Android所需的变量。这是一个伟大的语言功能,但是在进行广泛的Java开发后,仍然需要一些习惯。其中一个想法是,必须立即分配一个字段,或者声明为可能为null:
var total = 0 // 立即声明,没有nullvar工具栏的可能性:工具栏? = null //可以是工具栏,可以为null
处理Android版面时,这种语言功能会令人沮丧。这是因为我们知道视图将在Activity或Fragment中存在,但是我们无法立即声明它们,因为在布局膨胀后必须在onCreate / onCreateView中完成。您可以通过在整个活动中触摸视图的每个地方声明检查来处理此问题,但是从空检查的角度来看,处理和不必要的这将是令人沮丧的。相反,您可以使用lateinit修饰符:
lateinit var toolbar: Toolbar
现在,由开发人员在实际初始化之前不要引用工具栏。当与一个像黄油刀一样的图书馆一起使用时,这很有用:
@BindView(R.id.toolbar) lateinit var toolbar: Toolbaroverride 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约定需要安全类型转换,因为正常的类型转换会导致异常。例如,在活动中创建片段的典型方法是首先使用FragmentManager检查它是否已经存在。如果没有,您将创建它并将其添加到活动。当您首先在Kotlin中进行类型转换时,您可以这样实现:
var feedFragment: FeedFragment? = supportFragmentManager .findFragmentByTag(TAG_FEED_FRAGMENT) as FeedFragment
这实际上会导致崩溃。 当您调用“as”时,它会尝试对该对象进行转换,在这种情况下,该对象将为空,并将导致空指针异常。 您需要改为调用“as?”,这意味着一些沿着“转换对象并返回null(如果转换失败)”的一行。总而言之,片段的正确初始化将如下所示:
var feedFragment: FeedFragment? = supportFragmentManager .findFragmentByTag(TAG_FEED_FRAGMENT) as? FeedFragmentif (feedFragment == null) { feedFragment = FeedFragment.newInstance() supportFragmentManager.beginTransaction()
.replace(R.id.root_fragment, feedFragment, TAG_FEED_FRAGMENT) .commit()}
充分利用
如果对象的值不为空,则允许您执行块。这允许您避免空检查,并使代码更易读。在Java中,这看起来像:
if (currentUser != null) { text.setText(currentUser.name)}
而Kotlin则成为:
user?.let { println(it.name)}
在这种情况下,随着读者的友好程度的增加,让用户自动将用户分配给一个不可空的变量,它可以继续使用,而不用担心它被清空。
isNullOrEmpty | isNullOrBlank
我们需要在开发Android应用程序时多次验证。如果您在不使用Kotlin的情况下处理此问题,您可能已经在Android中发现了TextUtils类。 TextUtils类看起来像:
if (TextUtils.isEmpty(name)) { // alert the user!}
在这个例子中,你会意识到一个用户可以将它们的用户名设置为只有空格,并且它会通过验证。 isNullOrEmpty和isNullOrBlank内置到Kotlin语言中,并且消除了对TextUtils.isEmpty(someString)的需要,并提供了检查只是空格的额外好处。您可以在适当的时候使用两者:
// 如果我们不在乎只有空格的可能性... if(number.isNullOrEmpty()){//提醒用户填写他们的号码} //当我们需要阻止用户输入只有空格(name.isNullOrBlank()){//提醒用户填写他们的名字!}
字段的验证在应用程序的注册过程中是常见的。这些内置方法非常适用于验证字段,并且如果某些内容看起来不正确,则会提醒用户。您甚至可以利用扩展方法进行一些自定义验证,例如电子邮件地址:
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 }}
Android的高级Kotlin技巧
避免Kotlin类的单抽象方法
这个提示允许您使用lambdas,这样可以实现更简洁,更简洁的代码。例如,当在Java中工作时,有一个简单的监听器类是非常典型的,比如:
public interface OnClickListener { void onClick(View v);}
Kotlin的一个很大的特点是它为Java类执行SAM(单抽象方法)转换。 Java中的一个点击监听器,如下所示:
textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // do something }});
在Kotlin可以减少:
textView.setOnClickListener { view -> // do something}
但是,对于在Kotlin中创建的SAM接口,无法做到这一点。这是设计的,但对于较新的Kotlin用户来说可能有点令人惊讶和令人沮丧。如果在Kotlin中定义了相同的界面,则监听器必须看起来更像:
view.setOnClickListener(object : OnClickListener { override fun onClick(v: View?) { // do things }})
为了减少这种情况,你会想要像上面那样在类中写出你的听众:
private var onClickListener: ((View) -> Unit)? = nullfun setOnClickListener(listener: (view: View) -> Unit) { onClickListener = listener}// later, to invokeonClickListener?.invoke(this)
这将使您回到自动SAM转换成为可能的更简单的lambda语法。
协调代替AsyncTask
因为AsyncTask是笨重的并且往往会导致泄漏,我们更喜欢使用协程,因为它们提高了可读性,并且不会泄漏内存。查看此资源以了解协同程序的基础知识。请注意,由于协调程序在Kotlin 1.1中是实验性的,我们仍然建议使用RJJava来进行大多数异步目的。
目前,协调程序需要额外的依赖:
com pile "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.13"
and in the gradle.properties file:
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的文档。 Square的Android开发人员Jake Wharton还提供了一些有用的资源,包括他的演讲和他关于使用Kotlin for Android的可行性的笔记。