本文假设各位已经有了 Kotlin 基础,对 Kotlin 还不熟悉的小伙伴可以去看我之前发的文章-->【Kotlin Jetpack 实战】。
本文将带领各位用 Kotlin 一步步重构我们的 Demo 工程,顺便一窥Kotlin 编程的三重境界
。
说明:本系列文章都只探讨 Kotlin JVM,Kotlin JS/Native 都不在探讨范围内。
前期准备
第一重境界:用 Java 视角写 Kotlin
第二重境界:用 Kotlin 视角写 Kotlin
第三重境界:用 Bytecode 视角写 Kotlin
结尾
chapter_03_kotlin_refactor_training
上一章我们已经将 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" } } 复制代码
以上配置的作用,分别是:
以上修改的具体细节可以看我这个 GitHub Commit。
接下来我们进入正题,用 Kotlin 重构 Java 代码。
我一直认为 Kotlin 是一门易学难精的语言:入门易,精通难。如果要为 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。
各位小伙伴千万别看到这里就走了啊,请看我下一个境界是怎么写(演)的。
到第二重境界,我就是个成熟的 Kotlin 程序员了。我会用一些 Kotlin 独有特性去改善 Java 代码里的逻辑。
我们还是从最简单的 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 的时间。
接下来看 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
的作用是告诉编译器,我定义的这个不为空的变量,虽然目前没有对它赋值,但我在使用它之前,一定会对它赋值,肯定不为空,你不必报错。
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 复制代码
隐患
,我们后面再讲以下代码 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 的扩展函数能消灭一切 xxUtils.java。Kotlin 标准函数就已经为我们提供了相关扩展函数,帮助我们消灭 TextUtils
。
修改前:
... if (TextUtils.isEmpty(response)) { return } 复制代码
修改后:
... if (response.isNullOrBlank()) { return } 复制代码
上面修改后的代码看起来像是 response
有一个成员方法: isNullOrBlank()
,这样做有很多好处:
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) } } 复制代码
这个境界的特点是:
具体细节可以看这个 Github Commit。
Kotlin 号称 Java 100% 兼容,就是因为 Kotlin 最终会被编译成字节码
(Bytecode)。通过查看 Kotlin 编译后的字节码,我们既能了解 Kotlin 的原理,也能探索出一些 Kotlin 编程的 Tips。
受限于本文的篇幅,我们暂且不谈 Kotlin 的实现原理,也不去详细探讨 Kotlin 编程的 Tips。我们继续专注于实战。现阶段的项目中,我们已经尝试加入了一些 Kotlin 的特性,我们只研究现阶段用到的这些 Kotlin 特性。
Tools -> Kotlin -> Show Kotlin Bytecode
一般我们情况下我们只需要查看 Kotlin 等价的 Java 代码即可,因此我们可以在字节码弹窗的左上角找到 Decompile
按钮,这样就能看到 Kotlin 等价的 Java 代码了。
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(); } } } 复制代码
这一点在 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() }) ... } } 复制代码
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) } 复制代码
由于 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(); } } 复制代码
不少博客讲伴生对象
到这里就结束了。@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 是顶层常量。 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。
最佳实践
不一定对(包括本文),要独立思考本文只是借助我们的 Demo 一窥 Kotlin 编程的三重境界,让大家对 Kotlin 编程整体有个了解。后面我也许会写专题文章来讲《Kotlin 编译器漫游指南》,《Kotlin 最佳实践指北》,也许吧。
文章写到这已经接近尾声了,那我们的 Demo 工程改到这个程度是不是已经完美了呢?当然没有。但我不想写了,欢迎各位小伙伴留言一起讨论还有哪些地方能改进。
我们下一篇文章再见。
回目录-->【Kotlin Jetpack 实战】传送门