26. Room, LiveData, and ViewModel

26.1. What are Android Architecture Components?



The Android OS manages resources aggressively to perform well on a huge range of devices, and sometimes that makes it challenging to build robust apps. Android Architecture Components provide guidance on app architecture, with libraries for common tasks like lifecycle management and data persistence.

Architecture Components help you structure your app in a way that is robust, testable, and maintainable with less boilerplate code. Architecture Components provide a simple, flexible, and practical approach that frees you from some common problems so you can focus on building great experiences.

26.3. MutableLiveData

You can use LiveData independently from Room, but to do so you must manage data updates. However, LiveData has no publicly available methods to update the stored data.

Therefore, if you want to update the stored data, you must use MutableLiveData instead of LiveData. The MutableLiveData class adds two public methods that allow you to set the value of a LiveData object: setValue(T) and postValue(T).

MutableLiveData is usually used in the ViewModel, and then the ViewModel only exposes immutable LiveData objects to the observers.

See the Architecture Components’ BasicSample code for examples of using MutableLiveData. The example in Guide to App Architecture also shows a use MutableLiveData.

26.3.1. Observing LiveData

To update the data that is shown to the user, create an observer of the data in the onCreate() method of MainActivity and override the observer’s onChanged() method. When the LiveData changes, the observer is notified and onChanged() is executed. You then update the cached data, for example in the adapter, and the adapter updates what the user sees.

Usually you observe data in a ViewModel, not directly in the Repository or in Room. ViewModel is described in a later section.

The following code synatx shows how to attach an observer to LiveData:

// Create the observer which updates the UI.
final Observer<String> nameObserver = new Observer<String>() {
    @Override
    public void onChanged(@Nullable final String newName) {
        // Update the UI, in this case, a TextView.
        mNameTextView.setText(newName);
    }
};

mModel.getCurrentName().observe(this, nameObserver);

See the LiveData documentation to learn other ways to use LiveData, or watch this Architecture Components: LiveData and Lifecycle video.

26.4. Room database

Room is a database layer on top of an SQLite database. Room takes care of mundane tasks that you used to handle with an SQLiteOpenHelper.



To use Room:

  1. Create a public abstract class that extends RoomDatabase.
  2. Use annotations to declare the entities for the database and set the version number.
  3. Use Room’s database builder to create the database if it doesn’t exist.
  4. Add a migration strategy for the database. When you modify the database schema, you’ll need to update the version number and define how to handle migrations. For a sample, destroying and re-creating the database is a fine migration strategy. For a real app, you must implement a migration strategy. See Understanding migrations with Room.

Note that:

  • Room provides compile-time checks of SQLite statements.
  • By default, to avoid poor UI performance, Room doesn’t allow you to issue database queries on the main thread. LiveData applies this rule by automatically running the query asynchronously on a background thread, when needed.
  • Usually, you only need one instance of the Room database for the whole app. Make your RoomDatabase a singleton to prevent having multiple instances of the database opened at the same time, which would be a bad thing.

Here is a sample of a complete Room class:

@Database(entities = {Student.class} , version = 1 , exportSchema = false)
public abstract class StudentDataBase extends RoomDatabase {

    public abstract StudentDao myDao();

    private static StudentDataBase dataBase;

    static synchronized StudentDataBase getDataBase(Context context){
        if (dataBase == null){
            dataBase = Room.databaseBuilder(context,
                    StudentDataBase.class,"MyDb")
                    .allowMainThreadQueries()
                    .fallbackToDestructiveMigration().build();
        }
        return dataBase;
    }
}

26.4.1. Repository

A Repository is a class that abstracts access to multiple data sources. The Repository is not part of the Architecture Components libraries, but is a suggested best practice for code separation and architecture. A Repository class handles data operations. It provides a clean API to the rest of the app for app data.



A Repository is where you would put the code to manage query threads and use multiple backends, if appropriate. Once common use for a Repository is to implement the logic for deciding whether to fetch data from a network or use results cached in the database.



Here is the complete code for a basic Repository:

public class StudentRepository {

    private StudentDataBase studentDataBase;
    private LiveData<List<Student>> read;

    StudentRepository(Application application) {
        studentDataBase=StudentDataBase.getDataBase(application);
        read = studentDataBase.myDao().readData();
    }

    void insertData(Student student){
        new InsertTask().execute(student);
    }
    void deleteData(Student student){
        new DeleteTask().execute(student);
    }

    LiveData<List<Student>> readData(){
        return read;
    }
    
    class  InsertTask extends AsyncTask<Student,Void,Void>{
        @Override
        protected Void doInBackground(Student... students) {
            studentDataBase.myDao().insert(students[0]);
            return null;
        }
    }

    class DeleteTask extends AsyncTask<Student,Void,Void>{
        @Override
        protected Void doInBackground(Student... students) {
            studentDataBase.myDao().delete(students[0]);
            return null;
        }
    }
}

Note: In this example, the Repository doesn’t do much. See the BasicSample for an applied implementation.

The [Guide to App Architecture] includes a more complex example that uses a web service to fetch data.

26.4.2. ViewModel

The ViewModel is a class whose role is to provide data to the UI and survive configuration changes. A ViewModel acts as a communication center between the Repository and the UI. You can also use a ViewModel to share data between fragments. The ViewModel is part of the lifecycle library. For an introductory guide to this topic, see the ViewModel overview.



A ViewModel holds your app’s UI data in a lifecycle-conscious way that survives configuration changes. Separating your app’s UI data from your Activity and Fragment classes lets you better follow the single responsibility principle: Your activities and fragments are responsible for drawing data to the screen, while your ViewModel is responsible for holding and processing all the data needed for the UI.



Warning: Never pass context into ViewModel instances. Do not store Activity, Fragment, or View instances or their Context in the ViewModel. An Activity can be destroyed and created many times during the lifecycle of a ViewModel, such as when the device is rotated. If you store a reference to the Activity in the ViewModel, you end up with references that point to the destroyed Activity. This is a memory leak. If you need the application context, use AndroidViewModel instead of ViewModel.

In the ViewModel, use LiveData for changeable data that the UI will use or display, so that you can add an observer and respond to changes.

Here is the complete code for a sample ViewModel:

public class StudentViewModel extends AndroidViewModel {

    private StudentRepository repository;
    private LiveData<List<Student>> readAllData;
    public StudentViewModel(@NonNull Application application) {
        super(application);
        repository = new StudentRepository(application);
        readAllData = repository.readData();
    }

    void insert(Student student){
        repository.insertData(student);
    }

    void delete(Student student){
        repository.deleteData(student);
    }

    LiveData<List<Student>> readData(){
        return readAllData;
    }
}

Important: ViewModel is not a replacement for onSaveInstanceState(), because the ViewModel does not survive a process shutdown. See Saving UI States.

To learn more, watch this Architecture Components: ViewModel video.

26.5. Displaying LiveData

Finally, you can display all this interesting data to the user.



Whenever the data changes, the onChanged() method of your observer is called.

In the most basic case, this can update the contents of a TextView, as shown in this code:

final Observer<String> nameObserver = new Observer<String>() {
    @Override
    public void onChanged(@Nullable final String newName) {
        // Update the UI, in this case, a TextView.
        mNameTextView.setText(newName);
    }
};

Another common case is to display data in a View that works with an adapter. For example, if you are showing data in a RecyclerView, the onChanged() method updates the data cached in the adapter:

        viewModel.readData().observe(this, new Observer<List<Student>>() {
            @Override
            public void onChanged(List<Student> students) {
                rv.setLayoutManager(new LinearLayoutManager(MainActivity.this));
                rv.setAdapter(new MyDataAdapter(MainActivity.this,students));
            }
        });

Tip: One way to get a reference to the application context in a data repository is to extend the Application class and add a member of type Context to that custom subclass.

26.5.1. Lifecycle-aware components

Most of the app components that are defined in the Android framework have lifecycles attached to them. Lifecycles are managed by the operating system or the framework code running in your process. They are core to how Android works and your app must respect them. Not doing so may trigger memory leaks or even app crashes. Activities and fragments are examples of lifecycle-aware components, and LiveData is lifecycle aware.

A common pattern is to implement the actions of the dependent components in the lifecycle methods of activities and fragments. For example, you might have a listener class that connects to a service when the activity starts, and disconnects when the activity is stopped. In the activity, you then override the onStart() and onStop() methods to start and stop the listener.

@Override
    public void onStart() {
        super.onStart();
        myListener.start();
}

This code snippet looks innocent enough. However, once you have multiple components, using this pattern leads to poor code organization and a proliferation of errors, such as possible race conditions.

Lifecycle-aware components perform actions in response to a change in the lifecycle status of another component. For example, a listener could start and stop itself in response to an activity starting and stopping. This results in code that is better-organized, usually shorter, and always easier to maintain.

The android.arch.lifecycle package provides classes and interfaces that let you build lifecycle-aware components that automatically adjust their behavior based on the lifecycle state of an activity or fragment. That is, you can make any class lifecycle aware.

26.5.2. Use cases for lifecycle-aware components

Lifecycle-aware components can make it much easier for you to manage lifecycles in a variety of cases. For example, you can use lifecycle-aware components to:

  • Switch between coarse and fine-grained location updates.

    Use lifecycle-aware components to enable fine-grained location updates while your location app is visible, then switch to coarse-grained updates when the app is in the background. Add LiveData to automatically update the UI when your user changes locations.

  • Stop and start video buffering.

Use lifecycle-aware components to start video buffering as soon as possible, but defer playback until the app is fully started. You can also use lifecycle-aware components to end buffering when your app is destroyed.

  • nStart and stop network connectivity.

Use lifecycle-aware components to enable live updating (streaming) of network data while an app is in the foreground, then automatically pause when the app moves into the background.

  • Pause and resume animated drawables.

Use lifecycle-aware components to pause animated drawables while the app is in the background, then resume drawables after the app returns to the foreground.

26.5.3. Practical Example:

activity_main.xml

<?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"
    android:padding="20dp"
    tools:context=".MainActivity">
    
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_person_add_black_24dp"
        app:layout_constraintBottom_toBottomOf="@+id/recycler"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.954"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.96" />
    
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    FloatingActionButton fab;
    RecyclerView rv;
    static StudentViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fab=findViewById(R.id.fab);
        rv =findViewById(R.id.recycler);

        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(MainActivity.this,InsertActivity.class);
                startActivity(i);
            }
        });

        viewModel = ViewModelProviders.of(MainActivity.this)
                .get(StudentViewModel.class);

        viewModel.readData().observe(MainActivity.this, new Observer<List<Student>>() {
            @Override
            public void onChanged(List<Student> students) {
                rv.setLayoutManager(new LinearLayoutManager(MainActivity.this));
                rv.setAdapter(new MyDataAdapter(MainActivity.this,students));
            }
        });
    }
}

activity_insert.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"
    android:padding="10dp"
    tools:context=".InsertActivity">

    <EditText
        android:id="@+id/studentname"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter Student Name"
        android:inputType="text" />

    <EditText
        android:id="@+id/studentrollnumber"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter RollNumber"
        android:inputType="text" />

    <EditText
        android:id="@+id/studentmailid"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter Mail ID"
        android:inputType="textEmailAddress" />

    <EditText
        android:id="@+id/studentmobileNumber"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter Mobile Number"
        android:inputType="phone" />

    <Button
        android:id="@+id/save"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:onClick="save"
        android:text="Save" />
</LinearLayout>

InsertActivity.java

public class InsertActivity extends AppCompatActivity {

    EditText sname, sroll, smobile, smail;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_insert);

        sname = findViewById(R.id.studentname);
        sroll = findViewById(R.id.studentrollnumber);
        smail = findViewById(R.id.studentmailid);
        smobile = findViewById(R.id.studentmobileNumber);
    }
    public void save(View view) {
        String name = sname.getText().toString();
        String mailid = smail.getText().toString();
        String phone = smobile.getText().toString();
        String roll = sroll.getText().toString();

        Student student = new Student();
        student.setName(name);
        student.setMailID(mailid);
        student.setMobileNUmber(phone);
        student.setRollNumber(roll);

        MainActivity.viewModel.insert(student);

        finish();
    }
}

row_design.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="10dp">

    <TextView
        android:id="@+id/readname"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fontFamily="serif"
        android:text="Name"
        android:textColor="#E91E63"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/readroll"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:fontFamily="serif"
        android:text="RollNumber"
        android:textColor="#3F51B5"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/readMailid"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:fontFamily="serif"
        android:text="Mail ID"
        android:textColor="#BF3205"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/readmobile"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:fontFamily="serif"
        android:text="RollNumber"
        android:textColor="#039109"
        android:textSize="20sp" />

    <ImageView
        android:id="@+id/delete"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end"
        android:contentDescription="Delete"
        android:src="@drawable/ic_delete_forever_black_24dp" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginTop="5dp"
        android:background="#2196F3" />

</LinearLayout>

MyDataAdapter.java

public class MyDataAdapter extends RecyclerView.Adapter<MyDataAdapter.DataViewHolder> {

    Context ct;
    List<Student> students;

    public MyDataAdapter(MainActivity mainActivity, List<Student> studentList) {
        ct = mainActivity;
        students = studentList;
    }

    @NonNull
    @Override
    public MyDataAdapter.DataViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(ct).inflate(R.layout.row_design, parent, false);
        return new DataViewHolder(v);
    }

    @Override
    public void onBindViewHolder(@NonNull MyDataAdapter.DataViewHolder holder, int position) {
        final Student list = students.get(position);
        holder.rroll.setText(list.getRollNumber());
        holder.rname.setText(list.getName());
        holder.rmobile.setText(list.getMobileNUmber());
        holder.rmail.setText(list.getMailID());

        holder.delete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivity.viewModel.delete(list);
            }
        });
    }

    @Override
    public int getItemCount() {
        return students.size();
    }

    public class DataViewHolder extends RecyclerView.ViewHolder {

        TextView rname, rmail, rmobile, rroll;
        ImageView delete;

        public DataViewHolder(@NonNull View itemView) {
            super(itemView);
            rname = itemView.findViewById(R.id.readname);
            rmail = itemView.findViewById(R.id.readMailid);
            rmobile = itemView.findViewById(R.id.readmobile);
            rroll = itemView.findViewById(R.id.readroll);
            delete = itemView.findViewById(R.id.delete);
        }
    }
}