项目搭建履历记载
- Android App封装 ——架构(MVI + kotlin + Flow)
- Android App封装 —— ViewBinding
- Android App封装 —— DI框架 Hilt?Koin?
一、配景
在前面的Github wanandroid项目中可以看到,我获取控件对象照旧用的findviewbyId
button = findViewById(R.id.button)viewPager = findViewById(R.id.view_pager)recyclerView = findViewById(R.id.recycler_view)如今肯定是须要对这个最常用的获取View的findViewById代码举行优化,紧张是有两个缘故起因
- 过于冗余
findViewById对应全部的View都要誊写findViewById(R.id.xxx)的方法,代码过于繁琐
- 不安全
逼迫转换不安全,findViewById获取到的是一个View对象,是须要强转的,一旦范例给的不对则会出现非常,好比将TextView错转成ImageView
以是我们须要一个框架办理这个题目,大抵是有三个方案
二、方案
方案一 butterkniife
这个应该许多人都用过,由大大佬JakeWharton开发,通过注解天生findViewById的代码来获取对应的View。
@BindView(R.id.button) EditText mButton;但是2020年3月份,大佬已在GitHub上阐明不再维护,保举利用 ViewBinding了。
方案二 kotlin-android-extensions(KAE)
kotlin-android-extensions只须要直接引入结构可以直接利用资源Id访问View,节流findviewbyid()。
import kotlinx.android.synthetic.main.<结构>.*button.setOnClickListener{...} 但是这个插件也已经被Google废弃了,会影响服从而且安全性和兼容性都不太友爱,Google保举ViewBinding更换
方案三 ViewBinding
既然都保举ViewBinding,那如今来看看ViewBinding是啥。官网是这么说的
通过ViewBinding功能,您可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,体系会为该模块中的每个 XML 结构文件天生一个绑定类。绑定类的实例包罗对在相应结构中具有 ID 的全部视图的直接引用。在大多数环境下,视图绑定会更换 findViewById。
简而言之就是就是更换findViewById来获取View的。那我们来看看ViewBinding怎样利用呢?
三、ViewBinding利用
1. 条件
确保你的Android Studio是3.6或更高的版本
ViewBinding在 Android Studio 3.6 Canary 11 及更高版本中可用
2. 启用ViewBinding
在模块build.gradle文件android节点下添加如下代码
android { viewBinding{ enabled = true }}Android Studio 4.0 中,viewBinding 酿成属性被整合到了 buildFeatures 选项中,以是设置要改成:
// Android Studio 4.0android { buildFeatures { viewBinding = true }}设置好后就已经启用好了ViewBinding,重新编译后体系会为每个结构天生对应的Binding类,类中包罗结构ID对应的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 { ... } }}如今就要改为
- 对应的Binding类如ActivityMainBinding类去用inflate加载结构
- 然后通过getRoot获取到View
- 将View传入到setContentView(view:View)中
Activity就能表现activity_main.xml这个结构的内容了,并可以通过Binding对象直接访问对应View对象。
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中,利用方式大同小异,都是通过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)); }}可以看到关键的方法就是这个bind方法,内里通过ViewBindings.findChildViewById获取View对象,而继续查察这个方法
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; }}可见照旧利用的findViewById,ViewBinding这个框架只是帮我们在编译阶段自动天生了这些findViewById代码,省去我们去写了。
五、优缺点
优点
- 对比kotlin-extension,可以控制访问作用域,kotlin-extension可以访问不是该结构下的view;
- 对比butterknife,镌汰注解以及id的一对一匹配
- 兼容Kotlin、Java;
- 官方保举。
缺点
- 增长编译时间,由于ViwBinding是在编译时天生的,会产生而外的类,增长包的体积;
- include的结构文件无法直接引用,须要给include给id值,然后间接引用;
团体来说ViewBinding的优点照旧远宏大于缺点的,以是可以放心利用。
六、 封装
既然选择了方案ViewBinding,那我们要在项目中利用,肯定还须要对他加一些封装,我们可以用泛型封装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中利用起来就很方便,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 }}注意:
这里会发现Fragment和Activity的封装方式不一样,没有效lateinit。
由于binding变量只有在onCreateView与onDestroyView才是可用的,而fragment的生命周期和activity的差别,fragment可以超出其视图的生命周期,好比fragment hide的时间,如果不将这里置为空,有可能引起内存走漏。
以是我们要在onCreateView中创建,onDestroyView置空。
七、总结
ViewBinding相比优点照旧许多的,办理了安全性题目和兼容性题目,以是我们可以放心大胆的利用。
项目源码地点: Github wanandroid
干系链接: Android App封装 ——架构(MVI + kotlin + Flow)
作者:剑冲
链接:https://juejin.cn/post/7177673339517796413
|