kotlin-android-extensions插件也被廢棄了?扶我起來

本文同步發表於我的微信公衆號,掃一掃文章底部的二維碼或在微信搜索 郭霖 即可關注,每個工作日都有文章更新。

kotlin-android-extensions插件可能算得上是我最喜歡的一個Kotlin在Android上的特性了。

這麼說並不誇張,因爲以前在使用Java開發Android程序時,我們總是要寫一大堆的findViewById,枯燥又沒什麼意義。

雖然也有一些諸如ButterKnife之類的第三方庫,專門用於對findViewById的用法進行簡化,但是ButterKnife還是要通過註解來讓控件與資源id之間進行綁定,並不算是非常方便。

而kotlin-android-extensions插件的出現則讓這一情況完全發生了改變,我們可以不用再編寫煩瑣的findViewById代碼,同時能用一種非常簡便的寫法進行替代。

比如說這裏有一個佈局文件activity_main.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/viewToShowText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

非常簡單,佈局文件中只有一個TextView控件,它的id是viewToShowText。

那麼,如果我想要在MainActivity中去設置TextView控件的內容,使用Java語言的話通常需要這樣寫:

public class MainActivity extends AppCompatActivity {
   
   

    @Override
    protected void onCreate(Bundle savedInstanceState) {
   
   
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView viewToShowText = findViewById(R.id.viewToShowText);
        viewToShowText.setText("Hello");
    }
    
}

可以看到,這裏我們首先通過findViewById()函數獲取到了TextView控件的實例,然後再調用setText()函數將其顯示的內容設置成Hello。

這個findViewById()函數其實是很頭疼的,這裏我們只是獲取了一個控件的實例,所以可能感受還不太明顯。如果你要去獲取10個甚至100個控件的實例,每個都要去findViewById一遍,你一定會抓狂的。

那麼如果是使用Kotlin語言的話,這個問題要怎麼解決呢?藉助kotlin-android-extensions插件,我們可以使用如下代碼來完成同樣的功能:

class MainActivity : AppCompatActivity() {
   
   

    override fun onCreate(savedInstanceState: Bundle?) {
   
   
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewToShowText.text = "Hello"
    }
    
}

可以看到,這裏我們不再需要調用findViewById()函數去獲取控件的實例,而是直接調用該控件在xml中定義的id名稱,就能夠設置其顯示的內容了。

而這個神奇的功能就是由kotlin-android-extensions插件自動完成的,這個插件能夠幫助我們減少大量瑣碎無意義的代碼。


然而它被廢棄了

其實早在幾個月前,就有朋友在公衆號上詢問我,說自己升級了Android Studio 4.1之後,發現新建項目的時候Android Studio已經不會自動幫我們引入kotlin-android-extensions插件了,需要自己手動去添加才能使用,是不是Google不再推薦使用這個插件了?

當時我還說,不可能呀,這個插件這麼好用,而且Kotlin也是Google未來主推的技術,可能只是Android Studio 4.1的bug吧。

然而,沒過多久我就被打臉了。某天我將項目工程的Gradle版本升級到了最新,然後構建項目時發現了這樣一個警告提示:

圖1

Google明確地告訴我們,kotlin-android-extensions插件已被廢棄,現在推薦使用ViewBinding來進行替代。

對於Google的這種技術迭代頻率我是有點生氣的,如果kotlin-android-extensions插件是Google主推的技術,理應擁有更長的生命週期,不然的話就不該作爲默認插件 集成到Android Studio當中。要知道,去年我纔剛剛出版的新書《第一行代碼 第3版》裏還大量使用了這個技術。

不過,好在ViewBinding並不複雜,從kotlin-android-extensions插件切換到ViewBinding也是比較容易的,那麼本篇文章就作爲《第一行代碼 第3版》的另外一篇DLC,向大家介紹一下,如何使用ViewBinding來替代kotlin-android-extensions插件。


爲什麼會被廢棄

在開始介紹ViewBinding之前,我還是想先討論一下,爲什麼kotlin-android-extensions插件會被廢棄。

雖說Google的技術迭代頻率常常會讓我們直呼學不動了,但是Google也絕對不會無緣無故去廢棄一個之前主推的技術,說明kotlin-android-extensions插件肯定還是存在問題的。

那麼到底存在什麼問題呢?

比較容易讓人想到的一個缺點就是,kotlin-android-extensions插件只能支持Kotlin語言,而無法支持Java語言。當然這個我認爲並不是主要原因,因爲現在Google開發的各種新技術都在全面兼容Kotlin,而不再怎麼去考慮Java了,如協程、Jetpack Compose等。

那麼主要原因是什麼呢?這可能就要從kotlin-android-extensions插件的實現原理去理解了。剛纔我們已經看到過了使用kotlin-android-extensions插件後的代碼,非常簡單:

class MainActivity : AppCompatActivity() {
   
   

    override fun onCreate(savedInstanceState: Bundle?) {
   
   
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewToShowText.text = "Hello"
    }
    
}

那麼這段代碼爲什麼可以工作呢?

我們可以通過點擊Android Studio頂部導航欄的Tools -> Kotlin -> Show Kotlin Bytecode來查看這段代碼對應的Kotlin字節碼,然後在彈出窗口中點擊Decompile按鈕將字節碼反編譯成Java代碼。

爲了方便閱讀,我將反編譯後的代碼又做了些整理,大致如下所示:

public final class MainActivity extends AppCompatActivity {
   
   
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
   
   
      super.onCreate(savedInstanceState);
      this.setContentView(1300023);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.textView);
      var10000.setText((CharSequence)"Hello");
   }

   public View _$_findCachedViewById(int var1) {
   
   
      if (this._$_findViewCache == null) {
   
   
         this._$_findViewCache = new HashMap();
      }
      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
   
   
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }
      return var2;
   }
}

可以看到,實際上kotlin-android-extensions插件會幫我們生成一個_$_findCachedViewById()函數(使用這種奇怪的命名方式是爲了防止和開發者定義的函數名衝突)。在這個函數中首先會嘗試從一個HashMap中獲取傳入的資源id參數所對應的控件實例緩存,如果還沒有緩存的話,就調用findViewById()函數來查找控件實例,並寫入HashMap緩存當中。這樣當下次再獲取相同控件實例的話,就可以直接從HashMap緩存中獲取了。

這就是kotlin-android-extensions插件的實現原理,其實還是非常簡單的。

然而這種實現原理同時也暴露出來了一些問題。

比如說每一個Activity都需要使用一個額外的HashMap數據結構來存儲所有控件的實例,無形中增加了一些內存的開支。

還有,雖然HashMap是一種O(1)時間複雜度的數據結構,但這畢竟只是理論上的時間複雜度,實際調用肯定是沒有直接訪問控件實例快的,因此kotlin-android-extensions插件也在無形當中降低了程序的運行效率。

最重要的是,這些內容對於絕大部分開發者來說都是黑盒,使用kotlin-android-extensions插件的人可能並不知道這些隱藏的“坑”,這個問題在稍後介紹RecyclerView Adapter的時候會更加突顯,我們待會兒再聊。

不管我上面分析的這些足不足以成爲廢棄kotlin-android-extensions插件的理由,總之這已經是事實了。那麼接下來,我們的學習目標就變成了:如何使用ViewBinding來替代之前的kotlin-android-extensions插件。請放心,這並不是一件很難的事情。


什麼是ViewBinding

ViewBinding總體來說其實非常簡單,它的目的只有一個,就是爲了避免編寫findViewById,這和它另外一個非常複雜的兄弟DataBinding相比有明顯的區別。

要想使用ViewBinding需要注意兩件事。第一,確保你的Android Studio是3.6或更高的版本。第二,在你項目工程模塊的build.gradle中加入以下配置:

android {
   
   
    ...
    buildFeatures {
   
   
        viewBinding true
    }
}

這樣準備工作就完成了。接下來我會從Activity、Fragment、Adapter、引入佈局這4個方面,分別討論ViewBinding的用法。


在Activity中使用ViewBinding

一旦啓動了ViewBinding功能之後,Android Studio會自動爲我們所編寫的每一個佈局文件都生成一個對應的Binding類。

Binding類的命名規則是將佈局文件按駝峯方式重命名後,再加上Binding作爲結尾。

比如說,前面我們定義了一個activity_main.xml佈局,那麼與它對應的Binding類就是ActivityMainBinding。

當然,如果有些佈局文件你不希望爲它生成對應的Binding類,可以在該佈局文件的根元素位置加入如下聲明:

<LinearLayout
    xmlns:tools="http://schemas.android.com/tools"
    ...
    tools:viewBindingIgnore="true">
    ...
</LinearLayout>

接下來我們看一下如何使用ViewBinding來實現在MainActivity中去設置TextView內容的功能,代碼如下所示:

class MainActivity : AppCompatActivity() {
   
   

    override fun onCreate(savedInstanceState: Bundle?) {
   
   
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.textView.text = "Hello"
    }

}

ViewBinding的用法可以說就是這麼簡單。首先我們要調用activity_main.xml佈局文件對應的Binding類,也就是ActivityMainBinding的inflate()函數去加載該佈局,inflate()函數接收一個LayoutInflater參數,在Activity中是可以直接獲取到的。

接下來就更加簡單了,調用Binding類的getRoot()函數可以得到activity_main.xml中根元素的實例,調用getTextView()函數可以獲得id爲textView的元素實例。

那麼很明顯,我們應該把根元素的實例傳入到setContentView()函數當中,這樣Activity就可以成功顯示activity_main.xml這個佈局的內容了。然後獲取TextView控件的實例,並給它設置要顯示的文字即可。

當然,如果你需要在onCreate()函數之外的地方對控件進行操作,那麼就得將binding變量聲明成全局變量,寫法如下:

class MainActivity : AppCompatActivity() {
   
   

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
   
   
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.textView.text = "Hello"
    }

}

注意,Kotlin聲明的變量都必須在聲明的同時對其進行初始化。而這裏我們顯然無法在聲明全局binding變量的同時對它進行初始化,所以這裏又使用了lateinit關鍵字對binding變量進行了延遲初始化。

雖然這裏我舉的例子非常簡單,但實際上ViewBinding用法的套路都是如此,掌握了這一套規則之後基本上你就可以舉一反三了。


在Fragment中使用ViewBinding

下面我們學習一下,如何在Fragment中使用ViewBinding。這部分內容同樣非常簡單,因爲在Fragment中使用ViewBinding和在Activity基本是一樣的。

這裏我還是通過代碼的方式進行演示,順便介紹一下Fragment與Activity中ViewBinding用法的異同。

假設我們有一個佈局文件叫fragment_main.xml,那麼啓用ViewBinding功能之後,則必然會生成一個與其對應的FragmentMainBinding類。

如果我們想要在MainFragment中去顯示這個佈局,就可以這樣寫:

class MainFragment : Fragment() {
   
   

    private var _binding: FragmentMainBinding? = null

    private val binding get() = _binding!!

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

    override fun onDestroyView() {
   
   
        super.onDestroyView()
        _binding = null
    }

}

這段代碼的實際邏輯遠沒有看上去得複雜。

首先最核心的邏輯仍然是調用FragmentMainBinding的inflate()函數去加載fragment_main.xml佈局文件,但由於這是在Fragment當中,所以使用了3個參數的inflate()函數重載,這和我們平時在Fragment中去加載佈局文件的方式如出一轍。

接下來不一樣的地方在於,由於我們是在onCreateView()函數中加載的佈局,那麼理應在與其對應的onDestroyView()函數中對binding變量置空,從而保證binding變量的有效生命週期是在onCreateView()函數和onDestroyView()函數之間。

但由於Kotlin空類型系統的存在,導致爲了實現這一簡單的功能,需要額外編寫一些看上去很奇怪的代碼,上述代碼就是如此。關於Kotlin空類型系統這裏我就不展開介紹了,還不瞭解的朋友可以去參考《第一行代碼 第3版》第2章的內容。

好吧,這是我少有承認Java要比Kotlin更簡潔的地方,因爲使用Java代碼去實現同樣的功能只需要這樣寫:

public class MainFragment extends Fragment {
   
   
    
    private FragmentMainBinding binding;

    @Override
    public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
   
   
        binding = FragmentMainBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onDestroyView() {
   
   
        super.onDestroyView();
        binding = null;
    }
}

這兩段代碼實現的功能以及所想表達的意思是完全相同的,但是明顯Java版本要更加好理解一點。


在Adapter中使用ViewBinding

接下來,讓我們再來探討一下在Adapter中使用ViewBinding的場景,這個場景會相對比較有趣,同時也是之前kotlin-android-extensions插件被誤用比較多的地方。

相信每一位Android開發者都使用過RecyclerView,也都寫過Adapter,Adapter其實是很能考驗一個開發者功底的地方。

我在很早之前面試的時候被問到過,爲什麼我們要在ListView的Adapter當中去寫ViewHolder(那個時候還沒有RecyclerView)。答案就是,爲了不用在列表滾動的時候頻繁調用findViewById(),從而減少了一些沒必要的性能消耗。

而RecyclerView把ListView中的這個普遍應用的最佳實踐直接作爲默認實現集成了進去,所以只要我們使用RecyclerView,就一定要寫ViewHolder。

然而有些朋友在這裏卻存在一些誤用的情況,這裏我還是通過一個具體的示例進行說明。

假設我們定義了fruit_item.xml來作爲RecyclerView子項的佈局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp" />

    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginTop="10dp" />

</LinearLayout>

然後編寫如下RecyclerView Adapter來加載和顯示這個子項佈局:

class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
   
   

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
   
   
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
   
   
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   
   
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text = fruit.name
    }

    override fun getItemCount() = fruitList.size

}

這是比較標準和傳統的一種寫法,並且可以說沒有任何問題,《第一行代碼 第3版》中關於RecyclerView這部分講解也是使用的這種寫法。

然而有些讀者朋友跟我反饋,說這種寫法還要在ViewHolder當中聲明控件變量,還要編寫findViewById(),實在是太複雜了。自己找到了一種更簡單的寫法,只需要藉助kotlin-android-extensions插件,就可以這樣寫:

class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
   
   

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
   
   
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   
   
        val fruit = fruitList[position]
        holder.itemView.fruitImage.setImageResource(fruit.imageId)
        holder.itemView.fruitName.text = fruit.name
    }

    override fun getItemCount() = fruitList.size

}

可以看到,這裏ViewHolder中沒有進行任何控件聲明,相當於只是定義了一個空的ViewHolder。然後在onBindViewHolder()函數當中,直接調用holder.itemView再接上控件id的名稱就可以使用它了。

這種寫法確實簡化了不少代碼,但是這種寫法對嗎?

如果你的評判標準只是這段代碼能不能正常工作,那麼答案是肯定的,這樣寫確實可以正常工作。但是這種寫法我可以說是完全不正確的,爲什麼呢?我們只需要使用剛纔的手法把這段代碼反編譯一下,看看它對應的Java代碼是什麼樣的就知道了。

同樣爲了方便閱讀,我還是對代碼進行了簡化,只保留了關鍵部分,如下所示:

public final class FruitAdapter extends Adapter {
   
   
   ...

   public final class ViewHolder extends androidx.recyclerview.widget.RecyclerView.ViewHolder {
   
   
      public ViewHolder(@NotNull View view) {
   
   
         super(view);
      }
   }

   public void onBindViewHolder(@NotNull FruitAdapter.ViewHolder holder, int position) {
   
   
      Fruit fruit = (Fruit)this.fruitList.get(position);
      View var10000 = holder.itemView;
      ((ImageView)var10000.findViewById(id.fruitImage)).setImageResource(fruit.getImageId());
      var10000 = holder.itemView;
      TextView var4 = (TextView)var10000.findViewById(id.fruitName);
      var4.setText((CharSequence)fruit.getName());
   }

}

不知道你有沒有發現問題,現在onBindViewHolder()函數當中,每次都是調用了findViewById()來獲取控件實例,這樣就導致ViewHolder的作用完全失效了。

所以,上面這種寫法就是kotlin-android-extensions插件在Adapter當中一種比較典型的誤用方式。同時也算是一個隱藏的“坑”,因爲如果你不去將Kotlin代碼進行反編譯,可能都不知道自己的ViewHolder其實根本就沒有起到任何作用。

講完了kotlin-android-extensions插件的“坑”,接下來我們還是看一下如何在Adapter中使用ViewBinding,別忘了我們的目標始終是不寫findViewById。

其實如果你已經熟練掌握了ViewBinding在Activity和Fragment中的用法,那麼現在應該可以舉一反三了,因爲在Adapter中使用ViewBinding基本也是同樣的套路。

我們還是先來看一下代碼,然後我再稍微做下簡單的講解:

class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
   
   

    inner class ViewHolder(binding: FruitItemBinding) : RecyclerView.ViewHolder(binding.root) {
   
   
        val fruitImage: ImageView = binding.fruitImage
        val fruitName: TextView = binding.fruitName
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
   
   
        val binding = FruitItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   
   
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text = fruit.name
    }

    override fun getItemCount() = fruitList.size

}

這段代碼的核心基本都在onCreateViewHolder()函數和ViewHolder當中。

首先,我們在onCreateViewHolder()函數中調用FruitItemBinding的inflate()函數去加載fruit_item.xml佈局文件,這和ViewBinding在Fragment中的用法是一模一樣的。

接下來需要改造ViewHolder,讓其構造函數接收FruitItemBinding這個參數。但是注意,ViewHolder的父類RecyclerView.ViewHolder它只會接收View類型的參數,因此我們需要調用binding.root獲得fruit_item.xml中根元素的實例傳給RecyclerView.ViewHolder。

這樣,我們就不需要再使用findViewById()函數來查找控件實例了,而是調用binding.fruitImage和binding.fruitName就可以直接引用到相應控件的實例。

這就是ViewBinding在Adapter中的用法。


對引入佈局使用ViewBinding

關於ViewBinding的使用其實還有另外一種比較特殊的場景,那就是如何對引入佈局使用ViewBinding。

引入佈局一般有兩種方式,include和merge。關於這兩種方式的用法和區別,我在 Android最佳性能實踐(四)——佈局優化技巧 這篇文章中有比較詳細的講解,還不瞭解的朋友可以去參考一下。

接下來我們開始分別學習如何在include和merge的佈局中使用ViewBinding。

先來看include,這個情況比較簡單。假設我們有如下titlebar.xml佈局,是希望作爲一個通用佈局引入到各佈局當中的:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <Button
        android:id="@+id/back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:text="Back" />
 
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Title"
        android:textSize="20sp" />
 
    <Button
        android:id="@+id/done"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:text="Done" />
 
</RelativeLayout>

那麼如果我想要在activity_main.xml中引入這個佈局,只需要這樣寫:

<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/titlebar" />
    ...
</LinearLayout>

這種寫法雖然的確可以將titlebar.xml引入到activity_main.xml佈局當中,但問題是,你會發現ViewBinding是關聯不到titlebar.xml中的控件的。

那麼如何解決這個問題呢?很簡單,我們只需要在include的時候給被引入的佈局添加一個id,如下所示:

<?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 
        android:id="@+id/titleBar"
        layout="@layout/titlebar" />
    ...
</LinearLayout>

然後,在MainActivity中,我們即可通過如下的寫法引用到titlebar.xml中定義的控件:

class MainActivity : AppCompatActivity() {
   
   

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
   
   
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.titleBar.title.text = "Title"
        binding.titleBar.back.setOnClickListener {
   
   
        }
        binding.titleBar.done.setOnClickListener {
   
   
        }
    }

}

接下來我們再來看一下merge。merge和include最大的區別在於,使用merge標籤引入的佈局在某些情況下可以減少一層佈局的嵌套,而更少的佈局嵌套通常就意味着更高的效率。

比如說我們對titlebar.xml進行如下修改:

<merge xmlns:android="http://schemas.android.com/apk/res/android">
 
    <Button
        android:id="@+id/back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:text="Back" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Title"
        android:textSize="20sp" />

    <Button
        android:id="@+id/done"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:text="Done" />
 
</merge>

可以看到,這裏最外層的佈局使用了merge標籤,這就表示當有任何一個地方去include這個佈局時,會將merge標籤內包含的內容直接填充到include的位置,不會再添加任何額外的佈局結構。

但是很遺憾,如果使用這種寫法的話,運行程序將會直接崩潰。因爲merge標籤並不是一個佈局,所以我們無法像剛纔那樣在include的時候給它指定一個id。

那麼這種情況下應該怎麼使用ViewBinding呢?首先爲了避免崩潰,我們應該將activity_main.xml中引入佈局時指定的id移除,如下所示:

<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/titlebar" />

</LinearLayout>

然後修改MainActivity中的代碼,如下所示:

class MainActivity : AppCompatActivity() {
   
   

    private lateinit var binding: ActivityMainBinding
    private lateinit var titlebarBinding: TitlebarBinding

    override fun onCreate(savedInstanceState: Bundle?) {
   
   
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        titlebarBinding = TitlebarBinding.bind(binding.root)
        setContentView(binding.root)
        titlebarBinding.title.text = "Title"
        titlebarBinding.back.setOnClickListener {
   
   
        }
        titlebarBinding.done.setOnClickListener {
   
   
        }
    }

}

可以看到,這裏我們又定義了一個titlebarBinding變量。很明顯,TitlebarBinding就是Android Studio根據我們的titlebar.xml佈局文件自動生成的Binding類。

在onCreate()函數中,我們調用TitlebarBinding.bind()函數,讓titlebar.xml佈局和activity_main.xml佈局能夠關聯起來。

接下來的事情就很簡單了,直接使用titlebarBinding變量就可以引用到titlebar.xml中定義的各個控件了。

好了,這大概就是關於ViewBinding的所有內容了,至少我已經想不出還有什麼更多的用法,相信本篇文章也足以將你工作中可能遇到的ViewBinding相關的問題全部覆蓋到。

另外,如果想要學習Kotlin和最新的Android知識,可以參考我的新書 《第一行代碼 第3版》點擊此處查看詳情


關注我的技術公衆號,每個工作日都有優質技術文章推送。

微信掃一掃下方二維碼即可關注:


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