軟件環境
- Android Studio 3.6.1
- gradle 5.6.4
- targetSdkVersion 29
- buildToolsVersion “29.0.3”
- java 1.8
1.新建項目並啓用ViewBinding
在對應模塊中build.gradle
中添加以下配置(PS:暫無找到直接啓用所有模塊的方法)
android {
...
viewBinding {
enabled = true
}
}
初始佈局文件activity_main.xml
,給TextView
加個id
<?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/tv_content"
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>
初始的MainActivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
在MainActivity
中使用ViewBinding
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding mainBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mainBinding.getRoot());
mainBinding.tvContent.setText("使用ViewBinding");
}
}
運行效果
2.普通的BaseActivity
假設現在要封裝一個帶有公共Toolbar
的BaseActivity
。
佈局文件activity_base.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".BaseActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary" />
<FrameLayout
android:id="@+id/view_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
不使用ViewBinding
的BaseActivity
public abstract class BaseActivity extends AppCompatActivity {
private Toolbar toolbar;
private ViewGroup viewContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_base);
toolbar = findViewById(R.id.toolbar);
viewContainer = findViewById(R.id.view_container);
LayoutInflater.from(this).inflate(getLayoutRes(), viewContainer);
}
protected abstract int getLayoutRes();
public void setToolbarTitle(CharSequence title) {
toolbar.setTitle(title);
}
public void setToolbarTitle(@StringRes int stringId) {
toolbar.setTitle(stringId);
}
}
繼承BaseActivity
的MainActivity
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setToolbarTitle("不使用ViewBinding");
}
@Override
protected int getLayoutRes() {
return R.layout.activity_main;
}
}
主題改爲android:theme="@style/Theme.AppCompat.Light.NoActionBar"
,去掉ActionBar
,運行效果
3.基於ViewBinding的BaseActivity
想要封裝,就得先看看ViewBinding
是怎麼實現的,自動生成的ViewBinding
在該模塊的build/generated/data_binding_base_class_source_out/debug/out
目錄下
ActivityBaseBinding.java
源碼
public final class ActivityBaseBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;
@NonNull
public final Toolbar toolbar;
@NonNull
public final FrameLayout viewContainer;
private ActivityBaseBinding(@NonNull LinearLayout rootView, @NonNull Toolbar toolbar,
@NonNull FrameLayout viewContainer) {
this.rootView = rootView;
this.toolbar = toolbar;
this.viewContainer = viewContainer;
}
@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityBaseBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityBaseBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_base, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityBaseBinding 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.
String missingId;
missingId: {
Toolbar toolbar = rootView.findViewById(R.id.toolbar);
if (toolbar == null) {
missingId = "toolbar";
break missingId;
}
FrameLayout viewContainer = rootView.findViewById(R.id.view_container);
if (viewContainer == null) {
missingId = "viewContainer";
break missingId;
}
return new ActivityBaseBinding((LinearLayout) rootView, toolbar, viewContainer);
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
ActivityMainBinding.java
的源碼
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final TextView tvContent;
private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull TextView tvContent) {
this.rootView = rootView;
this.tvContent = tvContent;
}
@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.
String missingId;
missingId: {
TextView tvContent = rootView.findViewById(R.id.tv_content);
if (tvContent == null) {
missingId = "tvContent";
break missingId;
}
return new ActivityMainBinding((ConstraintLayout) rootView, tvContent);
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
通過源碼可以看出,其實ViewBinding
就是根據佈局文件自動生成相應的佈局加載和findViewById
代碼,免去了手動寫findViewById
的工作。所以如果想要將MainActivity
的佈局載入到BaseActivity
,關鍵就是要調用inflate(LayoutInflater inflater,ViewGroup parent, boolean attachToParent)
方法。
一般封裝的思想都是利用泛型,首先將BaseActivity
改爲使用ViewBinding
public abstract class BaseActivity extends AppCompatActivity {
private ActivityBaseBinding baseBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
baseBinding = ActivityBaseBinding.inflate(getLayoutInflater());
setContentView(baseBinding.getRoot());
getLayoutInflater().inflate(getLayoutRes(), baseBinding.viewContainer);
}
protected abstract int getLayoutRes();
public void setToolbarTitle(CharSequence title) {
baseBinding.toolbar.setTitle(title);
}
public void setToolbarTitle(@StringRes int stringId) {
baseBinding.toolbar.setTitle(stringId);
}
}
按照正常的思路,應該是所有的ViewBinding
都實現一個公共的接口,然後利用這個接口進行操作,比如
public abstract class BaseActivity<T extends ViewBinding> extends AppCompatActivity {
public ActivityBaseBinding baseBinding;
public T viewBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
baseBinding = ActivityBaseBinding.inflate(getLayoutInflater());
setContentView(baseBinding.getRoot());
viewBinding = getViewBinding();
//利用公共的方法進行初始化操作
viewBinding.inflate(getLayoutInflater(), baseBinding.viewContainer, true);
}
protected abstract T getViewBinding();
public void setToolbarTitle(CharSequence title) {
baseBinding.toolbar.setTitle(title);
}
public void setToolbarTitle(@StringRes int stringId) {
baseBinding.toolbar.setTitle(stringId);
}
}
ViewBinding
確實實現了同一個接口,那就是androidx.viewbinding.ViewBinding
,但是很可惜,來看看它的源碼,很簡單,裏面只有一個getRoot()
方法。
/** A type which binds the views in a layout XML to fields. */
public interface ViewBinding {
/**
* Returns the outermost {@link View} in the associated layout file. If this binding is for a
* {@code <merge>} layout, this will return the first view inside of the merge tag.
*/
@NonNull
View getRoot();
}
所以我們無法利用接口的特性來進行操作,只能退求其次的改爲
public abstract class BaseActivity<T extends ViewBinding> extends AppCompatActivity {
public ActivityBaseBinding baseBinding;
public T viewBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
baseBinding = ActivityBaseBinding.inflate(getLayoutInflater());
setContentView(baseBinding.getRoot());
viewBinding = getViewBinding();
}
protected abstract T getViewBinding();
public void setToolbarTitle(CharSequence title) {
baseBinding.toolbar.setTitle(title);
}
public void setToolbarTitle(@StringRes int stringId) {
baseBinding.toolbar.setTitle(stringId);
}
}
然後修改MainActivity
public class MainActivity extends BaseActivity<ActivityMainBinding> {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setToolbarTitle("使用ViewBinding");
viewBinding.tvContent.setText("繼承了BaseActivity");
}
@Override
protected ActivityMainBinding getViewBinding() {
return ActivityMainBinding.inflate(getLayoutInflater(), baseBinding.viewContainer, true);
}
}
運行效果
這樣使用實在是不夠方便,但是爲什麼官方要這樣做呢?
4.解析ViewBinding實現思路
首先可以看到所有的控件變量都是使用final
標識,這樣可以保證控件在使用過程中不會被修改,但是使用final
標記的成員變量,必須在定義的時候或者在構造函數中進行初始化。而想要賦值控件變量,就只能在構造函數中進行初始化,這樣也保證了獲取到的ViewBinding
必定是注入了所有的控件。
viewBinding = getViewBinding();
//通過實例調用
viewBinding.inflate(getLayoutInflater(), baseBinding.viewContainer, true);
按照上面的想法,想要通過實例來調用inflate
就必須要先拿到一個實例,但是受限於final
,無法使用無參的構造方法,所以想要在ViewBinding
內部注入所有控件,就只能通過static
方法來獲取實例,這樣就造成了衝突,所以魚與熊掌不可兼得。
//最終都是通過bind(View rootView)方法來生成實例
@NonNull
public static ActivityBaseBinding 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.
String missingId;
missingId: {
Toolbar toolbar = rootView.findViewById(R.id.toolbar);
if (toolbar == null) {
missingId = "toolbar";
break missingId;
}
FrameLayout viewContainer = rootView.findViewById(R.id.view_container);
if (viewContainer == null) {
missingId = "viewContainer";
break missingId;
}
//注入控件並生成實例
return new ActivityBaseBinding((LinearLayout) rootView, toolbar, viewContainer);
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
5.犧牲安全性的更方便的ViewBinding
如果犧牲安全性,即不使用final
標識控件變量,就可以獲得便利性。(PS:但我個人覺得官方現在的用法也沒什麼大問題,反正代碼都是直接複製就行。)
這樣,ViewBinding
可以改爲
public interface ViewBinding<T> {
@NonNull
View getRoot();
@NonNull
T inflate(@NonNull LayoutInflater inflater);
@NonNull
T inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, boolean attachToParent);
}
自動生成的ActivityBaseBinding.java
public final class ActivityBaseBinding implements ViewBinding<ActivityBaseBinding> {
@NonNull
private LinearLayout rootView;
@NonNull
public Toolbar toolbar;
@NonNull
public FrameLayout viewContainer;
@NonNull
@Override
public View getRoot() {
return rootView;
}
public static ActivityBaseBinding newInstance() {
return new ActivityBaseBinding();
}
@Override
public ActivityBaseBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@Override
public ActivityBaseBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_base, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public ActivityBaseBinding 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.
String missingId;
missingId:
{
Toolbar toolbar = rootView.findViewById(R.id.toolbar);
if (toolbar == null) {
missingId = "toolbar";
break missingId;
}
FrameLayout viewContainer = rootView.findViewById(R.id.view_container);
if (viewContainer == null) {
missingId = "viewContainer";
break missingId;
}
this.rootView = (LinearLayout) rootView;
this.toolbar = toolbar;
this.viewContainer = viewContainer;
return this;
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
自動生成的ActivityMainBinding.java
public final class ActivityMainBinding implements ViewBinding<ActivityMainBinding> {
@NonNull
private ConstraintLayout rootView;
@NonNull
public TextView tvContent;
@Override
@NonNull
public ConstraintLayout getRoot() {
return rootView;
}
public static ActivityMainBinding newInstance() {
return new ActivityMainBinding();
}
@Override
public ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@Override
public 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 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.
String missingId;
missingId:
{
TextView tvContent = rootView.findViewById(R.id.tv_content);
if (tvContent == null) {
missingId = "tvContent";
break missingId;
}
this.rootView = (ConstraintLayout) rootView;
this.tvContent = tvContent;
return this;
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
BaseActivity.java
public abstract class BaseActivity<T extends ViewBinding> extends AppCompatActivity {
public ActivityBaseBinding baseBinding;
public T viewBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
baseBinding = ActivityBaseBinding.newInstance().inflate(getLayoutInflater());
setContentView(baseBinding.getRoot());
try {
viewBinding = getViewBinding().newInstance();
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
throw new RuntimeException("create BaseActivity error");
}
viewBinding.inflate(getLayoutInflater(), baseBinding.viewContainer, true);
}
protected abstract Class<T> getViewBinding();
public void setToolbarTitle(CharSequence title) {
baseBinding.toolbar.setTitle(title);
}
public void setToolbarTitle(@StringRes int stringId) {
baseBinding.toolbar.setTitle(stringId);
}
}
MainActivity.java
public class MainActivity extends BaseActivity<ActivityMainBinding> {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setToolbarTitle("使用ViewBinding");
viewBinding.tvContent.setText("繼承了BaseActivity");
}
@Override
protected Class<ActivityMainBinding> getViewBinding() {
return ActivityMainBinding.class;
}
}
記錄,分享,交流。