公司剛來了一個小夥伴,名叫小白
,剛畢業的小夥子,這天茶餘飯後,聊天聊起了代碼複用的問題。確實,代碼複用,可以說是我們每一個有理想的程序員的追求。於是想借機考考他。
我
:說到代碼複用,那!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編程技術~"
}
}
然後運行一下,效果如下:
然後,在其他需要的頁面,如MyFragment2
、MyFragment3
,按照前面的步驟,引入佈局
,綁定數據
,就好了。
非常簡單,5分鐘就寫好了。小白
略帶微笑的說到。
我
:嗯,小夥子不錯不錯,這樣確實可以,佈局文件
確實複用了,但是你看看你的Fragment啊,比如我有4個Fragment,MyFragment1
、MyFragment2
,MyFragment3
、MyFragment4
,那其實我每個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" />
</LinearLayout>
然後在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了?妙啊!
我也去寫一個來試試!
最新的Android核心知識點↓↓↓
更多資源:在此處