基於ViewBinding的BaseActivity封裝嘗試

軟件環境

  • Android Studio 3.6.1
  • gradle 5.6.4
  • targetSdkVersion 29
  • buildToolsVersion “29.0.3”
  • java 1.8

Google官方文檔 — 視圖綁定(基於kotlin編寫)
本文demo的GitHub地址

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

假設現在要封裝一個帶有公共ToolbarBaseActivity

佈局文件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>

不使用ViewBindingBaseActivity

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);
    }
}

繼承BaseActivityMainActivity

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;
    }
}

記錄,分享,交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章