Model-view-presenter (MVP) is widely used in Android applications to separate views from presentation logic and models by introducing a presentation layer. Model-view-viewmodel (MVVM), or model-view-viewmodel, is very similar to MVP in that the ViewModel acts as an enhanced presentation layer, using data binders to keep the ViewModel and View in sync. By binding views to viewmodel properties, the data binder can handle view updates without manually changing the data to set the view (for example, without setting the setTest() or setVisibility() property of the TextView control). Like the presentation layer in MVP, the viewmodel can be easily unit tested. This article introduced the data binding library and THE MVVM architectural pattern, and how they work together on Android. Data Binding What is data binding?

Benefits of data binding libraries

TextView textView = (TextView) findViewById(R.id.label);
EditText editText = (EditText) findViewById(R.id.userinput);
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress);
 
editText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void afterTextChanged(Editable s) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { model.setText(s.toString()); }}); textView.setText(model.getLabel()); progressBar.setVisibility(View.GONE);Copy the code

As the code above shows, a lot of findViewById() calls are followed by a lot of setter/ Listener calls. Even using the ButterKnife injection library did not improve the situation. Data binding libraries are a good way to solve this problem.

Create a bound class at compile time that provides an ID field for all views, so you no longer need to call the findViewById() method. In fact, this is several times faster than calling the findViewById() method, because the data binding library creation code only needs to traverse the view structure once.

The binding logic of the view file is also implemented in the binding class, so all setters are called in the binding class and you don’t have to worry about it. In short, it makes your code much cleaner.

How do I set up data binding?

android {
   compileSdkVersion 25
   buildToolsVersion "25.0.1". dataBinding { enabled =true}... }Copy the code

Start by adding dataBinding {enabled = true} to your app’s build.gradle. The build system is then prompted to enable additional processing for data binding, such as creating binding classes from layout files.

<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
    <variable name="vm" type="com.example.ui.main.MainViewModel" />
    <import type="android.view.View" />
  </data>
  ...
</layout>
Copy the code

Next, wrap the top element in the layout below in a tag to create a bound class for the layout. The Binding class has the same name as the layout XML file, except that a Binding is added at the end. For example, the Binding class name for activity_main.xml is ActivityMainBinding. As shown above, the namespace declaration is also moved to the layout tag. You then declare the data you want to bind as variables within the layout tag, and set the name and type. In the example, the only variable is the viewmodel, but the variables will increase later. You can choose to import classes so that you can use constants such as view.Visible or static methods. How do I bind data?

<TextView
    android:id="@+id/my_layout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{vm.visible ? View.VISIBLE : View.GONE}">
    android:padding="@{vm.bigPadding ? @dimen/paddingBig : @dimen/paddingNormal}"
    android:text='@{vm.text ?? @string/defaultText + "Additional text."}' />
Copy the code

Data binding directives on view properties begin with @ and end with curly braces. You can use any variable to import your previously declared variable into the data segment. These expressions basically support everything you can do in your code, such as arithmetic operators or string concatenations.

The if-then-else ternary operator is also supported in the Visibility property. It also provides the merge operator?? , returns the right-hand operand if the left-hand value is null. In the code above, you can access resources just as you would in a normal layout, so you can select dimension resources based on Boolean variables or view them using the padding property.

Even if you use getters and setters in your code, the properties of the variables you declare can be accessed in the form of field access syntax. You can see this section in the text properties on slide, where vm.text calls the getText() method of the view model. Finally, some minor limitations apply, such as the fact that new objects cannot be created, but the data binding library is still very powerful. Which properties can be bound?

android:text="@{vm.text}"
android:visibility="@{vm.visibility}"
android:paddingLeft="@{vm.padding}"
android:layout_marginBottom="@{vm.margin}"
app:adapter="@{vm.adapter}"
Copy the code

In fact, most of the properties of the standard view are already supported by the data binding library. Inside the data binding library, when you use data binding, the library looks for setters for property names by view type. For example, when you bind data to a text property, the binding library looks for setText() methods in the view class with the appropriate parameter type, which in the example above is String.

You can also use setters for data binding when there is no corresponding layout property. For example, you can use the App: Adapter property on the Recycleler view in the XML layout to set adapter parameters using data binding.

For standard properties, not all of them have a setter method on the View. For example, in the paddingLeft case, the data binding library supports custom setters to transfer the binding to the padding property. But in the case of layout_marginBottom, what do we do when the binding library doesn’t provide a custom setter? Custom Setter

@BindingAdapter("android:layout_marginBottom")
public static void setLayoutMarginBottom(View v, int bottomMargin) {
   ViewGroup.MarginLayoutParams layoutParams =
           (ViewGroup.MarginLayoutParams) v.getLayoutParams();
  
   if (layoutParams != null) {
       layoutParams.bottomMargin = bottomMargin;
   }
}
Copy the code

For the above case, the custom setter can be overridden. Setters are implemented using the @bindingadapter annotation, and layout properties are named with parameters that cause the BindingAdapter to be called. The example above provides an adapter for binding layout_marginBottom.

The method must be public static void, and it must take the first view type called by the binding adapter as an argument, and then strongly bind the data to the type you want. In this example, we use an int to define a binding adapter for the type View (subtype). Finally, implement the binding adapter interface. For layout_marginBottom, we need to get the layout parameters and set the bottom interval:

@BindingAdapter({"imageUrl"."placeholder"})
public static void setImageFromUrl(ImageView v, String url, int drawableId) {
   Picasso.with(v.getContext().getApplicationContext())
           .load(url)
           .placeholder(drawableId)
           .into(v);
}
Copy the code

You may also need to set multiple properties to bind adapter calls. To do this, MMVM will provide a list of your property names and annotate them with the @BindingAdapter implementation. Also, in existing methods, each attribute has its own name. These BindingAdapters are called only after all the declared properties have been set.

While loading the image, I want to define a binding adapter for the loaded image to bind the URL to the placeHolder. As you can see, binding the adapter is very easy to do by using Picasso Image Loading Library. You can use any method you want in a custom binding adapter. Use bindings in your code

MyBinding binding;
 
// For Activity
binding = DataBindingUtil.setContentView(this, R.layout.layout);
// For Fragment
binding = DataBindingUtil.inflate(inflater, R.layout.layout, container, false);
// For ViewHolder
binding = DataBindingUtil.bind(view);
 
// Access the View with ID text_view
binding.textView.setText(R.string.sometext);
 
// Setting declared variables
binding.set<VariableName>(variable);
Copy the code

Now that we have defined the binding in our XML file and written our custom setters, how do we use the binding in our code? The data binding library does all the work for us by generating binding classes. To get an instance of the corresponding bound class for the layout, you use the helper methods provided by the library. Corresponding use DataBindingUtil Activity. The setContentView (), use fragment corresponding to inflate (), view the owner, please use the bind (). As mentioned earlier, the binding class provides all views for the IDS that define final fields. Also, you can set the variables you declare in the layout file of the binding object. With data binding, the library code can control the layout to update automatically when the data changes. However, the library still needs to be notified of changes to the data. This can be fixed if the bound variable implements the Observable interface (not to be confused with the RxJava Observable).

For simple data types like int and Boolean, the library already provides suitable Observable types, such as ObservableBoolean. There is also an ObservableField type for other objects, such as strings.

public class MyViewModel extends BaseObservable {
   private Model model = new Model();
 
   public void setModel(Model model) {
       this.model = model;
       notifyChange();
   }
   
   public void setAmount(int amount) {
       model.setAmount(amount);
       notifyPropertyChanged(BR.amount);
   }
 
   @Bindable public String getText() { return model.getText(); }
   @Bindable public String getAmount() { returnInteger.toString(model.getAmount()); }}Copy the code

In more complex cases, such as the Viewmodel, there is a BaseObservable class that provides utility methods to notify the layout of changes. As we saw above in the setModel() method, we can update the entire layout by calling notifyChange() after the model changes.

Looking at setAmount() again, you can see that only one property in the model has changed. In this case, we don’t want to update the entire layout, only the parts that use this attribute. To do this, add the @bindable annotation to the getter for the property. A field is then generated in the BR class that is passed to the notifyPropertyChanged() method. This way, the binding library can update only the parts of the layout that do depend on changing properties. Summary • Declare variables in the layout file and bind them to properties in the view.

• Create bindings in your code to set variables.

• Make sure your variable types implement the Observable interface — which can be inherited from The BaseObservable — so that data changes are automatically reflected in the layout.

Model, View, View Model (MVVM) architecture

A view is the user interface, or layout. In Android, this usually refers to activities, fragments, or ViewHolder and the XML layout files used with them.

A model is a layer of business logic that provides methods to interact with data.

The viewmodel acts as a middleman between the view and the model, providing access to both the model’s data and the UI state. It also defines commands that can be invoked by events, such as click events. The viewmodel contains the rendering logic in the application.

In the MVVM architectural pattern, the model and view model interact primarily through data binding. Ideally, views and viewmodels don’t have to know each other. The binding should be the glue between the view and the viewmodel, and handles most things in both directions. However, in Anroid they cannot really be separated:

You want to save and restore the state, but now the state is in the viewmodel.

You need to let the viewmodel know about lifecycle events.

You may encounter situations where you need to call view methods directly.

In these cases, the view and the viewmodel should implement interfaces and then communicate through commands when needed. The interface to the viewmodel is needed in any case, because the data binding library handles the interaction with the view and uses custom components when the context requires it.

The viewmodel also updates the model, such as adding new data to the database or updating an existing data. It is also used to retrieve data from the model. Ideally, the model should also notify the view-model of changes, but this depends on the implementation.

In general, the separation of views and viewmodels makes rendering logic easier to test and helps sustain long-term operations. Working with a data binding library results in less and cleaner code. The sample

<layout xmlns:android="...">
  <data>
    <variable name="vm" type="pkg.MyViewModel" />
  </data>
 
  <FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <EditText
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:visibility="@{vm.shouldShowText}"
      android:text="@={vm.text}" />
 
    <Button
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:onClick="@{vm::onButtonClick}"
      android:text="@string/button"/>
  </FrameLayout>
</layout>
Copy the code

With MVVM, the layout refers to only one variable, the view model of the view, in this case MyViewModel. In the viewmodel, you need to provide the properties needed for your layout, which can be as simple and complex as your use case.

public class MyViewModel extends BaseObservable {
   private Model model = new Model();
 
   public void setModel(Model model) {
       this.model = model;
       notifyChange();
   }
 
   public boolean shouldShowText() {
       return model.isTextRequired();
   }
 
   public void setText(String text) {
       model.setText(text);
   }
 
   public String getText() {
       return model.getText();
   }
 
   public void onButtonClick(View v) {
       // Save data
   }
}
Copy the code

There’s a text property here. When using EditText for user input, you can use bidirectional binding, while the data binding library feeds the input back into the viewmodel. To do this, we create a setter and getter and bind the property to the Text property of the EditText, where the = sign before the braces indicates that we are bidirectional binding.

In addition, we only want to display the EditText when the model needs to input the text. In this case, we provide a Boolean property in the viewmodel and bind it to the Visibility property. To make this work, we also create a BindingAdapter (BindingAdapter) that sets visibility to GONE when the value is false and VISIBLE when the value is true.

@BindingAdapter("android:visibility")
public static void setVisibility(View view, boolean visible) {
   view.setVisibility(visible ? View.VISIBLE : View.GONE);
}
Copy the code

Finally, we want to store information when a Button is clicked, so we create an onButtonClick() command in the viewmodel that handles the interaction with the model. In the layout, we bind the command to the Button’s onClick property by referring to this method. To make this work directly, we need to introduce a single View argument to the method, similar to OnClickListener. If you don’t want to use View arguments, you can also use lambda expressions directly in the layout.

For testing purposes, we need to present logical processing in the viewmodel, but try to avoid putting logical processing directly into it. Of course, you can also customize the binding adapter, which is easier. Life cycles and States Another thing to consider when implementing the MVVM architecture is how life cycles and states are handled in your application. First, I recommend that you create a base class for the viewmodel to handle this kind of problem.

public abstract class BaseViewModel<V extends MvvmView> extends BaseObservable {
   private V view;
 
   @CallSuper public void attachView(V view, Bundle sis) {
       this.view = view;
       if(sis ! = null) { onRestoreInstanceState(sis); } } @CallSuper public voiddetachView() {
       this.view = null;
   }
 
   protected void onRestoreInstanceState(Bundle sis) { }
   protected void onSaveInstanceState(Bundle outState) { }
 
   protected final V view() { returnview; }}Copy the code

Both activities and fragments have lifecycle callbacks. Now they’re all handled in the viewmodel. Therefore, we need to pass life cycle callbacks. I recommend using two callbacks that satisfy most needs: attachView(), which signals that the view is created, and detachView(), which signals that the view is destroyed. In attachView(), the incoming view interface is used to send commands to the view if necessary. AttachView () is normally called in the Fragment’s onCreate() or onCreateView(), detachView() in onDestory() and onDestoryView().

Activities and fragments now also provide callbacks to save state when the system destroys components or when configuration changes. We keep the state in the viewmodel, and we need to pass these callbacks to the viewmodel. I recommend passing savedInstanceState directly to attachView() to restore the state automatically there. The other onSaveInstanceState() method needed to save state must be called in the Activity and Fragment related callbacks. If you have UI state, create a separate state class for each viewmodel. When this class implements Parcelable, it’s easy to save and restore state because you only need to save or restore one object. view

public abstract class BaseActivity<B extends ViewDataBinding, V extends MvvmViewModel> 
   extends AppCompatActivity implements MvvmView {
 
   protected B binding;
   @Inject protected V viewModel;
  
   protected final void setAndBindContentView(@LayoutRes int layoutResId, @Nullable Bundle sis) {
       binding = DataBindingUtil.setContentView(this, layoutResId);
       binding.setVariable(BR.vm, viewModel);
       viewModel.attachView((MvvmView) this, sis);
   }
 
   @Override @CallSuper protected void onSaveInstanceState(Bundle outState) {
       super.onSaveInstanceState(outState);
       if(viewModel ! = null) { viewModel.onSaveInstanceState(outState); } } @Override @CallSuper protected voidonDestroy() {
       super.onDestroy();
       if(viewModel != null) { viewModel.detachView(); }
       binding = null;
       viewModel = null;
   }
}
Copy the code

Now, let’s discuss the details of the view. The above example creates the activity base class. The View model can be injected into the base class to initialize the schema configuration. Then you just need to call this method in the Activity’s onCreate() or fragment’s onCreateView().

The above code is handled using the setAndBindContentView() method, which can be called from onCreate() instead of the usual setContentView() call. This method sets the content view and creates a binding, sets viewmodel variables on the binding, and attaches the view to the viewmodel, as well as providing the saved sample state.

As you can see, onSaveInstanceState() and detachView() callbacks can also be implemented in base classes. OnSaveInstanceState () forwards the callback to the viewmodel, and onDestroy() calls the detachView() interface on the viewmodel.

With the base class set up this way, you can write your APP using the MVVM architecture. Other Considerations Once you understand the basics of the MVVM architecture for Android applications, you need to further refine the application architecture.

Dependency injection Using dependency injection makes it very easy to inject components into the viewmodel and join components together nicely, such as using the Dagger 2 dependency injection framework.

Dependency injection can further decouple code, making it simpler and easier to test. At the same time, it greatly enhances the maintainability of the code. More importantly, dependency interfaces can be truly decoupled.

Business logic Note: The viewmodel only contains rendering logic, so do not put business logic in the viewmodel. Create a storage interface for the model class and select the storage mode to implement it:

public interface ModelRepo {
   Single<List<Model>> findAll();
   Single<Model> findById(int id);
 
   void save(Model model);
   void delete(Model model);
}
Copy the code

For networks, Retrofit is used to create network-specific code to implement defined interfaces.

public interface ModelRepo {
   @GET("model")
   Single<List<Model>> findAll();
 
   @GET("model/{id}")
   Single<Model> findById(@Path("id") int id);
 
   @PUT("model")
   Completable create(@Body Model model);
}
Copy the code

For basic operations such as find and create, repositories can be injected into the viewmodel to retrieve and manipulate data. For other more complex cases, such as validation, separate components need to be created to implement these behaviors and injected into the viewmodel. Navigation Another important part of Android is navigation, because you need a view to provide a component, either a Context to start an Activity or a FragmentManager to replace a Fragment. At the same time, using the view interface to invoke navigation commands only complicates the architecture.

Therefore, we need a separate component to handle navigation in the application. The Navigator interface defines public methods for starting an Activity, handling fragments, and injecting them into the viewmodel. You can navigate directly in the viewmodel without the need for Context or FragmentManager, because those are handled by the navigator’s implementation.

public interface Navigator {
   String EXTRA_ARGS = "_args"; void finishActivity(); void startActivity(Intent intent); void startActivity(String action); void startActivity(String action, Uri uri); void startActivity(Class<? extends Activity> activityClass); void startActivity(Class<? extends Activity> activityClass, Bundle args); void replaceFragment(int containerId, Fragment fragment, Bundle args); void replaceFragmentAndAddToBackStack(int containerId, @NonNull Fragment fragment, Bundle args, String backstackTag); . }Copy the code

The view holder can easily navigate within the view model using the navigator. For example, clicking on a card in the recycle view can start a new Activity.

Unit Testing Finally, let’s look at the viewmodel and unit testing. As mentioned earlier, the MVVM architecture simplifies test rendering logic. I use Mockito more generally, which allows me to simulate the view interface and other injected view models and components. Of course, you can also do more demanding testing with PowerMock, which uses bytecode control and can simulate static methods.

public class MyViewModelUnitTest {
   @Mock ModelRepo modelRepo;
   @Mock Navigator navigator;
   @Mock MvvmView myView;
   MyViewModel myViewModel;
 
   @Before public void setup() {
       MockitoAnnotations.initMocks(this);
       myViewModel = new MyViewModel(modelRepo, navigator);
       myViewModel.attachView(myView, null);
   }
 
   @Test public void buttonClick_submitsForm() {
       final Model model = new Model();
       doReturn(model).when(modelRepo).create(); myViewModel.onButtonClick(null); verify(modelRepo).save(model); verify(navigator).finishActivity(); }}Copy the code

The mock is initialized in the setup() method, creating the view model, injecting the mock object and attaching the view interface to the view model. When writing test cases, specify the behavior of mock objects through Mockito’s doReturn().when() syntax, if necessary. The test method is then invoked in the viewmodel. Finally, use assertions and verify() to check that the return value is correct and that the mock method is called as expected.

conclusion

• About organizing the APP architecture using a data binding library according to the ModelViewViewModel model, the following is summarized: • A view model is an intermediary between a view and a model. • Views automatically update viewmodel properties through data binding. • View events invoke commands in the view model. • The viewmodel can also invoke commands on the view. • In Android, the viewmodel handles basic lifecycle callbacks and state saving and recovery. • Dependency injection facilitates testing and cleaner code. • Do not place business logic in the viewmodel; they only contain presentation logic. In addition, a repository is used for data access. • Use the Navigator component to navigate in the Android App.