Savvy Apps 在 2016 年底开始在新的 Android 项目中使用 Kotlin,就在 Kotlin 1.0.4 发布之际。最初,我们得到了在规模较小的项目中尝试 Kotlin 的机会,当尝试过后,我们发现了它的易用性,使用扩展函数可以很容易的将功能和业务逻辑分离开,而且它为我们节省了开发时间,因此,我们觉得它将是一门先进的语言选型。从那时开始,我们使用 Kotlin 创建了多个 Android App,同时也开发了一些内部的 Kotlin 函数库。
初级建议
延迟加载
val purchasingApi: PurchasingApi by lazy { val retrofit: Retrofit = Retrofit.Builder() .baseUrl(API_URL) .addConverterFactory(MoshiConverterFactory.create()) .build() retrofit.create(PurchasingApi::class.java) }
// 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()) }
自定义 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) }
val book = api.getBook() textAuthor.text = book.author
Lambdas 表达式
button.setOnClickListener { view -> startDetailActivity() }
toolbar.setOnLongClickListener { showContextMenu() true }
数据类
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 )
集合过滤
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)
对象表达式
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.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 对象
class User { companion object { const val DEFAULT_USER_AGE = 30 } } // later, accessed like you would a static variable: user.age = User.DEFAULT_USER_AGE
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) //... } }
val intent = ViewUserActivity.intent(context, user) startActivity(intent)
全局常量
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
Optional Parameters
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
中级建议
扩展
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 }
class Article(val title: String, val numberOfViews: Int, val topic: String)
// In another Kotlin file, possibly named ArticleLogic.kt or something similar fun Article.isArticleRelevant(user: User): Boolean { return user.favoriteTopics.contains(topic) }
lateinit
var total = 0 // declared right away, no possibility of null var toolbar: Toolbar? = null // could be a toolbar, could be null
lateinit var toolbar: Toolbar
@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") }
安全的类型转换
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
if (currentUser != null) { text.setText(currentUser.name) }
user?.let { println(it.name) }
isNullOrEmpty | isNullOrBlank
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 类定义唯一的抽象方法
public interface OnClickListener { void onClick(View v); }
textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // do something } });
textView.setOnClickListener { view -> // do something }
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)
使用协程代替 AnsyncTask
compile "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.13"
kotlin.coroutines=enable
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 }
结论
英文:https://savvyapps.com/blog/kotlin-tips-android-development