Android Architecture Patterns Model View View Model — Part 1
MVVM Android Step by Step implementation (Java and Kotlin)
In this series I will walk through step by step implementation of MVVM Android Architecture components.
In this project I created an App that displays a List of users and User Registration Fragments in a single activity.
User registers a new user and the newly created user is immediately reflected in the list of users screen on top of it.
Step1: Create an activity with 2 Fragments . The First fragment displays user List and second Fragment display user registration.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportFragmentManager().beginTransaction().replace(R.id.container_first,new UserListFragment()).commit();
getSupportFragmentManager().beginTransaction().replace(R.id.container_second,new UserInfoFragment()).commit();
}
}
layout for the above created Activity:
<LinearLayout 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"
tools:context=".MainActivity"
android:orientation="vertical"
android:weightSum="100">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="55"
android:id="@+id/container_first"></FrameLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"
android:textColor="@android:color/white"
android:text="User Registration"
android:layout_gravity="center"
android:gravity="center_vertical"
android:background="@color/colorAccent"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="35"
android:id="@+id/container_second"></FrameLayout>
</LinearLayout>
Step2:Create RoomDataBase
This app uses RoomDataBase to save registered users in DB for showing in list.
Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.
Apps that handle non-trivial amounts of structured data can benefit greatly from persisting that data locally. The most common use case is to cache relevant pieces of data. That way, when the device cannot access the network, the user can still browse that content while they are offline. Any user-initiated content changes are then synced to the server after the device is back online.
Room takes care of these concerns and it is highly recommended instead of SQLite as it avoids lot of boilerplate code that we write while using SQLite.
Room Components :
(1) Entity- Represents a table within the database.
(2)Dao- Contains the methods used for accessing the database.(Example: SQLite queries like insert,query,delete,update )
(3) DataBase: This serves as the main access point for the underlying connection to your app’s persisted, relational data.
should be annotated with @Database
The database class should satisfy below conditions:
- Be an abstract class that extends
RoomDatabase
. - Include the list of entities associated with the database within the annotation.
- Contain an abstract method that has 0 arguments and returns the class that is annotated with
@Dao
. - At runtime, you can acquire an instance of
Database
by callingRoom.databaseBuilder()
orRoom.inMemoryDatabaseBuilder()
.
(1) Create Entity class
Will create a model class User as below
@Entity(tableName = "user_info") //Table name of our data base
public class User {
//Column Definition which includes column name and constraints
@PrimaryKey
@NonNull
@ColumnInfo(name = "user_name")
private String userName;
@NonNull
@ColumnInfo(name = "user_occ")
private String userOccupation;public User(String userName,String userOccupation) {
this.userName = userName;
this.userOccupation = userOccupation;
}@NonNull
public String getUserName() {
return userName;
}public void setUserName(@NonNull String userName) {
this.userName = userName;
}public String getUserOccupation() {
return userOccupation;
}public void setUserOccupation(String userOccupation) {
this.userOccupation = userOccupation;}
The above code creates a Database table with columns.
The table name should be specified with Entity annotation as below:
@Entity(tableName = “user_info”) .
Similarly column name and its constraints are annotated with @ColumnInfo ,@PrimaryKey, @NonNull.
For more info on attributes and other annotations that can be used in Model class please check the below link
(2) Create Dao interface as below that contains methods to access database
This interface should be annotated with @Dao
@Dao
public interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(User user);@Query("SELECT * from user_info ORDER BY user_name ASC")
LiveData<List<User>> getAllUsers();@Query("DELETE FROM user_info")
void deleteAll();
}}
(3) Create DataBase class
@Database(entities = {User.class}, version = 1)
public abstract class UserRoomDataBase extends RoomDatabase {
public abstract UserDao userDao();private static UserRoomDataBase INSTANCE;static UserRoomDataBase getDatabase(final Context context) {
if (INSTANCE == null) {
synchronized (UserRoomDataBase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
UserRoomDataBase.class, "user_database")
.addCallback(sRoomDatabaseCallback)
.build();}
}
}
return INSTANCE;
}private static RoomDatabase.Callback sRoomDatabaseCallback =
new RoomDatabase.Callback(){@Override
public void onOpen (@NonNull SupportSQLiteDatabase db){
super.onOpen(db);
new PopulateDbAsync(INSTANCE).execute();
}
};
}
The above code creates an abstract class that extends RoomDatabase
We use @Database annotation where we specify the tables in entities blocks and version number of Database.
Also declared abstract method that has 0 arguments and returns the class that is annotated with @Dao
We create instance of the created database using the following code:
Room.databaseBuilder(context.getApplicationContext(),
UserRoomDataBase.class, "user_database")
.addCallback(sRoomDatabaseCallback)
.build();
Only one instance of database should exists as it is costly operation . We used singleton class.
One more point to mention is about the callback that is added to create database instance using addCallback(…) method.This callback takes abstract class RoomDatabase.Callback as argument that has method onOpen() which needs to be overridden.
The purpose of this callback is when the database is created for the first time we can do cleanup tasks like deleting the all table info of the database or any cleanup task we wish to do.
In the above code I called an asynctask(PopulateDbAsync) class that deletes all the data from the table as below :
private static RoomDatabase.Callback sRoomDatabaseCallback =
new RoomDatabase.Callback(){@Override
public void onOpen (@NonNull SupportSQLiteDatabase db){
super.onOpen(db);
new PopulateDbAsync(INSTANCE).execute();
}
};
Create a PopulateDBAsync as below:
public class PopulateDbAsync extends AsyncTask<Void, Void, Void> {private final UserDao mDao;PopulateDbAsync(UserRoomDataBase db) {
mDao = db.userDao();
}@Override
protected Void doInBackground(final Void... params) {
mDao.deleteAll();
User user = new User("Chandra","SW");
mDao.insert(user);
user = new User("Mohan","student");
mDao.insert(user);
return null;
}
}
The above class deletes all the data and inserts some dummy data when the DataBase is created for the first time.
This completes the steps to create RoomDataBase for our application.
Step3: Create Repository class
Repository modules are responsible for handling data operations. They provide a clean API to the rest of the app. They know where to get the data from and what API calls to make when data is updated. You can consider them as mediators between different data sources (persistent model, web service, cache, etc.).
The purpose of this repository class is to access the DataBase and perform the operations like insert or query the database. In simple words fetch the instance of DataBase ,using this instance fetch the instance of Dao interface and call the methods of Dao interface with proper data to access DataBase .
public class UserRepository {
private UserDao userDao;
LiveData<List<User>> mAllUsers;public UserRepository(Application application){
UserRoomDataBase db = UserRoomDataBase.getDatabase(application);
userDao = db.userDao();
mAllUsers = userDao.getAllUsers();
}public LiveData<List<User>> getAllUsers() {
return mAllUsers;
}
public void insert (User user) {
new insertAsyncTask(userDao).execute(user);
}private static class insertAsyncTask extends AsyncTask<User, Void, Void> {private UserDao mAsyncTaskDao;insertAsyncTask(UserDao dao) {
mAsyncTaskDao = dao;
}@Override
protected Void doInBackground(final User... params) {
mAsyncTaskDao.insert(params[0]);
return null;
}
}
}
The above completes creating Model component.
Step4 : Create ViewModel Class
public class UserListViewModel extends AndroidViewModel {private UserRepository userRepository;
private LiveData<List<User>> mAllUsers;public UserListViewModel(Application application){
super(application);
userRepository = new UserRepository(application);
mAllUsers = userRepository.getAllUsers();
}public LiveData<List<User>> getAllUsers() {
return mAllUsers;
}public void insert(User user) {
userRepository.insert(user);
}
}
The above code creates a View model class that extends AndroidViewModel.
We can also extend ViewModel instead of AndroidViewModel , but the difference is as below:
If you need to use context inside your viewmodel you should use AndroidViewModel, because it contains the application context (to retrieve the context call getApplication() ), otherwise use regular ViewModel.
In the above we observe context to access repository module, that’s the reason whay AndroidViewModel is used .
LiveData:
If we closely observe UserListViewModel and UserRepository ,we have a method getAllUsers() whose return type is LiveData<List<User>> instead of List<User>.
The purpose of LiveData is :
Instead of requesting the data each time from ViewModel, our Activity/Fragment will be notified every time the data changes in DataBase.
LiveData
is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.
Advantage of ViewModel
Architecture Components provides
ViewModel
helper class for the UI controller that is responsible for preparing data for the UI.
ViewModel
objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance.For example, if you need to display a list of users in your app, make sure to assign responsibility to acquire and keep the list of users to a
ViewModel ,
instead of an activity or fragment
Step5: View
In this step we will see how to create views and to access Viewmodel from Views.
Here we have 2 Fragments to display list of users and user Registration.
View1
UserRegistration Fragment:
public class UserInfoFragment extends Fragment implements View.OnClickListener{
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
private View userInfoView = null;
private UserListViewModel userListViewModel;
public UserInfoFragment() {
// Required empty public constructor
}/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment UserInfoFragment.
*/
// TODO: Rename and change types and number of parameters
public static UserInfoFragment newInstance(String param1, String param2) {
UserInfoFragment fragment = new UserInfoFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
userListViewModel = ViewModelProviders.of(getActivity()).get(UserListViewModel.class);
}@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
userInfoView = inflater.inflate(R.layout.fragment_user_info, container, false);
initViews(userInfoView);return userInfoView;
}
EditText userName,userOccupation;
Button saveUser;private void initViews(View view){
userName = (EditText) view.findViewById(R.id.user_name);
userOccupation = (EditText)view.findViewById(R.id.user_occupation);
saveUser = (Button) view.findViewById(R.id.save_info);
saveUser.setOnClickListener(this);}@Override
public void onClick(View view) {
if(view.getId()==R.id.save_info){
saveInfo();
}
}private void saveInfo(){
if(!TextUtils.isEmpty(userName.getText().toString()) && !TextUtils.isEmpty(userOccupation.getText().toString())){
String name = userName.getText().toString();
String userOccup = userOccupation.getText().toString();
User user = new User(name,userOccup);
userListViewModel.insert(user);
userName.setText("");
userOccupation.setText("");}else{
Toast.makeText(getActivity(),"Plz Fill Required info...",Toast.LENGTH_LONG).show();
}
}
}
The above code displays a screen that contains 2 EditText fields which accepts username and useroccupation as inputs to be saved to Database on Save button Click.
Using ViewModel class in View
Create an instance of class that extends AndroidViewModel in OnCreate() method of fragment as below:
userListViewModel = ViewModelProviders.of(getActivity()).get(UserListViewModel.class);
Save the data to Database on Click of Save button using ViewModel instance as below:
User user = new User(name,userOccup);
userListViewModel.insert(user);
View2 UserListFragment
public class UserListFragment extends Fragment {
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
private View userListView = null;
private UserListViewModel userListViewModel;public UserListFragment() {
// Required empty public constructor
}
public static UserListFragment newInstance(String param1, String param2) {
UserListFragment fragment = new UserListFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
userListViewModel = ViewModelProviders.of(getActivity()).get(UserListViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
userListView = inflater.inflate(R.layout.fragment_user_list, container, false);
setAdapter(userListView);
userListViewModel.getAllUsers().observe(this, new Observer<List<User>>() {
@Override
public void onChanged(@Nullable final List<User> users) {
// Update the cached copy of the words in the adapter.
adapter.setWords(users);
}
});
return userListView;
}
UserListAdapter adapter = null;
private void setAdapter(View view){
RecyclerView recyclerView = (RecyclerView)view.findViewById(R.id.user_list);
adapter = new UserListAdapter(getActivity());
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
}
}
The above code creates a Fragment that displays list of registered users.
In the onCreate() method we create instance of ViewModel and in onCreateView() method we fetch list of users from ViewModel which is LiveData.
Kotlin Version of Above Implementation
Step 1: Main Activity
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().replace(R.id.container_first, UserListFragment()).commit()
supportFragmentManager.beginTransaction().replace(R.id.container_second, UserInfoFragment()).commit()
}
}
Step 2: Room DataBase
(1) Create Model data class User.kt
@Entity(tableName = "user_info")
data class User(
@PrimaryKey
@NotNull
@ColumnInfo(name = "user_name")
var userName:String ,
@NotNull
@ColumnInfo(name = "user_occ")
var userOccupation:String
) {}
A data class is specified by the keyword data when declaring the class definition in Kotlin, it is like defining a pojo class in Java. The difference is that Kotlin will take care of all these getter and setter as well as equals and hashCode method for you. Here is an example of User class with 2 properties, you literally just need one line to define this data class, but you will need over 50 lines to do it in Java.
(2) UserDao interface
@Dao
interface UserDao {@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(user:User)@Query("SELECT * from user_info ORDER BY user_name ASC")
fun getAllUsers():LiveData<List<User>>@Query("DELETE FROM user_info")
fun deleteAll()
}
(3) UserRoomDataBase
@Database(entities = arrayOf(User::class), version = 1)
abstract class UserRoomDataBase: RoomDatabase(){
abstract fun userDao(): UserDaocompanion object {
private var INSTANCE: UserRoomDataBase? = nullfun getInstance(context: Context): UserRoomDataBase? {
if (INSTANCE == null) {
synchronized(UserRoomDataBase::class) {
INSTANCE = Room.databaseBuilder(context,
UserRoomDataBase::class.java, "userData.db")
.addCallback(sRoomDataBaseCallback)
.build()
}
}
return INSTANCE
}fun destroyInstance() {
INSTANCE = null
}val sRoomDataBaseCallback = object:RoomDatabase.Callback(){
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
InitDBAsync(INSTANCE!!).execute()
}
};
}}
(4)PopulateDbAsync
public class InitDBAsync(db:UserRoomDataBase): AsyncTask<Void,Void,Void>() {
private val mDao: UserDaoinit{
mDao = db.userDao()
}override fun doInBackground(vararg params: Void): Void? {
mDao.deleteAll()
var user = User("Chandra", "SW")
mDao.insert(user)
user = User("Mohan", "student")
mDao.insert(user)
return null
}}
(5)UserDBRepository
class UserDBRepository {
private var userDao: UserDao
private var mAllUsers: LiveData<List<User>>constructor(application: Application){
val db = UserRoomDataBase.getInstance(application)
userDao = db!!.userDao()
mAllUsers = userDao.getAllUsers()}fun getAllUsers(): LiveData<List<User>> {
return mAllUsers
}fun insert(user: User) {
InsertAsyncTask(userDao).execute(user)
}
class InsertAsyncTask internal constructor(userDao: UserDao): AsyncTask<User, Void, Void>(){
private var mAsyncUserDao: UserDao
init {
mAsyncUserDao = userDao
}
override fun doInBackground(vararg p0: User): Void? {
mAsyncUserDao.insert(p0[0])
return null}
}
}
Step3: ViewModel
class UserListViewModel:AndroidViewModel {private var userRepository: UserDBRepository
private var mAllUsers: LiveData<List<User>>constructor(application: Application) : super(application){
userRepository = UserDBRepository(application)
mAllUsers = userRepository.getAllUsers()
}fun getAllUsers(): LiveData<List<User>> {
return mAllUsers
}fun insert(user: User) {
userRepository.insert(user)
}}
Step4: Creating Views
Fragment1:
class UserInfoFragment: Fragment(),View.OnClickListener {
lateinit var userInfoFragmentView: View
lateinit var userViewModel: UserListViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userViewModel = ViewModelProviders.of(activity!!).get(UserListViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
userInfoFragmentView = inflater.inflate(R.layout.fragment_user_info,container,false)
initViews(userInfoFragmentView)
return userInfoFragmentView
}
lateinit var userName: EditText
lateinit var userOccupation:EditText
lateinit var saveUser: Buttonprivate fun initViews(view: View) {
userName = view.findViewById(R.id.user_name) as EditText
userOccupation = view.findViewById(R.id.user_occupation) as EditText
saveUser = view.findViewById(R.id.save_info) as Button
saveUser.setOnClickListener(this)}override fun onClick(p0: View?) {
saveInfo()
}
private fun saveInfo() {
if (!TextUtils.isEmpty(userName.text.toString()) && !TextUtils.isEmpty(userOccupation.text.toString())) {
val name = userName.text.toString()
val userOccup = userOccupation.text.toString()
val user = User(name, userOccup)
userViewModel.insert(user)
userName.setText("")
userOccupation.setText("")} else {
Toast.makeText(activity, "Plz Fill Required info...", Toast.LENGTH_LONG).show()
}
}
}
Fragment2:
class UserListFragment:Fragment() {
lateinit var listFragmentView: View
lateinit var userViewModel: UserListViewModel
lateinit var recyclerView:RecyclerView
lateinit var userAdapter:UserListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userViewModel = ViewModelProviders.of(activity!!).get(UserListViewModel::class.java)
}override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
listFragmentView = inflater.inflate(R.layout.fragment_user_list,container,false)
initVars(listFragmentView)
recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayout.VERTICAL, false)
userAdapter = UserListAdapter()
recyclerView.setAdapter(userAdapter)
userViewModel.getAllUsers().observe(this, object : Observer <List<User>> {
override fun onChanged(users: List<User>?) {
// Update the cached copy of the words in the adapter.
userAdapter.setListItems(users)
}
})
return listFragmentView
}private fun setAdapter(){}private fun initVars(view:View){
recyclerView = view.findViewById(R.id.user_list)
}
}
Next : MVVM with RetroFit
MVVM with Retrofit and Room