原文:Exploring View Binding in Depth — Using ViewBinding with < include>, < merge>, adapters, fragments, and activities
作者:Somesh Kumar
譯者:fly_with24
谷歌在2019 I/O 大會中的 What’s New in Architecture Components 介紹了 view binding
在 What’s New in Architecture Components 中,有一個簡短的關於view binding 的演講,演講中將 view binding 與現有解決方案進行了比較,並進一步討論了爲什麼view binding 比 data binding
或 Kotlin synthetics
等現有解決方案更好。
對我而言,Kotlin synthetics
運行良好,但是沒有編譯時的安全性,這意味着所有 ID 都位於全局命名空間中。因此,如果您使用的 ID 具有相同的名稱,並且從錯誤的佈局導入 ID, 由於ID不是當前佈局的一部分,導致崩潰,除非您將應用程序運行到該佈局,否則無法提前知道這一點。
這篇文章很好地概述了 Kotlin synthetics
的問題
The Argument Over Kotlin Synthetics
View Binding
將在 Android Studio 3.6 穩定版中提供(譯者注:當前Android Studio穩定版版本爲3.5.3),如果您想要使用它,您可以下載 Android Studio 3.6 RC3 或者 Android Studio 4.0 Canary 9
view binding 的主要優點是所有綁定類都是由Gradle插件生成的,因此它對構建時間沒有影響,並且具有編譯時安全性(我們將在示例中看到)。
首先,啓用 view binding, 我們需要在 module 的build.gradle文件中添加以下內容:
// Android Studio 3.6
android {
viewBinding {
enabled = true
}
}
// Android Studio 4.0
android {
buildFeatures {
viewBinding = true
}
}
注意:視圖綁定是逐模塊啓用的,因此,如果您具有多模塊項目設置,則需要在每個 build.gradle
文件中添加以上代碼。
如果要在特定的佈局禁用 view binding,則需要在佈局文件的根視圖中添加 tools:viewBindingIgnore = “true”
。
啓用後,我們可以立即開始使用它,並且當您完成同步 build.gradle 文件時,默認情況下會生成所有綁定類。
它通過將XML佈局文件名轉換爲駝峯式大小寫並在其末尾添加 Binding
來生成綁定類。 例如,如果您的佈局文件名爲 activity_splash
,則它將生成綁定類爲 ActivitySplashBinding
。
如何使用它?
activity 中使用
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvVersionName.text = getString(R.string.version)
}
我們有一個名爲 activity_splash
的佈局文件,裏面有一個ID爲 tvVersionName
的 TextView
,因此在使用view binding 時,我們要做的就是獲取綁定類的引用,例如:
val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater)
在 setContentView()
方法中使用 getRoot()
,該方法將返回佈局的根佈局。可以從我們創建的綁定類對象訪問視圖,並且可以在創建對象後立即使用它,如下所示:
binding.tvVersionName.text = getString(R.string.version)
在這裏,綁定類知道 tvVersionName
是TextView
,因此我們不必擔心類型轉換。
fragment 中使用
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
_binding = null
}
}
在 fragment 中,使用 view binding 有些不同。 我們需要傳遞 LayoutInflator
,ViewGroup
和一個 attachToRoot
布爾變量,這些變量是通過覆蓋 onCreateView
獲得的。
我們可以通過調用 binding.root
返回 view。您還注意到,我們使用了兩個不同的變量 binding
和 _binding
,並且 _binding
變量在 onDestroyView()
中設置爲null。
這是因爲該 fragment 的生命週期與 activity 的生命週期不同,並且該fragment 可以超出其視圖的生命週期,因此如果不將其設置爲null,則可能會發生內存泄漏。
另一個變量通過 !!
使一個變量爲可空值而使另一個變量爲非空值避免了空檢查。 。
在 RecyclerView adapter 中使用
class PaymentAdapter(private val paymentList: List<PaymentBean>) : RecyclerView.Adapter<PaymentAdapter.PaymentHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentHolder {
val itemBinding = RowPaymentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PaymentHolder(itemBinding)
}
override fun onBindViewHolder(holder: PaymentHolder, position: Int) {
val paymentBean: PaymentBean = paymentList[position]
holder.bind(paymentBean)
}
override fun getItemCount(): Int = paymentList.size
class PaymentHolder(private val itemBinding: RowPaymentBinding) : RecyclerView.ViewHolder(itemBinding.root) {
fun bind(paymentBean: PaymentBean) {
itemBinding.tvPaymentInvoiceNumber.text = paymentBean.invoiceNumber
itemBinding.tvPaymentAmount.text = paymentBean.totalAmount
}
}
}
row_payment.xml
是我們用於 RecyclerView
item 的佈局文件,對應生成的綁定類 RowPaymentBinding
。
現在,我們所需要做的就是在onCreateViewHolder()
中調用 inflate()
方法生成 RowPaymentBinding
對象並傳遞到 PaymentHolder
主構造器中,並將 itemBinding.root
傳遞給 RecyclerView .ViewHolder()
構造函數。
處理<include>
標籤
view binding 可以與 <include>
標籤一起使用。 佈局中通常包含兩種 <include>
標籤,帶或不帶<merge>
標籤。
<inlude>
不帶<merge>
標籤
我們需要爲<include>
分配一個 ID,然後使用該 ID 來訪問包含佈局中的視圖。讓我們來看一個例子。
app_bar.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:background="?colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
main_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/appbar"
layout="@layout/app_bar"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
在上面的代碼中,我們在佈局文件中包括了一個通用工具欄,<include>
有一個 android:id=“@+id/appbar”
ID,我們將使用它從 app_bar.xml
中訪問工具欄並將其設置爲我們的 action bar
。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MainLayoutBinding = MainLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.appbar.toolbar)
}
<inlude>
帶<merge>
標籤
當在一個佈局中包含另一個佈局時,我們通常使用一個帶有 <merge>
標記的佈局,這有助於消除佈局嵌套。
placeholder.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/tvPlaceholder"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</merge>
fragment_order.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/placeholder" />
</androidx.constraintlayout.widget.ConstraintLayout>
如果我們嘗試爲該 <include>
提供ID,view binding 不會在綁定類中生成ID,因此我們無法像使用普通 include
那樣訪問視圖。
在這種情況下,我們有 PlaceholderBinding
,它是 placeholder.xml
(<merge>
佈局文件)的自動生成的類。我們可以調用其bind()
方法並傳遞包含它的佈局的根視圖。
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentOrderBinding.inflate(layoutInflater, container, false)
placeholderBinding = PlaceholderBinding.bind(binding.root)
placeholderBinding.tvPlaceholder.text = getString(R.string.please_wait)
return binding.root
}
然後,我們可以從我們的類(如 placeholderBinding.tvPlaceholder.text
)訪問 placeholder.xml
內部的視圖。
感謝閱讀。希望收到您的評論。
譯者補充
在 fragment 中使用 view binding 比較麻煩,譯者提供一個 BaseFragment
的封裝供大家參考
abstract class BaseFragment<T : ViewBinding>(layoutId: Int) : Fragment(layoutId) {
private var _binding: T? = null
val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = initBinding(view)
init()
}
/**
* 初始化 [_binding]
*/
abstract fun initBinding(view: View): T
abstract fun init()
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
class HomeFragment : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) {
override fun initBinding(view: View): FragmentHomeBinding = FragmentHomeBinding.bind(view)
override fun init() {
binding.viewPager.adapter = SectionsPagerAdapter(this)
TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
tab.text = TAB_TITLES[position]
}.attach()
}
}
關於我
我是 fly_with24