An Android Jetpack ViewModel Tutorial

From Techotopia
Jump to: navigation, search

The previous chapter introduced the key concepts of Android Jetpack and outlined the basics of modern Android app architecture. Jetpack essentially defines a set of recommendations describing how an Android app project should be structured while providing a set of libraries and components that make it easier to conform with these guidelines with the goal of developing reliable apps with less coding and fewer errors.

To help re-enforce and clarify the information provided in the previous chapter, this chapter will step through the creation of an example app project that makes use of the ViewModel component. This example will be further enhanced in the next chapter with the inclusion of LiveData and data binding support.




About the Project

In the chapter entitled TBD, a project named AndroidSample was created in which all of the code for the app was bundled into the main Activity class file. In the chapter that followed, an AVD emulator was created and used to run the app. While the app was running, we experienced first-hand the kind of problems that occur when developing apps in this way when the data displayed on a TextView widget was lost during a device rotation.

This chapter will implement the same currency converter app, this time using the ViewModel component and following the Google app architecture guidelines to avoid Activity lifecycle complications.

Creating the ViewModel Example Project

The first step in this exercise is to create the new project. Begin by launching Android Studio and, if necessary, closing any currently open projects using the File -> Close Project menu option so that the Welcome screen appears.

Select the Start a new Android Studio project quick start option from the welcome screen and, within the resulting new project dialog, enter ViewModelDemo into the Application name field and ebookfrenzy.com as the Company Domain setting before clicking on the Next button.

On the form factors screen, enable the Phone and Tablet option and set the minimum SDK setting to API P: TBD before proceeding to the Activity selection screen.

When the AndroidSample project was created, the Basic Activity template was chosen as the basis for the project. For this project, however, the Activity & Fragment+ViewModel template will be used. This will generate an Android Studio project structured to confirm with the architectural guidelines. Select this option as shown in Figure 1-1 before clicking the Next button:

Android studio view model activity template.png 

On the final setup screen, accept the default file names and create the project using the Finish button.


Reviewing the Project

When a project is created using the Activity & Fragment+ViewModel template, the structure of the project differs in a number of ways from the Basic Activity used when the AndroidSample project was created. The key components of the project are as follows:

The Main Activity

The first point to note is that the user interface of the main activity has been structured so as to allow a single activity to act as a container for all of the screens that will eventually be needed for the completed app. The main user interface layout for the activity is contained within the app -> res -> layout -> main_activity.xml file and provides an empty container space in the form of a FrameLayout (highlighted in Figure 1-2) in which screen content will appear:

Android studio view model container area.png


The Content Fragment

The FrameLayout container is just a placeholder which will be replaced at runtime by the content of the first screen that is to appear when the app launches. This content will typically take the form of a Fragment consisting of an XML layout resource file and corresponding class file. In fact, when the project was created, Android Studio created an initial fragment for this very purpose. The layout resource file for this fragment can be found at app -> res -> layout -> main_fragment.xml and will appear as shown in Figure 1-3 when loaded into the layout editor:

Android studio view model fragment.png 

By default, the fragment simply contains a TextView displaying text which reads “MainFragment” but is otherwise ready to be modified to contain the layout of the first app screen. It is worth taking some time at this point to look at the code that has already been generated by Android Studio to display this fragment within the activity container area.

The process of replacing the FrameLayout placeholder with the fragment begins in the main Activity class file (app -> java -> <package name> -> MainActivity). The key lines of code appear within the onCreate() method of this class and replace the object with the id of container (which has already been assigned to the FrameLayout placeholder view) with the MainFragment class:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_activity);
    if (savedInstanceState == null) {
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.container, MainFragment.newInstance())
                .commitNow();
    }
}

The code that accompanies the fragment can be found in the MainFragment.java file (app -> <package name> -> ui.main -> MainFragment). Within this class file is the onCreateView() method which is called when the fragment is created. This method inflates the main_fragment.xml layout file so that it is displayed within the container area of the main activity layout:

@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    return inflater.inflate(R.layout.main_fragment, container, false);
}

The ViewModel

The ViewModel for the activity is contained within the MainViewModel.java class file located at app -> java -> ui.main -> MainViewModel. This is declared as a sub-class of the ViewModel Android architecture component class and is ready to be modified to store the data model for the app:

package com.ebookfrenzy.viewmodeldemo.ui.main;

import androidx.lifecycle.ViewModel;

public class  MainViewModel extends ViewModel {
    // TODO: Implement the ViewModel
}

Designing the Fragment Layout

The next step is to design the layout of the fragment. Locate the main_fragment.xml file in the Project tool window and double click on it to load it into the layout editor. Once the layout has loaded, select the existing TextView widget and use the Attributes tool window to change the id property to resultText.

Drag a Number (Decimal) view from the palette and position it above the existing TextView. With the view selected in the layout refer to the Attributes tool window and change the id to dollarText.

Drag a Button widget onto the layout so that it is positioned below the TextView, double-click on it and to edit the text and change it to read “Convert”. With the button still selected, change the id property to convertButton. At this point, the layout should resemble that illustrated in Figure 1-4:

Android studio view model ui.png 

Click on the Infer constraints button (Figure 1-5) to add any missing layout constraints:

Android studio layout infer constraints.png 

Finally, click on the warning icon in the top right-hand corner of the layout editor and convert the hardcoded strings to resources.

Implementing the View Model

With the user interface layout completed, the data model for the app needs to be created within the view model. Within the Project tool window, locate the MainViewModel.java file, double click on it to load it into the code editor and modify the class so that it reads as follows:

package com.ebookfrenzy.viewmodeldemo.ui.main;

import androidx.lifecycle.ViewModel;

public class  MainViewModel extends ViewModel {

    private static final Float usd_to_eu_rate = 0.74F;
    private Float value = 0F;
    private Float result = 0F;

    public void setAmount(Float value) {
        this.value = value;
        result = value*usd_to_eu_rate;
    }

    public Float getResult()
    {
        return result;
    }
}

The class declares variables to store the current dollar value and the converted amount together with getter and setter methods to provide access to those data values. When called, the setAmount() method takes as an argument the current dollar amount, stores it in the local value variable, converts it to euros using a fictitious exchange rate and stores the converted value in the result variable. The getResult() method, on the other hand, simply returns the current value assigned to the result variable.

Associating the Fragment with the View Model

Clearly, there needs to be some way for the fragment obtain a reference to the ViewModel in order to be able to access the model and observe data changes. A Fragment or Activity maintains references to the ViewModels on which it relies for data using an instance of ViewModelProvider class.

A ViewModelProvider instance is created via a call to the ViewModelProviders.of() method from within the Fragment. When called, the method is passed a reference to the current Fragment or Activity and returns a ViewModelProvider instance as follows:

ViewModelProvider viewModelProvider = ViewModelProviders.of(fragment);

Once the ViewModelProvider instance has been created, the get() method can be called on that instance passing through the class of specific ViewModel that is required. The provider will then either create a new instance of that ViewModel class, or return an existing instance:

ViewModel viewModel = viewModelProvider.get(MainViewModel.class);

Edit the MainFragment.java file and verify that Android Studio has already included this step within the onActivityCreated() method (albeit performing the operation in a single line of code for brevity):

mViewModel = ViewModelProviders.of(this).get(MainViewModel.class);

With access to the model view, code can now be added to the Fragment to begin working with the data model.

Modifying the Fragment

The fragment class now needs to be updated to react to button clicks and to interact with the data values stored in the ViewModel. The class will also need references to the three views in the user interface layout to react to button clicks, extract the current dollar value and to display the converted currency amount. With the MainFragment.java file loaded into the code editor, modify the onActivityCreated() method to obtain and store references to the three view objects as follows:

.
.
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.ebookfrenzy.viewmodeldemo.R;

public class MainFragment extends Fragment {

    private MainViewModel mViewModel;
    private EditText dollarText;
    private TextView resultText;
    private Button convertButton;
.
.    
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
        
        dollarText = getView().findViewById(R.id.dollarText);
        resultText = getView().findViewById(R.id.resultText);
        convertButton = getView().findViewById(R.id.convertButton);
    }
.
.

In the previous chapter, the onClick property of the Button widget was use to designate the method to be called when the button is clicked by the user. Unfortunately, this property is only able to call methods on an Activity and cannot be used to call a method in a Fragment. To get around this limitation, we will need to add some code to the Fragment class to set up an onClick listener on the button. The code to do this can be added to the onActivityCreated() method as follows:

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mViewModel = ViewModelProviders.of(this).get(MainViewModel.class);

    dollarText = getView().findViewById(R.id.dollarText);
    resultText = getView().findViewById(R.id.resultText);
    convertButton = getView().findViewById(R.id.convertButton);

    convertButton.setOnClickListener(new View.OnClickListener()
    {
        @Override
        public void onClick(View v) {

        }
    });
}

With the listener added, any code placed within the onClick() method will be called whenever the button is clicked by the user.

Accessing the ViewModel Data

When the button is clicked, the onClick() method needs to read the current value from the EditText view, confirm that it the field is not empty and then call the setAmount() method of the ViewModel instance. The method will then need to call the ViewModel’s getResult() method and display the converted value on the TextView widget.

Since LiveData is not yet being used in the project, it will also be important to get the latest result value from the ViewModel each time the Fragment is created.

Remaining in the MainFragment.java file, implement these requirements as follows in the onActivityCreated() method:

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mViewModel = ViewModelProviders.of(this).get(MainViewModel.class);


    dollarText = getView().findViewById(R.id.dollarText);
    resultText = getView().findViewById(R.id.resultText);
    convertButton = getView().findViewById(R.id.convertButton);

    resultText.setText(mViewModel.getResult().toString());

    convertButton.setOnClickListener(new View.OnClickListener()
    {
        @Override
        public void onClick(View v) {

            if (!dollarText.getText().toString().equals("")) {
                mViewModel.setAmount(
			Float.valueOf(dollarText.getText().toString()));
                resultText.setText(mViewModel.getResult().toString());
            } else {
                resultText.setText("No Value");
            }
        }
    });
}

Testing the Project

With this phase of the project development completed, build and run the app on the simulator or a physical device, enter a dollar value and click on the Convert button. The converted amount should appear on the TextView indicating that the UI controller and ViewModel re-structuring appears to be working as expected.

When the original AndroidSample app was run, rotating the device caused the value displayed on the resultText TextView widget to be lost. Repeat this test now with the ViewModelDemo app and note that the current euro value is retained after the rotation. This is because the ViewModel remained in memory as the Fragment was destroyed and recreated and code was added to the onActivityCreated() method to update the TextView with the result data value from the ViewModel each time the Fragment re-started.

While this is an improvement on the original AndroidSample app, there is much more that can be achieved to simplify the project by making use of LiveData and data binding, both of which are the topics of the next chapter.

Summary

In this chapter we revisited the AndroidSample project created earlier in the book and created a new version of the project structured to comply with the Android Jetpack architectural guidelines. The chapter outlined the structure of the Activity & Fragment+ViewModel project template and explained the concept of basing an app on a single Activity using Fragments to present different screens within a single Activity layout. The example project also demonstrated the use of ViewModels to separate data handling from user interface related code. Finally, the chapter showed how the ViewModel approach avoids some of the problems of handling Fragment and Activity lifecycles.