Android was introduced to the world back in 2005, and during those 12 years of existence the platform has achieved amazing success, becoming the most-installed mobile OS. During that time, 14 different versions of the operating system have been launched, with Android always becoming more mature. However, a very important area of the platform continued to be ignored: a standard architecture pattern, capable of handling the platform peculiarities and simple enough to be understood and adopted by the average developer.
Well, better late than never. At the last Google I/O, the Android team finally decided to address this problem and respond to the feedback from developers all over the world, announcing an official recommendation for an Android Application Architecture and providing the building blocks to implement it: the new Architecture Components. And better yet, they managed to do it without compromising the openness of the system that we all know and love.
In this tutorial, we'll explore the standardized architecture proposed by the Android team at Google I/O and look at the main elements of the new Architecture Components: Lifecycle
, ViewModel
, LifeData
, and Room
. We won't pay too much attention to the code, instead focusing on the concept and logic behind these themes. We'll also take a look at some simple snippets, all of them written using Kotlin, an amazing language that is now officially supported by Android.
1. What Was Android Missing?
If you're just beginning your journey as a developer, it's possible that you don't know exactly what I am talking about. After all, application architecture can be an obscure theme at first. But believe me, you will learn its importance soon enough! As an application grows and becomes more complex, its architecture will become more and more important. It can literally make your work a bliss or a living hell.
Application Architecture
Putting it roughly, an application architecture is a consistent plan that needs to be made before the development process starts. This plan provides a map of how the different application components should be organized and tied together. It presents guidelines that should be followed during the development process and forces some sacrifices (generally related to more classes and boilerplate) that in the end will help you to construct a well-written application that's more testable, expandable, and maintainable.
Software application architecture is the process of defining a structured solution that meets all of the technical and operational requirements, while optimizing common quality attributes such as performance, security, and manageability. It involves a series of decisions based on a wide range of factors, and each of these decisions can have considerable impact on the quality, performance, maintainability, and overall success of the application.
— Microsoft's Software Architecture and Design Guide
Good architecture takes many factors into consideration, especially the system characteristics and limits. There are many different architectural solutions out there, all of them with pros and cons. However, some key concepts are common between all visions.
Old Mistakes
Until the last Google I/O, the Android system didn't recommend any specific Architecture for application development. That means that you were completely free to adopt any model out there: MVP, MVC, MVPP, or even no pattern at all. On top of that, the Android framework didn't even provide native solutions for problems created by the system itself, specifically the component's lifecycle.
So, if you wanted to adopt a Model View Presenter pattern on your application, you needed to come up with your own solution from scratch, writing a lot of boilerplate code, or adopt a library without official support. And that absence of standards created a lot of poorly written applications, with codebases that were hard to maintain and test.
As I said, this situation has been criticized for years. In fact, I recently wrote about this problem and how to address it in my How to Adopt Model View Presenter on Android series. But the important thing is that after 12 long years, the Android team finally decided to listen to our complaints and to help us with this problem.
2. Android Architecture
The new Android Architecture Guide defines some key principles that a good Android application should conform to and also proposes a secure path for the developer to create a good app. However, the guide explicitly states that the route presented isn't obligatory, and ultimately the decision is personal; it's the developer who should decide which type of architecture to adopt.
According to the guide, a good Android application should provide a solid separation of concerns and drive the UI from a model. Any code that does not handle a UI or operating system interaction should not be in an Activity or Fragment, because keeping them as clean as possible will allow you to avoid many lifecycle-related problems. After all, the system can destroy Activities or Fragments at any time. Also, the data should be handled by models that are isolated from the UI, and consequently from lifecycle issues.
The New Recommended Architecture
The architecture that Android is recommending cannot be easily labeled among the standard patterns that we know. It looks like a Model View Controller pattern, but it is so closely tied to the system's architecture that it's hard to label each element using the known conventions. This isn't relevant, though, as the important thing is that it relies on the new Architecture Components to create a separation of concerns, with excellent testability and maintainability. And better yet, it is easy to implement.
To understand what the Android team is proposing, we have to know all the elements of the Architecture Components, since they are the ones that will do the heavy lifting for us. There are four components, each with a specific role: Room
, ViewModel
, LiveData
, and Lifecycle
. All of those parts have their own responsibilities, and they work together to create a solid architecture. Let's take a look at a simplified diagram of the proposed architecture to understand it better.
As you can see, we have three main elements, each one with its responsibility.
- The
Activity
andFragment
represent theView
layer, which doesn't deal with business logic and complex operations. It only configures the view, handles the user interaction, and most importantly, observes and exhibitsLiveData
elements taken from theViewModel
. - The
ViewModel
automatically observes theLifecycle
state of the view, maintaining consistency during configuration changes and other Android lifecycle events. It is also demanded by the view to fetch data from theRepository
, which is provided as observableLiveData
. It is important to understand that theViewModel
never references theView
directly and that the updates on the data are always done by theLiveData
entity. - The
Repository
isn't a special Android component. It is a simple class, without any particular implementation, which is responsible for fetching data from all available sources, from a database to web services. It handles all this data, generally transforming them to observableLiveData
and making them available to theViewModel
. - The
Room
database is an SQLite mapping library that facilitates the process of dealing with a database. It automatically writes a ton of boilerplate, checks errors at compile time, and best of all, it can directly return queries with observableLiveData
.
I'm sure you've noticed that we've talked a lot about observables. The Observer Pattern is one of the bases of the LiveData
element and Lifecycle
aware components. This pattern allows an object to notify a list of observers about any changes on its state or data. So when an Activity observes a LiveData
entity, it will receive updates when that data undergoes any kind of modification.
Another Android recommendation is to consolidate its architecture using a Dependency Injection system, like Google's Dagger 2 or using the Service Locator pattern (which is way simpler than DI, but without many of its advantages). We won't cover DI or Service Locator in this tutorial, but Envato Tuts+ has some excellent tutorials about those themes. However, be advised that there are some particularities of working with Dagger 2 and Android Components that will be explained in the second part of this series.
3. Architecture Components
We must dive deep into the aspects of the new components to be able to really understand and adopt this model of architecture. However, we won't get into all the details in this tutorial. Due to the complexity of each element, in this tutorial, we'll only talk about the general idea behind each one and look at some simplified code snippets. We'll try to cover enough ground to present the components and get you started. But fear not, because future articles in this series will dig deep and cover all the particularities of the Architecture Components.
Lifecycle-Aware Components
Most of the Android app components have lifecycles attached to them, which are managed directly by the system itself. Until recently it was up to the developer to monitor the components' state and act accordingly, initializing and ending tasks at the appropriate time. However, it was really easy to get confused and make mistakes related to this type of operation. But the android.arch.lifecycle
package changed all that.
Now, Activities and Fragments have a Lifecycle
object attached to them that can be observed by LifecycleObserver
classes, like a ViewModel
or any object that implements this interface. That means that the observer will receive updates about the state changes of the object that it is observing, like when an Activity is paused or when it is starting. It can also check the current state of the observed object. So it's much easier now to handle operations that must consider the framework lifecycles.
For now, to create an Activity
or Fragment
that conforms to this new standard, you have to extend a LifecycleActivity
or LifecycleFragment
. However, it's possible that this won't always be necessary, since the Android team is aiming to completely integrate these new tools with its framework.
class MainActivity : LifecycleActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
The LifecycleObserver
receives Lifecycle
events and can react through annotation. No method override is necessary.
class MainActivityObserver : LifecycleObserver, AnkoLogger { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { info("onResume") } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { info("onPause") } }
The LiveData
Component
The LiveData
component is a data holder that contains a value that can be observed. Given that the observer has provided a Lifecycle
during the LiveData
instantiation, LiveData
will behave according to Lifecycle
state. If the observer's Lifecycle
state is STARTED
or RESUMED
, the observer is active
; otherwise, it is inactive
.
LiveData
knows when the data was changed and also if the observer is active
and should receive an update. Another interesting characteristic of the LiveData
is that it's capable of removing the observer if it's in a Lifecycle.State.DESTROYED
state, avoiding memory leaks when observed by Activities and Fragments.
A LiveData
must implement onActive
and onInactive
methods.
class LocationLiveData(context: Context) : LiveData<Location>(), AnkoLogger, LocationListener { private val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager override fun onActive() { info("onActive") locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, this) } override fun onInactive() { info("onInactive") locationManager.removeUpdates(this) } // .... }
To observe a LiveData
component, you must call observer(LifecycleOwner, Observer<T>)
.
class MainActivity : LifecycleActivity(), AnkoLogger { fun observeLocation() { val location = LocationLiveData(this) location.observe(this, Observer { location -> info("location: $location") }) } }
The ViewModel
Component
One of the most important classes of the new Architecture Components is the ViewModel
, which is designed to hold data that is related to the UI, maintaining its integrity during configuration changes like screen rotations. The ViewModel
is able to talk with the Repository
, getting LiveData
from it and making it available in turn to be observed by the view. ViewModel
also won't need to make new calls to the Repository
after configuration changes, which optimizes the code a lot.
To create a view model, extend the ViewModel
class.
class MainActivityViewModel : ViewModel() { private var notes: MutableLiveData<List<String>>? = null fun getNotes(): LiveData<List<String>> { if (notes == null) { notes = MutableLiveData<List<String>>() loadNotes() } return notes!! } private fun loadNotes() { // do async operation to fetch notes } }
To access from a view, you may call ViewProviders.of(Activity|Fragment).get(ViewModel::class)
. This factory method will return a new instance of the ViewModel
or get the retained one, as appropriate.
class MainActivity : LifecycleActivity(), AnkoLogger { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val viewModel = ViewModelProviders.of(this) .get(MainActivityViewModel::class.java) viewModel.getNotes().observe( this, Observer { notes -> info("notes: $notes") } ) } }
The Room
Component
Android supported SQLite from the start; however, to make it work, it was always necessary to write a lot of boilerplate. Also, SQLite didn't save POJOs (plain-old Java objects), and didn't check queries at compile time. Along comes Room
to solve these issues! It is an SQLite mapping library, capable of persisting Java POJOs, directly converting queries to objects, checking errors at compile time, and producing LiveData
observables from query results. Room
is an Object Relational Mapping library with some cool Android extras.
Until now, you could do most of what Room
is capable of using other ORM Android libraries. However, none of them are officially supported and, most importantly, they can't produce LifeData
results. The Room
library fits perfectly as the persistent layer on the proposed Android Architecture.
To create a Room
database, you'll need an @Entity
to persist, which can be any Java POJO, a @Dao
interface to make queries and input/output operations, and a @Database
abstract class that must extend RoomDatabase
.
@Entity class Note { @PrimaryKey var id: Long? = null var text: String? = null var date: Long? = null }
@Dao interface NoteDAO { @Insert( onConflict = OnConflictStrategy.REPLACE ) fun insertNote(note: Note): Long @Update( onConflict = OnConflictStrategy.REPLACE ) fun updateNote(note: Note): Int @Delete fun deleteNote(note: Note): Int @Query("SELECT * FROM note") fun findAllNotes(): LiveData<Note> // on Kotlin the query arguments are renamed // to arg[N], being N the argument number. // on Java the arguments assume its original name @Query("SELECT * FROM note WHERE id = :arg0") fun findNoteById(id: Long): LiveData<Note> }
@Database( entities = arrayOf(Note::class), version = 1) abstract class Databse : RoomDatabase() { abstract fun noteDAO(): NoteDAO }
Adding Architecture Components to Your Project
For now, to use the new Architecture Components, you'll need to first add the Google repository to your build.gradle
file. For more details, see the official guide.
allprojects { repositories { jcenter() // Add Google repository maven { url 'https://maven.google.com' } } }
Conclusion
As you can see, the standardized architecture proposed by Android involves a lot of concepts. Don't expect to have a complete understanding of this topic yet. After all, we're merely introducing the theme. But you certainly have enough knowledge by now to understand the logic behind the architecture and the roles of the different Architecture Components.
We talked about most of the topics related to the proposed Android architecture and its components; however, details about the Components implementation and some extras, like the Repository
class and the Dagger 2 system cannot be covered by this first part. We'll explore those themes in the next posts.
See you soon!