[:ru]Продолжение Лекции 1 Курса по архитектуре андроид-приложений. Часть 1 здесь.

Обработка смены конфигурации

Общеизвестно, что Activity пересоздается при каждом изменений конфигурации (например, при смене ориентации или языка). Пересоздание означает уничтожение Activity и запуск ее заново. Уничтожение в свою очередь подразумевает то, что все поля, которые вы хранили в Activity, будут уничтожены. Что это означает на практике? Это означает то, что, если вы при создании Activity получаете информацию с сервера, сохраняете ее в какое-то поле в Activity и отображаете пользователю, то при пересоздании Activity вы потеряете всю информацию, запрос начнет выполняться заново со всеми возможными последствиями:

  • Пользователь не ожидает того, что ему снова покажется процесс загрузки, хотя он вроде бы ничего не делал, только повернул экран, к примеру.
  • Вы можете не получить данные, например, в случае ошибки сервера. Это будет еще более странным поведением для пользователя, так как данные исчезли после поворота.

Так как же с этим бороться? И почему вообще возникла такая проблема при таком невинном действии?

Ответить на вопрос о том, почему такая проблема возникла, достаточно сложно. Очевидно, что было необходимо уничтожать все данные при изменении конфигурации, чтобы не показать пользователю некорректное состояние. Но наверняка был путь, который позволил бы уменьшить количество таких проблем, но, вероятно, он был слишком непростым для первых версий Android, а сейчас необходимо поддерживать обратную совместимость.

Раз сделать с такой ситуацией ничего нельзя, то приходится привыкать и к таким условиям. В самом деле, есть немало способов, как корректно сохранить и восстановить данные при пересоздании Activity. Рассмотрим их.

Запрет смены ориентации

Разумеется, наиболее частой причиной пересоздания Activity по причине изменения конфигурации является смена ориентации. Поэтому достаточно многие разработчики, не желая иметь дела со всеми проблемами, связанными с обработкой жизненного цикла, жестко фиксируют ориентацию и дальше не думают об этой проблеме. Такая фиксация достигается за счет добавления флага в манифесте:

 

Конечно, такой подход многое упрощает, но все же он не всегда приемлем. В принципе, существует немало приложений, которым достаточно только портретной ориентации, но это скорее исключение, чем правило. Часто пользователи работают в альбомной ориентации (особенно на планшетах) и заставлять их менять ее ради вашего приложения не очень хорошо. И ко всему прочему нужно понимать, что фиксированная ориентация не избавляет вас от проблем с пересозданием Activity, так как для этого есть и другие причины, а не только смена ориентации. Поэтому такое решение не может считаться идеальным.

Самостоятельная обработка

Кроме того, можно не запретить изменение какой-то конфигурации (например, ориентации), а обрабатывать его самому. Для этого нужно указать флаг в манифесте с соответствующим значением:

 

 

В каких-то случаях эта обработка не требуется. Но нужно понимать, что такая обработка также чревата последствиями, так как система не применяет альтернативные ресурсы автоматически (а это уже относится не только к языковым ресурсам, но и к привычным layout-land, к примеру). Поэтому это достаточно редкий вариант, но его нужно также иметь в виду.

Сохранение состояния в Bundle

Android предоставляет нам способ сохранения состояния с последующим его восстановлением при пересоздании Activity. Здесь стоит обратить внимание на параметр savedInstanceState, который передается в методе onCreate в Activity. Этот экземпляр класса Bundle передается не просто так, он может хранить в себе различные поля, которые в него запишут. При первом запуске Activity этот параметр всегда будет равен null. При пересоздании Activity он уже не будет равен null, поэтому можно отследить, происходит ли первый запуск Activity или же это вызов после пересоздания, что весьма удобно. И теперь главное – вы можете сохранить в Bundle свои поля в методе onSaveInstanceState в классе Activity примерно следующим образом:

 

И этот объект класса Bundle, в который вы сохранили какие-то значения, после пересоздания попадет в качестве параметра в метод onCreate, и уже оттуда вы сможете извлечь все данные. При таком подходе код для обработки смены состояния экрана выглядит следующим образом:

 

То есть мы проверяем, если Activity запускается в первый раз (словосочетание “в первый раз” здесь не очень подходит, поскольку Activity может запускаться            несколько раз, но здесь понимается запуск не после пересоздания), то мы начинаем загружать информацию о погоде. Если же Activity пересоздается, то мы сохраняем информацию о погоде в методе onSaveInstanceState, а восстанавливаем в методе onCreate.

Тут нужно заметить важный факт – не всегда погода будет загружена до того, как Activity будет пересоздана. Поэтому код выше надо слегка модифицировать:

 

Возможно, что этот способ не настолько удобен, но он хорошо работает, когда вы должны сохранить небольшие данные на каком-то одном экране. Но при этом нужно учитывать, что таким образом нельзя сохранять большие объекты или огромные объемы данных, так как их сериализация и восстановление занимает много времени, и из-за этого приложение будет работать медленно.

Retain Fragments

Еще одним очень популярным и очень эффективным способом обработки смены конфигурации являются Retain Fragments. По сути это обычные фрагменты, для которых был вызван метод setRetainInstance:

 

Вызов этого метода меняет жизненный цикл фрагмента, а именно, он убирает из него вызовы onCreate и onDestroy при пересоздании Activity. Теперь при пересоздании Activity этот фрагмент не будет уничтожен, и все его поля сохранят свои значения. Но при этом остальные методы из жизненного цикла Fragment будут вызваны, так что не возникнет проблем с заменой ресурсов в зависимости от конфигурации. Поэтому нам только нужно добавить этот фрагмент при первом старте Activity и выполнять все запросы в нем, так как ему безразличны пересоздания Activity:

 

 

 

Во фрагменте в методе onViewCreated мы проверяем, если данные уже загрузились, то отображаем их, иначе начинаем загрузку данных и показываем процесс загрузки:

 

Кажется, что такой подход идеален. И в принципе, да, у него есть серьезные достоинства, по сравнению с предыдущими подходами:

  • Можно сохранять даже большие и сложные объекты
  • Не нужно сохранять данные вручную

 

Но при этом нужно понимать и ограничения такого подхода:

  • Во-первых, нужно быть крайне аккуратными с сохранением в таком фрагменте любых ссылок на Activity / Context. При пересоздании Activity она уничтожается, но, если ваш фрагмент будет держать ссылку на эту Activity, то сборщик мусора не сможет утилизировать ее, и вы можете получить утечки памяти. А это в свою очередь накладывает определенные ограничения – вы уже не можете сохранять ссылку на Activity в методе onCreate такого фрагмента.
  • Такой подход хорошо помогает против проблемы поворотов, но, к сожалению, он точно также привязан к текущему видимому экрану пользователя. И это значит, что, если пользователь решит закрыть приложение во время выполнения какого-то запроса, фрагмент будет уничтожен, и мы также потеряем нужные данные.

Поэтому и работа с retain фрагментами требует аккуратности и имеет свои минусы.

Лоадеры

И последним компонентом, который мы рассмотрим, будут лоадеры. Несмотря на принципиальные различия в сути, с точки зрения проблемы обработки смены конфигурации, этот компонент очень похож на предыдущий: он точно также без потери данных переживает пересоздание Activity, он точно также управляется специальным классом (LoaderManager), как и фрагменты (FragmentManager).

Лоадеры будут использоваться и дальше в ходе курса, поэтому сейчас мы остановимся на них чуть подробнее.

Даже если смотреть по названию, лоадеры должны быть предназначены для загрузки чего-либо. А обычно мы загружаем данные – из базы или с сервера. Поэтому решение использовать лоадеры для задачи обеспечения клиент-серверного взаимодействия выглядит логично. Так что же такое лоадеры и как их использовать?

Лоадер – это компонент Android, который через класс LoaderManager связан с жизненным циклом Activity и Fragment. Это позволяет использовать их без опасения, что данные будут утрачены при закрытии приложения или результат вернется не в тот коллбэк. Разберем простейший пример (который хоть и простейший, но требует немало кода, это один из недостатков лоадеров). Создаем класс лоадера (для простоты он не будет грузить данные с сервера, а лишь имитировать загрузку):

 

Класс лоадера очень похож на класс AsyncTask-а  (впрочем, не зря же мы наследуемся от AsyncTaskLoader). Понятно, что в методе loadInBackground мы должны загрузить данные, а вот для чего нужен метод onStartLoading (и другие методы) мы разберем позже. А пока перейдем к использованию. В отличие от AsyncTask-а лоадер не нужно запускать вручную, это делается неявным образом через класс LoaderManager. У этого класса есть два метода с одинаковой сигнатурой:

 

В методе onCreateLoader вы должны вернуть нужный лоадер в зависимости от переданного id и используя аргументы в Bundle. В методе onLoadFinished в параметре D вам придет результат работы лоадера. В методе onLoaderReset вы должны очистить все данные, которые связаны с этим лоадером.

Тогда давайте создадим экземпляр LoaderCallbacks для нашего лоадера:

 

Через 2 секунды после запуска Activity покажется тоаст. В принципе, ничего другого мы и не ждали. Но теперь главное – повернем устройство. Теперь по логике работа лоадера должна начаться заново и через 2 секунды покажется тоаст. Однако при этом мы видим, что тоаст показался мгновенно!

Вся магия заключается в методе initLoader и в классе LoaderManager. И теперь настала пора объяснить, как эта магия работает. Если лоадер еще не был создан (он не хранится в LoaderManager), то метод initLoader создает его и начинает его работу. Однако, если лоадер уже был создан (при первом запуске активити), то при повторном вызове initLoader, LoaderManager не пересоздаст лоадер и не будет его перезапускать. Вместо этого он заменит экземпляр LoaderCallbacks на новый переданный (что замечательно, ведь старый экземпляр был уничтожен вместе со старой Activity) и, если данные уже загрузились, передаст их в onLoadFInished. И при всем этом нам не нужно заниматься ручной проверкой на null для Bundle в onCreate. Если у нас на экране есть один запрос, то мы вполне можем при старте вызывать initLoader и не беспокоиться о пересоздании, что дает хороший уровень абстракции при обработке смены конфигурации.

Логичным будет вопрос о том, а что если мы все же хотим перезапустить выполнение лоадера (например, мы хотим обновить данные, а не получить предыдущий результат)? Для этого есть метод restartLoader, который всегда перезапускает выполнение лоадера. В остальном его работа абсолютно аналогична вызову initLoader.

Давайте теперь посмотрим, как это можно применить для решения нашей реальной задачи и реального запроса. Создадим лоадер, который будет загружать информацию о погоде:

 

И вызовем его в Activity следующим образом:

 

Обратите внимание на передаваемый флаг, который определяет, какой метод вызывать, initLoader или restartLoader. Даже на таком простом экране нам требуется вызов restartLoader, например, для обработки ошибки, когда пользователь хочет выполнить запрос повторно. Аналогичная ситуация возникает и при обновлении данных (например, Callback от SwipeRefreshLayout).

Нужно опять заметить важный плюс от использования лоадеров – мы нигде специально не пишем код для обработки пересоздания, что очень удобно.

Как уже говорилось выше, также очень важно понимать внутреннее устройство класса Loader. Если раньше особо не было другого пути, как изучать лоадеры, то сейчас ситуация изменилась. Сегодня технологии разработки под Android выросли очень серьезно, появилось множество библиотек для обеспечения асинхронности и работы с сетью (именно поэтому типичный путь разработчика сейчас выглядит следующим образом: Thread, AsyncTask, RxJava, минуя лоадеры). Но знать такой мощный компонент и уметь его использовать – это необходимо. Поэтому мы разберем и то, как лоадер устроен внутри.

Пока что мы наследовались всегда от класса AsyncTaskLoader, который обеспечивал работу в фоне. Но все же изначальным классом является именно класс Loader. При этом примечательно, что класс Loader не предоставляет никаких средств для обеспечения работы в фоне. И это не просто так. Лоадер в первую очередь предназначен для того, чтобы быть связанным с жизненным циклом Activity / Fragment. Для обеспечения же работы в фоне нужно либо использовать класс AsyncTaskLoader, либо использовать другие средства обеспечения многопоточности (например, Call из Retrofit, RxJava). И за такое решение нужно сказать большое спасибо разработчикам из Google. Ведь они позволили нам использовать свои средства для обеспечения многопоточности (иначе у нас, к примеру, не было бы возможности использовать RxJava в связке с лоадерами, чем мы займемся далее), при этом сохранив мощь лоадеров.

В классе Loader определено 3 основных метода, которые нужно переопределить для корректного написания своего лоадера. Это следующие методы:

 

Метод onStartLoading вызывается в случае, когда нужно загрузить данные и вернуть их в Callback. На самом деле этот метод вызывается как результат вызова метода startLoading. Обычно метод startLoading вызывается классом LoaderManager, и нам нет нужды использовать его самостоятельно.

Аналогично метод onStopLoading служит для уведомления о том, что нужно остановить загрузку данных (запрос к серверу, к примеру), но при этом не нужно очищать данные.

Методы onStartLoading и onStopLoading вызываются соответственно при вызове методов onStart и onStop в Activity, но при этом они не вызываются в ситуации, когда Activity пересоздается, а только при сворачивании / разворачивании. И это очень хорошо, ведь мы должны останавливать загрузку данных, когда пользователь не находится на экране, чтобы не тратить заряд батареи.

Так что же, это означает, что при каждом сворачивании / разворачивании экрана процесс загрузки будет начинаться заново? Вовсе нет, но для этого нам придется немного поработать самим. Поля в лоадере не уничтожаются, а значит, мы можем проверить, завершился ли уже запрос, и, если да, то мы можем вернуть полученные данные.

Поэтому написание собственного лоадера обычно выглядит следующим образом. Во-первых, определяются поля лоадера, а это обычно результат загрузки, который мы хотим получить, и объект для выполнения запроса к серверу:

 

 

После этого переопределяется метод onStartLoading, в котором мы проверяем, завершился ли запрос (доступны ли сохраненные данные). Если данные есть, то мы сразу возвращаем их, иначе начинаем загружать все заново:

 

Метод deliverResult возвращает данные в LoaderCallbacks. А метод forceLoad инициирует вызов метода onForceLoad, который мы упомянули, но обсудить не успели. По сути, этот метод служит только для удобства и логического разделения между методами жизненного цикла и методами для загрузки данных. В методе onForceLoad вы должны загрузить данные асинхронно и вернуть результат с помощью метода deliverResult.

 

 

И остался последний метод – onStopLoading, в котором мы должны остановить загрузку данных. Благо, с Retrofit это очень просто:

 

И это все – мы полностью изменили способ загрузки данных, но при этом не нужно ничего менять в UI-классах – потрясающий уровень абстракции!

Здесь можно закончить рассмотрение лоадеров, этого достаточно для дальнейшего изучения. Больше примеров и варианты работы с базой данных и обработкой ошибок можно найти в статье.

Несмотря на все удобства, которые мы рассмотрели, и у класса Loader есть свои слабые места, и они почти всегда аналогичны проблемам с Retain Fragment. Например, при полном закрытии приложения в момент загрузки данных, вы также можете получить рассинхронизированное состояние. При этом отличие заключается в том, что лоадер специально предназначен для загрузки данных и предоставляет больший уровень абстракции, но при этом он требует больше кода.

Практическое задание

  • Скачайте Проект LoaderWeather. Описание задачи — в файле ru.gdgkazan.simpleweather.screen.weatherlist.WeatherListActivity
  • Нужно загрузить погоду во всех городах при старте приложения
  • Сделать это наиболее быстрым способом (не каждый город последовательно)
  • Добавить возможность обновления через SwipeRefreshLayout
  • Реализовать обработку пересоздания Activity

Вопросы по уроку вы можете задать в комментариях.

Пример практического задания

Ссылки и полезные ресурсы

  1. Приложения из репозитория:
    1. SimpleWeather – демонстрация различных способов обработки смены конфигурации.
    2. LoaderWeather – использование лоадеров для загрузки данных и практическое задание.
  2. История развития системы Android по версиям.
  3. Книга “Clean Code” от Роберта Мартина.
  4. Clean Architecture от Роберта Мартина.
  5. Документация по обработке смены конфигурации.
  6. Хороший ответ про retain фрагменты.
  7. Документация по лоадерам.
  8. Статья про использование лоадеров для загрузки данных.

Продолжение:

Лекция 2 по архитектуре андроид-приложений. Паттерны A/B/C

[:en]Continuation of Lecture 1 Course on the architecture of android applications. Part 1 is here.

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.

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:

 

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:

 

 

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:

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:

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:

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:

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:

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:

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.
  • 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):

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:

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:

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:

And call it in Activity as follows:

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:

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:

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:

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.

And there was the last method — onStopLoading, in which we must stop loading data. Fortunately, with Retrofit it’s very simple:

 

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.

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

  1. Applications from the repository:
    1. SimpleWeather — demonstration of various ways of processing the configuration change.
    2. LoaderWeather — use loaders to load data and a practical task.
  2. History of the development of the Android system by version.
  3. The book «Clean Code» from Robert Martin.
  4. Clean Architecture from Robert Martin.
  5. Documentation for processing configuration changes.
  6. Good answer about retain fragments.
  7. Documentation for loaders.
  8. An article about using loaders to load data.

Continuation:

Lecture 2 on the architecture of android applications. Patterns A / B / C

[:]

6 thoughts on “[:ru]Лекция 1. Введение в архитектуру клиент-серверных андроид-приложений. Часть 2[:en]Lecture 1. Introduction to the architecture of client-server android applications. Part 2[:]

  1. С выходом новой Android Studio и Java8 никак не могу собрать и запустить проает LoaderWeather. Подскажите, какие изменения нужно внести в build.gradle?

    • замените build.gradle на

      apply plugin: 'com.android.application'

      android {
      compileSdkVersion 26

      defaultConfig {
      applicationId "ru.gdgkazan.simpleweather"
      minSdkVersion 17
      targetSdkVersion 26
      versionCode 1
      versionName "1.0"

      buildConfigField "String", "API_ENDPOINT", '"http://api.openweathermap.org/"'
      buildConfigField "String", "API_KEY", '"bc0ffae33833bd4d0214451ff2c0d4be"'
      }
      buildTypes {
      release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
      }
      }

      compileOptions {
      sourceCompatibility JavaVersion.VERSION_1_8
      targetCompatibility JavaVersion.VERSION_1_8
      }
      }

      apply from: '../versions.gradle'

      dependencies {
      implementation "com.android.support:support-v4:$supportVersion"
      implementation "com.android.support:support-v13:$supportVersion"
      implementation "com.android.support:appcompat-v7:$supportVersion"
      implementation "com.android.support:design:$supportVersion"
      implementation "com.android.support:cardview-v7:$supportVersion"

      implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
      implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"

      implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
      implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"

      implementation "io.reactivex:rxandroid:$rxandroidVersion"
      implementation "io.reactivex:rxjava:$rxjavaVersion"

      implementation "com.jakewharton:butterknife:$butterKnifeVersion"
      annotationProcessor "com.jakewharton:butterknife-compiler:$butterKnifeVersion"

      implementation "com.github.orhanobut:hawk:$hawkVersion"
      }

      замените versions.gradle на

      ext {
      supportVersion = '26.1.0'
      okhttpVersion = '3.4.1'
      retrofitVersion = '2.1.0'
      rxandroidVersion = '1.2.1'
      rxjavaVersion = '1.1.9'
      butterKnifeVersion = '8.8.1'
      hawkVersion = '1.23'

      junitVersion = '4.12'
      mockitoVersion = '2.0.111-beta'

      runnerVersion = '0.5'
      espressoVersion = '2.2.2'
      }

    • а также замените build.gradle (Project LoaderWeather) на:

      buildscript {
      repositories {
      jcenter()
      }
      dependencies {
      classpath 'com.android.tools.build:gradle:3.0.0'
      }

      configurations.classpath.exclude group: 'com.android.tools.external.lombok'
      }

      allprojects {
      repositories {
      jcenter()
      maven { url "https://jitpack.io" }
      maven { url 'https://maven.google.com' }
      }
      }

      task clean(type: Delete) {
      delete rootProject.buildDir
      }

  2. После замены build.gradle рекомендованной выше возникла ошибка в файле WeatherApp.java
    не видно библиотеку import ru.arturvasilov.sqlite.core.SQLite;
    ЦТА?!

Добавить комментарий