Android开发——View Binding的使用与解析

1. 前言

  • 如何干掉模版代码是很多第三方框架的设计初衷,在Android开发中,findViewById()是必不可少的存在,这样的冗余代码在很久以前充斥在Android工程中,因此也出现了很多精简方案。
  • 在Android Studio3.6中加入了很多新特性——View Binding就是其中之一。目前已经在工程中使用并上线,未出现稳定性问题,因此做以下记录。
  • 在谈View Binding之前,我们先聊一下在此之前有哪些代表性方案做过这个事情,以及它们的优缺点分析
1.1 Butter Knife/Kotter Knife

Butter Knife框架是17年前后很火的存在。但是在Kotlin中直接使用ButterKnife的注解方式的话,会出现空指针的异常并导致绑定失败。从而Kotter Knife应运而生,可以理解成是Butter Knife的Kotlin版本。用法如下:

//Butter Knife
@BindView(R.id.title) TextView title;

ButterKnife.bind(this);

// TODO Use fields...
// Kotter Knife
val submitButton: Button by bindView(R.id.submit_button)

// TODO Use val...

然而这种方案已经被作者标记为Deprecated,作者认为该框架为每个视图引用分配了一个对象,这种思路不应该被采用,以及使用这种思路的框架也应该被淘汰,并推荐使用ViewBinding的方式。

1.2 Data Binding

依稀记得Data Binding是16年底的时候推出的,那时候真的很火,因为它是谷歌对于MVVM开发模式在Android上体现之一,其底层通过Annotation Processor实现的。用法如下,关键就是新增了data节点,以及在layout中的属性表达式@{}。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
           
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

默认情况下,基于layout文件的名称会生成单词首字母大写并添加“Binding”后缀一个Binding类。此类包含layout属性以及在Views中的所有binding属性(例如user变量),并且能够为该属性赋值。因此在Java代码中:

MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("firstName", "lastName");
binding.setUser(user);

但是Data Binding的劣势也很明显:

  • 它侵入了xml文件,因为只有布局文件的根标签必须是layout时, Data Binding才会生成对应的Binding Class。
  • Annotation Processor不仅对构建速度有负面影响,也会产生部分性能问题
  • 最最关键的,一些Data Binding相关的bug很难跟踪和解决,开发者在MVVM模式上的bug需要花费更多的时间,我觉得这也是MVVM没有在移动端发展起来的原因之一。

2. View Binding

这就是今天的主角了,View Binding既不会像Butter Knife那样为View分配多余的对象,也不会像Data Binding那样使用Annotation Processor,它的实现既简单又强大。首先来看一下如何使用View Binding。

2.1 View Binding所需环境
  • 首先需要将Android Studio升级到3.6版本及以上
  • 升级Gradle plugin版本到3.6.1
  • 并在app模块中手动开启view binding开关(View Binding以模块为粒度进行开启/关闭)
buildscript {
	...
    dependencies {
        classpath "com.android.tools.build:gradle:3.6.1"
    }
}

android {
    ...
    viewBinding {
        enabled = true
    }
}

这里升级gradle的时候遇到了一些小坑,即工程中使用的tinker版本过低,其使用了旧版gradle中的语法,因此会报错,将tinker版本升级到’1.9.14.6’后问题解决。

2.2 View Binding的使用

activity_main.xml的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/first_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="First Button" />

    <include
        android:id="@+id/inner_layout"
        layout="@layout/inner_layout" />
</LinearLayout>

这里直接include了一个子布局,inner_layout.xml的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/inner_layout_second_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Inner Button" />
</FrameLayout>
  • 默认情况下,每一个布局xml文件都会生成一个对应的Binding类,名字的生成规则同Data Binding。
  • 在该例子中,会自动为我们生成一个ActivityMainBinding.java,以及include的标签对应的InnerLayoutBinding,这里include必须指定一个id
  • 当然,如果不需要为该xml生成Binding类,可以在xml的根布局中配置tools:viewBindingIgnore=“true”。
    在Java中的使用如下所示:
public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(LayoutInflater.from(this));
        setContentView(binding.getRoot());

        binding.firstButton.setText("Text Change");
        binding.innerLayout.innerLayoutSecondButton.setText("Inner Layout Button Text Change");
    }
}

用法也很简单,直接通过binding点名字的方式就可以获取到控件实例,消除了findViewById的模版代码。
这里可以看到,setContentView的入参写法都变了,因为可通过XXXBinding类的getRoot函数获取到布局的根View,再通过setContentView添加到Activity。

2.3 View Binding的一些注意点

在2.2中我们演示了include的用法,只需要加一个id,我们就可以通过binding.innerLayout获取到该子布局。但是有个特殊情况需要处理,就是merge标签。
如果子布局inner_layout.xml中的布局如下,多了一个merge标签:

<?xml version="1.0" encoding="utf-8"?>
<merge>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/inner_layout_second_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Inner Button" />
    </FrameLayout>
</merge>

那么在activity_main.xml的布局的include标签中就一定不要设置id了(这里就不贴activity_main.xml的代码了),否则会找不到View报空指针异常。这个情况,我们可以先初始化主布局,再初始带merge的布局,那么就需要用到View Binding的另一个构造方法了,处理如下:

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(LayoutInflater.from(this));
        setContentView(binding.getRoot());

        //使用
        binding.firstButton.setText("Text Change");

        //merge标签处理
        InnerLayoutBinding subBinding = InnerLayoutBinding.bind(binding.getRoot());
        subBinding.innerLayoutSecondButton.setText("Inner Layout Button Text Change");
    }
}
2.4 View Binding的原理解析

从2.2和2.3中我们看到了Binding类的两种构造方法,其实它有三种使用方法,如下所示:

inflate(@NonNull LayoutInflater inflater)
inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, boolean attachToParent)
bind(@NonNull View rootView)
  • 第一种和第二种直接调用inflate()方法,传入inflater即可得到Binding类的实例,它会在内部inflate对应的xml布局,并得到并持有该xml布局的根布局View实例,即例子中的binding.getRoot()。inflate()最后仍然会调用bind()。
  • 第三种方法就更简单粗暴了,直接使用bind()方法并传入View实例,这个View实例可以是你在外部自己生成的,即可为你生成一个Binding类的实例,再通过点view的方式,找到传入的根View下的子View实例。比如在我们的工程中,在Adapter中封装了ViewHolder加载xml的过程并将根布局view放在了ViewHolder的基类里,这种情况显然就只能使用bind(view)的方式了
  • 这里以2.2中生成的Binding类为例,该类的路径如下所示:
    在这里插入图片描述
  • 再看一下其生成的内容,分析一下它到底为我们做了什么事情:
// Generated by view binder compiler. Do not edit!
public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final LinearLayout rootView;

  @NonNull
  public final Button firstButton;

  @NonNull
  public final InnerLayoutBinding innerLayout;

  private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull Button firstButton,
      @NonNull InnerLayoutBinding innerLayout) {
    this.rootView = rootView;
    this.firstButton = firstButton;
    this.innerLayout = innerLayout;
  }

  @Override
  @NonNull
  public LinearLayout 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) {
    String missingId;
    missingId: {
      //绑定View且做了非空判断,否则抛出空指针
      Button firstButton = rootView.findViewById(R.id.first_button);
      if (firstButton == null) {
        missingId = "firstButton";
        break missingId;
      }
      View innerLayout = rootView.findViewById(R.id.inner_layout);
      if (innerLayout == null) {
        missingId = "innerLayout";
        break missingId;
      }
      InnerLayoutBinding innerLayoutBinding = InnerLayoutBinding.bind(innerLayout);
      return new ActivityMainBinding((LinearLayout) rootView, firstButton, innerLayoutBinding);
    }
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}
  • 可以看出View Binding的实现既简单又强大。它的主要逻辑在于bind()方法中,在绑定的过程做了非空判断。inflate()的逻辑也非常的清晰。
2.5 小彩蛋
  • 从Binding类的第43行代码开始,有一个很有意思的Java用法,叫做Java Label。形如string:{},并在代码块中满足条件时直接break string跳出指定的代码块,试想一下,如果不这样写,就要把throw new NullPointerException写N遍,可以学习一下这种写法。
block:
{
    if(condition) break block;
    // rest of code that won't be executed if condition is true
}
  • 同时,Java Label也可以用在双层循环中,随意指定你想结束内层循环,还是外层循环。这里将break改为continue同样适用。
outterLoop: for(int i = 0; i < 10; i++)
{
    while(condition)
    {
        if(someConditon) break outterLoop; // 结束for循环
        if(anotherConditon) break; // 结束while循环
        // more code
    }
    // more code
}
  • 17
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
在Groovy中,Binding是一个非常重要的类,它允许我们在运行时动态地创建变量和方法,并将它们绑定到一个特定的上下文中。这个上下文可以是一个Groovy脚本、一个Groovy类或一个Java类。 在实际开发中,我们通常需要在多个Groovy脚本之间共享一些代码或变量。这时,我们可以使用Binding来实现共享。具体来说,我们可以在一个Groovy脚本中创建一个Binding对象,并将需要共享的变量绑定到这个对象中。然后,在其他Groovy脚本中,我们可以通过访问这个Binding对象来获取这些共享的变量。 举个例子,假设我们有两个Groovy脚本A.groovy和B.groovy,它们都需要使用同一个变量greeting。我们可以在A.groovy中创建一个Binding对象,并将greeting绑定到这个对象中: ``` def binding = new Binding() binding.setVariable("greeting", "Hello, world!") ``` 然后,在B.groovy中,我们可以通过访问这个Binding对象来获取greeting变量: ``` def binding = new Binding() def greeting = binding.getVariable("greeting") println greeting ``` 这样,我们就可以在不同的Groovy脚本之间共享变量了。 除了变量,我们还可以通过Binding共享方法。具体来说,我们可以在一个Groovy脚本中定义一个方法,并将它绑定到一个Binding对象中。然后,在其他Groovy脚本中,我们可以通过访问这个Binding对象来调用这个方法。 举个例子,假设我们有两个Groovy脚本A.groovy和B.groovy,它们都需要使用同一个方法sayHello。我们可以在A.groovy中定义这个方法,并将它绑定到一个Binding对象中: ``` def sayHello(name) { println "Hello, $name!" } def binding = new Binding() binding.setVariable("sayHello", &sayHello) ``` 然后,在B.groovy中,我们可以通过访问这个Binding对象来调用sayHello方法: ``` def binding = new Binding() def sayHello = binding.getVariable("sayHello") sayHello("world") ``` 这样,我们就可以在不同的Groovy脚本之间共享方法了。 综上所述,通过Binding,我们可以在不同的Groovy脚本之间共享变量和方法。这种方式非常灵活,也非常适合一些需要共享代码的场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值