Android使用ViewBinding

前言

原始方式

在我们的开发过程中,需要获取XML布局文件中的ViewId,以便其赋值显示,我们习惯使用findViewById进行操作,可这样会导致很多的模版代码出现。

Butter Knife框架

直到Android大神 Jake Wharton开源了Butter Knife框架,通过Bind方式绑定获取ViewId。

基于Kotlin的扩展

近几年Android对Kotlin的支持,我们开始使用 Android Kotlin extensions。

在文件中导入布局文件直接引用viewId。无需做其他额外操作,最为方便。

生成代码的方式

谷歌在 Android Studio 3.6 Canary 11 及更高版本中加入了新的视图绑定方式ViewBinding。

注意:

要使用ViewBinding功能,AndroidStudio至少要升级到3.6。

ViewBinding和Kotlin扩展

ViewBindingKotlin 扩展 都是 Android 开发中常用的技术,用于简化视图查找和绑定的过程。

以下是它们之间的一些比较:

ViewBinding:

  • ViewBinding 是由 Android 官方推荐和支持的库,从 Android Studio 3.6 版本开始引入。
  • ViewBinding 使用了编译时生成的绑定类,在 xml 布局文件中的每个视图都会生成一个对应的绑定类对象,因此在编译时检测到视图名称的错误。
  • ViewBinding 可以生成类型安全的代码,避免了手动查找和强制转换视图对象的麻烦。
  • ViewBinding 不会增加 APK 大小,因为它只是编译时生成的代码。
  • 在多个模块中引用同一个视图时可能会出现命名冲突的问题,需要通过手动指定全限定名解决。

Kotlin扩展:

  • Kotlin扩展 是 Kotlin 语言的特性,其通过扩展函数的方式,允许开发者为现有的类添加新的函数或属性。
  • Kotlin扩展 使用起来相对简单,可以直接在布局文件中使用 Kotlin 扩展函数来查找和操作视图。
  • Kotlin扩展 对视图的数据获取和类型安全性没有提供直接的支持,需要手动处理可能的空指针异常和类型转换。
  • Kotlin扩展 会增加 APK 的大小,因为它是在运行时动态添加的函数。

总体而言

ViewBinding 在类型安全性和编译时错误检测方面比 Kotlin扩展 更好。

它是官方推荐的方式,并且可以避免一些潜在的运行时异常。

但是,如果你已经熟悉并且喜欢使用 Kotlin 扩展,并且对 APK 大小没有严格要求,那么你可以选择使用它。

ViewBinding

原理就是

Google在那个用来编译的gradle插件中增加了新功能,当某个module开启ViewBinding功能后,编译的时候就去扫描此模块下的layout文件,生成对应的binding类。

开启

KTS方式配置

build.gradle.kts中添加

1
2
3
4
5
android {
viewBinding{
enable = true
}
}

注意

只要开启后,会自动遍历layout下的xml文件自动生成对应的类。

使用

页面中使用

1
private lateinit var binding: ActivityTexBinding

原来的

1
setContentView(R.layout.activity_tex)

替换为

1
2
binding = ActivityTexBinding.inflate(layoutInflater)
setContentView(binding.root)

使用

1
2
val text = "$$ c = \\pm\\sqrt{a^2 + b^2} $$"
binding.katexText.setText(text)

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import cn.psvmc.texeditor.databinding.ActivityTexBinding

class TexActivity : AppCompatActivity() {
private lateinit var binding: ActivityTexBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTexBinding.inflate(layoutInflater)
setContentView(binding.root)
val text = "$$ c = \\pm\\sqrt{a^2 + b^2} $$"
binding.katexText.setText(text)
}
}

注意

ActivityTexBinding是自动生成的类,它会自动遍历layout下的xml文件自动生成对应的类。

比如我的XML是activity_tex.xml,它自动生成的类就是ActivityTexBinding

如果想在生成绑定类时忽略某个布局文件,将tools:viewBindingIgnore=”true”`属性添加到相应布局文件的根视图中。

基类

Activity

1
2
3
4
5
6
7
8
9
10
11
12
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)
}

protected abstract fun getViewBinding(): T
}

使用

1
2
3
4
5
6
7
8
class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.textView.text = "这是MainActivity"
}

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

Fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class BaseFragment<T : ViewBinding> : Fragment() {
private lateinit var _binding: T
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
}

使用

1
2
3
4
5
6
7
8
9
10
11
class FirstFragment : BaseFragment<FragmentFirstBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textView.text = "这是FirstFragment"
}

override fun getViewBinding(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentFirstBinding.inflate(inflater, container, false)
}

其他

Fragment中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MyFragment extends Fragment {
private FragmentMyBinding binding;

public MyFragment() {

}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

}

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

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.textView.setText("这是Fragment");
binding.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d("Fragment", "点击了按钮");
}
});
}

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

Dialog中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyDialog extends Dialog {
protected View mView;
protected MyDialogBinding mBinding;

public MyDialog(@NonNull Context context) {
super(context,R.style.Dialog);
mBinding = MyDialogBinding.inflate(getLayoutInflater());
mView = mBinding.getRoot();
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(mView);
Window window = this.getWindow();
WindowManager.LayoutParams lp = window.getAttributes();
Display d = window.getWindowManager().getDefaultDisplay();
lp.width = (int) (d.getWidth() * 0.9F);
window.setAttributes(lp);
}


}

Adapter 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class MainAdapter extends RecyclerView.Adapter<MainAdapter.ViewHolder> {

private List<String> mList;

public MainAdapter(List<String> list) {
mList = list;
}

@NonNull
@Override
public MainAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
//之前的写法
//View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_comment, parent, false);
//ViewHolder holder = new ViewHolder(view);

//使用ViewBinding的写法
LayoutCommentBinding commentBinding = LayoutCommentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
ViewHolder holder = new ViewHolder(commentBinding);
return holder;
}

@Override
public void onBindViewHolder(@NonNull MainAdapter.ViewHolder holder, int position) {
holder.mTextView.setText(mList.get(position));
}

@Override
public int getItemCount() {
return mList.size();
}

static class ViewHolder extends RecyclerView.ViewHolder {

TextView mTextView;

//之前的写法
//public ViewHolder(@NonNull View itemView) {
// super(itemView);
// mTextView = itemView.findViewById(R.id.tv_include);
//}

//使用ViewBinding的写法
ViewHolder(@NonNull LayoutCommentBinding commentBinding) {
super(commentBinding.getRoot());
mTextView = commentBinding.tvInclude;
}

}
}

自定义View中使用

如果我们的自定义View中使用了layout布局,比如layout_my_view.xml,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="这是自定义布局"
android:textSize="50sp" />

</androidx.constraintlayout.widget.ConstraintLayout>

会生成一个名为LayoutMyViewViewBinding.java文件,在自定义View通过如下方式绑定,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyView extends View {
public MyView (Context context) {
this(context, null);
}

public MyView (Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public MyView (Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutMyViewViewBinding viewBinding = LayoutMyViewViewBinding.inflate(LayoutInflater.from(getContext()), this, true);
}
}

如果自定义View布局文件中使用merge标签,

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="这是自定义merge"
android:textSize="50sp" />

</merge>

此时要写成下面这个样子,

1
LayoutMyViewViewBinding viewBinding = LayoutMyViewViewBinding.inflate(LayoutInflater.from(context), this);

include标签的使用

include 标签不带 merge 标签,需要给 include 标签添加 id, 直接使用 id 即可,用法如下所示。

1
2
3
<include
android:id="@+id/include"
layout="@layout/layout_include_item" />

代码

1
2
ActivityMainBinding binding = ActivityMainBinding.inflate(layoutInflater);
binding.include.includeTvTitle.setText("使用 include 布局中的控件, 不包含 merge");

include 标签带 merge 标签,需要通过bind()将merge布局绑定到主布局上,用法如下所示。

1
2
<include
layout="@layout/layout_merge_item" />

代码

1
2
3
ActivityMainBinding binding = ActivityMainBinding.inflate(layoutInflater);
LayoutMergeItemBinding mergeItemBinding = LayoutMergeItemBinding.bind(binding.getRoot());
mergeItemBinding.mergeTvTitle.setText("使用 include 布局中的控件, 包含 merge");