Android App封裝 —— ViewBinding

項(xiàng)目搭建經(jīng)歷記錄

  1. Android App封裝 ——架構(gòu)(MVI + kotlin + Flow)
  2. Android App封裝 —— ViewBinding
  3. Android App封裝 —— DI框架 Hilt爽柒?Koin?

一橄维、背景

在前面的Github wanandroid項(xiàng)目中可以看到忌堂,我獲取控件對(duì)象還是用的findviewbyId

button = findViewById(R.id.button)
viewPager = findViewById(R.id.view_pager)
recyclerView = findViewById(R.id.recycler_view)

現(xiàn)在肯定是需要對(duì)這個(gè)最常用的獲取View的findViewById代碼進(jìn)行優(yōu)化盒至,主要是有兩個(gè)原因

  1. 過(guò)于冗余

    findViewById對(duì)應(yīng)所有的View都要書寫findViewById(R.id.xxx)的方法,代碼過(guò)于繁瑣

  2. 不安全

    強(qiáng)制轉(zhuǎn)換不安全士修,findViewById獲取到的是一個(gè)View對(duì)象枷遂,是需要強(qiáng)轉(zhuǎn)的,一旦類型給的不對(duì)則會(huì)出現(xiàn)異常棋嘲,比如將TextView錯(cuò)轉(zhuǎn)成ImageView

所以我們需要一個(gè)框架解決這個(gè)問(wèn)題酒唉,大致是有三個(gè)方案

二、方案

方案一 butterkniife

這個(gè)應(yīng)該很多人都用過(guò)沸移,由大大佬JakeWharton開(kāi)發(fā)痪伦,通過(guò)注解生成findViewById的代碼來(lái)獲取對(duì)應(yīng)的View。

@BindView(R.id.button) 
EditText mButton;

但是2020年3月份阔籽,大佬已在GitHub上說(shuō)明不再維護(hù)流妻,推薦使用 ViewBinding了。

方案二 kotlin-android-extensions(KAE)

kotlin-android-extensions只需要直接引入布局可以直接使用資源Id訪問(wèn)View笆制,節(jié)省findviewbyid()绅这。

import kotlinx.android.synthetic.main.<布局>.*

button.setOnClickListener{...} 

但是這個(gè)插件也已經(jīng)被Google廢棄了,會(huì)影響效率并且安全性和兼容性都不太友好在辆,Google推薦ViewBinding替代

方案三 ViewBinding

既然都推薦ViewBinding证薇,那現(xiàn)在來(lái)看看ViewBinding是啥。官網(wǎng)是這么說(shuō)的

通過(guò)ViewBinding功能匆篓,您可以更輕松地編寫可與視圖交互的代碼浑度。在模塊中啟用視圖綁定之后,系統(tǒng)會(huì)為該模塊中的每個(gè) XML 布局文件生成一個(gè)綁定類鸦概。綁定類的實(shí)例包含對(duì)在相應(yīng)布局中具有 ID 的所有視圖的直接引用箩张。在大多數(shù)情況下,視圖綁定會(huì)替代 findViewById。

簡(jiǎn)而言之就是就是替代findViewById來(lái)獲取View的先慷。那我們來(lái)看看ViewBinding如何使用呢饮笛?

三、ViewBinding使用

1. 條件

確保你的Android Studio是3.6或更高的版本

ViewBinding在 Android Studio 3.6 Canary 11 及更高版本中可用

2. 啟用ViewBinding

在模塊build.gradle文件android節(jié)點(diǎn)下添加如下代碼

android {
    viewBinding{
        enabled = true
    }
}

Android Studio 4.0 中论熙,viewBinding 變成屬性被整合到了 buildFeatures 選項(xiàng)中福青,所以配置要改成:

// Android Studio 4.0
android {
     buildFeatures {
          viewBinding = true
     }
}

配置好后就已經(jīng)啟用好了ViewBinding,重新編譯后系統(tǒng)會(huì)為每個(gè)布局生成對(duì)應(yīng)的Binding類脓诡,類中包含布局ID對(duì)應(yīng)的View引用无午,并采取駝峰式命名。

3. 使用

以activity舉例祝谚,我們的MainActivity的布局是activity_main宪迟,之前我們布局代碼是:

class MainActivity : BaseActivity() {

    private lateinit var button: Button
    private lateinit var viewPager: ViewPager2
    private lateinit var recyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button = findViewById(R.id.button)
        button.setOnClickListener { ... }
    }
}

現(xiàn)在就要改為

  1. 對(duì)應(yīng)的Binding類如ActivityMainBinding類去用inflate加載布局
  2. 然后通過(guò)getRoot獲取到View
  3. 將View傳入到setContentView(view:View)中

Activity就能顯示activity_main.xml這個(gè)布局的內(nèi)容了,并可以通過(guò)Binding對(duì)象直接訪問(wèn)對(duì)應(yīng)View對(duì)象交惯。

class MainActivity : BaseActivity() {

    private lateinit var mBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        mBinding.button.setOnClickListener { ... }
    }
}

而在其他UI elements中踩验,如fragment、dialog商玫、adapter中箕憾,使用方式大同小異,都是通過(guò)inflate去加載出View拳昌,然后后面加以使用袭异。

四、原理

生成的類可以在/build/generated/data_binding_base_class_source_out下找到

public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final Button button;

  @NonNull
  public final RecyclerView recyclerView;

  @NonNull
  public final ViewPager2 viewPager;

  private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull Button button,
      @NonNull RecyclerView recyclerView, @NonNull ViewPager2 viewPager) {
    this.rootView = rootView;
    this.button = button;
    this.recyclerView = recyclerView;
    this.viewPager = viewPager;
  }

  @Override
  @NonNull
  public ConstraintLayout getRoot() {
    return rootView;
  }

  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.activity_main, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static ActivityMainBinding bind(@NonNull View rootView) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    int id;
    missingId: {
      id = R.id.button;
      Button button = ViewBindings.findChildViewById(rootView, id);
      if (button == null) {
        break missingId;
      }

      id = R.id.recycler_view;
      RecyclerView recyclerView = ViewBindings.findChildViewById(rootView, id);
      if (recyclerView == null) {
        break missingId;
      }

      id = R.id.view_pager;
      ViewPager2 viewPager = ViewBindings.findChildViewById(rootView, id);
      if (viewPager == null) {
        break missingId;
      }

      return new ActivityMainBinding((ConstraintLayout) rootView, button, recyclerView, viewPager);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

可以看到關(guān)鍵的方法就是這個(gè)bind方法炬藤,里面通過(guò)ViewBindings.findChildViewById獲取View對(duì)象御铃,而繼續(xù)查看這個(gè)方法

public class ViewBindings {

    private ViewBindings() {
    }

    /**
     * Like `findViewById` but skips the view itself.
     *
     * @hide
     */
    @Nullable
    public static <T extends View> T findChildViewById(View rootView, @IdRes int id) {
        if (!(rootView instanceof ViewGroup)) {
            return null;
        }
        final ViewGroup rootViewGroup = (ViewGroup) rootView;
        final int childCount = rootViewGroup.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final T view = rootViewGroup.getChildAt(i).findViewById(id);
            if (view != null) {
                return view;
            }
        }
        return null;
    }
}

可見(jiàn)還是使用的findViewById,ViewBinding這個(gè)框架只是幫我們?cè)诰幾g階段自動(dòng)生成了這些findViewById代碼沈矿,省去我們?nèi)懥恕?/p>

五上真、優(yōu)缺點(diǎn)

優(yōu)點(diǎn)

  1. 對(duì)比kotlin-extension,可以控制訪問(wèn)作用域羹膳,kotlin-extension可以訪問(wèn)不是該布局下的view;
  2. 對(duì)比butterknife睡互,減少注解以及id的一對(duì)一匹配
  3. 兼容Kotlin、Java陵像;
  4. 官方推薦就珠。

缺點(diǎn)

  1. 增加編譯時(shí)間,因?yàn)閂iwBinding是在編譯時(shí)生成的醒颖,會(huì)產(chǎn)生而外的類妻怎,增加包的體積;
  2. include的布局文件無(wú)法直接引用泞歉,需要給include給id值逼侦,然后間接引用匿辩;

整體來(lái)說(shuō)ViewBinding的優(yōu)點(diǎn)還是遠(yuǎn)遠(yuǎn)大于缺點(diǎn)的,所以可以放心使用榛丢。

六撒汉、 封裝

既然選擇了方案ViewBinding,那我們要在項(xiàng)目中使用涕滋,肯定還需要對(duì)他加一些封裝,我們可以用泛型封裝setContentView的代碼

abstract class BaseActivity<T : ViewBinding> : AppCompatActivity() {

    private lateinit var _binding: T
    protected val binding get() = _binding;

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = getViewBinding()
        setContentView(_binding.root)

        initViews()
        initEvents()
    }

    protected abstract fun getViewBinding(): T
    open fun initViews() {}
    open fun initEvents() {}
}
class MainActivity : BaseActivity<ActivityMainBinding>() {

    override fun getViewBinding() = ActivityMainBinding.inflate(layoutInflater)

    override fun initViews() {
        binding.button.setOnClickListener {
            ...
        }
    }

}

這樣在Activity中使用起來(lái)就很方便挠阁,fragment也可以做類似的封裝

abstract class BaseFragment<T : ViewBinding> : Fragment() {
    private var _binding: T? = null
    protected val binding get() = _binding!!

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

    protected abstract fun getViewBinding(inflater: LayoutInflater, container: ViewGroup?): T

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

注意:

這里會(huì)發(fā)現(xiàn)Fragment和Activity的封裝方式不一樣宾肺,沒(méi)有用lateinit
因?yàn)?code>binding變量只有在onCreateView與onDestroyView才是可用的侵俗,而fragment的生命周期和activity的不同锨用,fragment可以超出其視圖的生命周期,比如fragment hide的時(shí)候隘谣,如果不將這里置為空增拥,有可能引起內(nèi)存泄漏
所以我們要在onCreateView中創(chuàng)建寻歧,onDestroyView置空掌栅。

七、總結(jié)

ViewBinding相比優(yōu)點(diǎn)還是很多的码泛,解決了安全性問(wèn)題和兼容性問(wèn)題猾封,所以我們可以放心大膽的使用。

項(xiàng)目源碼地址: Github wanandroid

相關(guān)鏈接: Android App封裝 ——架構(gòu)(MVI + kotlin + Flow)

作者:劍沖
鏈接:https://juejin.cn/post/7177673339517796413

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末噪珊,一起剝皮案震驚了整個(gè)濱河市晌缘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌痢站,老刑警劉巖磷箕,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異阵难,居然都是意外死亡岳枷,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門呜叫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)嫩舟,“玉大人,你說(shuō)我怎么就攤上這事怀偷〖已幔” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵椎工,是天一觀的道長(zhǎng)饭于。 經(jīng)常有香客問(wèn)我蜀踏,道長(zhǎng),這世上最難降的妖魔是什么掰吕? 我笑而不...
    開(kāi)封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任果覆,我火速辦了婚禮,結(jié)果婚禮上殖熟,老公的妹妹穿的比我還像新娘局待。我一直安慰自己,他們只是感情好菱属,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布钳榨。 她就那樣靜靜地躺著,像睡著了一般纽门。 火紅的嫁衣襯著肌膚如雪薛耻。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天赏陵,我揣著相機(jī)與錄音饼齿,去河邊找鬼。 笑死蝙搔,一個(gè)胖子當(dāng)著我的面吹牛缕溉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吃型,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼倒淫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了败玉?” 一聲冷哼從身側(cè)響起敌土,我...
    開(kāi)封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎运翼,沒(méi)想到半個(gè)月后返干,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡血淌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年矩欠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悠夯。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡癌淮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沦补,到底是詐尸還是另有隱情乳蓄,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布夕膀,位于F島的核電站虚倒,受9級(jí)特大地震影響美侦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望种远。 院中可真熱鬧,春花似錦隐砸、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)棺妓。三九已至,卻和暖如春买鸽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贯被。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工眼五, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人彤灶。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓看幼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親幌陕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诵姜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容