Глава 34. Локальные базы данных и SQLite Листинг 34.24 (продолжение) String provider = getString(getColumnIndex(COLUMN_LOCATION_PROVIDER)); Location loc = new Location(provider); // Заполнение остальных свойств loc.setLongitude(getDouble(getColumnIndex(COLUMN_LOCATION_LONGITUDE))); loc.setLatitude(getDouble(getColumnIndex(COLUMN_LOCATION_LATITUDE))); loc.setAltitude(getDouble(getColumnIndex(COLUMN_LOCATION_ALTITUDE))); loc.setTime(getLong(getColumnIndex(COLUMN_LOCATION_TIMESTAMP))); return loc; } } Объект LocationCursor выполняет те же функции, что и RunCursor , но в нем упа- ковывается курсор для возвращения строк таблицы location , а их различные поля преобразуются в свойства объекта Location . В этой реализации есть одна тонкость: конструктору Location требуется имя поставщика данных, поэтому мы извлекаем его из текущей строки, прежде чем задавать остальные свойства. Метод queryLastLocationForRun(long) очень похож на queryRun(long) , за исклю- чением того, что он ищет последнее местоположение для заданной серии и упако- вывает результат в LocationCursor Как и в случае с queryRun(long) , мы должны создать в RunManager метод для его вызова и вернуть Location из единственной строки курсора. Листинг 34.25. Получение последнего местоположения для серии (RunManager.java) public Location getLastLocationForRun(long runId) { Location location = null; LocationCursor cursor = mHelper.queryLastLocationForRun(runId); cursor.moveToFirst(); // Если набор не пуст, получить местоположение if (!cursor.isAfterLast()) location = cursor.getLocation(); cursor.close(); return location; } Теперь новый метод RunFragment может использоваться для выборки последнего местоположения текущей серии при создании фрагмента. Листинг 34.26. Получение последнего местоположения для текущей серии (RunFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); mRunManager = RunManager.get(getActivity()); // Проверить идентификатор Run и получить объект серии Bundle args = getArguments(); if (args != null) { long runId = args.getLong(ARG_RUN_ID, -1); if (runId != -1) { Упражнение. Выделение текущей серии 567 mRun = mRunManager.getRun(runId); mLastLocation = mRunManager.getLastLocationForRun(runId); } } } В результате наш объект RunTracker способен создавать и отслеживать столько серий, сколько выдержит диск (и батарея) вашего устройства. Все эти данные выводятся на экран в логичном, последовательном представлении. Приятного отслеживания! Упражнение. Выделение текущей серииВ текущей реализации опознать текущую серию можно только одним способом: вручную открыть ее в списке и проверить состояние кнопок запуска и остановки. Было бы удобнее, если бы пользователь мог быстрее и проще обратиться к текущей серии. Простое упражнение: обеспечьте визуальное выделение строки списка, соответству- ющей текущей серии (например, выведите рядом с ней значок или измените ее цвет). Чтобы усложнить задачу, используйте непрерывное оповещение о включенном режиме отслеживания, при нажатии на котором должна запускаться активность RunActivity Асинхронная загрузка данных В главе 34 мы реализовали хранение данных с использованием SQLite и использо- вали объекты Cursor в главном потоке приложения. Однако такое решение нельзя признать полноценным; операции с базой данных следует по возможности вынести из главного потока приложения. В этой главе мы используем объекты Loader для выборки из базы данных в фоновом потоке. Интерфейс Loader был введен в Android 3.0 (Honeycomb); он также досту- пен в библиотеке поддержки, поэтому ничто не препятствует его использованию в современных приложениях. Loader и LoaderManager Загрузчик (loader) предназначен для загрузки некоторых данных (объекта) из ис- точника. Источником может быть диск, база данных, ContentProvider , сеть или другой процесс. Загрузчик производит выборку данных без блокировки главного потока и доставляет результаты стороне, которая в них заинтересована. Существуют три встроенных типа загрузчиков: Loader , AsyncTaskLoader и Cursor- Loader (рис. 35.1). Loader — базовый класс, который сам по себе не очень полезен. Он определяет API, используемый LoaderManager для взаимодействия со всеми загрузчиками. AsyncTaskLoader — абстрактный класс Loader , использующий AsyncTask для пере- дачи работы в другой поток. Почти все полезные классы загрузчиков, которые вы будете создавать, будут представлять собой субклассы AsyncTaskLoader Наконец, CursorLoader расширяет AsyncTaskLoader для загрузки Cursor из Con- tentProvider через ContentResolver . К сожалению, в приложении RunTracker не 35
Loader и LoaderManager 569существует возможности использования Cursor- Loader с курсорами, полученными от SQLiteDatabase Все взаимодействие с загрузчиками осуществляется классом LoaderManager . Этот класс отвечает за за- пуск, остановку и сопровождение жизненного цикла всех загрузчиков, связанных с вашим компонентом. В пределах Fragment или Activity для получения экземпляра LoaderManager используется метод get- LoaderManager() Метод initLoader(int, Bundle, LoaderCallbacks ) запускает инициализацию Loader . В первом аргумен- те передается целочисленный идентификатор загруз- чика, во втором — объект Bundle с аргументами (или null ), а последний аргумент содержит реализацию интерфейса LoaderCallbacks Как вы увидите в следующих разделах, существует много способов реализации LoaderCallbacks , но чаще всего они реализуются непосредственно в Fragment Метод restartLoader(int, Bundle, Loader Call- backs) выполняет принудительный перезапуск существующего загрузчика. Обычно перезапуск используется для перезагрузки заведомо (или потенциально) устаревших данных. Интерфейс LoaderCallbacks состоит из трех методов: onCreateLoader(…) , onLoadFinished(…) и onLoaderReset(…) . Все три метода будут подробно рассмотрены в ходе их реализации в RunTracker. Почему стоит использовать загрузчик вместо, допустим, прямого использования AsyncTask ? Самая убедительная причина заключается в том, что LoaderManager обеспечивает жизнеспособность загрузчиков компонента вместе с их данными при изменениях конфигурации (например, при поворотах). Если вы будете использовать AsyncTask для загрузки данных, вам придется управ- лять жизненным циклом объектов при изменениях конфигурации и сохранять данные в месте, которое эти изменения переживет. Часто задача упрощается ис- пользованием вызова setRetainInstance(true) для Fragment , и все же в отдельных ситуациях разработчику приходится вмешиваться в происходящее и писать код, который обеспечивает правильность выполнения всего происходящего. Загрузчики в основном (хотя и не полностью!) избавляют вас от этих хлопот. Если при изменении конфигурации вы инициализируете загрузчик, который уже завершил загрузку своих данных, он может выдать эти данные немедленно, не пытаясь загрузить их заново. Данный механизм работает независимо от того, был ваш фрагмент сохранен (retained) или нет; это упрощает вашу работу, так как вам не приходится учитывать дополнительные аспекты жизненного цикла, обусловленные сохранением фрагментов. наследует от наследует от Рис. 35.1. Иерархия классов Loader
570 Глава 35. Асинхронная загрузка данных Использование загрузчиков в RunTracker В настоящее время RunTracker загружает три блока данных: список серий ( RunCur- sor ), данные отдельной серии ( Run ) и последнее местоположение серии ( Location ). Все данные читаются из базы данных SQLite, поэтому все операции выборки следует переместить в Loader для повышения эффективности работы пользовательского интерфейса. В следующих разделах мы создадим два абстрактных субкласса AsyncTaskLoader Первый, SQLiteCursorLoader , представляет собой упрощенную версию системного класса CursorLoader , который работает с объектами Cursor , поступившими из любого источника. Второй, DataLoader , способен загружать произвольные данные; он упрощает использование AsyncTaskLoader для субклассов. Загрузка списка серий Текущая реализация RunListFragment напрямую запрашивает у RunManager объект RunCursor , представляющий список серий, в onCreate(Bundle) . В этом разделе мы напишем загрузчик, который будет опосредованно выполнять этот запрос в другом потоке. RunListFragment приказывает LoaderManager запускать (и перезапускать) загрузчик и реализует интерфейс LoaderCallbacks для получения информации о готовности данных. Чтобы упростить код RunListFragment (и классов, которые будут написаны позднее), начнем с создания абстрактного субкласса AsyncTaskLoader с именем SQLiteCur- sorLoader (листинг 35.1). Этот класс повторяет большую часть кода CursorLoader , но без обязательного использования ContentProvider Листинг 35.1. Загрузчик для курсоров SQLite (SQLiteCursorLoader.java) public abstract class SQLiteCursorLoader extends AsyncTaskLoader { private Cursor mCursor; public SQLiteCursorLoader(Context context) { super(context); } protected abstract Cursor loadCursor(); @Override public Cursor loadInBackground() { Cursor cursor = loadCursor(); if (cursor != null) { // Проверить, что окно контента заполнено cursor.getCount(); } return cursor; } @Override public void deliverResult(Cursor data) {
Загрузка списка серий 571 Cursor oldCursor = mCursor; mCursor = data; if (isStarted()) { super.deliverResult(data); } if (oldCursor != null && oldCursor != data && !oldCursor.isClosed()) { oldCursor.close(); } } @Override protected void onStartLoading() { if (mCursor != null) { deliverResult(mCursor); } if (takeContentChanged() || mCursor == null) { forceLoad(); } } @Override protected void onStopLoading() { // Попытаться отменить текущую задачу загрузки, если возможно. cancelLoad(); } @Override public void onCanceled(Cursor cursor) { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } @Override protected void onReset() { super.onReset(); // Убедиться в том, что загрузчик остановлен onStopLoading(); if (mCursor != null && !mCursor.isClosed()) { mCursor.close(); } mCursor = null; }} SQLiteCursorLoader реализует AsyncTaskLoader API для эффективной загрузки и хранения Cursor в поле mCursor . Метод loadInBackground() вызывает абстрактный метод loadCursor() для получения Cursor , а затем метод getCount() для курсора, чтобы убедиться в доступности данных в памяти после их передачи главному потоку. Метод deliverResult(Cursor) решает две задачи. Если загрузчик запущен (это озна- чает, что данные могут быть поставлены), вызывается реализация deliverResult(…) 572 Глава 35. Асинхронная загрузка данных суперкласса. Если старый курсор больше не нужен, он закрывается для освобож- дения ресурсов. Поскольку существующий курсор может быть кэширован и ис- пользован заново, очень важно перед закрытием старого курсора убедиться в том, что старый и новый курсоры различны. Остальные реализации методов не критичны для понимания смысла RunTracker, но вы можете найти дополнительную информацию в документации API по Async- TaskLoader . При наличии этого базового класса можно реализовать очень простой субкласс RunListCursorLoader в RunListFragment в виде внутреннего класса. Листинг 35.2. Реализация RunListCursorLoader (RunListFragment.java) @Override public void onListItemClick(ListView l, View v, int position, long id) { // Аргумент id содержит идентификатор серии; // CursorAdapter автоматически предоставляет эту информацию. Intent i = new Intent(getActivity(), RunActivity.class); i.putExtra(RunActivity.EXTRA_RUN_ID, id); startActivity(i); } private static class RunListCursorLoader extends SQLiteCursorLoader { public RunListCursorLoader(Context context) { super(context); } @Override protected Cursor loadCursor() { // Запрос на получение списка серий return RunManager.get(getContext()).queryRuns(); } } private static class RunCursorAdapter extends CursorAdapter { Теперь мы можем обновить RunListFragment и реализовать интерфейс LoaderCall- backs для Cursor . Добавьте приведенные ниже методы и включите обратные вызовы в объявление класса. Листинг 35.3. Реализация LoaderCallbacks (RunListFragment.java) public class RunListFragment extends ListFragment implements LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { return new RunListCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader loader, Cursor cursor) { // Создание адаптера, ссылающегося на этот курсор RunCursorAdapter adapter =
Загрузка списка серий 573 new RunCursorAdapter(getActivity(), (RunCursor)cursor); setListAdapter(adapter); } @Override public void onLoaderReset(Loader loader) { // Прекращение использования курсора (через адаптер) setListAdapter(null); } Метод onCreateLoader(int, Bundle) вызывается объектом LoaderManager , когда ему потребуется создать загрузчика. Аргумент id полезен при наличии нескольких однотипных загрузчиков, которых необходимо различать, а объект Bundle содержит все передаваемые аргументы. В этой реализации никакие аргументы не использу- ются; она просто создает новый экземпляр RunListCursorLoader , ссылающийся на текущий экземпляр Activity для контекста. Метод onLoadFinished(Loader, Cursor) будет вызван для главного потока после того, как данные будут загружены в фоновом режиме. В этой версии мы назна- чаем адаптером ListView объект RunCursorAdapter , ссылающийся на новый курсор. Наконец, метод onLoaderReset(Loader) будет вызываться при отсутствии доступных данных. Чтобы исключить возможные проблемы, мы прекращаем ис- пользовать курсор, присваивая адаптеру списка null Наконец, после реализации обратных вызовов мы можем приказать LoaderManager выполнить его работу. Также можно удалить поле mCursor и метод onDestroy() , в котором эта переменная очищалась. Листинг 35.4. Использование Loader (RunListFragment.java) public class RunListFragment extends ListFragment implements LoaderCallbacks { private static final int REQUEST_NEW_RUN = 0; private RunCursor mCursor; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); // Запрос на получение списка серий mCursor = RunManager.get(getActivity()).queryRuns(); // Создание адаптера, ссылающегося на этот курсор RunCursorAdapter adapter = new RunCursorAdapter(getActivity(), mCursor); setListAdapter(adapter); // Инициализация загрузчика для загрузки списка серий getLoaderManager().initLoader(0, null, this); } @Override public void onDestroy() { mCursor.close(); продолжение
574Глава 35. Асинхронная загрузка данных Листинг 35.4 (продолжение) super.onDestroy(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (REQUEST_NEW_RUN == requestCode) { mCursor.requery(); ((RunCursorAdapter)getListAdapter()).notifyDataSetChanged(); // Перезапуск загрузчика для получения новых серий getLoaderManager().restartLoader(0, null, this); } } После внесения всех изменений запустите приложение и убедитесь в том, что дан- ные в списке заполняются так же, как прежде. При некоторой наблюдательности вы заметите, что во время загрузки данных в фоновом режиме в списке ненадолго появляется вращающийся индикатор прогресса. ListFragment автоматически предоставляет эту функциональность, если адаптеру задается null Загрузка одной серииКласс SQLiteCursorLoader хорошо подходит для загрузки данных, которые должны оставаться в курсоре (например, списков), но в RunFragment мы загружаем два от- дельных объекта, а курсор скрывается RunManager . Для работы с произвольными данными нужен более общий загрузчик. В этом разделе мы создадим новый класс DataLoader , являющийся субклассом AsyncTaskLoader DataLoader выполняет некоторые простые операции, которые должны выполняться всеми субклассами AsyncTaskLoader , оставляя своим суб- классам только реализацию loadInBackground() Код класса DataLoader приведен в листинге 35.5. Листинг 35.5. Простой загрузчик данных (DataLoader.java) public abstract class DataLoader extends AsyncTaskLoader { private D mData; public DataLoader(Context context) { super(context); } @Override protected void onStartLoading() { if (mData != null) { deliverResult(mData); } else { forceLoad(); } }
Загрузка одной серии 575 @Override public void deliverResult(D data) { mData = data; if (isStarted()) super.deliverResult(data); }} Класс DataLoader использует обобщенный тип D для хранения экземпляра загружа- емых данных. В методе onStartLoading() он проверяет доступность этих данных, и если они доступны — немедленно поставляет их. В противном случае он вызывает метод forceLoad() суперкласса для загрузки данных. Реализация deliverResult(D) сохраняет новый объект данных, и если загрузчик запущен — вызывает реализацию суперкласса для выполнения поставки. Чтобы использовать новый класс, субклассируйте DataLoader и используйте суб- класс RunLoader в RunFragment Листинг 35.6. Загрузчик серии (RunLoader.java) public class RunLoader extends DataLoader { private long mRunId; public RunLoader(Context context, long runId) { super(context); mRunId = runId; } @Override public Run loadInBackground() { return RunManager.get(getContext()).getRun(mRunId); } } Конструктор RunLoader ожидает получить Context ( Activity ) и значение long , представляющее идентификатор загружаемой серии. Метод loadInBackground() запрашивает у RunManager серию с заданным идентификатором и возвращает ее. Когда все будет сделано, вы сможете использовать RunLoader в RunFragment вместо прямого взаимодействия с RunManager в главном потоке. Однако в реа- лизации существует одно отличие: так как RunFragment загружает два разных типа данных, серию и позицию, механизм LoaderCallbacks в сочетании с ограничениями обобщенных типов Java не позволяет напрямую реализовать интерфейс в методах RunFragment . Чтобы обойти ограничения, можно создать внутренние классы, реализующие LoaderCallbacks для Run и Location по от- дельности, и передать экземпляр каждого из них при вызове метода initLoader() класса LoaderManager Интеграция начинается с добавления внутреннего класса RunLoaderCallbacks в RunFragment
| Образовательный портал
Как узнать результаты егэ
Стихи про летний лагерь
3агадки для детей |