阅读 3574

Kotlin Jetpack 实战 | 03. Kotlin 编程的三重境界

往期文章

《Kotlin Jetpack 实战:开篇》

《00. 写给 Java 开发者的 Kotlin 入坑指南》

《01. 从一个膜拜大神的 Demo 开始》

《02. 用 Kotlin 写 Gradle 脚本是一种什么体验?》

简介

本文假设各位已经有了 Kotlin 基础,对 Kotlin 还不熟悉的小伙伴可以去看我之前发的文章。

本文将带领各位用 Kotlin 一步步重构我们的 Demo 工程,顺便一窥Kotlin 编程的三重境界

说明:本系列文章都只探讨 Kotlin JVM,Kotlin JS/Native 都不在探讨范围内。

主要内容

前期准备

第一重境界:用 Java 视角写 Kotlin

第二重境界:用 Kotlin 视角写 Kotlin

第三重境界:用 Bytecode 视角写 Kotlin

结尾

前期准备

  • 将 Android Studio 版本升级到最新
  • 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开:

github.com/chaxiu/Kotl…

  • 切换到分支:chapter_03_kotlin_refactor_training
  • 强烈建议各位小伙伴小伙伴跟着本文一起实战,实战才是本文的精髓

为工程添加 Kotlin 支持

上一章我们已经将 Groovy 改成了 Kotlin DSL,但工程本身还不支持我们用 Kotlin 写 Android App。所以我们还需要做一些配置:

Libs.kt 增加以下依赖常量:

const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlinVersion}"
const val ktxCore = "androidx.core:core-ktx:${Versions.ktxCore}"
复制代码

根目录下的 build.gradle.kt 新增:

dependencies {
    ...
    classpath(kotlin("gradle-plugin", version = Versions.kotlinVersion))
}
复制代码

app/build.gradle.kt 新增:

plugins {
    ...
    kotlin("android")
    kotlin("android.extensions")
}

dependencies {
    ...
    implementation(Libs.kotlinStdLib)
    implementation(Libs.ktxCore)
}
复制代码

注意事项:纯 Kotlin 开发的话做以上配置就够,但如果有 Java 混合开发的话,最好加上以下编译器参数配置,防止出现兼容性问题: app/build.gradle.kt 新增:

android {
    ...
    // Configure Java compiler compatible with Java 1.8
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    // Configure Kotlin compiler target Java 1.8 when compile Kotlin to bytecode
    kotlinOptions {
        this as KotlinJvmOptions
        jvmTarget = "1.8"
    }
}
复制代码

以上配置的作用,分别是:

  • 配置 Java 编译器兼容 Java 1.8
  • 配置 Kotlin 编译器以 Java 1.8 的规范生成字节码

以上修改的具体细节可以看我这个 GitHub Commit

接下来我们进入正题,用 Kotlin 重构 Java 代码。

正文

我一直认为 Kotlin 是一门易学难精的语言:入门,精通。如果要为 Kotlin 程序员划分境界,我觉得可以划分三重境界。

1. 第一重境界:用 Java 视角写 Kotlin

这几乎是每个 Kotlin 程序员都会经历的境界(包括曾经的我)。我曾以为学会 Kotlin 的语法就能写好 Kotlin 代码,然而我只是把脑子里的 Java/C 代码用 Kotlin 语法翻译一遍写出来了而已。

接下来我就以第一重境界的"功力",来重构我们的 Demo 工程。大家看看热闹就行,千万别学进脑子里啊。[狗头]

我现在假装自己是个新手,刚学会 Kotlin 语法。正所谓,柿子要挑软的捏,咱们重构代码当然也从最简单的开始。于是我找到 Demo 工程里的 User.java,一咬牙,就你了:

public class User {
    // 工程简单到没有数据库,所以将 API 请求写死缓存到这里
    public static final String CACHE_RESPONSE = "{"login":"JakeWharton","id":66577,"node_id":"MDQ6VXNlcjY2NTc3","avatar_url":"https://avatars0.githubusercontent.com/u/66577?v=4","gravatar_id":"","url":"https://api.github.com/users/JakeWharton","html_url":"https://github.com/JakeWharton","followers_url":"https://api.github.com/users/JakeWharton/followers",小伙伴"following_url":"https://api.github.com/users/JakeWharton/following{/other_user}","gists_url":"https://api.github.com/users/JakeWharton/gists{/gist_id}","starred_url":"https://api.github.com/users/JakeWharton/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/JakeWharton/subscriptions","organizations_url":"https://api.github.com/users/JakeWharton/orgs","repos_url":"https://api.github.com/users/JakeWharton/repos","events_url":"https://api.github.com/users/JakeWharton/events{/privacy}","received_events_url":"https://api.github.com/users/JakeWharton/received_events","type":"User","site_admin":false,"name":"Jake Wharton","company":"Square","blog":"https://jakewharton.com","location":"Pittsburgh, PA, USA","email":null,"hireable":null,"bio":null,"twitter_username":null,"public_repos":104,"public_gists":54,"followers":57849,"following":12,"created_at":"2009-03-24T16:09:53Z","updated_at":"2020-05-28T00:07:20Z"}";

    private String id;
    private String login;
    private String avatar_url;
    private String name;
    private String company;
    private String blog;
    private Date lastRefresh;

    public User() { }

    public User(@NonNull String id, String login, String avatar_url, String name, String company, String blog, Date lastRefresh) {
        this.id = id;
        this.login = login;
        this.avatar_url = avatar_url;
        this.name = name;
        this.company = company;
        this.blog = blog;
        this.lastRefresh = lastRefresh;
    }

    public String getId() { return id; }
    public String getAvatar_url() { return avatar_url; }
    public Date getLastRefresh() { return lastRefresh; }
    public String getLogin() { return login; }
    public String getName() { return name; }
    public String getCompany() { return company; }
    public String getBlog() { return blog; }

    public void setId(String id) { this.id = id; }
    public void setAvatar_url(String avatar_url) { this.avatar_url = avatar_url; }
    public void setLastRefresh(Date lastRefresh) { this.lastRefresh = lastRefresh; }
    public void setLogin(String login) { this.login = login; }
    public void setName(String name) { this.name = name; }
    public void setCompany(String company) { this.company = company; }
    public void setBlog(String blog) { this.blog = blog; }
复制代码

一顿操作,我把这个 Java Bean 用 Kotlin 语法翻译成了这样:

class User {

    companion object {
        val CACHE_RESPONSE = "..."
    }

    private var id: String? = null
    private var login: String? = null
    private var avatar_url: String? = null
    private var name: String? = null
    private var company: String? = null
    private var blog: String? = null
    private var lastRefresh: Date? = null
    
    constructor() {}
    constructor(id: String, login: String?, avatar_url: String?, name: String?, company: String?, blog: String?, lastRefresh: Date?) {
        this.id = id
        this.login = login
        this.avatar_url = avatar_url
        this.name = name
        this.company = company
        this.blog = blog
        this.lastRefresh = lastRefresh
    }

    fun getId(): String? { return id }
    fun getAvatar_url(): String? { return avatar_url }
    fun getLastRefresh(): Date? { return lastRefresh }
    fun getLogin(): String? { return login }
    fun getName(): String? { return name }
    fun getCompany(): String? { return company }
    fun getBlog(): String? { return blog }

    fun setId(id: String?) { this.id = id }
    fun setAvatar_url(avatar_url: String?) { this.avatar_url = avatar_url }
    fun setLastRefresh(lastRefresh: Date?) { this.lastRefresh = lastRefresh }
    fun setLogin(login: String?) { this.login = login }
    fun setName(name: String?) { this.name = name }
    fun setCompany(company: String?) { this.company = company }
    fun setBlog(blog: String?) { this.blog = blog }
}
复制代码

我看着自己一行一行写出来的 Kotlin 代码,心里成就感满满。So easy![狗头]

为了让工程能够模拟 Kotlin/Java 混编,我们让 ImagePreviewActivity 继续维持 Java 状态,所以接下来就剩下 MainActivity.java 的重构了。我们先看 MainActivity 的 Java 代码。

public class MainActivity extends AppCompatActivity {
    public static final String TAG = "Main";
    public static final String EXTRA_PHOTO = "photo";

    StringRequest stringRequest;
    RequestQueue requestQueue;

    private ImageView image;
    private ImageView gif;
    private TextView username;
    private TextView company;
    private TextView website;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        image = findViewById(R.id.image);
        gif = findViewById(R.id.gif);
        username = findViewById(R.id.username);
        company = findViewById(R.id.company);
        website = findViewById(R.id.website);

        display(User.CACHE_RESPONSE);
        requestOnlineInfo();
    }

    private void requestOnlineInfo() {
        requestQueue = Volley.newRequestQueue(this);
        String url ="https://api.github.com/users/JakeWharton";
        stringRequest = new StringRequest(Request.Method.GET, url,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        display(response);
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });
        stringRequest.setTag(TAG);
        requestQueue.add(stringRequest);
    }

    private void display(@Nullable String response) {
        if (TextUtils.isEmpty(response)) { return; }

        Gson gson = new Gson();
        final User user = gson.fromJson(response, User.class);
        if (user != null){
            Glide.with(this).load("file:///android_asset/bless.gif").into(gif);
            Glide.with(this).load(user.getAvatar_url()).apply(RequestOptions.circleCropTransform()).into(image);
            this.username.setText(user.getName());
            this.company.setText(user.getCompany());
            this.website.setText(user.getBlog());

            image.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    gotoImagePreviewActivity(user);
                }
            });
        }
    }

    private void gotoImagePreviewActivity(User user) {
        Intent intent = new Intent(this, ImagePreviewActivity.class);
        intent.putExtra(EXTRA_PHOTO, user.getAvatar_url());
        startActivity(intent);
    }

    @Override
    protected void onStop () {
        super.onStop();
        if (requestQueue != null) {
            requestQueue.cancelAll(TAG);
        }
    }
}
复制代码

一通操作,我把 MainActivity 重构成了这样:

class MainActivity : AppCompatActivity() {
    companion object {
        val TAG = "Main"
        val EXTRA_PHOTO = "photo"
    }

    var stringRequest: StringRequest? = null
    var requestQueue: RequestQueue? = null

    private var image: ImageView? = null
    private var gif: ImageView? = null
    private var username: TextView? = null
    private var company: TextView? = null
    private var website: TextView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        init()
    }

    private fun init() {
        image = findViewById(R.id.image)
        gif = findViewById(R.id.gif)
        username = findViewById(R.id.username)
        company = findViewById(R.id.company)
        website = findViewById(R.id.website)
        display(User.CACHE_RESPONSE)
        requestOnlineInfo()
    }

    private fun requestOnlineInfo() {
        requestQueue = Volley.newRequestQueue(this)
        val url = "https://api.github.com/users/JakeWharton"
        stringRequest = StringRequest(Request.Method.GET, url,
                object: Response.Listener<String> {
                    override fun onResponse(response: String?) {
                        display(response)
                    }
                }, object: Response.ErrorListener {
            override fun onErrorResponse(error: VolleyError?) {
                Toast.makeText(this@MainActivity, error?.message, Toast.LENGTH_SHORT).show()
            }
        })
        stringRequest!!.tag = TAG
        requestQueue!!.add(stringRequest)
    }

    private fun display(response: String?) {
        if (TextUtils.isEmpty(response)) {
            return
        }
        val gson = Gson()
        val user = gson.fromJson(response, User::class.java)
        if (user != null) {
            Glide.with(this).load("file:///android_asset/bless.gif").into(gif!!)
            Glide.with(this).load(user.getAvatar_url()).apply(RequestOptions.circleCropTransform()).into(image!!)
            username!!.text = user.getName()
            company!!.text = user.getCompany()
            website!!.text = user.getBlog()
            image!!.setOnClickListener(object: View.OnClickListener{
                override fun onClick(v: View?) {
                    gotoImagePreviewActivity(user)
                }
            })
        }
    }

    private fun gotoImagePreviewActivity(user: User) {
        val intent = Intent(this, ImagePreviewActivity::class.java)
        intent.putExtra(EXTRA_PHOTO, user.getAvatar_url())
        startActivity(intent)
    }

    override fun onStop() {
        super.onStop()
        if (requestQueue != null) {
            requestQueue!!.cancelAll(TAG)
        }
    }
}
复制代码

由于 MainActivity 重构成了 Kotlin,ImagePreviewActivity.java 需要对应做一些调整。原因是 Java 还不能很好的识别伴生对象。

修改前:

public class ImagePreviewActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        String url = intent.getStringExtra(MainActivity.EXTRA_PHOTO);
        ...
    }
}
复制代码

修改后:

public class ImagePreviewActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        String url = intent.getStringExtra(MainActivity.Companion.getEXTRA_PHOTO());
        ...
    }
}
复制代码

小结

这个境界的特点是:一行 Kotlin 对应一行 Java,还不会运用 Kotlin 独有的特性。

以上修改的具体细节可以看我这个 GitHub Commit

各位小伙伴千万别看到这里就走了啊,请看我下一个境界是怎么写(演)的。

2. 第二重境界:用 Kotlin 视角写 Kotlin

到第二重境界,我就是个成熟的 Kotlin 程序员了。我会用一些 Kotlin 独有特性去改善 Java 代码里的逻辑。

2-1 Data Class

我们还是从最简单的 User.kt 开始,看过《写给 Java 开发者的 Kotlin 入坑指南》的小伙伴一定知道 Data Class,我们来将 User.kt 重构成 Data Class,真的会省不少代码:

data class User(
        var id: String? = null,
        var login: String? = null,
        var avatar_url: String? = null,
        var name: String? = null,
        var company: String? = null,
        var blog: String? = null,
        var lastRefresh: Date? = null
) {
    companion object {
        val CACHE_RESPONSE = "..."
    }
}
复制代码
小结

Data Class 可以节省我们编写 Java Bean 的时间。

2-2 lateinit

接下来看 MainActivity.kt,我们从最上面的变量开始。之前我们定义的变量都是可为空的(Nullable),导致这些变量在使用的时候都需要判空,或者使用非空断言!!。这很不Kotlin。解决这个问题的办法很多,这里我先用 lateinit 来解决网络请求的两个变量。

修改前:

class MainActivity : AppCompatActivity() {
    ...
    var stringRequest: StringRequest? = null
    var requestQueue: RequestQueue? = null

    private fun requestOnlineInfo() {
        ...
        stringRequest!!.tag = TAG
        requestQueue!!.add(stringRequest)
    }
}
复制代码

修改后:

class MainActivity : AppCompatActivity() {
    ...
    private lateinit var stringRequest: StringRequest
    private lateinit var requestQueue: RequestQueue

    private fun requestOnlineInfo() {
        ...
        stringRequest.tag = TAG
        requestQueue.add(stringRequest)
    }
}
复制代码

小结

一般来说,我们定义不为空的变量需要在构造函数或者 init 代码块里赋值,这样编译器才不会报错。但很多时候我们的变量赋值并不能在以上情况下完成赋值,比如:findViewById。

lateinit 的作用是告诉编译器,我定义的这个不为空的变量,虽然目前没有对它赋值,但我在使用它之前,一定会对它赋值,肯定不为空,你不必报错。

2-3 Kotlin-Android-Extensions

KTX 是 Android 官方提供的一个 Gradle 插件,能够为开发者提供便利,它最著名的功能就是能够省掉 findViewById。之前我们在工程里已经添加了这个插件,接下来直接使用就可以了。

直接将控件的申明和赋值都删掉,然后在调用的地方我们按 option + return 选择 import

修改前:

private var image: ImageView? = null
private var gif: ImageView? = null
private var username: TextView? = null
private var company: TextView? = null
private var website: TextView? = null

image = findViewById(R.id.image)
gif = findViewById(R.id.gif)
username = findViewById(R.id.username)
company = findViewById(R.id.company)
website = findViewById(R.id.website)

...
username!!.text = user.name
company!!.text = user.company
website!!.text = user.blog
复制代码

修改后:

// 注意这里
import kotlinx.android.synthetic.main.activity_main.*

//    private var image: ImageView? = null
//    private var gif: ImageView? = null
//    private var username: TextView? = null
//    private var company: TextView? = null
//    private var website: TextView? = null

//    image = findViewById(R.id.image)
//    gif = findViewById(R.id.gif)
//    username = findViewById(R.id.username)
//    company = findViewById(R.id.company)
//    website = findViewById(R.id.website)

...
username.text = user.name
company.text = user.company
website.text = user.blog
复制代码

小结

  • KTX 提供的便利当然不止是替代 findViewById,后面我们慢慢讲
  • KTX 提供便利的同时其实有一定隐患,我们后面再讲

2-4 Lambda

以下代码 Android Studio 会提示 Convert to lambda 我们只需要按 option + return,Android Studio 就会帮我们重构。

修改前:

...
stringRequest = StringRequest(Request.Method.GET, url,
        object : Response.Listener<String> {
            override fun onResponse(response: String?) {
                display(response)
            }
        }, object : Response.ErrorListener {
    override fun onErrorResponse(error: VolleyError?) {
        Toast.makeText(this@MainActivity, error?.message, Toast.LENGTH_SHORT).show()
    }
})     
...
image.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        gotoImagePreviewActivity(user)
    }
})
复制代码

修改后:

...
stringRequest = StringRequest(Request.Method.GET,
        url,
        Response.Listener { response ->
            display(response)
        },
        Response.ErrorListener { error ->
            Toast.makeText(this@MainActivity, error?.message, Toast.LENGTH_SHORT).show()
        })
...
image.setOnClickListener { gotoImagePreviewActivity(user) }
...
复制代码

小结

  • Kotlin Lambda 要讲清楚能专门写一本书,本文暂时只管怎么用
  • 在这里使用 Lambda 作为接口实现,最大的好处其实是提高了代码的可读性

2-5 扩展函数

使用 Kotlin 的扩展函数能消灭一切 xxUtils.java。Kotlin 标准函数就已经为我们提供了相关扩展函数,帮助我们消灭 TextUtils

修改前:

...
if (TextUtils.isEmpty(response)) {
    return
}
复制代码

修改后:

...
if (response.isNullOrBlank()) {
    return
}
复制代码

上面修改后的代码看起来像是 response 有一个成员方法: isNullOrBlank(),这样做有很多好处:

  • 写代码更流畅,一个类有哪些可以调用的方法,IDE 会自动提示,而不用去找 xxUtils
  • 代码可读性更好

2-6 标准函数 apply

Kotlin 提供了一系列标准函数,比如: let, also, with, apply 帮助开发者简化逻辑。这里我们使用 apply,它的作用解释起来很麻烦,看代码更明了:

修改前:

if (user != null) {
    ...
    username.text = user.name
    website.text = user.blog
    image.setOnClickListener { gotoImagePreviewActivity(user) }
}
复制代码

修改后:

user?.apply {
    ...
    username.text = name
    website.text = blog
    image.setOnClickListener { gotoImagePreviewActivity(this) }
}
复制代码

小结

这个境界的特点是:

  • 一行 Kotlin 代码能对应多行 Java 代码
  • 代码可读性增强
  • 代码健壮性更好

具体细节可以看这个 Github Commit

第三重境界:用 Bytecode 视角写 Kotlin

Kotlin 号称 Java 100% 兼容,就是因为 Kotlin 最终会被编译成字节码(Bytecode)。通过查看 Kotlin 编译后的字节码,我们既能了解 Kotlin 的原理,也能探索出一些 Kotlin 编程的 Tips

受限于本文的篇幅,我们暂且不谈 Kotlin 的实现原理,也不去详细探讨 Kotlin 编程的 Tips。我们继续专注于实战。现阶段的项目中,我们已经尝试加入了一些 Kotlin 的特性,我们只研究现阶段用到的这些 Kotlin 特性。

3-1 如何查看 Kotlin 对应的 字节码?

Tools -> Kotlin -> Show Kotlin Bytecode 一般我们情况下我们只需要查看 Kotlin 等价的 Java 代码即可,因此我们可以在字节码弹窗的左上角找到 Decompile 按钮,这样就能看到 Kotlin 等价的 Java 代码了。

3-2 尽可能消灭可变性(Mutability)

Java 中被 final 修饰的变量一旦赋值后就无法被修改。这在 Java 中也是很好的习惯,我们在 Kotlin 中也应该沿用。Kotlin 没有 final,但是有 val

我们还是先从 User.kt 开始。

data class User(
        var id: String? = null,
        var login: String? = null,
        var avatar_url: String? = null,
        var name: String? = null,
        var company: String? = null,
        var blog: String? = null,
        var lastRefresh: Date? = null
) {
    companion object {
        val CACHE_RESPONSE = "..."
    }
}
复制代码

User.kt 反编译成 Java 后:

...
public final class User {
   @Nullable
   private String id;
   ...
   @NotNull
   private static final String CACHE_RESPONSE = "...";
   public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);

   @Nullable
   public final String getId() {
      return this.id;
   }

   public final void setId(@Nullable String var1) {
      this.id = var1;
   }
   ...
   public static final class Companion {
      @NotNull
      public final String getCACHE_RESPONSE() {
         return User.CACHE_RESPONSE;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}
复制代码

我们将 User.kt 里面的 var 都替换成 val

data class User(
        val id: String? = null,
        val login: String? = null,
        val avatar_url: String? = null,
        val name: String? = null,
        val company: String? = null,
        val blog: String? = null,
        val lastRefresh: Date? = null
) {
    companion object {
        val CACHE_RESPONSE = "..."
    }
}
复制代码

它反编译成 Java 代码变成了这样:

public final class User {
   @Nullable
   private final String id; // 多了 final
   ...
   @NotNull
   private static final String CACHE_RESPONSE = "...";
   public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);

   @Nullable
   public final String getId() {
      return this.id;
   }
   // setId() 没有了
   ...

   public static final class Companion {
      @NotNull
      public final String getCACHE_RESPONSE() {
         return User.CACHE_RESPONSE;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}
复制代码

小结:

  • Kotlin 基于 JVM,所以从前 Java 的编程经验也是有用的
  • 将 Data Class 的 var 改成 val 后,它的成员变量就有 final 修饰了,同时set 方法也没了,一个 Data Class 在被实例化后,就无法再被修改了
  • 如果要修改 Data Class 的成员变量怎么办?用 copy 方法

3-3 尽可能缩小变量的作用域(Scope)

这一点在 Java 和 Kotlin 中同样有用。MainActivity.kt 中有两个成员变量,其中的 stringRequest 其实是可以改为局部变量的。

修改前:

class MainActivity : AppCompatActivity() {
    ...
    private lateinit var stringRequest: StringRequest
    private lateinit var requestQueue: RequestQueue
}
复制代码

修改后:

class MainActivity : AppCompatActivity() {
    ...
//    private lateinit var stringRequest: StringRequest
    private lateinit var requestQueue: RequestQueue
    
    private fun requestOnlineInfo() {
    ...
    val stringRequest = StringRequest(Request.Method.GET,
            url,
            Response.Listener { response ->
                display(response)
            },
            Response.ErrorListener { error ->
                Toast.makeText(this@MainActivity, error?.message, Toast.LENGTH_SHORT).show()
            })
    ...
    }
}
复制代码

3-4 巧用 by lazy

MainActivity 只剩下一个成员变量 requestQueue,它还是用的 var 修饰的,我们能不能把它改为 val 呢?当然可以,但我们需要借助 by lazy,委托。

修改后:

class MainActivity : AppCompatActivity() {
    ...
    private val requestQueue: RequestQueue by lazy { 
        Volley.newRequestQueue(this)
    }
}
复制代码

让我们看看它等价的 Java 代码,它的初始化交给了 LazyKt.lazy

private final Lazy requestQueue$delegate = LazyKt.lazy((Function0)(new Function0() {
  // $FF: synthetic method
  // $FF: bridge method
  public Object invoke() {
     return this.invoke();
  }

  public final RequestQueue invoke() {
     return Volley.newRequestQueue((Context)MainActivity.this);
  }
}));
复制代码

再看看 LazyKt.lazy 的实现,实际上是 SynchronizedLazyImpl

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
复制代码

再看看 SynchronizedLazyImpl:

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}
复制代码

果然,和我们之前文章提到的一样,by lazy 默认情况下会使用同步的方式进行初始化。但我们当前项目并不需要,毕竟多线程同步也是有开销的。

修改后:

private val requestQueue: RequestQueue by lazy(LazyThreadSafetyMode.NONE) {
    Volley.newRequestQueue(this)
}
复制代码

3-5 不要用错伴生对象

由于 Java 无法识别 Kotlin 里面的伴生对象,所以我们在 Java 里访问的时候比较别扭。

class MainActivity : AppCompatActivity() {
    companion object {
        ...
        val EXTRA_PHOTO = "photo"
    }
}
复制代码

在 Java 中访问:

public class ImagePreviewActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        String url = intent.getStringExtra(MainActivity.Companion.getEXTRA_PHOTO());
        ...
    }
}
复制代码

反编译后:

...
@NotNull
private static final String EXTRA_PHOTO = "photo";
public static final MainActivity.Companion Companion = new MainActivity.Companion((DefaultConstructorMarker)null);

...
public static final class Companion {
  @NotNull
  public final String getEXTRA_PHOTO() {
     return MainActivity.EXTRA_PHOTO;
  }

  private Companion() {
  }

  // $FF: synthetic method
  public Companion(DefaultConstructorMarker $constructor_marker) {
     this();
  }
}
复制代码

我们可以看到,默认情况下,Kotlin 为伴生对象里的变量生成了 get 方法,Java 代码里要访问这个变量必须这样: MainActivity.Companion.getEXTRA_PHOTO(),这很不友好。

为了让 Java 能够更好的识别伴生对象里的变量和方法,我们可以这么做:

使用 const:

class MainActivity : AppCompatActivity() {
    companion object {
        ...
        const val EXTRA_PHOTO = "photo"
    }
}
复制代码

或者使用 @JvmField 注解:

class MainActivity : AppCompatActivity() {
    companion object {
        ...
        @JvmField
        val EXTRA_PHOTO = "photo"
    }
}
复制代码

在 Java 中访问:

public class ImagePreviewActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        String url = intent.getStringExtra(MainActivity.EXTRA_PHOTO);
        ...
    }
}
复制代码

以上两种情况反编译成 Java 的代码如下:

...
@NotNull
public static final String EXTRA_PHOTO = "photo";
public static final MainActivity.Companion Companion = new MainActivity.Companion((DefaultConstructorMarker)null);

...
public static final class Companion {
  @NotNull
  public final String getTAG() {
     return MainActivity.TAG;
  }

  private Companion() {
  }

  // $FF: synthetic method
  public Companion(DefaultConstructorMarker $constructor_marker) {
     this();
  }
}
复制代码

不少博客讲伴生对象到这里就结束了。@JvmFieldconst@JvmStatic,这些确实是使用伴生对象需要注意的。

可是,咱们的代码到这里是不是就完美了?并不。

我们可以看到,即使我们加上了 @JvmField 或者 const,伴生对象仍然为常量生成了 get 方法,同时也定义了一个 Companion 的类,还有一个 instance。然而我们最初的需求只是要定义一个 public static final String 的常量而已。

这个小结的标题是不要用错伴生对象。它的前提是什么?它的前提是:该不该用。在这里我不禁要问一句:这种情况下,真的需要伴生对象吗?答案是:不需要。

MainActivity 中的 TAG 不需要在类以外被访问,因此可以直接定义为成员变量:

class MainActivity : AppCompatActivity() {
    private val TAG = "Main"
}
复制代码

现在只剩下 EXTRA_PHOTO,我们应该怎么处理?在 Java 中,我们经常会定义一个类来专门存放常量,Kotlin 中我们同样可以借鉴:

让我们创建一个 Constant.kt:

//注意这里,它要放到 package 的前面
@file:JvmName("Constant")

package com.boycoder.kotlinjetpackinaction

const val EXTRA_PHOTO = "photo"

const val CACHE_RESPONSE = "..."
复制代码

在 Kotlin 中可以直接这样使用:

// Kotlin 中甚至可以省略掉 Constant,因为 CACHE_RESPONSE 是顶层常量。
display(CACHE_RESPONSE)
复制代码

在 Java 中要这样使用:

// 由于 @file:JvmName("Constant") 的存在,Java 中也能很好的访问 Constant.EXTRA_PHOTO
String url = intent.getStringExtra(Constant.EXTRA_PHOTO);
复制代码

Constant.kt 反编译成 Java 后是这样的:

public final class Constant {
  @NotNull
  public static final String EXTRA_PHOTO = "photo";
  @NotNull
  public static final String CACHE_RESPONSE = "...";
}
复制代码

所以说,如果只是需要定义静态常量,哪用得上 Kotlin 的伴生对象?

以上修改的具体细节可以看我这个 Github Commit

总结:

  • Java 的编程经验在 Kotlin 中也是有用的,但我们又不能被 Java 里的经验禁锢
  • Kotlin 中引入了 Java 所没有的特性和概念,我们在使用前最好能用清楚底层实现
  • 网上博客写的最佳实践不一定对(包括本文),要独立思考

4. 结尾

本文只是借助我们的 Demo 一窥 Kotlin 编程的三重境界,让大家对 Kotlin 编程整体有个了解。后面我也许会写专题文章来讲《Kotlin 编译器漫游指南》,《Kotlin 最佳实践指北》,也许吧。

文章写到这已经接近尾声了,那我们的 Demo 工程改到这个程度是不是已经完美了呢?当然没有。但我不想写了,欢迎各位小伙伴留言一起讨论还有哪些地方能改进。

我们下一篇文章再见。

回目录-->《Kotlin Jetpack 实战》

文章分类
Android
文章标签