Android大變天,是時候和ButterKnife說再見了!

馬可沒有菠蘿發表於2020-11-29

最近Android Studio更新到了4.1版本,發現專案中使用ButterKnife註解id的程式碼出現了警告,警告資訊如下:

Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them as annotation attributes

從警告資訊中可以看到在Gradle 5.0的外掛中Resource 的Id值將不會再是final型別,因此應該避免在註解屬性中使用Id。這意味著當我們把Gradle外掛升級到5.0版本之後ButterKnife將無法再被使用!同時,我們在ButterKnife的官方文件上也看到了ButterKnife被標註棄用的資訊:
在這裡插入圖片描述
陪伴我們多年,曾經輝煌一時,不可一世的ButterKnife也要壽終正寢,即將迎來它生命的終點。借這個機會,我們不妨來回顧一下Android開發中findView的發展史,以及展望下findView的未來。

一、Android繫結View的發展史

從Android系統誕生至今,在程式碼中findView一直是Android開發者無法繞開的一道程式。從最初的findViewbyId到如今炙手可熱的ViewBinding,期間湧現出了許多findView的方式,它們讓findView變得更加簡單,也讓我們的程式碼變得更加簡潔。但隨著Android新技術的發展,這些findView的方法也正在被一個一個的拋棄。本節內容我們就來回顧一下Android開發中findView的發展史。

1.findViewById

findViewById是Android開發中最原始,也是最基礎的一種獲取View的方法。它由Google官方提供,在Android開發生態的早期也是唯一一種能夠獲取View的方式。雖然它使用簡單且根正苗紅,貫穿古今。但由於高度重複的程式碼結構深受開發者詬病。在一個複雜佈局的頁面僅僅是findViewById的程式碼往往就能達到數十行。開發者無時無刻不想著棄用這一方案,因此後續衍生出了多種獲取View的方式來簡化程式碼。但萬變不離其宗,歸根結底,這些方式最終都還是通過findViewById來實現的。雖然它是最不被開發者認可的一種的方式,但時至今日開發者也無法擺脫它籠罩著的陰影。一臉你看不慣我你打我呀的表情!

2.ButterKnife

就在大家都在唾棄findViewById的大量重複程式碼時,一個外掛橫空出世。它通過一個BindView註解,傳入一個Resource Id就能輕鬆獲取到Id對應的View。程式碼如下:

public class MainActivity extends AppCompatActivity {
	 @BindView(R.id.text_view)
	 TextView mTextView;
	
	    @Override
	    protected void onCreate(Bundle savedInstanceState) {
	        super.onCreate(savedInstanceState);
	        setContentView(R.layout.activity_main);
	        ButterKnife.bind(this);
	    }
    }

它就是紅極一時,時至今日大家依然還在用著的ButterKnife。ButterKnife通過最前沿的Java技術(最初的版本可能是反射,未加考究)–Java編譯時註解處理器,在編譯時自動生成findViewById的程式碼。例如,上邊的例子通過ButterKnife會生成一個MainActivity_ViewBinding 類,在這個類中通過findViewById為mTextView賦值,其程式碼如下:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.mTextView = Utils.findRequiredViewAsType(source, R.id.text_view, "field 'mTextView'", TextView.class);
  }

  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleare![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190825162306549.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70)d.");
    this.target = null;

    target.mTextView = null;
  }
}

這一操作省去了開發者手動編寫findViewById的時間,大大簡化了程式碼,同時提高了開發效率。在當時的開發者看來ButterKnife不得不說是一個神器,以至於到後來成了Android專案開發的標配。

後來,隨著Android Studio的誕生,Eclipse開發Android專案逐漸淡出歷史舞臺。Android studio的出現,帶來了全新的技術,模組化風靡一時。大概在這個時候,Google官方似乎就已經有了改造R類的想法。在Android專案的library模組中,生成R類中的成員變數就已經改為了非final修飾。同時,Google官方也不再建議在app模組的程式碼中使用像:switch(view.getId())這樣的程式碼。

正如Android studio官網文件《Non-constant Fields in Case Labels》上給出的原因:

In other words, the constants are not final in a library project. The reason for this is simple: When multiple library projects are combined, the actual values of the fields (which must be unique) could collide. Before ADT 14, all fields were final, so as a result, all libraries had to have all their resources and associated Java code recompiled along with the main project whenever they were used. This was bad for performance, since it made builds very slow. It also prevented distributing library projects that didn’t include the source code, limiting the usage scope of library projects.

這一改變直接致使ButterKnife無法在Android專案的library模組中使用。而此時,ButterKnife正是如日中天,追隨的開發者不計其數。為了能夠讓ButterKnife執行在library模組,ButterKnife的作者Jake Wharton大佬曲線救國,通過生成R2類讓ButterKnife在library模組中復活,並且得以發展壯大。但不得不說,此時的ButterKnife就已經埋下了深深的隱患,並導致了其最終的潰敗。

3.DataBinding

DataBinding是Google官方在2015年穀歌I/O大會上釋出的一個資料繫結框架,它並非專為findView而生,而是作為MVVM架構的雙向繫結資料的工具。findView的功能僅僅是DataBinding的一個附贈品。

開發者一般會在MVVM架構的專案中使用DataBinding來獲取View。但是它也有很多詬病,比如需要修改xml的結構,在xml外部巢狀一個標籤。並且很多情況下需要手動build才能生成DataBinding相關類。諸如此類問題,自然不會得到開發者的青睞。

關於DataBinding的詳細使用在這裡不做探討。

4.Kotlin Android Extensions

2017年Google I/O開發者大會中,Google宣佈Kotlin成為Android開發的一級語言,自此,Kotlin “轉正”與Java並駕齊驅。而JetBrain推出的Kotlin Android Extension(以下簡稱KAE)外掛成為了有史以來最簡單的獲取View的方法,簡單到無需任何程式碼,直接通過id作為View使用。這一功能足以讓所有Android開發者抓狂,紛紛感嘆這才是findView的未來啊,終於可以和裹挾開發者十多年的findViewById說拜拜了!
作為一個Android開發者,不知道你是否會好奇Kotlin是如何將Id作為View的?我們不妨寫一個簡單的例子:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView.text = "Test"
    }
}

佈局檔案中TextView的id設定為“textView”,則在Activity中可以直接將textView作為一個TextView來使用。我們通過Android Studio的工具將kotlin的位元組碼反編譯成Java程式碼看下
在這裡插入圖片描述
通過上述操作,開啟kotlin的位元組碼後,再通過Decompile反編譯成Java程式碼,則會得到如下圖所示的結果:
在這裡插入圖片描述
通過反編譯得到的Java程式碼我們發現Kotlin的這一操作其實也是通過findViewById實現的。只是通過外掛的方式讓我們感覺上是用了View的Id。

通過Kotlin的擴充套件外掛來find view,無疑是一種優秀的方案。但這一方案並不是無懈可擊。它存在以下幾個缺點:

  • 型別安全:res下的任何id都可以被訪問,有可能因訪問了非當前Layout下的id而出錯
  • 空安全:這主要體現在Configuration中的對應佈局不全時,執行時可能出現NPE
  • 相容性:只能在kotlin中使用,java不友好
  • 侷限性:不能跨module使用

也正是這幾個缺點導致了KAE的大潰敗。隨著Google對親兒子ViewBinding的大力推廣,KAE最終也招架不住,只能繳械投降—Jetbrains在官網宣佈廢棄KAE,並推薦開發者使用ViewBinding.
在這裡插入圖片描述

5.ViewBinding

到這裡,以上提到的多種findView方案都已經被廢棄,唯獨只剩Google官方正在大力推廣的ViewBinding元件。ViewBinding是Google在2019年I/O大會上公佈的一款Android檢視繫結工具。它的使用方式有點類似DataBinding,但相比DataBinding,ViewBinding是一個更輕量級、更純粹的findViewById的替代方案。它具有以下幾個優點:

  • 型別安全: ViewBinding會基於佈局中的View生成型別正確的屬性。比如,在佈局中放入了一個 TextView ,檢視繫結就會暴露出一個 TextView 型別的屬性供開發中使用。
  • 空安全:ViewBinding會檢測某個檢視是不是隻在一些配置下存在,並依據結果生成帶有 @Nullable 註解的屬性。所以即使在多種配置下定義的佈局檔案,檢視繫結依然能夠保證空安全。
  • ViewBinding生成的繫結類是一個Java類,並且新增了Kotlin的註解,可以很好的支援 Java 和 Kotlin 兩種程式語言。

同時,Google官方還給出了一個ViewBinding、ButterKnife以及KAE的對比,如下圖:
在這裡插入圖片描述
總而言之,到目前為止除了ViewBinding我們已經別無選擇。那麼不妨接下來詳細探究下ViewBinding的使用方法。

二、ViewBinding使用詳解

1.開啟ViewBinding

Android Studio對於ViewBinding的支援是從3.6版本開始的,AS 3.6版本內建了Gradle外掛。只需要在build.gradle中通過以下配置即可開啟ViewBinding:

android {
    buildFeatures {
        viewBinding = true
    }
}

如果,你的專案存在多個模組,則需要在每個模組的gradle中新增上述配置。完成以上配置後ViewBinding會為所有佈局檔案自動生成對應的繫結類。且無須修改原有佈局的 XML 檔案,ViewBinding會根據現有的佈局自動完成所有工作。

2.在Activity中使用ViewBinding

首先編寫activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

完成後gradle外掛會自動生成一個名為ActivityMainBinding的Java類,在Activity中通過ActivityMainBinding獲取Binding例項,如下:

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

3.ViewBinding與include標籤

在專案開發中,通常我們會使用include標籤來簡化佈局檔案,那麼在使用了include標籤的佈局檔案中,應該如何使用ViewBinding呢?且看程式碼:

// activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

</androidx.constraintlayout.widget.ConstraintLayout>


// layout_include.xml
<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">

    <TextView
        android:id="@+id/tv_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

上述兩個佈局檔案會分別生成ActivityMainBinding與LayoutIncludeBinding兩個Java類,並且ActivityMainBinding類中通過組合依賴了LayoutIncludeBinding類。因此,使用方式如下:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        //  從ActivityMainBinding中獲取LayoutIncludeBinding
        val include = binding.include
        // 通過LayoutIncludeBinding為TextView賦值
        include.tvText.text = "Hello World"
    }

如果layout_include.xml檔案位於子模組,經實踐與以上程式碼的使用方式並無任何差異,但一定要在子模組中開啟ViewBinding才行。

4.ViewBinding在Fragment中的使用
在Fragment中使用ViewBinding與Activity中有些差異,這裡為了簡便,我們使用上述中的activity_main.xml作為Fragment的佈局檔案,則Fragment的程式碼如下:

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.textView.text="Hello World"
    }

5.ViewBinding在RecyclerView#Adapter中的使用

佈局檔案不再貼出,直接看Adapter的程式碼,如下所示:

 class TestAdapter : RecyclerView.Adapter<TestViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder {
        val binding =
            ItemTestBinding.inflate(LayoutInflater.from(parent.context))
        return TestViewHolder(binding)
    }

    override fun onBindViewHolder(holder: TestViewHolder, position: Int) {
        holder.binding.textView.text = "Hello World"
    }

    override fun getItemCount(): Int {
        return 10
    }

    class TestViewHolder(var binding: ItemTestBinding) :
        RecyclerView.ViewHolder(binding.root)
}

通過以上幾個例項可以看到ViewBinding的使用是非常簡單的。而ViewBinding的實現原理也並不難,Gradle外掛會根據佈局檔案在專案的build目錄下生成相應的ViewBinding類,並且,最終也是通過findViewById來完成View的獲取的。具體實現程式碼不再貼出,感興趣的同學可以自行檢視。

三、展望與總結

本篇文章詳細介紹了Android開發中find view的發展史,以及當下正火的ViewBinding元件。時代在發展,Android獲取View的方式仍在變化。ViewBinding是一個優秀的元件,但它真的是Android開發中獲取View的最優方案嗎?顯然,並不是!因為ViewBinding歸根結底還是通過findViewById實現,且需要外掛生成相關的Binding類,雖然省去了手動編寫,但是ViewBinding仍然沒能解決程式碼冗餘的問題。那什麼才是findViewById的未來呢?大概最好的find view就是沒有find view吧!目前Google正在朝著這一方向努力,正在開發的Jetpack Compose庫就是要取代Android的佈局檔案,徹底消除findViewById。相信在未來某一天,隨著Jetpack Compose庫的普及,這個曠日持久的findViewById之爭也最終會畫上一個句號。

參考&推薦閱讀

Non-constant Fields in Case Labels

Kotlin Android Extensions遭廢棄,官方推薦使用ViewBinding

使用檢視繫結替代 findViewById

相關文章