有关Databinding与MVVM的一些事

作者: LSteven | 来源:发表于2018-05-22 19:51 被阅读22次

    DataBinding

    说到DataBinding,大家就会想到双向绑定。那究竟什么是双向绑定,其实对于刚接触的人来说是需要去理解一下的。

    MVVM中,ViewModel是互相隔离的。假设有以下的EditText布局:

    android:text="@{echo.text}"
    

    那么显而易见,当echo.text发生变化时,我们希望EditText中的android:text属性可以自动变化。这是Model层->View层的绑定。

    反之,当用户通过View层在EditText中进行输入时,我们希望echo.text字段也可以同步更新到最新的输入值。这是ViewModel层的绑定。这一点很多人都会忽略。

    那么首先我们看是Model层->View层的绑定是怎么实现的,在DataBinding中大家都知道有以下两种方式:

    • Model继承BaseObservable,get带上@Bindable注解
    • 相应字段使用Observablexxx变量,如下text字段(其实Observablexxx就是继承BaseObservable)

    可以简单看下BaseObservable的源码,后面会用到:

    public class BaseObservable implements Observable {
       @Override
        public void addOnPropertyChangedCallback(@NonNullOnPropertyChangedCallback callback) {
            ...
            mCallbacks.add(callback);
        }
       
        public void notifyPropertyChanged(int fieldId) {
            ...
            mCallbacks.notifyCallbacks(this, fieldId, null);
        }
    

    就是一个回调模式。

    public class Echo {
      public ObservableField<String> text = new ObservableField<>();
    }
    

    当使用ObservableField后,就真正使用了观察者的模式。也就是说当调用setEcho方法后,一个监听器就被注册了,这个监听器会在每次text字段被更新后去更新视图。

    先来一段小小的源码分析,每个layout生成的xxxBinding都是关键的类,里面有一个executeBinding方法。

    我们来看,一个简单的

    • android:text="@{model.str}"

    会生成什么模板代码?

       @Override
        protected void executeBindings() {
            long dirtyFlags = 0;
            synchronized(this) {
                dirtyFlags = mDirtyFlags;
                mDirtyFlags = 0;
            }
            java.lang.String modelStr = null;
            me.lizz0y.myapplication.VM model = mModel;
    
            if ((dirtyFlags & 0x3L) != 0) {
                    if (model != null) {
                        // read model.str
                        modelStr = model.str;
                    }
            }
            // batch finished
            if ((dirtyFlags & 0x3L) != 0) {
                // api target 1
                android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, modelStr);
            }
        }
    

    关键代码:

    modelStr = model.str;
    android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, modelStr);//这个`xxxAdapter`后面再说,这里就看出简单的赋值就好了
    

    如果变成

    • public ObservableField<String> str = new ObservableField<String>("sss");

    我们会发现,在executeBinding中多了一句:

    updateRegistration(0, modelStr);//localFieldId

    将这个变量的fieldIdmodel.str这个Observable绑定在一起,同时使用前面的addOnPropertyChangedCallbackViewBinding类作为回调传进去。

    最终,当我们对mode.str进行set操作时,一系列回调最终走到ViewDataBinding

       @Override
        protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
            switch (localFieldId) {
                case 0 :
                    return onChangeModelStr((android.databinding.ObservableField<java.lang.String>) object, fieldId);
            }
            return false;
        }
    

    其实就是对str这个变量赋予脏位,让下次屏幕刷新时更新这个变量对应的View

    介绍一些常用的运算符:

    运算符

    @BindingConversion

    如果在xml里我这么写:android:background="@{@color/blue}"

    会报错,因为background应该是drawable。所以要进行自动转化,所以需要进行如下定义:

    //转化@color/blue为drawable
    @BindingConversion
    public static ColorDrawable convertColorToDrawable(int color) {
       return new ColorDrawable(color);
    }
    

    经过前面分析也很简单:

    android.databinding.adapters.ViewBindingAdapter.setBackground(
    this.mboundView0,me.lizz0y.myapplication.VM.convertColorToDrawable(
        mboundView0.getResources().getColor(R.color.blue)));
            
    

    当然这里显然有人会问,假设我定义了多个怎么破,结论就是后面的覆盖前面的。。。

    @BindAdapter

    举个栗子就明白了:

    @BindingAdapter({"imageUrl"})  
    public static void loadImage(ImageView view, String u) {  
        RequestOptions options = new RequestOptions()  
                .centerCrop()  
                .placeholder(R.mipmap.ic_launcher_round)  
                .error(R.mipmap.ic_launcher)  
                .priority(Priority.HIGH)  
                .diskCacheStrategy(DiskCacheStrategy.NONE);  
    
        Glide.with(view.getContext()).applyDefaultRequestOptions(options).load(u).transition(new DrawableTransitionOptions().crossFade(1000)).into(view);  
    }  
    

    xml里这么写:

     <ImageView  
        android:layout_width="100dp"  
        android:layout_height="100dp"  
        app:imageUrl="@{user.url}" />  
    

    这就每次更新user.url时就会自动重新设置图片。

    DataBindingset属性attr时会先看View有没有setXXX方法。如果没有就去找有没有BindingAdapter注解设置对应的方法。

    前面分析源码的时候提到过

    if ((dirtyFlags & 0x3L) != 0) {
        // api target 1
        android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, modelStr);
    }
    

    我们看这里的BindingAdapter

    @BindingAdapter("android:text")
    public static void setText(TextView view, CharSequence text) {
        final CharSequence oldText = view.getText();
        if (text == oldText || (text == null && oldText.length() == 0)) {
            return;
        }
        if (text instanceof Spanned) {
            if (text.equals(oldText)) {
                return; // No change in the spans, so don't set anything.
            }
        } else if (!haveContentsChanged(text, oldText)) {
            return; // No content changes, so don't set anything.
        }
        view.setText(text);
    }
    

    可以看到会比较oldText&newText,以防无限循环。

    Component

    直接看这篇吧,写的很好

    我们可以定义多个BindingAdapter,但究竟想要哪个发挥作用呢? 就可以使用这个Component

    BindingMethod

    该注解可以帮助我们重新命名view属性对应的setter方法名称。

    @BindingMethods({@BindingMethod(type = NestedScrollView.class, attribute = "custom", method = "setMyCustomAttr")})
    

    注解在类上。

    个人觉得他与BindingAdapter的区别在于:

    • 参数,BindingAdapter可以拿到view引用
    • component

    监听属性变更

    databinding让我们省去了各种监听函数,但有的时候我们需要在属性变化时做一些额外的事情:

    mModel.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
        @Override
        public void onPropertyChanged(Observable observable, int i) {
            if (i == BR.name) {
                Toast.makeText(TwoWayActivity.this, "name changed",
                        Toast.LENGTH_SHORT).show();
            } else if (i == BR.password) {
                Toast.makeText(TwoWayActivity.this, "password changed",
                        Toast.LENGTH_SHORT).show();
            }
        }
    });
    

    双重绑定

    下面我们来说说如果从View->Model的绑定。我们看在前面已有的基础上,我们如何自己实现双重绑定

    <EditText
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="Text 1"
      android:text="@{echo.text}"
      android:addTextChangedListener="@{echo.watcher}"/>
    

    假设有两个EditText都使用了android:text,可以看到一个改变并不能连带带动另一个EditText,因为EditText的输入并没有手动去调用setField方法。所以显而易见需要再使用DataBinding赋予textChangedListener

     public TextWatcher watcher = new TextWatcherAdapter() {
        @Override public void afterTextChanged(Editable s) {
          if (!Objects.equals(text.get(), s.toString())) { //防止无限循环
            text.set(s.toString());
          }
        }
      };
    
    customBinding
    public class BindableString extends BaseObservable {
      private String value;
      public String get() {
        return value != null ? value : “”;
      }
      public void set(String value) {
        if (!Objects.equals(this.value, value)) {
          this.value = value;
          notifyChange();
        }
      }
      public boolean isEmpty() {
        return value == null || value.isEmpty();
      }
    }
    
    @BindingConversion
    public static String convertBindableToString(
        BindableString bindableString) {
      return bindableString.get();
    }
    
    

    当主动调用BindableString.set时会通过notifyChange去触发UI更新,UI更新时调用convertBindableToString取出string绑定

    或者BindAdapter:

    @BindingAdapter({“app:binding”})
    public static void bindEditText(EditText view,
        final BindableString bindableString) {
      Pair<BindableString, TextWatcherAdapter> pair = 
        (Pair) view.getTag(R.id.bound_observable);
      if (pair == null || pair.first != bindableString) {
        if (pair != null) {
         view.removeTextChangedListener(pair.second);
        }
        TextWatcherAdapter watcher = new TextWatcherAdapter() {
          public void onTextChanged(CharSequence s, 
              int start, int before, int count) {
            bindableString.set(s.toString());
          }
        };
        view.setTag(R.id.bound_observable, 
           new Pair<>(bindableString, watcher));
        view.addTextChangedListener(watcher);
      }  
      String newValue = bindableString.get();
      if (!view.getText().toString().equals(newValue)) {
        view.setText(newValue);
      }
    }
    
    <EditText
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="Text 1"
      app:binding="@{echo.text}"/>
    

    或者可以使用BindAdapter:

    @BindingAdapter({“app:binding”})
    public static void bindEditText(EditText view,
        final BindableString bindableString) {
      Pair<BindableString, TextWatcherAdapter> pair = 
        (Pair) view.getTag(R.id.bound_observable);
      if (pair == null || pair.first != bindableString) {
        if (pair != null) {
         view.removeTextChangedListener(pair.second);
        }
        TextWatcherAdapter watcher = new TextWatcherAdapter() {
          public void onTextChanged(CharSequence s, 
              int start, int before, int count) {
            bindableString.set(s.toString());
          }
        };
        view.setTag(R.id.bound_observable, 
           new Pair<>(bindableString, watcher));
        view.addTextChangedListener(watcher);
      }  
      String newValue = bindableString.get();
      if (!view.getText().toString().equals(newValue)) {
        view.setText(newValue);
      }
    }
    

    当然,google不可能真的这么蠢。。让我们自己去实现这一套,它在xml里给我们提供了很简单的运算符@=

    @=

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textNoSuggestions"
        android:text="@={model.name}"/>
    

    这么搞完当EditText更新时就自动更新model.name字段。当然,肯定很好奇背后的实现(这一part看了我半天。。) 它的实现是由多个注解完成的,先看其中两个:

    • @InverseBindingAdapter
    • @InverseBindingListener

    TextViewBindingAdapter.java:

    @BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
                "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
    public static void setTextWatcher(TextView view, final BeforeTextChanged before,
            final OnTextChanged on, final AfterTextChanged after,
            final InverseBindingListener textAttrChanged) {
        final TextWatcher newValue;
        if (before == null && after == null && on == null && textAttrChanged == null) {
            newValue = null;
        } else {
            newValue = new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                    if (before != null) {
                        before.beforeTextChanged(s, start, count, after);
                    }
                }
    
                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (on != null) {
                        on.onTextChanged(s, start, before, count);
                    }
                    if (textAttrChanged != null) {
                        textAttrChanged.onChange();
                    }
                }
    
                @Override
                public void afterTextChanged(Editable s) {
                    if (after != null) {
                        after.afterTextChanged(s);
                    }
                }
            };
        }
        final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
        if (oldValue != null) {
            view.removeTextChangedListener(oldValue);
        }
        if (newValue != null) {
            view.addTextChangedListener(newValue);
        }
    }
        
    @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
    public static String getTextString(TextView view) {
        return view.getText().toString();
    }
    

    xxxViewBinding.java

      private android.databinding.InverseBindingListener mboundView2androidTextAttrChanged = new android.databinding.InverseBindingListener() {
            @Override
            public void onChange() {
                // Inverse of model.str.get()
                //         is model.str.set((java.lang.String) callbackArg_0)
                java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView2);
                // localize variables for thread safety
                // model
                me.lizz0y.myapplication.VM model = mModel;
                // model.str != null
                boolean modelStrJavaLangObjectNull = false;
                // model != null
                boolean modelJavaLangObjectNull = false;
                // model.str.get()
                java.lang.String modelStrGet = null;
                // model.str
                android.databinding.ObservableField<java.lang.String> modelStr = null;
                modelJavaLangObjectNull = (model) != (null);
                if (modelJavaLangObjectNull) {
                    modelStr = model.str;
                    modelStrJavaLangObjectNull = (modelStr) != (null);
                    if (modelStrJavaLangObjectNull) {
                        modelStr.set(((java.lang.String) (callbackArg_0)));
                    }
                }
            }
        };
    

    也很简单,当text发生变化时,触发onTextChange,然后调用mboundView2androidTextAttrChanged.onChange,里面调用了由@InverseBindingAdapter注解的getTextString去获取值赋给model

    总结一下,假设你要给一个自定义属性双向绑定,写上@=时:你需要写以下函数:

    @InverseBindingAdapter(attribute = "refreshing", event = "refreshingAttrChanged")
    public static boolean getRefreshing(PhilView view) { //赋值时来这里取
        return isRefreshing;
    }
    
    @BindingAdapter(value = {"refreshingAttrChanged"}, requireAll = false)
    public static void setRefreshingAttrChanged(PhilView view, final InverseBindingListener inverseBindingListener) {
        Log.d(TAG, "setRefreshingAttrChanged");
    
        if (inverseBindingListener == null) {
            view.setRefreshingListener(null);
        } else {
            mInverseBindingListener = inverseBindingListener;
            view.setRefreshingListener(mOnRefreshingListener);
        }
    }
    
    @InverseMethod & @InverseBindingMethod[s]

    参考这篇

    只是简化了一下@InverseBindingAdapter的注解。

    与RecyclerView

    public class MyViewHolder extends RecyclerView.ViewHolder {
        private final ItemBinding binding;
    
        public MyViewHolder(ItemBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    
        public void bind(Item item) {
            binding.setItem(item);
            binding.executePendingBindings();
        }
    }
    

    强刷executePendingBinding

    强制绑定操作马上执行,而不是推迟到下一帧刷新时。RecyclerView 会在 onBindViewHolder 之后立即测量 View。如果因为绑定推迟到下一帧绘制时导致错误的数据被绑定到 View 中, View 会被不正确地测量,因此这个 executePendingBindings() 方法非常重要!

    todoApp结构

    1.png

    现在先让我们忘记前面说的一切有关双向绑定的事情。。。来看看google推出的架构LiveData&ViewModel

    我们先看平时开发时会有哪些问题:

    通常Android系统来管理UI controllers(如Activity、Fragment)的生命周期,由系统响应用户交互或者重建组件,用户无法操控。当组件被销毁并重建后,原来组件相关的数据也会丢失,如果数据类型比较简单,同时数据量也不大,可以通过onSaveInstanceState()存储数据,组件重建之后通过onCreate(),从中读取Bundle恢复数据。但如果是大量数据,不方便序列化及反序列化,则上述方法将不适用。

    UI controllers经常会发送很多异步请求,有可能会出现UI组件已销毁,而请求还未返回的情况,因此UI controllers需要做额外的工作以防止内存泄露。
    当Activity因为配置变化而销毁重建时,一般数据会重新请求,其实这是一种浪费,最好就是能够保留上次的数据。

    解决fragmentfragment之间通信的问题

    LiveData

    LiveData,顾名思义和生命周期绑定,解决上面的第二个异步问题:

    • 能够感知组件(Fragment、Activity、Service)的生命周期;

    • 只有在组件出于激活状态(STARTED、RESUMED)才会通知观察者有数据更新;

    public class NameViewModel extends ViewModel{
        // Create a LiveData with a String
        private MutableLiveData<String> mCurrentName;
        // Create a LiveData with a String list
        private MutableLiveData<List<String>> mNameListData;
    
        public MutableLiveData<String> getCurrentName() {
            if (mCurrentName == null) {
                mCurrentName = new MutableLiveData<>();
            }
            return mCurrentName;
        }
    
        public MutableLiveData<List<String>> getNameList(){
            if (mNameListData == null) {
                mNameListData = new MutableLiveData<>();
            }
            return mNameListData;
        }
    }
    
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mNameViewModel = ViewModelProviders.of(this).get(NameViewModel.class);
        mNameViewModel.getCurrentName().observe(this,(String name) -> {
            mTvName.setText(name);
            Log.d(TAG, "currentName: " + name);
        }); // 订阅LiveData中当前Name数据变化,以lambda形式定义Observer
        mNameViewModel.getNameList().observe(this, (List<String> nameList) -> {
            for (String item : nameList) {
                Log.d(TAG, "name: " + item);
            }
        }); // 订阅LiveData中Name列表数据变化,以lambda形式定义Observer
    }
    
    

    当组件处于激活状态,并且mCurrentName变量发生变化时,fragment观察者就会收到监听

    ViewModel

    [站外图片上传中...(image-93bfcd-1526989876647)]

    说实话一开始看到这个我总以为跟MVVMViewModel有什么异曲同工之妙,事实证明我想多了。此ViewModel是用来存储和管理UI相关的数据。

    ViewModel是生存在整个生命周期内的,所以在这个类中不能存在android.content.Context; 简单来说不能有viewcontext的引用,所以一般都会传ApplicationContext进去。

    用法很简单:

    public class MyActivity extends AppCompatActivity {
        public void onCreate(Bundle savedInstanceState) {
            // Create a ViewModel the first time the system calls an activity's onCreate() method.
            // Re-created activities receive the same MyViewModel instance created by the first activity.
    
            MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
            model.getUsers().observe(this, users -> {
                // update UI
            });
        }
    }
    
    public class MyViewModel extends ViewModel {
        private MutableLiveData<List<User>> users;
        public LiveData<List<User>> getUsers() {
            if (users == null) {
                users = new MutableLiveData<List<Users>>();
                loadUsers();
            }
            return users;
        }
    
        private void loadUsers() {
            // Do an asynchronous operation to fetch users.
        }
    }
    

    很容易看出它为什么可以解决以上问题, ViewModelProviders.of(this).get(MyViewModel.class);调用这个时,内部代码会帮我们做存储。至于怎么做存储,也很常见,加了一个fragmentsetRetain(true)就可以了。这样重建恢复后拿的还是同一个ViewModel,因此显然数据也都还在。同时,一个Activity对应的两个fragemt也可以通信:

    public class SharedViewModel extends ViewModel {
        private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
    
        public void select(Item item) {
            selected.setValue(item);
        }
    
        public LiveData<Item> getSelected() {
            return selected;
        }
    }
    
    
    public class MasterFragment extends Fragment {
        private SharedViewModel model;
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
            itemSelector.setOnClickListener(item -> {
                model.select(item);
            });
        }
    }
    
    public class DetailFragment extends Fragment {
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
            model.getSelected().observe(this, { item ->
               // Update the UI.
            });
        }
    }
    

    使用getActivity拿到同一份ViewModel,就可以拿到同一份数据。也很容易看到,其实LiveData是需要与ViewModel结合在一起用的

    todoApp-mvvm-live

    todoApp可以看出最明显的区别:

    @NonNull
    public static TaskDetailViewModel obtainViewModel(FragmentActivity activity) {
        // Use a Factory to inject dependencies into the ViewModel
        ViewModelFactory factory = ViewModelFactory.getInstance(activity.getApplication());
    
        return ViewModelProviders.of(activity, factory).get(TaskDetailViewModel.class);
    }
    

    其次,其实这个例子还是跟dataBinding搞在一起了,否则拿回数据更新UI时需要用到大量LiveDataobserve函数。

    最后所以用了LiveData的作用在哪,我们看一个例子,点击某个按钮后跳转Activity:

    before

    
    public class TasksActivity extends AppCompatActivity implements TaskItemNavigator, TasksNavigator {
    
        ....
        
    }
    
    
    @Nullable
    private WeakReference<TaskItemNavigator> mNavigator;
    
    
    

    ViewModel中:

    public void taskClicked() {
        String taskId = getTaskId();
        if (taskId == null) {
            // Click happened before task was loaded, no-op.
            return;
        }
        if (mNavigator != null && mNavigator.get() != null) { //烦躁
            mNavigator.get().openTaskDetails(taskId);
        }
    }
    

    after

    TasksActivity.java

    // Subscribe to "open task" event
    mViewModel.getOpenTaskEvent().observe(this, new Observer<String>() {
        @Override
        public void onChanged(@Nullable String taskId) {
            if (taskId != null) {
                openTaskDetails(taskId);
            }
        }
    });
    

    // mTasksViewModel.getOpenTaskEvent().setValue(task.getId());

    利用liveData的生命周期特性,就不用管activity是否已经消失。

    调试

    Databinding想调试自动生成的代码,需要在setting里选择Reference code generated by the compiler

    一些额外的技巧

    image
    image
    image

    相关文章

      网友评论

        本文标题:有关Databinding与MVVM的一些事

        本文链接:https://www.haomeiwen.com/subject/owpkjftx.html