Android Lifecycle and Room
Google I/O 2017 was last month and it included a lot of really awesome announcements, especially in the Android world. We'll explore one of those announcements in this blog post as we dive into the Lifecycle and Room libraries released by Google.
Let's begin by taking a look at the Lifecycle Library. Seasoned Android developers know that one of the hardest things about learning to develop on the Android platform is dealing with the lifecycle. The Lifecycle library will hopefully help alleviate some of the pain that comes from dealing with the lifecycle of Android components by introducing a few helpful classes and interfaces.
- Lifecycle Owner: Represents a class that has an Android lifecycle.
- Lifecycle Observer: A class that cares about receiving lifecycle events from a lifecycle owner.
- Lifecycle Registry: An implementation of Lifecycle that can help you create your own custom lifecycle components. You will have to use this for your activities and fragments until the libraries are finalized. Once they are finalized the Support library activities and fragments will implement Lifecycle on their own.
- LiveData: A lifecycle aware data holder and observer.
- ViewModel: A data management class. It can hold the LiveData that you may wish to display in an Activity or Fragment
So how can we make use of these new classes and how might they help us avoid some of the pitfalls of the android lifecycle?
Let's imagine a scenario where we want to check the user's login status when our activity starts and then proceed with a long running background task that will periodically update the UI if the user is logged in.
public class SomeActivity {
public void onStart(){
someAsyncLoginChecker.check(new ResultCallback(Status loginStatus){
if(loginStatus){
someBackgroundRunner.start(new UIUpdatingCallBack(){
void notifyUI(int progress){
updateSomeWidget(progress);
});
}
}
public void onStop(){
someBackgroundRunner.stop();
}
}
In our example we avoid the first potential lifecycle pitfall by remembering that we need to stop our background runner in the activity's onStop method, but what if our async login check hasn't returned when onStop executes?
Did we make sure our BackgroundRunner object can handle that situation?
What happens if this activity ends up hosting just 2 or 3 more of these kinds of objects/tasks?
You can see how it can get very messy and complicated very quickly.
The LifecycleOwner/LifecycleObserver pair can rescue us from this situation by making it very easy for us to take most of this code out of the activity and deal with it in its own class. This separation of concern helps make our app more maintainable and testable. Let's look at how this same situation might be handled with the Lifecycle library.
public class SomeActivity implements LifecycleOwnerRegistry {
private BackgroundRunner someBackgroundRunner;
public void onCreate(){
someBackgroundRunner = new BackgroundRunner(
getLifecycle(),
new UIUpdatingCallBack() {
void notifyUI(int progress){
updateSomeWidget(progress);
}
}
public void onStart(){
someAsyncLoginChecker.check(new ResultCallback(Status loginStatus){
if(loginStatus){
someBackgroundRunner.setIsLoggedIn(true);
someBackgroundRunner.start();
}
}
}
public class BackgroundRunner implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void onStart(){
if(userIsLoggedIn){
start();
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void onStop(){
stop();
}
void start(){
if(userIsLoggedIn && lifecycle.getState().isAtLeast(STARTED)){
doWork();
}
}
}
Our activity is now simply creating a new BackgroundRunner in its onCreate method and supplying its lifecycle to it. We still check our login status in onStart but the rest of the code is in the BackgroundRunner, and we didn't even have to implement anything in our onStop method.
Our BackgroundRunner implements the LifecycleObserver interface and then makes use of some annotations to get notified of various lifecycle events. We can then just react to those events in the class and make sure that we are doing exactly as we should be when we should be. You can see in our start method that we are even able of checking what the current state of the lifecycle is before we proceed to call our doWork method. Our code is now more cleanly separated and much easier to test.
Now that we've covered LifecycleOwner and LifecycleObserver we can talk about LiveData. This is a lifecycle aware data holder class. You can store any data you might want to share in LiveData and then objects that are interested in that data can observe the LiveData object. Since LiveData is lifecycle aware it will make sure that it only notifies observers if they are in the STARTED or RESUMED state.
We can make use of a LiveData object in an activity like this:
public class MyActivity{
public void onCreate(Bundle savedInstanceState){
myLiveDataRepo.getSomeLiveData().observe(this, new Observer(){
onChanged(Object someData){
updateUi(someData);
}
}
}
}
We can start observing the LiveData object in our onCreate method, and because we have passed the lifecycle to the LiveData object, we don't need to worry about stoppping observation ourselves. LiveData will take care of that for us when it receives our onDestroy event. The LiveData object is also smart enough to not bother with notifying our activity if it isn't in a state where it can receive updates to its UI.
The final piece of the Lifecycle library I want to cover is the ViewModel. In the previous examples we may have fetched some LiveData right in our Activity but the library provides us with a class that can handle that kind of work for us. If we use a viewmodel our activities will be even slimmer and we'll be able to test our application logic without needing to worry about wiring up an activity.
Here is a simple example of how we can make use of the Lifecycle viewmodels:
public void onCreate(Bundle savedInstanceState){
SomeViewModel myViewModel = ViewModelProviders.of(this).get(SomeViewModel.class);
myViewModel.someLiveData.observe(this, new Observer(){
public void onChanged(Object someData){
updateUi(someData);
}
}
}
The additional benefit of using a viewmodel in this manner is that it will survive activity orientation changes. We can even re-use viewmodels across different activities and fragments!
Hopefully these examples have won you over to giving the Lifecycle library a try in your app. It really can help take a lot of pain out of Android development.
Now let's move on to the Room Persistence library. This is a sqlite ORM that was announced alongside the Lifecycle library. Room provides an easy to use and compile time safe wrapper around sqlite.
The queries that you write for use in Room are verified at compile time and the library even helps you perform database migrations when necessary. It is a night and day difference compared to the built in framework components for using sqlite.
Room has three main annotations that make it come to life.
Database: Used on an abstract class that must extends from RoomDatabase. You will declare your DAO's in this class. The annotation can specify a list of entities as well as the database version number
Entity: Used to mark a simple data class that you wish to store in your sqlite database.
Dao: An annotation placed on an interface used to abstract away database access from your application.
Let's look at how you can put these pieces together to create an easy to use sqlite interface for your app.
First the database:
@Database(version = 1, entities = {User.class, Weight.class})
abstract class MyAppDatabase extends RoomDatabase {
abstract public UserDao userDao();
abstract public WeightDao weightDao();
}
Once we've declared our database class like the example above, we can get an instance of it in our application using this code:
Room.databaseBuilder(appContext, MyAppDatabase.class, "databaseFileName").build();
Now let's look take a closer look at one of our Entity classes
@Entity(tableName = "weights", indices = {@Index(value = "weighInDate", unique = true), @Index("weightId")}, foreignKeys = @ForeignKey(onUpdate = CASCADE, onDelete = CASCADE, entity = User.class, parentColumns = "id", childColumns = "userId")
public class Weight {
@PrimaryKey(autoGenerate = true)
private long id;
private long userId;
private Date weighInDate;
}
That looks like a lot but I really just wanted to show an example that exercised a lot of the options that Room gives to you. It really is a very feature rich sqlite ORM. You can see that in the @Entity annotation we can declare our table name, indices, and foreign keys. We can also annotate a field in the class with an auto generated primary key.
Now let's look at the final piece of the puzzle. The Dao
@Dao
public interface WeightDao {
@Insert(onConflict = OnConflictStrategy.ROLLBACK)
long insertWeight(Weight weight);
@Update(onConflict = OnConflictStrategy.REPLACE)
void updateWeight(Weight weight);
@Delete
void deleteWeight(Weight weight);
@Query("SELECT * from weights where userId = :userId")
LiveData<Weight[]> getUserWeights(long userId);
}
As you can see the Dao is what allows us to easily communicate with our sqlite database via our application code. The annotations are powerful enough to write basic queries for us and even let us specify things like conflict resolution. The nicest feature though is the @Query annotation. The sqlite query that you write in that annotation will be compile time verified against your schema so you won't have any nasty surprises waiting for you at runtime.
You may also notice that our Dao is returning a LiveData object. This is because Room has been built to easily integrate with the Lifecycle library. If you take advantage of this integration you can let your lifecycle aware LiveData objects handle deciding when to re-query the database for fresh data. If LiveData isn't your thing they even have an integration with RxJava2.
That's it for this post. I would encourage you to check out the links below to dive a little deeper into these libraries and check out the github examples that Google has provided. You should also checkout their architecture guide. Its opinionated but it covers most of the use cases that I think we as android developers run into when developing our apps and gives some very good advice on how to handle those use cases.