Android architecture components include:
-
Lifecycle-aware components
-
LiveData is used to "build data objects that notify views when the underlying database changes".
-
ViewModel - surviving configuration changes, such as rotation. Managing UI data in a lifecycle-aware way.
-
Room - SQLite object mapping library. So it converts SQLite table data to Java objects easily. plus, "Room provides compile time checks of SQLite statements and can return RxJava, Flowable and LiveData observables."
First we make POJO(plain old java object) class, let's name it someEntry
. And we make it an Entity by an annotation:
@Entity(tableName = table_name)
where table_name is the string of the name of the table.
Also, we can make a column primary key, by:
@PrimaryKey(autoGenerate = true)
(default is false)
We can use @Ignore
annotation when we want to add a field to Entity but make Room ignore it, i.e. not persist it.
DAO class is a class which provides methods for working with database.
Example DAO:
@Dao
public interface SomeDAO {
@Query("SELECT * FROM table ORDER BY id")
List<SomeEntry> loadAll();
@Query("SELECT * FROM table WHERE id=:id")
//as I understood ':id' means that it'll use our id variable(which we passed as a parameter)
SomeEntry getEntry(int id);
@Insert
void insert(SomeEntry entry);
@Insert
void insert(SomeEntry... entry); //we can have several different methods for one operation. e.g. array of some entries.
@Update(onConflict = OnConflictStrategy.REPLACE)
void update(SomeEntry entry);
@Delete
void delete(SomeEntry entry);
@Database(entities = {SomeEntry.class}, version = 1, exportSchema = false)
//set exportSchema to 'true' if we want to export db to folder
public abstract class AppDatabase extends RoomDatabase {
private static final String LOG_TAG = AppDatabase.class.getSimpleName();
private static final Object LOCK = new Object(); //monitor object; private lock object
private static final String DATABASE_NAME = "database.db";
private static AppDatabase sInstance;
public static AppDatabase getInstance(Context context) {
if (sInstance == null) {
synchronized (LOCK) { //only one thread will execute this block,
// "All other threads attempting to enter the synchronized block are blocked"
Log.d(LOG_TAG, "Creating new database instance");
sInstance = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, AppDatabase.DATABASE_NAME)
.allowMainThreadQueries() //should be removed
//we can allow queries on main thread just TEMPORARILY for testing. Not in production.
.build();
}
}
Log.d(LOG_TAG, "Getting the database instance"); // "If you want to keep a log of variable values, use this(Log.d)."
return sInstance;
}
public abstract SomeDAO someDao();
}
Note: So as I understood what's going on behind the scenes with synchronized (LOCK)
block is that in Java we have object locks naturally and so if some thread, say t1
accesses the block on LOCK
object, other threads cannot access it because they can't get a lock from the same object until t1
has finished its work, because t1
hasn't released the lock yet.
Good blogpost about object locks
In Java an implicit lock is associated with each object
If a thread wants to execute synchronized method on the given object; First, it has to get lock of that object. Once thread got the lock then it is allowed to execute any synchronized method on that object. Once method execution completes automatically thread releases the lock. Acquiring and release lock internally is taken care by JVM and programmer is not responsible for these activities.
This blog has good notes about synchronization
.. including:
.. But as a best practice, create a new private scoped Object instance
And by having an Object
instance private
, we make it "inaccessible to callers that are outside the class's scope."
Protect static data by locking on a private static final Object. Reducing the accessibility of the class to package-private provides further protection against untrusted callers.
from this brilliant paper.
LiveData notifies Observers about data updates.
.. it is also lifecycle aware which means that this is going to update the component which is in the active lifecycle state.
First of all, we should wrap the return type with LiveData on query methods. In our example it will be:
@Query("SELECT * FROM table ORDER BY id")
LiveData<List<SomeEntry>> loadAll();
@Query("SELECT * FROM table WHERE id=:id")
LiveData<SomeEntry> getEntry(int id);
An example method for loading all entries with LiveData will be:
private void retrieveData() {
Log.d(TAG, "Actively retrieving data from database");
final LiveData<List<SomeEntry>> entries = mDb.SomeDao().loadAll();
tasks.observe(this, new Observer<List<SomeEntry>>() { //observe() starts running on other thread AFAIK
@Override
public void onChanged(@Nullable List<SomeEntry> someEntries) { //happens on main/UI thread
mAdapter.setEntries(someEntries);
}
});
}
and then we must call retrieveData()
in onCreate
, because it's necessary to retrieve data as soon as activity becomes active and run observe(LifecycleOwner owner, Observer<? super T> observer)
right away.
ViewModel allows data to survive configuration changes, such as rotation.
Lifecycle of ViewModel starts once activity is created and lasts until it is finished.
In other words, this means that a ViewModel will not be destroyed if its owner is destroyed for a configuration change (e.g. rotation). The new instance of the owner will just re-connected to the existing ViewModel.
Let's create our class that extends ViewModel:
public class MainViewModel extends AndroidViewModel { //we extended AndroidViewModel because we need Application context..
private static final String TAG = MainViewModel.class.getSimpleName();
private LiveData<List<SomeEntry>> entries; //we are storing LiveData object in ViewModel
public MainViewModel(@NonNull Application application) {
super(application);
AppDatabase db = AppDatabase.getInstance(this.getApplication()); //..see?
entries = db.someDao().loadAll();
Log.d(TAG, "retrieving data from the database");
}
public LiveData<List<SomeEntry>> getEntries() {
return entries;
}
Then, let's refactor retrieveData()
to setData()
, because we'll be actually setting it; We handle data loading in our ViewModel.
private void setData() {
MainViewModel viewModel = ViewModelProviders.of(this).get(MainViewModel.class);
/*
get() method Returns an existing ViewModel or creates a new one in the scope
(usually, a fragment or an activity), associated with this ViewModelProvider.
The created ViewModel is associated with the given scope
and will be retained as long as the scope is alive
(e.g. if it is an activity, until it is finished or process is killed).
*/
viewModel.getEntries().observe(this, new Observer<List<SomeEntry>>() {
@Override
public void onChanged(@Nullable List<SomeEntry> someEntries) {
// onChanged gets called on activity rotation because:
// "LiveData notifies Observer objects when the lifecycle state changes"
Log.d(TAG, "updating/setting list of entries from LiveData in ViewModel");
mAdapter.setEntries(someEntries);
}
});
}
So what's going on:
- When first created: binds ViewModel with Activity, makes a call to database;
- After e.g. device rotation: connects with existing ViewModel. Sets entries. Makes another call to database if and only if update has occurred.
Now if we need to pass some parameters to ViewModel
, we must create a Factory class, which will then used by ViewModelProviders
to create the ViewModel
. It cannot instantiate it without a factory class(it seems), which must extend ViewModelProvider.NewInstanceFactory
.
First, let's make a ViewModel
for, e.g. entry creation purposes:
public class AddEntryViewModel extends ViewModel {
LiveData<SomeEntry> entry;
public AddEntryViewModel(AppDatabase db, int id) {
entry = db.someDao().loadEntryById(id);
}
public LiveData<SomeEntry> getEntry() {
return entry;
}
}
And then a factory class:
public class AddEntryViewModelFactory extends ViewModelProvider.NewInstanceFactory {
private final AppDatabase db;
private final int entryId;
public AddTaskViewModelFactory(AppDatabase db, int entryId) {
this.db = db;
this.entryId = entryId;
}
@SuppressWarnings("unchecked")
@Override
public <T extends ViewModel> T create(Class<T> modelClass) { //ViewModelProvider calls this
return (T) new AddEntryViewModel(db, entryid);
}
}
And in the Activity where we are adding entries to database:
AddEntryViewModelFactory factory = new AddEntryViewModelFactory(mDb, mEntryId); //factory init
final AddEntryViewModel entryViewModel = ViewModelProviders.of(this, factory)
.get(AddEntryViewModel.class);
entryViewModel.getEntry().observe(this, new Observer<SomeEntry>() {
@Override
public void onChanged(@Nullable SomeEntry someEntry) {
entryViewModel.getEntry().removeObserver(this);
Log.d(TAG, "Receiving database update from LiveData");
populateUI(someEntry); //i.e. setting entry attributes to UI
}
});