Написание простого приложения для работы с RESTful API под Android

в 11:17, , рубрики: android, java, rest, Разработка под android, метки: , ,

Работа с API различных порталов — одна из самых распространенных задач, возникающих при разработке под Android. Казалось бы, ничего сложного — асинхронно посылать HTTP-запросы и отображать ответы, но дьявол, как всегда, кроется в деталях.
Написание простого приложения для работы с RESTful API под Android
Основные антипаттерны:

  • Отправка запроса прямо из кода 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 и создадим простейшее приложение, использующее его для отображения постов некоторого пользователя из твиттера. Подробная схема паттерна:
Написание простого приложения для работы с RESTful API под Android

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 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 в манифест и запускаем приложение:
Написание простого приложения для работы с RESTful API под Android

Ссылки

Автор: therussianphysicist

Источник

Поделиться

* - обязательные к заполнению поля