項(xiàng)目搭建經(jīng)歷記錄
- Android App封裝 ——架構(gòu)(MVI + kotlin + Flow)
- Android App封裝 —— ViewBinding
- 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è)原因
-
過(guò)于冗余
findViewById對(duì)應(yīng)所有的View都要書寫findViewById(R.id.xxx)的方法,代碼過(guò)于繁瑣
-
不安全
強(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)在就要改為
- 對(duì)應(yīng)的Binding類如ActivityMainBinding類去用inflate加載布局
- 然后通過(guò)getRoot獲取到View
- 將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)
- 對(duì)比kotlin-extension,可以控制訪問(wèn)作用域羹膳,kotlin-extension可以訪問(wèn)不是該布局下的view;
- 對(duì)比butterknife睡互,減少注解以及id的一對(duì)一匹配
- 兼容Kotlin、Java陵像;
- 官方推薦就珠。
缺點(diǎn)
- 增加編譯時(shí)間,因?yàn)閂iwBinding是在編譯時(shí)生成的醒颖,會(huì)產(chǎn)生而外的類妻怎,增加包的體積;
- 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)