Home Development for Android The new architecture of Android applications – trying it out

The new architecture of Android applications – trying it out

by admin

Hi all.At this past Google I/O we were finally introduced to the official vision of of Google on the architecture of Android apps, as well as the libraries for its implementation. It’s not even ten years later. Of course I immediately wanted to try what was on offer there.

Beware:the libraries are in alpha version, so we can expect some compatibility-breaking changes.

Lifecycle

The main idea of the new architecture is to take the logic out of activities and fragments as much as possible. The company argues that we should consider these components as belonging to the system and not to the developer’s area of responsibility. The idea itself is not new, MVP/MVVP are already actively used today. However, the interrelation with component lifecycles has always been left to the developers.

Now it’s not. We are presented with a new package android.arch.lifecycle , which contains the classes Lifecycle , LifecycleActivity and LifecycleFragment In the not-too-distant future, it is assumed that all system components that live in some lifecycle will provide Lifecycle through the implementation of the interface LifecycleOwner :

public interface LifecycleOwner {Lifecycle getLifecycle();}

Since the package is still in alpha version and its API cannot be mixed with the stable one, the LifecycleActivity and LifecycleFragment classes were added. Once the package is stable, LifecycleOwner will be implemented in Fragment and AppCompatActivity, and LifecycleActivity and LifecycleFragment will be removed.

Lifecycle contains the current state of a component’s lifecycle and allows LifecycleObserver subscribe to lifecycle transition events. A good example of :

class MyLocationListener implements LifecycleObserver {private boolean enabled = false;private final Lifecycle lifecycle;public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {This.lifecycle = lifecycle;This.lifecycle.addObserver(this);// Some code.}@OnLifecycleEvent(Lifecycle.Event.ON_START)void start() {if (enabled) {// Sign up for location change}}public void enable() {enabled = true;if (lifecycle.getState().isAtLeast(STARTED)) {// we're signing up for the location change, // if not already signed up}}@OnLifecycleEvent(Lifecycle.Event.ON_STOP)void stop() {// unsubscribe from change of location}}

Now we just have to create MyLocationListener and forget about it :

class MyActivity extends LifecycleActivity {private MyLocationListener locationListener;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);locationListener = new MyLocationListener(this, this.getLifecycle(), location -> {// Processing the location, e.g., displaying it on the screen});// Something that takes a long time to execute and is asynchronous.Util.checkUserStatus(result -> {if (result) {locationListener.enable();}});}}

LiveData

LiveData – is some analog of Observable in rxJava, but aware of the existence of Lifecycle. LiveData contains a value, each change of which comes to the Observables.

Three basic LiveData methods:

setValue() – change the value and notify the servers about it;
onActive() – at least one active server appeared;
onInactive() – there is no more active server.

Consequently, if LiveData has no active servers, the data update can be stopped.

The active server is the one whose Lifecycle is STARTED or RESUMED. If a new active Lifecycle is joined to LiveData, it immediately gets the current value.

This allows you to store a LiveData instance in a static variable and subscribe to it from UI components :

public class LocationLiveData extends LiveData<Location> {private LocationManager locationManager;private SimpleLocationListener listener = new SimpleLocationListener() {@Overridepublic void onLocationChanged(Location location) {setValue(location);}};public LocationLiveData(Context context) {locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);}@Overrideprotected void onActive() {locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);}@Overrideprotected void onInactive() {locationManager.removeUpdates(listener);}}

Let’s make a regular static variable :

public final class App extends Application {private static LiveData<Location> locationLiveData = new LocationLiveData();public static LiveData<Location> getLocationLiveData() {return locationLiveData;}}

And let’s sign up for location changes, e.g., in two activities :

public class Activity1 extends LifecycleActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity1);getApplication().getLocationLiveData().observe(this, (location) -> {// do something})}}public class Activity2 extends LifecycleActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity2);getApplication().getLocationLiveData().observe(this, (location) -> {// do something})}}

Note that the observe method takes LifecycleOwner as its first parameter, thereby tying each subscription to the lifecycle of a particular activity.

As soon as the life cycle of an activity goes to DESTROYED the subscription is destroyed.

Pros of this approach: no spaghetti from the code, no memory leaks and the handler will not be called on a killed activity.

ViewModel

ViewModel – a data store for UI that can survive the destruction of a UI component, such as a configuration change (yes, MVVM is now the officially recommended paradigm). A freshly created activity is reconnected to a previously created model :

public class MyActivityViewModel extends ViewModel {private final MutableLiveData<String> valueLiveData = new MutableLiveData<> ();public LiveData<String> getValueLiveData() {return valueLiveData;}}public class MyActivity extends LifecycleActivity {MyActivityViewModel viewModel;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity);viewModel = ViewModelProviders.of(this).get(MyActivityViewModel.class);ViewModel.getValueLiveData().observe(this, (value) - > {// Displaying the value on the screen});}}

The of method parameter defines the scope of the model instance. That is, if the same value is passed to of, it will return the same instance of the class. If there is no instance yet, it will be created.

As a scope, you can pass not just a reference to yourself, but something more subtle. Three approaches are currently recommended :

  1. activity transfers itself;
  2. fragment transfers itself;
  3. the fragment transmits its activity.

The third way allows to organize data transfer between fragments and their activiti via common ViewModel. That is, you don’t need to make any more arguments to fragments and specific interfaces to activitie. Nobody knows anything about each other.

When all components to which a model instance is attached are destroyed, the onCleared event is called and the model is destroyed.

Important point: since ViewModel generally does not know how many components the same model instance uses, we should never store a reference to a component inside the model.

Room Persistence Library

Our happiness would be incomplete without the ability to save data locally after the untimely death of an application. This is where the out-of-the-box SQLite comes in. However, the database API is rather inconvenient, mainly because it doesn’t provide ways to check the code at compile time. The typos in SQL-expressions are detected at runtime, which is good, if not by the client.

But that’s a thing of the past – Google has introduced us to ORM library with static analysis of SQL expressions at compile time.

We need to implement at least three components : Entity , DAO and Database

Entity is a single entry in a table :

@Entity(tableName = «users»)public class User() {@PrimaryKeypublic int userId;public String userName;}

DAO (Data Access Object) is a class that encapsulates the handling of records of a particular type :

@Daopublic interface UserDAO {@Insert(onConflict = REPLACE)public void insertUser(User user);@Insert(onConflict = REPLACE)public void insertUsers(User… users);@Deletepublic void deleteUsers(User… users);@Query(«SELECT * FROM users»)public LiveData<List<User> > getAllUsers();@Query(«SELECT * FROM users WHERE userId = :userId LIMIT 1»)LiveData<User> load(int userId);@Query(«SELECT userName FROM users WHERE userId = :userId LIMIT 1»)LiveData<String> loadUserName(int userId);}

Note, DAO is an interface, not a class. Its implementation is generated at compile time.

The most amazing thing, in my opinion, is that the compilation falls down if the Query passed an expression that accesses nonexistent tables and fields.

As an expression in Query can be passed, including table associations. However, Entity itself can’t contain fields referring to other tables, this is due to the fact that the lazy loading of data when accessing them will start in the same thread and it will surely be a UI thread. That’s why Google decided to forbid this practice completely.

It’s also important that as soon as any code changes a record in a table, all LiveData that includes that table passes the updated data to its observers. That is, the database in our application is now the "truth in the last resort". This approach finally gets rid of data inconsistencies in different parts of the application.

Not only that, but Google promises us that in the future change tracking will be done line by line, not by table as it is now.

Finally, we need to set the database itself :

@Database(entities = {User.class}, version = 1)public abstract class AppDatabase extends RoomDatabase {public abstract UserDAO userDao();}

Here also codegeneration applies, so we write an interface rather than a class.

Create a base singleton in the Application class or in the Dagger module :

AppDatabase database = Room.databaseBuilder(context, AppDatabase.class, "data").build();

Get a DAO out of it and you are ready to work :

database.userDao().insertUser(new User(...));

When DAO methods are invoked for the first time, tables are automatically created/re-created or the schema update SQL scripts are executed, if specified.Schema update scripts are set through Migration objects:

AppDatabase database = Room.databaseBuilder(context, AppDatabase.class, "data").addMigration(MIGRATION_1_2).addMigration(MIGRATION_2_3).build();static Migration MIGRATION_1_2 = new Migration(1, 2) {@Overridepublic void migrate(SupportSQLDatabase database) {database.execSQL(…);}}static Migration MIGRATION_2_3 = new Migration(2, 3) {…}

Plus, don’t forget to specify the current version of the schema in the annotation with the AppDatabase.

Of course, schema update SQL scripts should just be strings and should not rely on external constants, because after some time the table classes will change significantly, and the old database update should still run without errors.

After all scripts are executed, it automatically checks if database and Entity classes match, and throws Exception if they don’t.

Caution : If the chain of conversions from the actual version to the latest version fails, the base is deleted and re-created.

In my opinion, the schema update algorithm has flaws.If you have an outdated base on your device, it will update, all is well. But if you don’t have a base and the required version > 1 and some Migration set is specified, the base will be created based on Entity and Migration will not be performed.
It’s like we’re being hinted that Migration can only be about changing the structure of tables, but not about populating them with data. This is unfortunate. I guess we can expect improvements to this algorithm.

Pure architecture

All of the above entities are the building blocks of the proposed new application architecture. I should note that Google doesn’t write clean architecture anywhere, it’s a bit of a liberty on my part, but the idea is similar.
The new architecture of Android applications - trying it out
No entity knows anything about the entities above it.

Model and Remote Data Source are responsible for storing data locally and querying it over the network, respectively. Repository manages caching and aggregates individual entities according to business objectives. Repository classes are simply an abstraction for developers; there is no special underlying Repository class. Finally, ViewModel combines different Repositories in a way suitable for a particular UI.

Data is transferred between layers via LiveData subscriptions.

Example

I wrote a small demo application. It shows the current weather in a number of cities. For simplicity, the list of cities is pre-defined. The service used as a data provider is OpenWeatherMap

We have two fragments: with the list of cities (CityListFragment) and with the weather in the selected city (CityFragment). Both fragments are located in MainActivity.

Activiti and fragments use the same MainActivityViewModel.

MainActivityViewModel requests data from WeatherRepository.

WeatherRepository returns old data from the database and immediately initiates a request for updated data over the network. If the updated data is successful, it is saved to the database and updated at the user’s screen.

For correct work it is necessary to put API key into WeatherRepository. You can get the key for free after registration on OpenWeatherMap.

Repository at GitHub

The innovations look very interesting, but the urge to redo everything is worth holding back. Don’t forget it’s only an alpha.

Comments and suggestions are welcome. Yay!

You may also like