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

от автора

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

Основные антипаттерны:

  • Отправка запроса прямо из кода 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 в манифест и запускаем приложение:

Ссылки

ссылка на оригинал статьи http://habrahabr.ru/post/176729/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *