[譯]深入研究 ViewBinding 在 < include>, < merge>, adapter, fragment, 和 activity 中使用View Binding

原文:Exploring View Binding in Depth — Using ViewBinding with < include>, < merge>, adapters, fragments, and activities

作者:Somesh Kumar

譯者:fly_with24

Image Source: Google I/O 2019
谷歌在2019 I/O 大會中的 What’s New in Architecture Components 介紹了 view binding

What’s New in Architecture Components 中,有一個簡短的關於view binding 的演講,演講中將 view binding 與現有解決方案進行了比較,並進一步討論了爲什麼view binding 比 data bindingKotlin 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爲 tvVersionNameTextView ,因此在使用view binding 時,我們要做的就是獲取綁定類的引用,例如:

val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater) 

setContentView() 方法中使用 getRoot() ,該方法將返回佈局的根佈局。可以從我們創建的綁定類對象訪問視圖,並且可以在創建對象後立即使用它,如下所示:

binding.tvVersionName.text = getString(R.string.version)

在這裏,綁定類知道 tvVersionNameTextView,因此我們不必擔心類型轉換。

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 有些不同。 我們需要傳遞 LayoutInflatorViewGroup和一個 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

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