天天看點

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 {
        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 {
        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 =      

​修改後:​

public class ImagePreviewActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        String url =      

小結

這個境界的特點是:一行 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()      

修改後:

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

    private fun requestOnlineInfo()      

小結

一般來說,我們定義不為空的變量需要在構造函數或者 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      

修改後:

// 注意這裡
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)      

小結

  • 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?)      

​修改後:​

...
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 {
      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      

修改後:

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 =      

反編譯後:

...
@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 =      

以上兩種情況反編譯成 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();
  }
}      

不少部落格講​

​伴生對象​

​​到這裡就結束了。​

​@JvmField​

​​,​

​const​

​​,​

​@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 是頂層常量。      

在 Java 中要這樣使用:

// 由于 @file:JvmName("Constant") 的存在,Java 中也能很好的通路 Constant.EXTRA_PHOTO
String url =      

​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. 結尾