In this article, I will explain how to implement MVVM pattern with Android Architecture Components. Android Architecture components contain a bunch of libraries that will help you to build android applications in the MVVM pattern. Few of them are Room, ViewModel, LiveData. Before going to example let’s understand these components one by one.
Lifecycle-Aware Components
These are the components that automatically responds according to life-cycle events. These components will help you produce better, lightweight, easier to maintain code. Take the example of LocationListener. When we create LocationListener in Activity we have to manage its connectivity. When activity launches we have started listening to location changes. Now when activity pauses we have to pause listening to location changes. But now with Lifecycle-Awareness of the component, we don’t need to handle pausing location updates. Just pass the lifecycle owner to the component and rest will be taken care of by the component itself.
ViewModel
The ViewModel is used to persist data during various configuration changes like screen rotation. Suppose we are fetching data from the server inside the activity. Now if we did not persist data inside ViewModel. The user rotates the screen then it will again fetch data which is a costly process. To avoid these unnecessary transactions to the server we can use ViewModel.
LiveData
LiveData is a data holder class. Data inside this holder can be observed for changes. So we can add data and keep track of data changes to modify UI accordingly. LiveData is lifecycle-aware that is it responds according to the state of activity or fragment. It keeps your UI up to date with the latest data. It avoids memory leaks as data get cleared as the associated lifecycle is destroyed. no crashes due to stopped activities. You can create a single point of contact for data retrieval that keeps your code robust, clean, and easier to maintain.
Room Persistence Library
Room Persistence Library provides an abstraction layer over the SQLite database. The room makes database CRUD operations easier and more maintainable. The room shows an error if you have written the wrong query while compiling your application. Room works with LiveData to keep your UI and Data in sync. Room uses entities for structuring database and dao( data access object ) to perform CRUD( Create, Read, Update, Delete) operations. we will see more about it as we go in this article.
Repository
The repository can be called a single source of truth for all application data. When we have both local and online database then it becomes hard to manage both databases. With Repository we can create a single source for any type of data source. Now data coming from a single source will make code easier to maintain.
Demo
Download Project
MVVM in Android with Room, LiveData and ViewModel
Below is stranded MVVM architecture in android using Room, LiveData, and ViewModel. Using this architecture we will build notes application. In this application, we will be able to add notes and display notes in recycler view.
Android Architecture Components | MVVM in Android with Room, LiveData, and ViewModel
Adding Room, LiveData, ViewModel Library to Android Studio Project
Create new or open an existing android studio project. Open build.greadle(module app). Add dependencies for Room, LiveData, ViewModel as shown below. CardView dependency is optional due to the design requirement of this example.
apply plugin: 'com.android.application' android { compileSdkVersion 27 defaultConfig { applicationId "com.loopwiki.androidarchitecturecomponants" minSdkVersion 15 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } buildToolsVersion '27.0.3' } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.0' implementation 'com.android.support:design:27.1.1' // ViewModel and LiveData library depenedecies implementation 'android.arch.lifecycle:extensions:1.1.1' implementation 'com.android.support:support-v4:27.1.1' annotationProcessor "android.arch.lifecycle:compiler:1.1.1" // Room library depenedecies implementation 'android.arch.persistence.room:runtime:1.0.0' annotationProcessor "android.arch.persistence.room:compiler:1.0.0" //CardView dependency optional required in design of example implementation 'com.android.support:cardview-v7:27.1.1' }
Project Structure
Create packages for activities, adapters, databases, repositories, utils, viewModels, Daos, and models. This will maintain the project in a good structure.

Creating Entity
Create new class in Package Name --> database.-> models -> Note.java. This class will represent the note structure in the database. The note will have a unique id, title, description, and created as shown below.
package com.loopwiki.androidarchitecturecomponants.database.models; import android.arch.persistence.room.Entity; import android.arch.persistence.room.PrimaryKey; import android.arch.persistence.room.TypeConverters; import com.loopwiki.androidarchitecturecomponants.utils.DateConverter; import java.util.Date; // Entity class model of room database @Entity public class Note { // room database entity primary key @PrimaryKey(autoGenerate = true) public int id; private String noteTitle; private String noteDescription; //type converter for date @TypeConverters(DateConverter.class) private Date createdAt; public Note(String noteTitle, String noteDescription, Date createdAt) { this.noteTitle = noteTitle; this.noteDescription = noteDescription; this.createdAt = createdAt; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getNoteTitle() { return noteTitle; } public void setNoteTitle(String noteTitle) { this.noteTitle = noteTitle; } public String getNoteDescription() { return noteDescription; } public void setNoteDescription(String noteDescription) { this.noteDescription = noteDescription; } public Date getCreatedAt() { return createdAt; } public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } }
Now we can not store data directly we need type converter for that so create new class Package Name -> utils -> DateConverter.java. Add the following lines of code into it.
package com.loopwiki.androidarchitecturecomponants.utils; import android.arch.persistence.room.TypeConverter; import android.text.format.DateFormat; import java.util.Date; public class DateConverter { @TypeConverter public static Date toDate(Long timestamp) { return timestamp == null ? null : new Date(timestamp); } @TypeConverter public static Long toTimestamp(Date date) { return date == null ? null : date.getTime(); } public static String getDayMonth(Date date) { String day = (String) DateFormat.format("dd", date); // 20 String monthString = (String) DateFormat.format("MMM", date); // Jun return day + monthString; } }
Creating Dao( Data Access Object)
Dao( Data Access Object) used to perform CRUD operations on the database. Create new class Package Name -> database -> NoteDao.java. Define Methods to insert, delete, getting all notes as shown below.
package com.loopwiki.androidarchitecturecomponants.database.Daos; import android.arch.lifecycle.LiveData; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Delete; import android.arch.persistence.room.Insert; import android.arch.persistence.room.Query; import android.arch.persistence.room.TypeConverters; import com.loopwiki.androidarchitecturecomponants.utils.DateConverter; import com.loopwiki.androidarchitecturecomponants.database.models.Note; import java.util.List; import static android.arch.persistence.room.OnConflictStrategy.REPLACE; //note dao(data access object) @Dao @TypeConverters(DateConverter.class) public interface NoteDao { // Dao method to get all notes @Query("SELECT * FROM Note") LiveData<List<Note>> getAllNotes(); // Dao method to insert note @Insert(onConflict = REPLACE) void insertNote(Note note); // Dao method to delete note @Delete void deleteNote(Note note); }
Creating Room Database
Create new abstract class extending RoomDatabase inside Package Name -> database -> NoteDatabase.java. This class used to create an instance of Room Database. Inside this class provide all entities and dao’s. The structure of this class is as shown below.
package com.loopwiki.androidarchitecturecomponants.database; import android.arch.persistence.room.Database; import android.arch.persistence.room.Room; import android.arch.persistence.room.RoomDatabase; import android.content.Context; import com.loopwiki.androidarchitecturecomponants.database.Daos.NoteDao; import com.loopwiki.androidarchitecturecomponants.database.models.Note; // Room database class @Database(entities = Note.class, version = 1, exportSchema = false) public abstract class NoteDatabase extends RoomDatabase { //define static instance private static NoteDatabase mInstance; //method to get room database public static NoteDatabase getDatabase(Context context) { if (mInstance == null) mInstance = Room.databaseBuilder(context.getApplicationContext(), NoteDatabase.class, "notes_db") .build(); return mInstance; } //method to remove instance public static void closeDatabase() { mInstance = null; } //define note dao ( data access object ) public abstract NoteDao noteDao(); }
Creating Repository
Create new class Package Name -> repositories -> NotesRepository.java. This class will serve as a true source of data. Create methods to get data from Room database or any other database like firebase. Inside create LiveData of List of all notes and methods to add a note inside Room Database as shown below.
package com.loopwiki.androidarchitecturecomponants.repositories; import android.app.Application; import android.arch.lifecycle.LiveData; import android.os.AsyncTask; import android.support.annotation.NonNull; import com.loopwiki.androidarchitecturecomponants.database.Daos.NoteDao; import com.loopwiki.androidarchitecturecomponants.database.NoteDatabase; import com.loopwiki.androidarchitecturecomponants.database.models.Note; import java.util.List; //Notes repository public class NotesRepository { //Live Data of List of all notes private LiveData<List<Note>> mAllNotes; //Define Notes Dao NoteDao mNoteDao; public NotesRepository(@NonNull Application application) { NoteDatabase noteDatabase = NoteDatabase.getDatabase(application); //init Notes Dao mNoteDao = noteDatabase.noteDao(); //get all notes mAllNotes = mNoteDao.getAllNotes(); } //method to get all notes public LiveData<List<Note>> getAllNotes() { return mAllNotes; } //method to add note public void addNote(Note note) { new AddNote().execute(note); } //Async task to add note public class AddNote extends AsyncTask<Note, Void, Void> { @Override protected Void doInBackground(Note... notes) { mNoteDao.insertNote(notes[0]); return null; } } }
We will display all notes inside recycler view. Before creating activity we will create an adapter and required layouts for recyclerview.
Creating Adapter for Recyclerview
Create new file in res -> layout -> custom_row_note.xml. This row will represent the view of a single note in recyclerview as shown below.
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/backStrip" android:layout_width="match_parent" android:layout_height="wrap_content"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:background="@android:color/white"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_marginBottom="8dp" android:layout_marginLeft="4dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:orientation="vertical"> <TextView android:id="@+id/noteTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="end" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textStyle="bold" /> <TextView android:id="@+id/noteDescription" android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="end" android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> <TextView android:id="@+id/createdAt" android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="end" android:lines="1" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textColor="@color/colorPrimary" /> </LinearLayout> </RelativeLayout> </FrameLayout> </android.support.v7.widget.CardView>
Create new class inside Package Name -> adapters -> NotesAdapter.java. This class will serve as an adapter for recyclerview. Create viewholder and bind data to the view inside onCreateViewHolder() and onBindViewHolder() methods respectively as shown below.
package com.loopwiki.androidarchitecturecomponants.adapters; import android.graphics.Color; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; import com.loopwiki.androidarchitecturecomponants.utils.DateConverter; import com.loopwiki.androidarchitecturecomponants.R; import com.loopwiki.androidarchitecturecomponants.database.models.Note; import java.util.ArrayList; import java.util.List; import java.util.Random; public class NotesAdapter extends RecyclerView.Adapter { //Create list of notes List<Note> notes = new ArrayList<>(); @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { //Get layout inflater LayoutInflater inflater = LayoutInflater.from(parent.getContext()); //Inflate layout View row = inflater.inflate(R.layout.custom_row_note, parent, false); //return notes holder and pass row inside return new NoteHolder(row); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { //Get current note Note currentNote = notes.get(position); //cast notes holder NoteHolder noteHolder = (NoteHolder) holder; //set title description and created at noteHolder.mNoteTitle.setText(currentNote.getNoteTitle()); noteHolder.mNoteDescription.setText(currentNote.getNoteDescription()); noteHolder.createdAt.setText(DateConverter.getDayMonth(currentNote.getCreatedAt())); //create random color and set it Random rnd = new Random(); int color = Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)); noteHolder.backStrip.setBackgroundColor(color); } @Override public int getItemCount() { return notes.size(); } public class NoteHolder extends RecyclerView.ViewHolder { TextView mNoteTitle, mNoteDescription, createdAt; FrameLayout backStrip; public NoteHolder(View itemView) { super(itemView); mNoteTitle = itemView.findViewById(R.id.noteTitle); mNoteDescription = itemView.findViewById(R.id.noteDescription); createdAt = itemView.findViewById(R.id.createdAt); backStrip = itemView.findViewById(R.id.backStrip); } } public void addNotes(List<Note> notes) { this.notes = notes; notifyDataSetChanged(); } }
Create new class inside Package Name -> utils -> Space.java. This class is recyclerview decoration class used to add space between recyclerview items. Add the following lines to it
package com.loopwiki.androidarchitecturecomponants.utils; import android.content.Context; import android.graphics.Rect; import android.support.annotation.DimenRes; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.View; public class Space extends RecyclerView.ItemDecoration { private int mItemOffset; public Space(int itemOffset) { mItemOffset = itemOffset; } public Space(@NonNull Context context, @DimenRes int itemOffsetId) { this(context.getResources().getDimensionPixelSize(itemOffsetId)); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); if (parent.getChildLayoutPosition(view) == 0) outRect.top = mItemOffset; outRect.left = mItemOffset; outRect.right = mItemOffset; outRect.bottom = mItemOffset; } }
Creating ViewModel
It’s time to create viewmodel for notes. Create class inside Package name -> viewModels -> NotesListViewModel.java. This ViewModel will contain LiveData of all notes and methods to insert notes as defined below.
package com.loopwiki.androidarchitecturecomponants.viewModels; import android.app.Application; import android.arch.lifecycle.AndroidViewModel; import android.arch.lifecycle.LiveData; import android.support.annotation.NonNull; import com.loopwiki.androidarchitecturecomponants.repositories.NotesRepository; import com.loopwiki.androidarchitecturecomponants.database.models.Note; import java.util.List; public class NotesListViewModel extends AndroidViewModel { private LiveData<List<Note>> mAllNotes; NotesRepository mNotesRepository; public NotesListViewModel(@NonNull Application application) { super(application); mNotesRepository = new NotesRepository(application); mAllNotes = mNotesRepository.getAllNotes(); } public LiveData<List<Note>> getAllNotes() { return mAllNotes; } public void addNote(Note note) { mNotesRepository.addNote(note); } }
Creating Activity
Finally create new layout res -> layout -> content_main.xml. Inside this layout define recyclerview as given below.
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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:id="@+id/content_main" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".activities.MainActivity" tools:showIn="@layout/activity_main"> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerViewNotes" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.constraint.ConstraintLayout>
Create one more layout res -> layout -> activity_main.xml. This layout will contain contain_main.xml, app bar, floating action button inside coordinator layout as below.
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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=".activities.MainActivity"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" app:layout_scrollFlags="snap|enterAlways|scroll" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <include layout="@layout/content_main" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:src="@drawable/ic_add_black_24dp" android:tint="@android:color/white" android:layout_margin="16dp" /> </android.support.design.widget.CoordinatorLayout>
Create custom layout for add note dialogue res -> layout -> add_note_dialog.xml. Add the following code to it.
<?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="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimary" android:gravity="center" android:text="Add Note" android:textAppearance="@style/TextAppearance.AppCompat.Large.Inverse" /> <android.support.design.widget.TextInputLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" android:hint="Title"> <android.support.design.widget.TextInputEditText android:id="@+id/editTextTitle" android:layout_width="match_parent" android:layout_height="wrap_content" /> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" android:hint="Description"> <android.support.design.widget.TextInputEditText android:id="@+id/editTextDescription" android:layout_width="match_parent" android:layout_height="wrap_content" /> </android.support.design.widget.TextInputLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="end|right" android:orientation="horizontal"> <TextView android:id="@+id/textViewAdd" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="16dp" android:text="Add" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textColor="@color/colorPrimary" /> <TextView android:id="@+id/textViewCancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="16dp" android:text="Cancel" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textColor="@color/colorPrimary" /> </LinearLayout> </LinearLayout>
Create new class Package Name -> activities -> MainActivity.java. Bind recyclerview and other views inside activity. Then get ViewModel for the activity using ViewModelProviders as shown below. showDialog() method used to show add notes dialogue.
package com.loopwiki.androidarchitecturecomponants.activities; import android.app.Dialog; import android.arch.lifecycle.Observer; import android.arch.lifecycle.ViewModelProviders; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.EditText; import android.widget.TextView; import com.loopwiki.androidarchitecturecomponants.R; import com.loopwiki.androidarchitecturecomponants.adapters.NotesAdapter; import com.loopwiki.androidarchitecturecomponants.database.models.Note; import com.loopwiki.androidarchitecturecomponants.utils.Space; import com.loopwiki.androidarchitecturecomponants.viewModels.NotesListViewModel; import java.util.Calendar; import java.util.Date; import java.util.List; public class MainActivity extends AppCompatActivity { NotesListViewModel mNotesListViewModel; FloatingActionButton fab; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); // Initialize floating action button fab = (FloatingActionButton) findViewById(R.id.fab); //show add notes dialogue fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showDialog(); } }); // bind recyclerview to object RecyclerView mNotesRecyclerView = findViewById(R.id.recyclerViewNotes); // set layout manager mNotesRecyclerView.setLayoutManager(new LinearLayoutManager(this)); // create new notes adapter final NotesAdapter notesAdapter = new NotesAdapter(); // set adapter to recyclerview mNotesRecyclerView.setAdapter(notesAdapter); // add decoration to recyclerview mNotesRecyclerView.addItemDecoration(new Space(20)); // get ViewModel of this activity using ViewModelProviders mNotesListViewModel = ViewModelProviders.of(this).get(NotesListViewModel.class); // observe for notes data changes mNotesListViewModel.getAllNotes().observe(this, new Observer<List<Note>>() { @Override public void onChanged(@Nullable List<Note> notes) { //add notes to adapter notesAdapter.addNotes(notes); } }); } public void showDialog() { fab.hide(); final Dialog dialog = new Dialog(this); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); dialog.setContentView(R.layout.add_note_dialog); dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); final EditText editTextTitle = dialog.findViewById(R.id.editTextTitle); final EditText editTextDescription = dialog.findViewById(R.id.editTextDescription); TextView textViewAdd = dialog.findViewById(R.id.textViewAdd); TextView textViewCancel = dialog.findViewById(R.id.textViewCancel); textViewAdd.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String Title = editTextTitle.getText().toString(); String Description = editTextDescription.getText().toString(); Date createdAt = Calendar.getInstance().getTime(); //add note mNotesListViewModel.addNote(new Note(Title, Description, createdAt)); fab.show(); dialog.dismiss(); } }); textViewCancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dialog.dismiss(); fab.show(); } }); dialog.show(); } }
Modify colors.xml, styles.xml, manifest.xml as below.
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#ffc107</color> <color name="colorPrimaryDark">#ffa000</color> <color name="colorAccent">#6200ea</color> </resources>
<resources> <string name="app_name">Android Architecture Componants</string> </resources>
<resources> <! – Base application theme. – > <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <! – Customize your theme here. – > <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> <style name="AppTheme.NoActionBar"> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> </style> <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> </resources>
Run application
If you still have any queries, please post them in the comments section below, I will be happy to help you.
3 Comments
Thankssss, You are save my final project
welcome buddy
Hi nice elaborate tutorial. How can I represent the created date as days ago? 4 days ago