Основные антипаттерны:
- Отправка запроса прямо из кода Activity в основном треде — тут без комментариев, т.к. это приводит к заморозке UI, вследствие чего система может предложить убить приложением;
- Отправка запроса из кода Activity при помощи AsyncTask — плохо, т.к. если пользователь, к примеру, повернет экран, Activity пересоздастся и запрос придется выполнять заново, что приводит увеличению времени ожидания и количества потребляемого трафика;
- Отсутствие кэширования — после каждого действия пользователя ему придется ждать полной загрузки данных.
Решение проблем
Эти проблемы далеко не новы, и в 2010 году на коференции Google I/O Virgil Dobjanschi представил три патерна для REST клиентов (слайды с презентации). Их объединяют следующие черты:
- Все данные кэшируются в базу данных SQLite, работа с ней ведется при помощи ContentProvider-а, что обеспечивает отделение данных от представление и удобство работы с ними;
- Сетевые вопросы выполняются через специально созданный сервис (каждый запрос в отдельном потоке), это необходимо для того, чтобы даже если пользователь выйдет из приложения, данные все равно докачались и сохранились.
Сами паттерны:
A. Activity → Service → ContentProvider, Activity взаимодействует с сервисом для выполнения сетевых запросов, сервис сохраняет полученные из сети данные в ContentProvider, откуда потом Activity их получает;
B. Activity → ContentProvider → Service, Activity взаимодействует только с ContentProvider, ContentProvider выполняет сетевые запросы через сервис;
C. Activity → ContentProvider → SyncAdapter, аналогично предыдущему, но предназначено для специфической ситуации, в которой локальные данные полностью синхронизируются с данными в облаке. В качестве примера можно
указать на контакты в аккаунте Google.
Наше приложение
В этой статье мы разберемся с паттерном A и создадим простейшее приложение, использующее его для отображения постов некоторого пользователя из твиттера. Подробная схема паттерна:
ContentProvider
В первую очередь, нам необходимо создать модель данных для нашего приложения. Данные будем хранить в виде таблицы tweets, в которой два поля кроме _id: user_name и body — имя пользователя и текст твита, соответственно.
Опишем контакт нашего провайдера:
public final class Contract { public static final String AUTHORITY = "com.example.rest_a"; public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); public interface TweetsCoulmns { public static final String USER_NAME = "user_name"; public static final String BODY = "body"; public static final String DATE = "date"; } public static final class Tweets implements BaseColumns, TweetsCoulmns { public static final String CONTENT_PATH = "tweets"; public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, CONTENT_PATH); public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd." + AUTHORITY + "." + CONTENT_PATH; } }
Исходный код самого класса провайдера можно опустить, в нем создается база данных SQLite при помощи DatabaseHelper-а с названными полями и обеспечивается прямая работа с ней, в точности как в примере из документации. Стоит отметить только необходимость оповещать об изменениях данных подключенных наблюдателей, для чего в методе insert() имеется строка
getContext().getContentResolver().notifyChange(Contract.Tweets.CONTENT_URI, null);
а в методе query(), соответственно,
cursor.setNotificationUri(getContext().getContentResolver(), Contract.Tweets.CONTENT_URI);
Service
Теперь пришло время создать сервис, который бы выполнял запросы к серверу. Мы не будем изобретать велосипед и воспользуемся библитекой DataDroid для этого.
Для начала создадим напишем код, который получает данные из твиттера из объекта и вставляет их в базу данных. В DataDroid его нужно писать в классе, реализующем интерфейс Operation:
public final class TweetsOperation implements Operation { @Override public Bundle execute(Context context, Request request) throws ConnectionException, DataException, CustomRequestException { NetworkConnection connection = new NetworkConnection(context, "https://api.twitter.com/1/statuses/user_timeline.json"); HashMap<String, String> params = new HashMap<String, String>(); params.put("screen_name", request.getString("screen_name")); connection.setParameters(params); ConnectionResult result = connection.execute(); ContentValues[] tweetsValues; try { JSONArray tweetsJson = new JSONArray(result.body); tweetsValues = new ContentValues[tweetsJson.length()]; for (int i = 0; i < tweetsJson.length(); ++i) { ContentValues tweet = new ContentValues(); tweet.put("user_name", tweetsJson.getJSONObject(i).getJSONObject("user").getString("name")); tweet.put("body", tweetsJson.getJSONObject(i).getString("text")); tweetsValues[i] = tweet; } } catch (JSONException e) { throw new DataException(e.getMessage()); } context.getContentResolver().delete(Contract.Tweets.CONTENT_URI, null, null); context.getContentResolver().bulkInsert(Contract.Tweets.CONTENT_URI, tweetsValues); return null; }
Как видно из исходника, параметр screen_name передается через объект Request из DataDroid, который реализует интерфейс Parcelable и может быть передан через Intent. Напишем вспомогательный класс, который создавал бы Request-ы:
public final class RequestFactory { public static final int REQUEST_TWEETS = 1; public static Request getTweetsRequest(String screenName) { Request request = new Request(REQUEST_TWEETS); request.put("screen_name", screenName); return request; } private RequestFactory() { } }
и класс сервиса, родителем которого является RequestService из DataDroid, так что нам достаточно определить соответствие типов запросов и объектов Operation:
public class RestService extends RequestService { @Override public Operation getOperationForType(int requestType) { switch (requestType) { case RequestFactory.REQUEST_TWEETS: return new TweetsOperation(); default: return null; } }
Осталось определить синглотон типа RequestManager из DataDroid, в конструктор которому передать наш сервис, и модель готова.
Activity
Реализуем Activity. Для начала создадим layout со списком, правда, вместо ListView будем использовать PullToRefreshListView для обновления ленты скроллингом вниз.
Создаем SimpleCursorAdapter и подключаем к нему CursorLoader, который будет загружать данных из нашего ContentProvider-а:
private static final int LOADER_ID = 1; private static final String[] PROJECTION = { Tweets._ID, Tweets.USER_NAME, Tweets.BODY }; private LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(int loaderId, Bundle arg1) { return new CursorLoader( MainActivity.this, Tweets.CONTENT_URI, PROJECTION, null, null, null ); } @Override public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) { adapter.swapCursor(cursor); } @Override public void onLoaderReset(Loader<Cursor> arg0) { adapter.swapCursor(null); } }; protected void onCreate(Bundle savedInstanceState) { // ... adapter = new SimpleCursorAdapter(this, R.layout.tweet_view, null, new String[]{ Tweets.USER_NAME, Tweets.BODY }, new int[]{ R.id.user_name_text_view, R.id.body_text_view }, 0); listView.setAdapter(adapter); getSupportLoaderManager().initLoader(LOADER_ID, null, loaderCallbacks); // ... }
Теперь осталось добавить загрузку твитов. Для этого нужно при помощи RequestFactory создать объект запроса для загрузки твитов и запустить этот запрос при помощи RequestManager-а. Все это повесим на событие pull-down нашего списка. Вот код:
private RestRequestManager requestManager; // ... protected void onCreate(Bundle savedInstanceState) { // ... listView.setOnRefreshListener(new OnRefreshListener<ListView>() { @Override public void onRefresh(PullToRefreshBase<ListView> refreshView) { update(); } }); requestManager = RestRequestManager.from(this); } // ... void update() { listView.setRefreshing(); Request updateRequest = new Request(RequestFactory.REQUEST_TWEETS); updateRequest.put("screen_name", "habrahabr"); requestManager.execute(updateRequest, requestListener); } RequestListener requestListener = new RequestListener() { @Override public void onRequestFinished(Request request, Bundle resultData) { listView.onRefreshComplete(); } void showError() { listView.onRefreshComplete(); AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder. setTitle(android.R.string.dialog_alert_title). setMessage(getString(R.string.faled_to_load_data)). create(). show(); } @Override public void onRequestDataError(Request request) { showError(); } @Override public void onRequestCustomError(Request request, Bundle resultData) { showError(); } @Override public void onRequestConnectionError(Request request, int statusCode) { showError(); } };
Все, прописываем Service и ContentProvider в манифест и запускаем приложение:
Ссылки
- Исходники на github: github.com/therussianphysicist/rest_a
- Исходники из книги O’Reilly «Programming Android», реализующие паттерн B: github.com/bmeike/ProgrammingAndroidExamples (проекты FinchFramework и FinchVideo)
ссылка на оригинал статьи http://habrahabr.ru/post/176729/
Добавить комментарий