继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Android 中使用 Kotlin 的一些注意事项

浮云间
关注TA
已关注
手记 146
粉丝 16
获赞 47

Data Class

Data class 自动实现  equals(), hashCode(), copy(), 和 toString()。

使用 data 关键字来定义:

data class User(val name: String, val age: Int)

如果使用 Gson 等从 JSON 中解析数据的时候,还可以在默认构造函数上设置默认值:

// 使用 GSON 的 @SerializedName 注解
data class User(
   @SerializedName("name") val name: String = "",
   @SerializedName("age") val age: Int = 0
)

Lazy 属性

使用 lazy() 函数延时初始化一个对象,这样当第一次使用这个属性的时候才去初始化,实现 Java 中的延时初始化效果,可以提高 App 启动的速度:

    val githubService: GithubService by lazy {
       Retrofit.Builder()
               .baseUrl("https://api.github.com/")
               .addConverterFactory(GsonConverterFactory.create())
               .addCallAdapterFactory(LiveDataCallAdapterFactory())
               .build()
               .create(GithubService::class.java)
   }

下面是另外一个示例:

class NamePrinter(val firstName: String, val lastName: String) {
 fun printName() {
   println("$firstName $lastName")
 }
}

上面定义了一个 printName 函数,打印出用户的名字。如果你调用这个函数多次,则每次都需要重新把性和名字合成一遍,由于这里的 性 和 名字是 val(其值不可变),如果能把拼接后的姓名缓存起来就可以提高性能,下面是使用 lazy 改进后的代码:

class NamePrinter(val firstName: String, val lastName: String) {
 val fullName: String by lazy { "$firstName $lastName" }
 fun printName() {
   println(fullName)
 }
}

注意 by lazy 所赋值的变量必须为 val 类型。

自定义 getter 和 setter

对于一些继承至其他类的情况,可以自定义 getter 和 setter,

class CountPreference(val context: Context) {
   val pref: SharedPreferences = context.getSharedPreferences("pref", Context.MODE_APPEND)

   var count: Int
       get() = pref.getInt("count", 0)
       set(value) = pref.edit().putInt("count", value).apply()
}

// 可以像访问属性一样来访问 count

   val pref = CountPreference(this)
   Log.d("TAG", "get count ${pref.count}")

Lambdas

Kotlin 中使用 Lambdas 是很方便的:

    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_kotlin)

       button.setOnClickListener { buttonClicked(it) }
       button.setOnLongClickListener { buttonLongClicked() }
       button2.setOnClickListener { Log.d("TAG", "Button2 clicked! $it") }
       button2.setOnLongClickListener {
           Log.d("TAG", "Button2 long clicked! $it")
           true // 返回值
       }
   }

   private fun buttonLongClicked(): Boolean {
       Log.d("TAG", "Button long clicked!")
       return true
   }

   private fun buttonClicked(v: View?) {
       Log.d("TAG", "Button clicked! $v")
   }

inline

在使用高级函数的时候注意使用 inline 关键字来提高代码执行效率:

比如下面定义了一个高阶扩展函数(参数为函数的函数为高阶函数),

    fun SQLiteDatabase.inTransaction(func: SQLiteDatabase.() -> Unit) {
       beginTransaction()
       try {
           func()
           setTransactionSuccessful()
       } finally {
           endTransaction()
       }
   }

可以通过下面方式来使用:

    db.inTransaction {
       delete("users", "first_name = ?", arrayOf("cat"))
   }

上面的代码编译为 Java 字节码的时候等价于下面的代码:

    DbExtensions.inTransaction(db, new Function1<SQLiteDatabase, Unit>() {
        @Override public Unit invoke(SQLiteDatabase db) {
            db.delete("users", "first_name = ?", new String[] { "cat "});
            return null;
        }
    });

也就是在每次执行 inTransaction 函数的时候都创建了一个匿名内部类。
如果 inTransaction 在一个循环中执行,则会创建很多局部匿名内部类,导致频繁垃圾回收从而引起性能问题,如果把 inTransaction 函数定义为 inline 函数,则可以避免这个问题:

    inline fun SQLiteDatabase.inTransaction(func: SQLiteDatabase.() -> Unit) {
       beginTransaction()
       try {
           func()
           setTransactionSuccessful()
       } finally {
           endTransaction()
       }
   }

这样当调用 inline 函数的时候,在 Java 中的字节码等价于如下的代码,避免匿名内部类开销:

// 和 手写的 Java 代码一样!
db.beginTransaction();
try {
  db.delete("users", "first_name = ?", new String[] { "cat "});
  db.setTransactionSuccessful();
} finally {
  db.endTransaction();
}

inline 顾名思义,把这个高阶函数中的代码直接插入调用这个函数的地方。这样这个 inline 的高阶函数就不存在了,可以把 inline 函数看做代码模板。所以当有很多地方(不是在循环中)调用 inline 函数的时候,由于需要把 inline 函数中的内容直接放到代码调用处,所以代码总行数会增加。

表达式

下面是一些和 Java 有区别的表达式使用方式:

override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
   R.id.action_settings -> consume { navigateToSettings() }
   R.id.nav_camera -> drawer.consume { navigateToCamera() }
   R.id.nav_gallery -> drawer.consume { loadGallery() }
   R.id.nav_slideshow -> drawer.consume { loadSlideshow() }
   else -> super.onOptionsItemSelected(item)
}

inline fun consume(f: () -> Unit): Boolean {
   f()
   return true
}

inline fun DrawerLayout.consume(f: () -> Unit): Boolean {
   f()
   closeDrawers()
   return true
}

Singleton & Object declarations

在 Kotlin 中可以使用 object 关键字来声明单例,这样创建单例更加容易:

object DataProviderManager {
   fun registerDataProvider(provider: DataProvider) {
       // ...
   }

   val allDataProviders: Collection<DataProvider>
       get() = // ...
}

上面的代码被称之为 object declaration(对象声明),也就是通过 object 关键字定义了一个对象(class 关键字定义了一个类),直接定义的这个对象是一个单例,使用名字引用里面的方法和属性:

DataProviderManager.registerDataProvider(...)

对象声明也可以继承:

object DefaultListener : MouseAdapter() {
   override fun mouseClicked(e: MouseEvent) {
       // ...
   }

   override fun mouseEntered(e: MouseEvent) {
       // ...
   }
}

通常可以使用 对象声明来替代匿名内部类:

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)
   }
});

Java 中需要通过关键字 new 来调用构造函数来创建对象,而 Kotlin 中通过关键字 object 可以直接定义对象,这样在语言规范上确保这个对象是单例的。

Companion Objects

Companion Objects 是定义在 class 内的 object,并使用 companion 关键字:

class MyClass {
   companion object Factory/* 名字可以忽略 */ {
       fun create(): MyClass = MyClass()
   }
}

访问类里面的 companion object 的时候可以直接使用类名字来访问:

val instance = MyClass.create()

companion object 看起来和 Java 中的静态成员一样。

如果省略了 companion object 的 名字,在引用 companion object对象的时候可以使用 Companion 这个特定的名字:

class MyClass {
   companion object {
   }
}

val x = MyClass.Companion

在 Android 中可以用 companion object 来定义 Fragment 或者 Activity 中常见的静态常量和启动方法:

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)

可选参数

可选参数可以让调用函数的时候更简洁,不需要的参数不用使用 null 来替代了,Kotlin 中的参数还可以带有默认值:

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

Java 中的大量重载函数(参数个数不同),使用 Kotlin 的可选参数只需要一个函数就可以搞定了。调用起来更清晰。

lateinit

由于 Kotlin 语言是 null 安全的,在声明一个变量的时候就制定了这个变量是否可以为 null, 如果不为 null 就需要在声明的时候或者在构造函数中初始化这个变量,但是在 Android 中很多类中我们没法使用构造函数也没法在声明这个变量的时候就直接赋值,这个时候可以使用 lateinit 关键字, lateinit 关键字的意思是 这个变量将会被初始化,只不过初始化的时机晚了一点:

@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")
}

上面示例中是在 onCreate 函数中初始化的 toolbar 变量。如果不用 lateinit 关键字则需要把 toolbar 声明为可以为 null 类型的:

var toolbar: Toolbar? = null

但是这样使用 toolbar 变量会很不方便,每次使用都需要判断是否为 null。

let

let 函数可以在对象不为 null 的时候执行里面的代码,这样可以避免 Java 中的 null 判断,比如下面是 Java 代码:

if (currentUser != null) {
   text.setText(currentUser.name)
}

使用 let 这可以保证 let 中的代码只有当 currentUser 不为  null 的时候才执行,看起来逻辑更清晰:

    currentUser?.let { 
     text.setText(currentUser.name)
   }

isNullOrEmpty | isNullOrBlank

两个 Kotlin 内置的扩展函数可以替代 Android 中的 TextUtils :

//Java
if (TextUtils.isEmpty(name)) {
   // alert the user!
}

// Kotlin

// 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!
}

Sealed Classes

Sealed Classes 类似于 Java 中的 Enum 不过每个类都是一个普通的类。用来表示有限的子类型。

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

上面的 Expr 为 sealed class,则其所有的子类都定义在同一个文件中,这样编译器就知道 Expr 只有三个子类 Const、Sum 和 NotANumber,然后可以使用 when 表达式来判断每种情况:

fun eval(expr: Expr): Double = when(expr) {
   is Const -> expr.number
   is Sum -> eval(expr.e1) + eval(expr.e2)
   NotANumber -> Double.NaN
   // the `else` clause is not required because we've covered all the cases
}

在 Android 中一个具体的例子就是代表 RecyclerView ViewHolder 数据更新的 Payload,

sealed class Payloads {
   data class Favorite(val favorited: Boolean) : Payloads()
   data class Retweet(val retweeted: Boolean) : Payloads()
   data class CountUpdate(
       val favorites: Long,
       val retweets: Long,
       val replies: Long) : Payloads()
}

比如上面定义了更新 ViewHolder 的三种情况,一种是收藏状态改变了、一种是被转发了、一种是各种微博数据更新了。 这样在 ViewHolder 中就可以根据具体的情况只更新数据变化的 View 了:

  override fun onBindViewHolder(holder: TweetViewHolder,
                               position: Int, payloads: List<Any>) {
   payloads.forEach {
     when (it) {
       is Favorite -> holder.favoriteIcon.isActivated = it.favorited
       is Retweet -> holder.retweetIcon.isActivated = it.retweeted
       is CountUpdate -> {
         holder.apply {
           favoriteCount.text = it.favorites.toString()
           retweetCount.text = it.retweets.toString()
           replyCount.text = it.replies.toString()
         }
       }
     }
   }
 }

Delegation

Kotlin 在语言级别就支持代理, 很多 Listener 都定义了不只一个函数,如果我们只关心其中的一个,其他的函数就需要用空的函数体来实现,这样代码看起来不够简洁,使用代理可以解决这个问题:

  class MyListener : TransitionListener by EmptyTransitionListener {T
   override fun onTransitionStart(transition: Transition) {
   }
 }

 object EmptyTransitionListener : TransitionListener {
   override fun onTransitionEnd(transition: Transition) {}
   override fun onTransitionResume(transition: Transition) {}
   override fun onTransitionPause(transition: Transition) {}
   override fun onTransitionCancel(transition: Transition) {}
   override fun onTransitionStart(transition: Transition) {}
 }

比如上面的 TransitionListener 定义了 5 个函数,MyListener 只关心其中一个,使用 object 来定义一个空实现对象 EmptyTransitionListener ,这个 EmptyTransitionListener 对象是单例,MyListener 中没有实现的函数都被代理到 EmptyTransitionListener 对象对应的函数。

语义验证

比如在 Java 中通常需要验证函数参数是否满足要求:

static String join(String sep, List<String> strings) {
  if (sep == null) throw new NullPointerException("sep == null");
  if (sep.length() < 2) {
    throw new IllegalArgumentException("sep.length() < 2: " + sep);
  }
  if (strings == null) throw new NullPointerException("strings == null");
  // ...
}

上面的验证代码写起来需要不少时间。也有不少第三方扩展库可以简化一下这个步骤:

static String join(String sep, List<String> strings) {
  Preconditions.checkNotNull(sep, "sep == null");
  Preconditions.checkArgument(sep.length() < 2, "sep.length() < 2: " + sep);
  Preconditions.checkNotNull(strings, "strings == null");
    // ...
}

而在 Kotlin 中呢,声明变量的时候就带有是否为 null 的限制了,这样就不用去判断是否为 null 了:

fun join(sep: String, strings: List<String>): String {
 Preconditions.checkArgument(sep.length < 2, "sep.length < 2: " + sep)
// ...
}

Kotlin  还自带了一些验证函数可以进一步简化上面的代码:

fun join(sep: String, strings: List<String>): String {
 require(sep.length < 2) { "sep.length < 2: " + sep }
// ...
}

下面是 Kotlin 中一些验证函数:

// throws IllegalArgumentException
require(value: Boolean)
require(value: Boolean, lazyMessage: () -> Any)
requireNotNull(value: T?): T
requireNotNull(value: T?, lazyMessage: () -> Any): T
// throws IllegalStateException
check(value: Boolean)
check(value: Boolean, lazyMessage: () -> Any)
checkNotNull(value: T?): T
checkNotNull(value: T?, lazyMessage: () -> Any): T
// throws AssertionError
assert(value: Boolean)
assert(value: Boolean, lazyMessage: () -> Any)

measureTimeMillis

measureTimeMillis 是 Kotlin 中的一个函数,用来测量执行一个函数所需要的时间,在调试和分析性能的时候,如果需要测量函数执行时间,则使用起来非常方便:

var helloTook = measureTimeNanos {
 println("Hello, world!")
}

helloTook += measureTimeNanos {
 println("Hello, world!")
}

println("Saying 'hello' twiceZtook ${helloTook}ns")

注明注解应用的目标(Annotation Use-site Targets)

当在 Kotlin 中使用 Java 注解的时候,某些情况下需要指定该注解应用到那个 Java 中的属性。 比如在 Kotlin 中使用 Dagger 2 的 @Qualifier 注解来注入特定的对象。
将会出现错误:

 @Inject @Login lateinit var mLogin: BooleanPreference

// 上面的使用方式将会导致如下错误

e: \dagger\AppComponent.java:10: 错误: util.BooleanPreference cannot be provided without an @Inject constructor or from an @Provides- or @Produces-annotated method.
e:

e:     public abstract void inject(@org.jetbrains.annotations.NotNull()
e:                          ^
e:       util.BooleanPreference is injected at
e:           LauncherActivity.mLogin
e:       LauncherActivity is injected at
e:           dagger.AppComponent.inject(p0)

原因在于 mLogin 变量这行代码编译为 Java 字节码的时候会对应三个目标元素,一个是变量本身、还有 getter 和 setter,Kotlin 不知道这个变量的注解应该使用到那个目标上。
要解决这个方式,需要使用 Kotlin 提供的注解目标关键字来告诉 Kotlin 所注解的目标是那个,上面示例中需要注解应用到 变量上,所以使用 field 关键字:

@Inject @field:[Login] lateinit var mLogin: BooleanPreference

// 也就是把 @XXX(XX) 的 @ 后面的内容放到 @field:[] 的中括号内即可
// 另外 为了统一最好把该变量上的所有注解都放到 @field:[] 中

@field:[Inject Login] lateinit var mLogin: BooleanPreference

同样在测试的时候如果需要使用注解,也可能遇到这种情况:

  @Rule 
 var mActivityTestRule = ActivityTestRule(TasksActivity::class.java)

上面在 Kotlin 中会报错:

org.junit.internal.runners.rules.ValidationError: The @Rule ‘mActivityTestRule’ must be public.

下面是修改的方法:

  @Rule @JvmField
 var mActivityTestRule = ActivityTestRule(TasksActivity::class.java)

 //或者
 @get:Rule
 var mActivityTestRule = ActivityTestRule(TasksActivity::class.java)

Extensions

扩展函数是 Kotlin 的一大特色, 在 Java 中通常有很多工具助手类,比如 XXXUtils 等,这些都可以通过扩展函数来实现,

比如在 Java 中解析一个 layout:

LayoutInflater.from(vg.getContext()).inflate(R.id.a_layout, vg, false);

上面的代码在 Kotlin 中可以通过扩展函数实现:

    fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {
       return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
   }

上面的函数 inflate 是一个扩展 ViewGroup 的函数,就像这个函数定义在 ViewGroup 中一样,然后这样使用:

        vg.inflate(R.layout.a_layout)
       vg.inflate(R.layout.a_layout, true)

这样代码看起来更自然。

扩展函数很强大,肯多地方都可以使用,比如:

fun ImageView.loadUrl(url: String) {
   Picasso.with(context).load(url).into(this)
}

imageView.loadUrl("http://....")

遍历 ViewGroup 中的子 View:

LinearLayout views = // ...
for (int i = 0; i < views.getChildCount(); i++) {
 View view = views.getChildAt(i);
 // TODO do something with view
}

在 Kotlin 中可以写成:

val views = // ...
for (index in 0 until views.childCount) {
val view = views.getChildAt(index)
// TODO do something with view
}

上面只是 for 循环的条件变了,其他还是一样并没有太大变化。如果使用扩展函数则可以继续改进。

定义如下的扩展函数:

inline fun ViewGroup.forEach(action: (View) -> Unit) {
for (index in 0 until childCount) {
action(getChildAt(index))
}
}

inline 可以把高阶函数参数生成的匿名内部类给消除了,参考上面关于 inline 部分。

然后可以这样使用:

val views = // ...
views.forEach {view ->
// TODO do something with view
}

如果需要访问 index 则可以这样定义扩展函数:

inline fun ViewGroup.forEachIndexed(action: (Int, View) -> Unit) {
for (index in 0 until childCount) {
  action(index, getChildAt(index))
}
}

views.forEachIndexed { index, view -> /* ... */ }1

还可以扩展操作符和属性:

operator fun ViewGroup.get(index: Int): View? = getChildAt(index)
operator fun ViewGroup.minusAssign(child: View) = removeView(child)
operator fun ViewGroup.plusAssign(child: View) = addView(child)
operator fun ViewGroup.contains(child: View) = indexOfChild(child) != -1
val ViewGroup.size: Int
get() = childCount

上面分别扩展了 get、-、 +、 属性 等操作符和函数,然后访问 ViewGroup 中的子元素就非常简洁了,看起来就和访问普通的集合元素一样:

val views = // ...
val first = views[0]
views -= first
views += first
if (first in views) doSomething()
Log.d("MainActivity", "View count: ${views.size}")

还可以扩展一个函数返回一个 Interable ,然后使用集合操作函数来使用:

fun ViewGroup.children() = object : Iterable<View> {
override fun iterator() = object : Iterator<View> {
var index = 0
override fun hasNext() = index < childCount
override fun next() = getChildAt(index++)
}
}

children 函数返回了一个 Iterable 对象,然后可以当做集合使用:

val views = // ...
for (view in views.children()) {
// TODO do something with view
}
val visibleHeight = views.children()
.filter { it.visibility == View.VISIBLE }
.sumBy { it.measuredHeight }

原文链接:http://www.apkbus.com/blog-822717-68183.html

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP