Skip to content

Instantly share code, notes, and snippets.

@getsadzeg
Last active May 12, 2019 16:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save getsadzeg/b61f8cfd8ceee16c7ef9e1a894a27b7b to your computer and use it in GitHub Desktop.
Save getsadzeg/b61f8cfd8ceee16c7ef9e1a894a27b7b to your computer and use it in GitHub Desktop.
Android Architecture Components

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."

Step N1 - Entity class

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.

Step N2 - DAO(Data Access Object) class

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);

Step N3 - Creating a database class

@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 & Observer Pattern

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.

Introducing ViewModel

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
                  }
              });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment