Continuation of Lecture 1 Course on the architecture of android applications. Part 1 is here:
- Prohibition of change of orientation
- Self-processing
- Saving state to Bundle
- Retain Fragments
- Loaders
- Practical assignment
- Links and useful resources
Change configuration processing
It is well known that Activity is recreated with each configuration change (for example, when you change the orientation or language). Re-creation means the destruction of Activity and its re-start. Destruction in turn means that all the fields you stored in Activity will be destroyed. What does this mean in practice? This means that if you create information from the server when you create Activity, store it in a field in Activity and display it to the user, then if you recreate Activity, you lose all the information, the query will start to be executed again with all possible consequences:
- The user does not expect that the boot process will seem to him again, although he seems to have done nothing, just turned the screen, for example.
- You may not receive data, for example, in the event of a server error. This will be an even more bizarre behavior for the user, since the data disappeared after the turn.
So how do you deal with it? And why did such a problem arise with such an innocent act?
To answer the question about why such a problem arose is quite difficult. Obviously, it was necessary to destroy all data when changing the configuration, so as not to show the user an incorrect state. But there was certainly a way that would reduce the number of such problems, but it was probably too difficult for the first versions of Android, and now it is necessary to maintain backward compatibility.
[wpanchor id=”1″]
If you can not do anything with this situation, then you have to get used to such conditions. In fact, there are many ways how to correctly save and restore data when recreating Activity. Consider them.
Prohibition of change of orientation
Of course, the most common reason for recreating Activity due to configuration changes is the orientation change. Therefore, quite a lot of developers, not wanting to deal with all the problems related to the processing of the life cycle, rigidly fix the orientation and do not think about this problem any further. Such fixation is achieved by adding a flag in the manifest:
<activity android:name=".WeatherActivity" android:screenOrientation="portrait"/>
[wpanchor id=”2″]
Of course, this approach simplifies a lot, but it is not always acceptable. In principle, there are many applications that only portrait orientation is sufficient for, but this is more an exception than the rule. Often users work in landscape orientation (especially on tablets) and make them change it for the sake of your application is not very good. And everything else you need to understand that fixed orientation does not save you from problems with recreating Activity, because there are other reasons for this, and not just a change of orientation. Therefore, such a decision can not be considered ideal.
Self-processing
In addition, you can not prevent changing any configuration (for example, orientation), but to process it yourself. To do this, you must specify a flag in the manifest with the appropriate value:
<activity android:name=".WeatherActivity" android:configChanges="orientation|keyboardHidden|screenSize"/> В этом случае при изменении какой-то конфигурации система уведомит Activity о том, что произошло такое изменение и нужно его обработать. Для этого служит метод onConfigurationChanged в классе Activity: @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // handle new configuration }
[wpanchor id=”3″]
In some cases this treatment is not required. But you need to understand that such processing is also fraught with consequences, since the system does not automatically apply alternative resources (and this applies not only to linguistic resources, but also to the usual layout-land, for example). Therefore, this is a rather rare option, but it must also be borne in mind.
Saving state to Bundle
Android provides us with a way to save the state and then restore it when you recreate Activity. Here it is worth paying attention to the parameter savedInstanceState, which is passed in the onCreate method in Activity. This instance of the Bundle class is not simply passed on, it can store various fields in it that will be written to it. The first time you start Activity, this parameter will always be null. If you recreate Activity, it will no longer be null, so you can track whether the first Activity is started or whether it is a call after the re-creation, which is very convenient. And now the main thing is that you can save your fields in the Bundle method in the onSaveInstanceState method in the Activity class in approximately the following way:
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putSerializable(WEATHER_KEY, mCity); }
And this object of class Bundle, in which you saved some values, after the re-creation will get as a parameter in the onCreate method, and from there you can extract all the data. With this approach, the code for processing the screen state change looks like this:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_weather); if (savedInstanceState == null) { loadWeather(); } else { mCity = (City) savedInstanceState.getSerializable(WEATHER_KEY); showWeather(); } }
That is, we check if the Activity is started for the first time (the phrase “for the first time” here is not very suitable, because the Activity can run several times, but here it is understood that the launch is not after re-creation), then we start to download weather information. If Activity is re-created, then we save weather information in the onSaveInstanceState method, and restore it in the onCreate method.
Here you need to notice an important fact – not always the weather will be loaded before Activity is recreated. Therefore, the code above should be slightly modified:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_weather); if (savedInstanceState == null || !savedInstanceState.containsKey(WEATHER_KEY)) { loadWeather(); } else { mCity = (City) savedInstanceState.getSerializable(WEATHER_KEY); showWeather(); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mCity != null) { outState.putSerializable(WEATHER_KEY, mCity); } }
[wpanchor id=”4″]
It’s possible that this method is not so convenient, but it works well when you have to save small data on one single screen. But you need to take into account that you can not save large objects or huge amounts of data in this way, as their serialization and recovery takes a long time, and because of this the application will work slowly.
Retain Fragments
Another very popular and very effective way of handling configuration changes are Retain Fragments. In fact, these are the usual fragments for which the setRetainInstance method was called:
public class WeatherFragment extends Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } }
Calling this method changes the life cycle of the fragment, namely, it removes calls from onCreate and onDestroy when it recreates the Activity. Now when recreating Activity, this fragment will not be destroyed, and all its fields will retain their values. But with the rest of the methods from the life cycle of the Fragment will be called, so there will be no problems with replacing the resources depending on the configuration. Therefore, we only need to add this fragment at the first start of Activity and execute all requests in it, since it does not care about re-creating Activity:
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_weather_retain); if (savedInstanceState == null) { WeatherFragment fragment = new WeatherFragment(); getSupportFragmentManager().beginTransaction() .replace(R.id.container, fragment) .commit(); } }
In the fragment in the onViewCreated method, we check if the data has already loaded, then display it, otherwise we start downloading the data and show the download process:
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (mCity == null) { loadWeather(); } else { showWeather(); } }
It seems that this approach is ideal. And in principle, yes, he has serious merits, in comparison with previous approaches:
- You can save even large and complex objects
- No need to save data manually
But you also need to understand the limitations of this approach:
- First, you need to be extremely careful with saving any links to the Activity / Context in such a fragment. When you recreate Activity, it is destroyed, but if your snippet keeps a reference to this Activity, then the garbage collector can not recycle it, and you can get memory leaks. And this, in turn, imposes certain restrictions – you can not save a link to Activity in the onCreate method of such a fragment.[wpanchor id=”5″]
- This approach works well against the problem of rotations, but, unfortunately, it is also tied to the user’s current visible screen. And this means that if the user decides to close the application during the execution of some query, the fragment will be destroyed, and we will also lose the necessary data.
Therefore, work with retain fragments requires accuracy and has its drawbacks.
Loaders
And the last component, which we will consider, will be loaders. Despite the fundamental differences in the essence, from the point of view of the problem of processing the configuration change, this component is very similar to the previous one: it is also experiencing the re-creation of the Activity without loss of data, it is also precisely controlled by the special class (LoaderManager), as well as fragments (FragmentManager).
Loaders will be used further in the course of the course, so now we will dwell on them a little more.
Even if you look at the name, the loaders should be designed to load something. And usually we download data – from the database or from the server. Therefore, the decision to use loaders for the task of providing client-server interaction looks logical. So what are loaders and how to use them?
A loader is an Android component that, through the LoaderManager class, is associated with the Activity and Fragment lifecycle. This allows them to be used without fear that the data will be lost when the application is closed or the result returns to the wrong callback. Let’s analyze the simplest example (which though the simplest, but requires a lot of code, this is one of the drawbacks of loaders). Create a loader class (for simplicity, it will not load data from the server, but only simulate the load):
public class StubLoader extends AsyncTaskLoader<Integer> { public StubLoader(Context context) { super(context); } @Override protected void onStartLoading() { super.onStartLoading(); forceLoad(); } @Override public Integer loadInBackground() { // emulate long-running operation SystemClock.sleep(2000); return 5; } }
The loader class is very similar to the AsyncTask class (however, for good reason we inherit from AsyncTaskLoader). It is clear that in the loadInBackground method we need to load the data, but what we need to use the onStartLoading (and other methods) method will be discussed later. In the meantime, let’s move on to use. Unlike AsyncTask, the loader does not need to be started manually, it’s done implicitly via the LoaderManager class. This class has two methods with the same signature:
public abstract <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback); public abstract <D> Loader<D> restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback); Про различие этих методов будет рассказано сразу после того, как мы рассмотрим их применение. Какие есть параметры у этих методов? Во-первых, вы можете запускать несколько лоадеров в одной Activity / Fragment, и вам нужно будет различать их. Для этого служит параметр id. Вторым параметром идет Bundle, с помощью которого вы можете передать аргументы для создания лоадера. И последний параметр является основным – это Callback для создания лоадера и получения результата его работы: public interface LoaderCallbacks<D> { public Loader<D> onCreateLoader(int id, Bundle args); public void onLoadFinished(Loader<D> loader, D data); public void onLoaderReset(Loader<D> loader); }
In the onCreateLoader method, you must return the desired loader, depending on the passed id and using arguments in the Bundle. In the onLoadFinished method in the D parameter, you will get the output of the loader. In the onLoaderReset method, you must clear all data that is associated with this loader.
Then let’s create an instance of LoaderCallbacks for our loader:
private class StubLoaderCallbacks implements LoaderManager.LoaderCallbacks<Integer> { @Override public Loader<Integer> onCreateLoader(int id, Bundle args) { if (id == R.id.stub_loader_id) { return new StubLoader(WeatherActivity.this); } return null; } @Override public void onLoadFinished(Loader<Integer> loader, Integer data) { if (loader.getId() == R.id.stub_loader_id) { Toast.makeText(WeatherActivity.this, R.string.load_finished, Toast.LENGTH_SHORT).show(); } } @Override public void onLoaderReset(Loader<Integer> loader) { // Do nothing } } И теперь запустим лоадер, наконец-то подобравшись к сути: @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_weather); getSupportLoaderManager().initLoader(R.id.stub_loader_id, Bundle.EMPTY, new StubLoaderCallbacks()); }
In 2 seconds after the Activity starts, it will appear toast. In principle, we did not expect anything else. But now the main thing is to turn the device. Now logically, the work of the loader should start anew and after 2 seconds it will appear toast. However, we see that the toast seemed instantaneous!
All magic lies in the initLoader method and in the LoaderManager class. And now it’s time to explain how this magic works. If the loader has not yet been created (it is not stored in the LoaderManager), the initLoader method creates it and starts it. However, if the loader has already been created (the first time it is activated), then when the initLoader is called again, LoaderManager does not rebuild the loader and will not restart it. Instead, it replaces the LoaderCallbacks instance with the new one (which is fine, because the old instance was destroyed along with the old Activity), and if the data is already loaded, it will pass them to onLoadFInished. And with all this, we do not need to do manual null checking for Bundle in onCreate. If we have one query on the screen, then we can start initLoader at startup and do not worry about re-creating, which gives a good level of abstraction when processing the configuration change.
The logical question is, what if we still want to restart the loader (for example, we want to update the data, but not get the previous result)? For this, there is a restartLoader method, which always restarts the loader’s execution. Otherwise, its work is completely analogous to calling initLoader.
Let’s now see how this can be applied to solve our real problem and a real query. Create a loader that will download weather information:
public class WeatherLoader extends AsyncTaskLoader<City> { public WeatherLoader(Context context) { super(context); } @Override protected void onStartLoading() { super.onStartLoading(); forceLoad(); } @Override public City loadInBackground() { String city = getContext().getString(R.string.default_city); try { return ApiFactory.getWeatherService().getWeather(city).execute().body(); } catch (IOException e) { return null; } } }
And call it in Activity as follows:
private void loadWeather(boolean restart) { mWeatherLayout.setVisibility(View.INVISIBLE); mErrorLayout.setVisibility(View.GONE); mLoadingView.showLoadingIndicator(); LoaderManager.LoaderCallbacks<City> callbacks = new WeatherCallbacks(); if (restart) { getSupportLoaderManager().restartLoader(R.id.weather_loader_id, Bundle.EMPTY, callbacks); } else { getSupportLoaderManager().initLoader(R.id.weather_loader_id, Bundle.EMPTY, callbacks); } }
Pay attention to the flag being passed, which determines which method to call, initLoader or restartLoader. Even on such a simple screen, we need a restartLoader call, for example, to handle an error when the user wants to execute the request again. A similar situation occurs when updating data (for example, Callback from SwipeRefreshLayout).
We again need to notice an important plus from the use of loaders – we never specifically write code to process the re-creation, which is very convenient.
As mentioned above, it is also very important to understand the internal device of the Loader class. If before there was no other way, how to study the loaders, now the situation has changed. Today, development technologies for Android have grown very seriously, many libraries have appeared to provide asynchrony and work with the network (which is why the typical developer path now looks like this: Thread, AsyncTask, RxJava, avoiding the loaders). But to know such a powerful component and be able to use it is necessary. Therefore, we will analyze how the loader is arranged inside.
So far we have always inherited from the AsyncTaskLoader class, which provided work in the background. However, the original class is the Loader class. It is noteworthy that the Loader class does not provide any tools to support the work in the background. And it’s not just that. The loader is primarily intended to be associated with the Activity / Fragment lifecycle. To ensure the same work in the background, you either need to use the AsyncTaskLoader class, or use other means of providing multithreading (for example, Call from Retrofit, RxJava). And for such a decision it is necessary to say a big thank you to the developers from Google. After all, they allowed us to use our means to ensure multithreading (otherwise we, for example, would not have had the opportunity to use RxJava in conjunction with loaders, what we’ll do next) while retaining the power of the loaders.
The Loader class defines 3 basic methods that you need to redefine to correctly write your loader. These are the following methods:
protected void onStartLoading() { } protected void onForceLoad() { } protected void onStopLoading() { }
The onStartLoading method is called when you need to load data and return it to Callback. In fact, this method is called as the result of calling the startLoading method. Typically, the startLoading method is called by the LoaderManager class, and we do not need to use it ourselves.
Similarly, the onStopLoading method is used to notify you that you need to stop loading data (a request to the server, for example), but you do not need to clear the data.
The methods onStartLoading and onStopLoading are called respectively when calling the onStart and onStop methods in the Activity, but they are not called in the situation when Activity is recreated, but only when it is minimized / expanded. And it’s very good, because we have to stop downloading data when the user is not on the screen, so as not to waste battery power.
So, does this mean that every time the screen is rolled up / rolled out, will the download process start again? Not at all, but for this we will have to work a little ourselves. The fields in the loader are not destroyed, which means that we can check if the query has already completed, and if so, we can return the received data.
Therefore, writing your own loader usually looks like this. First, the loader fields are defined, which is usually the result of the load that we want to receive, and the object to execute the request to the server:
public class RetrofitWeatherLoader extends Loader<City> { private final Call<City> mCall; @Nullable private City mCity; public RetrofitWeatherLoader(Context context) { super(context); String city = context.getString(R.string.default_city); mCall = ApiFactory.getWeatherService().getWeather(city); } }
After this, the onStartLoading method is overridden, in which we check if the request is completed (if the saved data is available). If the data is, then we immediately return them, otherwise we start to download everything again:
@Override protected void onStartLoading() { super.onStartLoading(); if (mCity != null) { deliverResult(mCity); } else { forceLoad(); } }
The deliverResult method returns the data in LoaderCallbacks. And the forceLoad method initiates the onForceLoad method call, which we mentioned, but did not have time to discuss it. In fact, this method serves only for convenience and logical separation between life-cycle methods and methods for loading data. In the onForceLoad method, you need to load the data asynchronously and return the result using the deliverResult method.
@Override protected void onForceLoad() { super.onForceLoad(); mCall.enqueue(new Callback<City>() { @Override public void onResponse(Call<City> call, Response<City> response) { mCity = response.body(); deliverResult(mCity); } @Override public void onFailure(Call<City> call, Throwable t) { deliverResult(null); } }); }
And there was the last method – onStopLoading, in which we must stop loading data. Fortunately, with Retrofit it’s very simple:
@Override protected void onStopLoading() { mCall.cancel(); super.onStopLoading(); }
Here you can finish the review of loaders, this is enough for further study. More examples and options for working with the database and handling errors can be found in the article.
[wpanchor id=”6″]
Despite all the convenience that we’ve considered, the Loader class also has its weak points, and they are almost always similar to the problems with Retain Fragment. For example, if the application is completely closed at the time of loading the data, you can also get the out-of-sync state. The difference is that the loader is specifically designed to load data and provides a greater level of abstraction, but it requires more code.
Practical assignment
- Download the LoaderWeather Project. The task description is in the file ru.gdgkazan.simpleweather.screen.weatherlist.WeatherListActivity
- You need to download the weather in all cities when the application starts
- Make it the fastest way (not every city consistently)
- Add upgrade via SwipeRefreshLayout
- Implement re-create Activity
Questions about the lesson you can ask in the comments.
Example of a practical task
Links and useful resources
- Applications from the repository:
- SimpleWeather – demonstration of various ways of processing the configuration change.
- LoaderWeather – use loaders to load data and a practical task.
- History of the development of the Android system by version.
- The book “Clean Code” from Robert Martin.
- Clean Architecture from Robert Martin.
- Documentation for processing configuration changes.
- Good answer about retain fragments.
- Documentation for loaders.
- An article about using loaders to load data.