Kotlin基礎:抽象屬性的應用場景

唐子玄發表於2019-07-08

這是該系列的第五篇,系列文章目錄如下:

  1. Kotlin基礎:白話文轉文言文般的Kotlin常識

  2. Kotlin基礎:望文生義的Kotlin集合操作

  3. Kotlin實戰:用實戰程式碼更深入地理解預定義擴充套件函式

  4. Kotlin實戰:使用DSL構建結構化API去掉冗餘的介面方法

  5. Kotlin基礎:屬性也可以是抽象的

  6. Kotlin進階:動畫程式碼太醜,用DSL動畫庫拯救,像說話一樣寫程式碼喲!

  7. Kotlin基礎:用約定簡化相親

  8. Kotlin基礎 | 2 = 12 ?泛型、類委託、過載運算子綜合應用

在程式設計中,抽象意味著“類是部分實現的”。部分實現的類只有等完全實現後才能例項化。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宣告,使用抽象屬性好處多多:

  1. 第一個好處是強制性,它強制在構建物件時必須定義如何獲取值如何改變值
  2. 第二個好處是解耦,文中 Adapter 中背景色的值來自於ViewModel中的LiveData,但 Adapter 沒有和它們倆耦合。(好吧,java 通過依賴注入也可以實現這個效果)
  3. 第三個好處是惰性載入,只有當背景色被引用的時候才會去呼叫其get()方法為屬性賦值,而 java 中呼叫set()賦值的時機肯定遭遇屬性值被訪問的計時。

關鍵詞object用於宣告一個類的同時構造一個例項。

相關文章