阅读 580

Kotlin基础:抽象属性的应用场景

这是该系列的第五篇,系列文章目录如下:

  1. Kotlin基础:白话文转文言文般的Kotlin常识

  2. Kotlin基础:望文生义的Kotlin集合操作

  3. Kotlin实战:用实战代码更深入地理解预定义扩展函数

  4. Kotlin实战:使用DSL构建结构化API去掉冗余的接口方法

  5. Kotlin基础:属性也可以是抽象的

  6. Kotlin进阶:动画代码太丑,用DSL动画库拯救,像说话一样写代码哟!

  7. Kotlin基础:用约定简化相亲

在编程中,抽象意味着“类是部分实现的”。部分实现的类只有等完全实现后才能实例化。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用于声明一个类的同时构造一个实例。

关注下面的标签,发现更多相似文章
评论