Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

RoboMVVM TipCalc

4.92/5 (8 votes)
22 Sep 2014CPOL5 min read 14.1K  
Tip calculator using RoboMVVM- An MVVM framework for Android.

Introduction

RoboMVVM is an open source MVVM Framework for Android. The MVVM pattern utilizes Data Binding to keep arbitrary data of arbitrary components in sync. Those familiar with the .NET world can appreciate the ability of the MVVM pattern to greatly simplify the building, testing and refactoring of UI applications.

RoboMVVM will save countless hours of your time by providing you with the tools you need to quickly setup bindings between your views and your data models.

This is the first of a series of tutorials detailing the use of this library in your apps. 

Installation

Since RoboMVVM is in rapid development, we do not provide pre-compiled repositories yet. This may change in the near future. Till then, just clone the repository from Github Here, and add a reference to it from Android Studio.

We do not develop RoboMVVM in Eclipse. However, you should be able to manually set up the project in Eclipse by referencing the source. RoboMVVM is plain jane java code, and there is no magic. Hence, setting it up in a different environment should be as simple as copying the source files and creating a new project. 

 

TipCalc

TipCalc is inspired by a similar tutorial for MCCMCross - An MVVM library for C#. You can find it Here.  TipCalc lets you calculate a tip based on your subtotal, and a generosity, which ranges from 0% to 100%. The tip is calculated using the formula tip = subtotal * generosity / 100. 

The source code for this tutorial can be found in the githib repository Here. Make sure to clone the repo and play around with it to get a better understanding of RoboMVVM while following this tutorial. 

 

Setting Up Your ViewModel

A ViewModel consists of properties and actions. Our model consists of three properties - A subtotal, a generosity value, and the calculated tip. The ViewModel must subclassed from the ViewModel class. The ViewModel is also attributed with the @SetLayout attribute which sets the layout xml to use for this ViewModel. 

You can check out the entire code for this class Here

Java
@SetLayout(R.layout.tipcalc_layout)
public class TipCalcViewModel extends ViewModel {

    public TipCalcViewModel(Context context) {
        super(context);
    }

}

We start by defining these properties in the TipCalcViewModel class:

Java
public  float getSubTotal() {
    return subTotal;
}

public void setSubTotal(float subTotal) {
    this.subTotal = subTotal;
    raisePropertyChangeEvent("subTotal");
    recalculate();
}

public float getGenerosity() {
    return generosity;
}

public void setGenerosity(float generosity) {
    this.generosity = generosity;
    raisePropertyChangeEvent("generosity");
    recalculate();

}

public float getTip() {

    return tip;
}

public void setTip(float tip) {
    this.tip = tip;
    raisePropertyChangeEvent("tip");
}

Properties are defined by setters and getters. For instance, to define the subTotal property, we ceate the getter/setter pair float getSubTotal() and void setSubtotal(float subTotal). 

The setters of the properties contain a raisePropertyChangeEvent call that notifies that the property has changed. We also call the recalculate function whenever either the subtotal or the generosity property is set, which sets the value of the tip. The recalculate function simply sets the value of the tip based on the subTotal and generosity values: 

Java
private void recalculate() {
    setTip(getSubTotal() * getGenerosity() / 100);
}

 

Creating Your View

Views are created as you normally would in a layout xml. This layout is called tipcalc_layout.xml, and is referenced from the TipCalcViewModel using the @SetLayout attribute. 

The layout basically consists of an EditText for the subtotal, a SeekBar for the generosity, and a TextView for the final calculated tip. We also have a TextView to display the generosity value that has been set by the SeekBar.

The relevant views have Ids that we are going to use to set up the bindings. The final view looks like this:

 

 

The layout XML for this is pretty straightforward, and it looks like this:

XML
<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:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:text="Subtotal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"/>

        <EditText
            android:id="@+id/subtotal_edit_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="12" />

        </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Generosity"
            android:layout_marginTop="20dp"
            android:layout_marginBottom="12dp"
            android:layout_weight="20"/>

        <TextView
            android:id="@+id/generosity_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="10"
            android:layout_marginTop="20dp"
            android:layout_marginBottom="12dp"
            android:layout_weight="0.5"/>

    </LinearLayout>

    <SeekBar
        android:id="@+id/generosity_seek_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="12dp"
        android:max="10000"
        android:progress="1000"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Tip"
            android:layout_marginTop="20dp"
            android:layout_marginBottom="12dp"
            android:layout_weight="20"/>

        <TextView
            android:id="@+id/tip_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""
            android:layout_marginTop="20dp"
            android:layout_marginBottom="12dp"
            android:layout_weight="0.5"/>

    </LinearLayout>

</LinearLayout>

 

Binding the ViewModel To The View

The binding consists of bindProperty calls that bind ViewModel properties to View properties. It requires the view model property name, the view Id and the view property name. It also optionally takes a ValueConverter to convert values between the ViewModel and the View, and a BindMode that specifies the direction of the binding.

When no BindMode is mentioned, the bind mode is always taken to be BindMode.SOURCE_TO_TARGET, which reflects ViewModel changes in the View, and not vice versa. You can aso use BindMode.TARGET_TO_SOURCE, and BindMode.BIDIRECTIONAL. 

In our case, we need to use bidirectional binding for the EditText and the SeekBar, and source to target bindings for the TextViews. ValueConverters also need to be used as the EditText's text property is a String and the Seekbar's progress property is an int. We need to convert these to floats. Also, we need to convert the float values of generosity and tip to string values expected by the respective TextViews.

The binding code  must be written in an overriden bind() function provided in the ViewModel base class. It looks like this:

Java
@Override
protected void bind() {

    bindProperty("subTotal", R.id.subtotal_edit_text, "text", new ValueConverter() {

        @Override
        public Object convertToTarget(Object value) {
            return value.toString();
        }

        @Override
        public Object convertToSource(Object value) {
            try {
                return Float.parseFloat((String) value);
            } catch(NumberFormatException e) {
                return 0;
            }
        }
    }, BindMode.BIDIRECTIONAL);


    bindProperty("generosity", R.id.generosity_seek_bar, "progress", new ValueConverter() {
        @Override
        public Object convertToTarget(Object value) {
            float floatVal = (Float) value;

            int ret = (int)(floatVal * 100);

            return ret;
        }

        @Override
        public Object convertToSource(Object value) {
            int intVal = (Integer) value;

            float ret = ((float)intVal) / 100f;

            return ret;
        }
    }, BindMode.BIDIRECTIONAL);

    bindProperty("generosity", R.id.generosity_text_view, "text",
            new TypedValueConverter(Float.class, String.class), BindMode.SOURCE_TO_TARGET);

    bindProperty("tip", R.id.tip_text_view, "text",  new TypedValueConverter(Float.class, String.class), BindMode.SOURCE_TO_TARGET);

}

 
A TypedValueConverter class is provided that attempts to convert between any two types. It can convert between any types that can be casted to each other. It can also convert any type to a string, by calling the toString() function. This will work well for us in the float -> string conversion we need to do to bind our TextViews. 

 

Setting Up The Activity

The business logic and view bindings are all taken care of by the view model. hence, the activity class can be simple. All we need to do is to assign a view corresponding to the ViewModel to the root of the Activity by calling setContentView. The code looks like this: 

Java
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new TipCalcViewModel(this).createView());
    }
}

 

In Conclusion

Hopefully, I have demonstrated to you the ease with which UI apps can be created and bound to data using RoboMVVM. Watch this space for more tutorials, and follow the repository for staying up to date with further developments to RoboMVVM.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)