這是該系列的第五篇,系列文章目錄如下:
在程式設計中,抽象意味著“類是部分實現的”。部分實現的類只有等完全實現後才能例項化。Java 中可以將方法設定為抽象的。Kotlin 更上一層樓:屬性也可以是抽象的。
引子
寫程式碼會遇到這樣的場景:類中包含了若干屬性,其中有一些屬性是構造類時必須的,通常會通過建構函式的引數將這些屬性值傳遞進來。另一些屬性雖然在構造時非必須但在稍後的時間點會用到它,通常會用set()函式來為這些屬性賦值。
如果忘記呼叫 set() 會發生什麼?程式會出錯甚至崩潰,這很常見,特別是當別人使用你的類時,他並不知道除了構造物件之外還需要在另一個地方呼叫 set() 為某個屬性賦值,雖然你可能已經把這個潛規則寫在了註釋裡。
那為什麼不把這類屬性也作為建構函式的引數傳入?因為構造的時候屬性值還未準備好。那等它好了在構造物件不行嗎?也不是不可以,這樣就延後了物件的構建。
有什麼辦法強制使用者必須為該屬性賦值呢?
抽象屬性
在 Java 中類有抽象方法,在構造類物件時強制要求實現,即強制為行為賦值。但 Java 中沒有強制為屬性賦值的特性。Kotlin 的抽象屬性填補了這個空白。
抽象屬性的語法如下:
abstract class A{
abstract val name: String
}
複製程式碼
只需要在宣告變數的關鍵詞val
之前加上abstract
,因為屬性是抽象的,所以整個類也變成抽象的。
為了展示抽象屬性的使用場景,設計瞭如下這個case:有一個列表用於展示新聞,列表背景會動態變化,比如夏天展示清爽的背景,某地發生地震時展示黑色的背景。
顯然,除了新聞內容,列表背景顏色也得從伺服器拉取,如果列表內容先返回則按預設背景色展示列表,當背景顏色返回時重新整理下列表。
先定義兩個資料實體類用於存放伺服器返回的資料:
//'列表內容'
data class MyBean(val name:String?)
//'列表背景'
data class ColorBean(val color:String?)
複製程式碼
列表的資料介面卡 Adapter 包含兩個屬性:內容列表和背景顏色,前者是構造時必須引數,後者是非必須的,將非必須的實現為抽象屬性:
//'內容列表是構造時必要屬性'
abstract class MyAdapter(private val myBean: List<MyBean>?) : RecyclerView.Adapter<MyViewHolder>() {
//'背景顏色是抽象屬性'
abstract val color: ColorBean?
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.my_viewholder, parent, false))
}
override fun getItemCount(): Int { return myBean?.size ?: 0 }
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
//'將內容列表和背景色傳遞給Holder繫結到控制元件'
myBean?.get(position)?.let { holder.bind(it,color) }
}
}
複製程式碼
在 Holder 中若存在顏色屬性則替換列表背景,否則保持其為 xml 定義的顏色:
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(myBean: MyBean?, colorBean: ColorBean?) {
itemView.apply {
colorBean?.run { myBackground.setBackgroundColor(Color.parseColor(color)) }
tvMyViewHolder.text = myBean?.name ?: "no name"
}
}
}
複製程式碼
使用 ViewModel + LiveData 存放伺服器返回資料:
class MyViewModel : ViewModel() {
//'列表內容'
internal val beanLiveData = MutableLiveData<List<MyBean>>()
//'列表背景色'
internal val colorLiveData = MutableLiveData<ColorBean>()
fun fetchBean() {
//省略了拉取伺服器資料
beanLiveData.postValue(value)
}
fun fetchColor() {
//省略了拉取伺服器資料
colorLiveData.postValue(value)
}
}
複製程式碼
Activity 作為資料的觀察者:
class MyActivity : AppCompatActivity() {
private val viewModel by lazy { ViewModelProviders.of(this).get(MyViewModel::class.java) }
private var myAdapter: MyAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.my_activity)
registerObserver()
viewModel.fetchBean()
viewModel.fetchColor()
}
private fun registerObserver() {
viewModel.colorLiveData.observe(this@OverridePropertyActivity, Observer {
//'獲取背景色後重新整理列表'
myAdapter?.notifyDataSetChanged()
})
viewModel.beanLiveData.observe(this@OverridePropertyActivity, Observer {
//'獲取列表內容後構建列表介面卡例項'
myAdapter = object : MyAdapter(it) {
//'重寫屬性'
override val color: ColorBean?
//'color的值從colorLiveData中獲取'
get() = viewModel.colorLiveData.value
}
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = myAdapter
})
}
}
複製程式碼
MyAdapter 是抽象的,在構造例項時得重寫其抽象屬性color
,它是常量,所以只需定義如何獲取屬性,即實現get()
函式,如果是變數還必須定義set()
,就像這樣:
myAdapter = object : MyAdapter(it) {
override var color: ColorBean?
get() = viewModel.colorLiveData.value
set(value) { viewModel.colorLiveData.postValue(value) }
}
複製程式碼
object
其中的object
關鍵詞有很多種用法,它們的共性是“宣告一個類的同時建立一個例項”,文中的用法叫物件表示式,這等同於 Java 中的匿名物件。下面這兩段程式碼是等價的:
//'java'
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.v(...)
}
});
//'kotlin'
view.setOnClickListener(object : View.OnClickListener{
override fun onClick(v: View?) {
Log.v(...)
}
})
//'kotlin中通常會採用這種更簡單的方式'
view.setOnClickListener { v -> Log.v(...) }
複製程式碼
總結
抽象屬性通過關鍵詞abstract
宣告,使用抽象屬性好處多多:
- 第一個好處是強制性,它強制在構建物件時必須定義
如何獲取值
及如何改變值
。 - 第二個好處是解耦,文中 Adapter 中背景色的值來自於
ViewModel
中的LiveData
,但 Adapter 沒有和它們倆耦合。(好吧,java 通過依賴注入也可以實現這個效果) - 第三個好處是惰性載入,只有當背景色被引用的時候才會去呼叫其
get()
方法為屬性賦值,而 java 中呼叫set()
賦值的時機肯定遭遇屬性值被訪問的計時。
關鍵詞object
用於宣告一個類的同時構造一個例項。