Глава 26. HTTP и фоновые задачи Итак, разметка XML с Flickr получена; что теперь с ней делать? То же, что мы де- лаем со всеми данными — пометить в один или несколько объектов модели. Класс модели, который мы создадим для PhotoGallery, называется GalleryItem . На рис. 26.9 изображена диаграмма объектов PhotoGallery. Представление Модель Контроллер Рис. 26.9. Разметка XML в Flickr Обратите внимание: на рис. 26.9 не показана активность-хост, чтобы диаграмма была сконцентрирована на фрагментах и сетевом коде. Создайте класс GalleryItem и добавьте следующий код. Листинг 26.10. Создание класса объекта модели (GalleryItem.java) package com.bignerdranch.android.photogallery; public class GalleryItem { private String mCaption; private String mId; private String mUrl; public String toString() { return mCaption; } } Прикажите Eclipse сгенерировать get - и set -методы для mId , mCaption и mUrl Загрузка XML из Flickr 433 После того как объекты модели будут созданы, их следует заполнить данными из разметки XML, полученной от Flickr. Для получения данных из XML используется интерфейс XmlPullParser Использование XmlPullParser XmlPullParser — интерфейс, который может использоваться для выделения собы- тий разбора в потоке разметки XML. XmlPullParser используется во внутренней реализации Android для заполнения ваших файлов макетов. С таким же успехом его можно использовать для разбора объектов GalleryItem Добавьте в FlickrFetchr константу, определяющую имя элемента XML, содержа- щего фотографию. Затем напишите метод, который использует XmlPullParser для идентификации всех фотографий в XML. Создайте объект GalleryItem для каждой фотографии и добавьте его в ArrayList Листинг 26.11. Разбор фотографий Flickr (FlickrFetchr.java) public class FlickrFetchr { public static final String TAG = "FlickrFetchr"; private static final String ENDPOINT = "https://api.flickr.com/services/rest/"; private static final String API_KEY = "your API key"; private static final String METHOD_GET_RECENT = "flickr.photos.getRecent"; private static final String XML_PHOTO = "photo"; public void fetchItems() { } void parseItems(ArrayList items, XmlPullParser parser) throws XmlPullParserException, IOException { int eventType = parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG && XML_PHOTO.equals(parser.getName())) { String id = parser.getAttributeValue(null, "id"); String caption = parser.getAttributeValue(null, "title"); String smallUrl = parser.getAttributeValue(null, EXTRA_SMALL_URL); GalleryItem item = new GalleryItem(); item.setId(id); item.setCaption(caption); item.setUrl(smallUrl); items.add(item); } eventType = parser.next(); } } }
434Глава 26. HTTP и фоновые задачи Представьте, что XmlPullParser водит пальцем по документу XML, последовательно перебирая различные события, такие как START_TAG , END_TAG и END_DOCUMENT . На каждом шаге вы можете вызывать такие методы, как getText() , getName() или ge- tAttributeValue(…) , для получения информации о событии, на которое в настоящий момент указывает XmlPullParser . Чтобы переместить XmlPullParser к следующему интересному событию в XML, вызовите next() . Кстати, этот метод также возвращает тип события, к которому он только что переместился. Рис. 26.10. Как работает XmlPullParser Методу parseItems(…) передаются XmlPullParser и ArrayList . Получите экземпляр парсера и передайте ему данные xmlString , полученные от Flickr. Затем вызовите parseItems(…) с настроенным парсером и пустым массивом. Листинг 26.12. Вызов parseItems(…) (FlickrFetchr.java) public void fetchItems() {public ArrayList fetchItems() { ArrayList items = new ArrayList(); try { String url = Uri.parse(ENDPOINT).buildUpon() .appendQueryParameter("method", METHOD_GET_RECENT) .appendQueryParameter("api_key", API_KEY) .appendQueryParameter(PARAM_EXTRAS, EXTRA_SMALL_URL) .build().toString(); String xmlString = getUrl(url); Log.i(TAG, "Received xml: " + xmlString); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser parser = factory.newPullParser(); parser.setInput(new StringReader(xmlString)); parseItems(items, parser); } catch (IOException ioe) { От AsyncTask к главному потоку 435 Log.e(TAG, "Failed to fetch items", ioe); } catch (XmlPullParserException xppe) { Log.e(TAG, "Failed to parse items", xppe); } return items; } Запустите приложение PhotoGallery и протестируйте код разбора XML. У Pho- toGallery пока нет средств для вывода информации о содержимом ArrayList , по- этому если вы захотите убедиться в том, что все работает правильно, вам придется установить точку прерывания в отладчике. От AsyncTask к главному потоку Напоследок мы вернемся к уровню представления и выведем некоторые названия фотографий в виджете GridView экземпляра PhotoGalleryFragment Класс GridView , как и ListView , происходит от AdapterView , поэтому ему нужен адаптер, поставляющий отображаемые представления. В файле PhotoGalleryFragment.java добавьте объявление ArrayList с элементами Gal- leryItems , а затем создайте экземпляр ArrayAdapter для простого макета, предо- ставляемого Android. Листинг 26.13. Реализация setupAdapter() (PhotoGalleryFragment.java) public class PhotoGalleryFragment extends Fragment { private static final String TAG = "PhotoGalleryFragment"; GridView mGridView; ArrayList mItems; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_gallery, container, false); mGridView = (GridView)v.findViewById(R.id.gridView); setupAdapter(); return v; } void setupAdapter() { if (getActivity() == null || mGridView == null) return; if (mItems != null) { mGridView.setAdapter(new ArrayAdapter(getActivity(), android.R.layout.simple_gallery_item, mItems)); } else { mGridView.setAdapter(null); } }
436Глава 26. HTTP и фоновые задачи Так как у GridView нет удобного класса GridFragment , нам придется самостоятельно построить код управления адаптером. Например, для этого можно использовать метод наподобие только что добавленного метода setupAdapter() . Этот метод про- веряет текущее состояние модели и соответствующим образом настраивает адаптер GridView . Этот метод должен вызываться в onCreateView(…) , чтобы при каждом создании нового экземпляра GridView при повороте он заново настраивался соот- ветствующим адаптером. Метод также должен вызываться при каждом изменении набора объектов модели. Макет android.R.layout.simple_gallery_item состоит из элемента TextView . Вспом- ните, что в GalleryItem мы переопределили метод toString() для возвращения значения mCaption объекта. Таким образом, для отображения названий фотографий в GridView достаточно передать адаптеру массив экземпляров GalleryItem и этот макет. Обратите внимание: перед назначением адаптера мы проверяем getActivity() на null . Помните, что фрагменты могут существовать и автономно, не будучи связанны- ми с какой-либо активностью. До настоящего момента мы не сталкивались с такой возможностью, потому что вызовы методов управлялись обратными вызовами от инфраструктуры. Если фрагмент получает обратные вызовы, он определенно при- соединен к активности. Нет активности — нет обратных вызовов. Теперь, когда мы используем AsyncTask , некоторые действия инициируются самосто- ятельно, и мы уже не можем предполагать, что фрагмент присоединен к активности. Таким образом, сначала необходимо убедиться в том, что фрагмент остается присо- единенным; в противном случае попытка выполнения операции завершится неудачей. После получения данных от Flickr следует вызвать метод setupAdapter() . Первое, что приходит в голову, — вызов setupAdapter() в конце метода doInBackground(…) класса FetchItemsTask . Это не лучшая мысль. Вспомните, что сейчас «в магазине работают два Флэша» — один обслуживает многочисленных покупателей, другой общается по телефону с Flickr. Что произойдет, если второй Флэш, повесив трубку, захочет подключиться к обслуживанию покупателей? Скорее всего, два Флэша только начнут мешать друг другу. На компьютере такая путаница может привести к повреждению объектов в памя- ти. По этой причине фоновым потокам запрещается обновлять пользовательский интерфейс, поскольку такие операции явно небезопасны. Что делать? У AsyncTask имеется метод onPostExecute(…) , который можно пере- определить. Метод onPostExecute(…) выполняется после завершения doInBack- ground(…) . Кроме того, onPostExecute(…) выполняется в главном, а не в фоновом потоке, поэтому обновление пользовательского интерфейса в нем безопасно. Внесите изменения в метод FetchItemsTask , чтобы он обновлял поле mItems и вы- зывал setupAdapter() после загрузки фотографий. Листинг 26.14. Добавление кода обновления адаптера (PhotoGalleryFragment.java) private class FetchItemsTask extends AsyncTask {private class FetchItemsTask extends AsyncTask> { @Override protected Void doInBackground(Void... params) { protected ArrayList doInBackground(Void... params) { От AsyncTask к главному потоку 437 new FlickrFetchr().fetchItems(); return new FlickrFetchr().fetchItems(); return null; } @Override protected void onPostExecute(ArrayList items) { mItems = items; setupAdapter(); } } Мы внесли три изменения. Во-первых, мы изменили тип третьего обобщенного параметра FetchItemsTask . Этот параметр определяет тип результата, производи- мого AsyncTask ; он задает тип значения, возвращаемого doInBackground(…) , а также тип входного параметра onPostExecute(…) . Во-вторых, мы изменили метод doIn- Background(…) так, чтобы он возвращал список элементов GalleryItem . Тем самым мы устранили ошибку в коде и обеспечили его нормальную компиляцию. Также при вызове передается список элементов, чтобы он мог использоваться в коде onPostExecute(…) . В-третьих, была добавлена реализация onPostExecute(…) . Этот метод получает список, загруженный в doInBackground(…) , помещает его в mItems и обновляет адаптер GridView На этом наша работа для этой главы завершается. Запустите приложение, и вы увидите текст, отображаемый для каждого загруженного элемента GalleryItem Рис. 26.11. Заголовки фотографий Flickr
438Глава 26. HTTP и фоновые задачи Для любознательных: подробнее об AsyncTaskВ этой главе вы видели пример использования последнего параметра-типа AsyncTask , определяющего возвращаемый тип. А как насчет двух других параметров? Первый параметр-тип позволяет задать тип входных параметров. Он используется следующим образом: AsyncTask task = new AsyncTask() { public Void doInBackground(String... params) { for (String parameter : params) { Log.i(TAG, "Received parameter: " + parameter); } return null; } }; task.execute("First parameter", "Second parameter", "Etc."); Входные параметры передаются методу execute(…) , который вызывается с пере- менным числом аргументов. Эти переменные аргументы передаются doInBack- ground(…) Второй параметр-тип позволяет задать тип для передачи информации о ходе вы- полнения операции. Вот как это выглядит: final ProgressBar progressBar = /* Индикатор прогресса */; progressBar.setMax(100); AsyncTask task = new AsyncTask() { public Void doInBackground(Integer... params) { for (Integer progress : params) { publishProgress(progress); Thread.sleep(1000); } } public void onProgressUpdate(Integer... params) { int progress = params[0]; progressBar.setProgress(progress); } }; task.execute(25, 50, 75, 100); Обновление обычно выполняется в продолжительном фоновом процессе. Проблема в том, что необходимые обновления пользовательского интерфейса не могут вы- полняться прямо из фонового процесса, поэтому AsyncTask предоставляет методы publishProgress(…) и onProgressUpdate(…) Механизм обновления работает следующим образом: в методе doInBackground(…) в фоновом потоке вызывается publishProgress(…) . Это приводит к вызову onPro- gressUpdate(…) в потоке пользовательского интерфейса. Таким образом, пользова- тельский интерфейс обновляется в onProgressUpdate(…) , но управление обновле- ниями осуществляется вызовами publishProgress(…) в doInBackground(…)
Упражнение. Страничная навигация439Уничтожение AsyncTaskВ этой главе реализация AsyncTask была тщательно структурирована таким об- разом, чтобы нам не приходилось хранить информацию о ней. Однако в других ситуациях может возникнуть необходимость в отслеживании AsyncTask и даже их периодической отмене и повторном запуске. В подобных более сложных сценариях использования AsyncTask присваивается пере- менной экземпляра, после чего для нее можно вызвать AsyncTask.cancel(boolean) для отмены текущей фоновой операции AsyncTask AsyncTask.cancel(boolean) может работать в более жестком или менее жестком режиме. Если вызвать cancel(false) , метод действует менее жестко и просто воз- вращает true при вызове isCancelled() . Далее AsyncTask проверяет isCancelled() внутри doInBackground(…) и принимает решение о досрочном завершении операции. Но в случае вызова cancel(true) метод действует жестко и прерывает программный поток, в котором выполняется doInBackground(…) . Вызов AsyncTask.cancel(true) является агрессивным способом остановки AsyncTask . Если этого можно избежать — лучше так и сделать. Упражнение. Страничная навигацияПо умолчанию метод getRecent возвращает одну страницу со 100 результатами. При помощи дополнительного параметра page получить вторую, третью и так далее страницу результатов. Добавьте в адаптер код, который обнаруживает переход к концу массива элементов и заменяет текущую страницу следующей страницей результатов. Чтобы немного усложнить упражнение, организуйте присоединение данных последующих страниц к результатам. Looper, Handler и HandlerThread После загрузки и разбора разметки XML от Flickr нашей следующей задачей станет загрузка и вывод изображений. В этой главе вы научитесь использовать классы Looper , Handler и HandlerThread для динамической загрузки и вывода фотографий в PhotoGallery. Подготовка GridView к выводу изображений Текущий адаптер PhotoGalleryFragment просто предоставляет виджеты TextView для вывода в GridView . В каждом представлении TextView выводится содержимое заголовка GalleryItem Чтобы выводить фотографии, вам понадобится нестандартный адаптер, который вместо текстовых представлений предоставляет ImageView . В конечном итоге Ima- geView выведет фотографию, загруженную в поле mUrl экземпляра GalleryItem Начнем с создания нового файла макета для элементов фотогалереи в файле gal- lery_item.xml . Макет будет состоять из единственного виджета ImageView (рис. 27.1). Рис. 27.1. Макет элемента фотогалереи (res/layout/gallery_item.xml) 27
Подготовка GridView к выводу изображений 441 Эти виджеты ImageView находятся под управлением GridView , это означает, что их ширина будет величиной переменной. Тем не менее высота будет оставаться фиксированной. Чтобы наиболее эффективно использовать пространство виджета ImageView , следует задать его свойству scaleType значение centerCrop . С этим зна- чением изображение выравнивается по центру и масштабируется, чтобы меньшая сторона была равна размеру представления, а большая усекалась с обеих сторон. Также нам понадобится временное изображение для каждого виджета ImageView , которое будет отображаться до завершения загрузки изображения. Найдите файл brian_up_close.jpg в файле решений и поместите его в каталог res/drawable-hdpi В классе PhotoGalleryFragment замените базовую реализацию ArrayAdapter поль- зовательской реализацией, у которой реализация getView(…) возвращает виджет ImageView с временным изображением. Листинг 27.1. Создание GalleryItemAdapter (PhotoGalleryFragment.java) public class PhotoGalleryFragment extends Fragment { void setupAdapter() { if (getActivity() == null || mGridView == null) return; if (mItems != null) { mGridView.setAdapter(new ArrayAdapter(getActivity(), android.R.layout.simple_gallery_item, mItems)); mGridView.setAdapter(new GalleryItemAdapter(mItems)); } else { mGridView.setAdapter(null); } } private class FetchItemsTask extends AsyncTask> { } private class GalleryItemAdapter extends ArrayAdapter { public GalleryItemAdapter(ArrayList items) { super(getActivity(), 0, items); } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = getActivity().getLayoutInflater() .inflate(R.layout.gallery_item, parent, false); } ImageView imageView = (ImageView)convertView .findViewById(R.id.gallery_item_imageView); imageView.setImageResource(R.drawable.brian_up_close); return convertView; } } }
|