啥?ViewBinding還能替換自定義View!

公司剛來了一個小夥伴,名叫小白,剛畢業的小夥子,這天茶餘飯後,聊天聊起了代碼複用的問題。確實,代碼複用,可以說是我們每一個有理想的程序員的追求。於是想借機考考他。

我:說到代碼複用,那!Android開發中,佈局該如何複用呢?

比如,像下面所示的這樣一個卡片設計,很多頁面都有用到,不可能每個頁面都去寫一遍吧?如何能很好的實現複用呢?

小白:西哥,你這個問題也太簡單了,雖然我才學Android不久,但是這個我還是知道的,我們都知道,Android 佈局中,有個一個<include /> 標籤,可以飲用一個佈局。我們可以把這個複用的卡片寫成一個單獨的佈局,然後在每個頁面使用<include />包含進來就好了呀!

於是二話沒說,就是幹,馬上就開始寫起了代碼!

首先,抽出一個公共的佈局叫card_item.xml代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardCornerRadius="5dp"
    android:layout_margin="10dp"
    app:cardElevation="2dp">
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <ImageView
        android:id="@+id/avatar"
        android:layout_width="80dp"
        android:layout_height="90dp"
        android:src="@mipmap/logo"
        android:scaleType="centerCrop"
        android:layout_centerVertical="true"
        android:layout_marginLeft="15dp"
        />
    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#333"
        android:textSize="18sp"
        android:layout_toRightOf="@+id/avatar"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="10dp"
        />
    <TextView
        android:id="@+id/des"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#999"
        android:textSize="12sp"
        android:layout_below="@+id/name"
        android:layout_toRightOf="@+id/avatar"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="10dp"
        />
</RelativeLayout>
</androidx.cardview.widget.CardView>

接着,在每一個使用該卡片設計的地方,使用<include /> 標籤將card_item.xml佈局引入進來。新建佈局文件fragment.xml,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/card_item" />

</LinearLayout>

然後新建一個Fragment,名叫MyFragment,代碼如下:

class MyFragment: Fragment() {
    private lateinit var avatar: ImageView
    private lateinit var name: TextView
    private lateinit var desc: TextView

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.my_fragment,container,false)
        avatar = view.findViewById(R.id.avatar)
        name = view.findViewById(R.id.name)
        desc = view.findViewById(R.id.des)
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        avatar.setImageResource(R.mipmap.logo)
        name.text = "技術最TOP"
        desc.text = "扒最前沿科技動態,聊最TOP編程技術~"
    }
}

然後運行一下,效果如下:

然後,在其他需要的頁面,如MyFragment2MyFragment3,按照前面的步驟,引入佈局綁定數據,就好了。

非常簡單,5分鐘就寫好了。小白略帶微笑的說到。

我:嗯,小夥子不錯不錯,這樣確實可以,佈局文件確實複用了,但是你看看你的Fragment啊,比如我有4個Fragment,MyFragment1MyFragment2,MyFragment3MyFragment4,那其實我每個Fragment中的大部分代碼都是相同的。

如下:

    // 聲明View
    private lateinit var avatar: ImageView
    private lateinit var name: TextView
    private lateinit var desc: TextView

    // 綁定View
    val view = inflater.inflate(R.layout.my_fragment1,container,false)
    avatar = view.findViewById(R.id.avatar)
    name = view.findViewById(R.id.name)
    desc = view.findViewById(R.id.des)
    
    // 綁定數據
    avatar.setImageResource(R.mipmap.logo)
    name.text = "技術最TOP"
    desc.text = "扒最前沿科技動態,聊最TOP編程技術~"

上面這些樣板代碼看起來很難受啊,每個頁面都要這樣寫,並且後期不好維護,比如,我CardView 裏面新增加一個View,那麼這些用到的頁面都得改。有沒有辦法能把這些樣板代碼也一起復用呢?

小白有點迷惑,用手撓撓頭,若有所思。

自定義View包裝

不一會兒,小白大叫一聲,我有辦法了!

小白:我們可以藉助自定義View來封裝一下,我們把Fragment中的樣板代碼,抽到一個View 中去,然後提供一個API方法給外部來設置數據,每個使用的地方,將<include /> 引入的佈局換成自定以的View, 然後在Fragment中調用API設置數據就可以了。

小白一臉自豪,說幹就幹,又開始重構前面的代碼。

首先,將樣板代碼抽取一個View名叫CardItem,將聲明View、綁定View、綁定數據的邏輯都放在這裏,代碼如下:

class CardItem @JvmOverloads constructor(
    context: Context, attributes: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attributes, defStyleAttr) {

    private  var ivAvatar: ImageView
    private  var tvName: TextView
    private  var tvDesc: TextView

    init {
        val view = LayoutInflater.from(context).inflate(R.layout.card_item,null,false)
        ivAvatar = view.findViewById(R.id.avatar)
        tvName = view.findViewById(R.id.name)
        tvDesc = view.findViewById(R.id.des)

        addView(view)
    }

    fun setData(imageAvatarRes: Int, name: String, desc: String) {
        ivAvatar.setImageResource(imageAvatarRes)
        tvName.text = name
        tvDesc.text = desc
    }
}

如上面代碼所示,我們提供了一個方法setData來綁定數據。

然後使用的地方,先替換佈局文件的<include /> ,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

<!--    <include layout="@layout/card_item" />-->

    <com.jay.jetpack.viewbinding.CardItem
        android:id="@+id/card_item"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</Line

然後在Fragment中,調用setData綁定數據

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        cardItem.setData(imageAvatarRes = R.mipmap.logo,name="技術最TOP",desc = "扒最前沿科技動態,聊最TOP編程技術~")
    }

運行一下代碼,效果如下圖所示:

才過10分鐘,小白就把代碼重構好了。

我: 不錯不錯,小夥子,這種方案很好,幾乎大部分代碼都公用了。

但是不夠完美,有一個小問題,你看這個自定義View類,裏面同樣是很多樣板代碼,如果我們又有另一個佈局需要公用,那麼我可能就需要在添加一個自定義View,把CardItem裏面的代碼拷貝過去,然後改吧改吧,改成對應的佈局和View,當項目越來越大的時候,這種自定義View可能就越多。但是他們的大部分代碼其實是相同的。

有沒有辦法能夠解決這個問題,把這裏面的樣板代碼也消除?

小白又陷入了沉思!

小白:這我真不知道了,還有什麼辦法?西哥給我講講唄。

:你又聽說過ViewBinding嗎?

小白:聽過聽過!就是Google 最新出的Jetpack組件嘛,江湖上聲稱幹掉findViewById,取代黃油刀ButterKnife的大殺器。

:對,就是這個,我們可以用這個,加上Kotlin 的特性來做更完美的優化。

ViewBingding的救贖

ViewBinding是Jetpack中新添加的組件,首先,在build.gradle中開啓:

 viewBinding {
        enabled = true
 }

開啓ViewBinding後,他會自動幫我的佈局生成對用的類,比如我們上面的card_item.xml,會給我生成一個CardItemBinding.java類,my_fragment2.xml會生成MyFragment2Binding.java,生成規則爲:佈局文件的名字去掉下劃線 + Binding後綴,以駝峯的形式。如下:

首先,把佈局中的<CardItem /> 換成 <include /> 標籤。代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include android:id="@+id/topCard" layout="@layout/card_item"/>

</LinearLayout>

然後,我們就可以不用findViewById()來綁定View了,可以直接使用xxBinding類訪問View,Fragment代碼如下:

class MyFragment2: Fragment(R.layout.my_fragment2) {
    private lateinit var binding: MyFragment2Binding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = MyFragment2Binding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.topCard.apply {
            avatar.setImageResource(R.mipmap.logo)
            name.text="技術最TOP"
            des.text = "扒最前沿科技動態,聊最TOP編程技術。"
        }
    }
}

這樣,我們就把一個幾十行代碼的自定義View類,變成了4代碼,是不是就非常爽了。先別高興,還有點問題,雖然我們去掉了樣板代碼,但是還是存在我們最初的那個問題,那就是,如果複用的佈局增加或者減少View的話,那麼在每個調用的地方都要更改。 這可不是我們想要的,怎麼解決這個問題呢?

還好有Kotlin,我們可以用Kotlin的擴展函數來優化!

Kotlin擴展函數 + ViewBinding

我們把綁定數據的那一段代碼,抽一個擴展函數:

fun CardItemBinding.bind(imageResId: Int,nameStr: String, descStr: String){
        avatar.setImageResource(imageResId)
        name.text = nameStr
        des.text = descStr
}

我們在CardItemBinding上擴展了一個bind方法。

現在我們如何調用了?下面這樣:

 binding.topCard.bind(imageResId = R.mipmap.logo,
            nameStr = "技術最TOP Super",
            descStr = "扒最前沿科技動態,聊最TOP編程技術。Super")

運行一下,效果如下:

完美實現,我們把自定義View,替換成了一個ViewBinding的擴展函數,代碼從原來的33行,減少到了現在的4行。

後期維護也很方便,增加減少View,直接在擴展方法裏面更改就好。

並且,如果還有其他的複用佈局,我們再添加一個擴展方法就好了,這就非常爽了!

小白:啥?等於說,利用Kotin + ViewBinding 可以替換自定義View了?妙啊!

我也去寫一個來試試!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章