Конечно, сейчас уже не 2010 год, когда разработчикам приходилось использовать знаменитые паттерны A/B/C или вообще запускать AsyncTask-и
Раньше в Android единственным доступным средством для выполнения сетевых запросов был клиент Apache, который на самом деле далек от идеала, и не зря сейчас Google усиленно старается избавиться от него в новых приложениях. Позже плодом стараний разработчиков Google стал класс HttpUrlConnection. Он ситуацию исправил не сильно. По-прежнему не хватало возможности выполнять асинхронные запросы, хотя модель HttpUrlConnection + Loaders уже является более-менее работоспособной.
2013 год стал в этом плане весьма эффективным. Появились замечательные библиотеки Volley и Retrofit. Volley — библиотека более общего плана, предназначенная для работы с сетью, в то время как Retrofit специально создана для работы с REST Api. И именно последняя библиотека стала общепризнанным стандартом при разработке клиент-серверных приложений.
У Retrofit, по сравнению с другими средствами, можно выделить несколько основных преимуществ:
1) Крайне удобный и простой интерфейс, который предоставляет полный функционал для выполнения любых запросов;
2) Гибкая настройка — можно использовать любой клиент для выполнения запроса, любую библиотеку для разбора json и т.д.;
3) Отсутствие необходимости самостоятельно выполнять парсинг json-а — эту работу выполняет библиотека Gson (и уже не только Gson);
4) Удобная обработка результата и ошибок;
5) Поддержка Rx, что тоже является немаловажным фактором сегодня.
Если вы еще не знакомы с библиотекой Retrofit, самое время изучить ее. Но я в любом случае сделаю небольшое введение, а заодно мы немного рассмотрим новые возможности версии 2.0.0 (советую также посмотреть презентацию по Retrofit 2.0.0).
В качестве примера я выбрал API для аэропортов за его максимальную простоту. И мы решаем самую банальную задачу — получение списка ближайших аэропортов.
В первую очередь нам нужно подключить все выбранные библиотеки и требуемые зависимости для Retrofit:
compile 'com.squareup.retrofit:retrofit:2.0.0-beta1'
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1'
compile 'com.squareup.okhttp:okhttp:2.0.0'
Мы будем получать аэропорты в виде списка объектов определенного класса.
public class Airport {
@SerializedName("iata")
private String mIata;
@SerializedName("name")
private String mName;
@SerializedName("airport_name")
private String mAirportName;
public Airport() {
}
}
Создаем сервис для запросов:
public interface AirportsService {
@GET("/places/coords_to_places_ru.json")
Call<List<Airport>> airports(@Query("coords") String gps);
}
Примечание про Retrofit 2.0.0
Раньше для выполнения синхронных и асинхронных запросов мы должны были писать разные методы. Теперь при попытке создать сервис, который содержит void метод, вы получите ошибку. В Retrofit 2.0.0 интерфейс Call инкапсулирует запросы и позволяет выполнять их синхронно или асинхронно.
public interface AirportsService {
@GET("/places/coords_to_places_ru.json")
List<Airport> airports(@Query("coords") String gps);
@GET("/places/coords_to_places_ru.json")
void airports(@Query("coords") String gps, Callback<List<Airport>> callback);
}
AirportsService service = ApiFactory.getAirportsService();
Call<List<Airport>> call = service.airports("55.749792,37.6324949");
//sync request
call.execute();
//async request
Callback<List<Airport>> callback = new RetrofitCallback<List<Airport>>() {
@Override
public void onResponse(Response<List<Airport>> response) {
super.onResponse(response);
}
};
call.enqueue(callback);
Теперь создадим вспомогательные методы:
public class ApiFactory {
private static final int CONNECT_TIMEOUT = 15;
private static final int WRITE_TIMEOUT = 60;
private static final int TIMEOUT = 60;
private static final OkHttpClient CLIENT = new OkHttpClient();
static {
CLIENT.setConnectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS);
CLIENT.setWriteTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS);
CLIENT.setReadTimeout(TIMEOUT, TimeUnit.SECONDS);
}
@NonNull
public static AirportsService getAirportsService() {
return getRetrofit().create(AirportsService.class);
}
@NonNull
private static Retrofit getRetrofit() {
return new Retrofit.Builder()
.baseUrl(BuildConfig.API_ENDPOINT)
.addConverterFactory(GsonConverterFactory.create())
.client(CLIENT)
.build();
}
}
Отлично! Подготовка завершена, и теперь мы можем выполнить запрос:
public class MainActivity extends AppCompatActivity implements Callback<List<Airport>> {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AirportsService service = ApiFactory.getAirportsService();
Call<List<Airport>> call = service.airports("55.749792,37.6324949");
call.enqueue(this);
}
@Override
public void onResponse(Response<List<Airport>> response) {
if (response.isSuccess()) {
List<Airport> airports = response.body();
//do something here
}
}
@Override
public void onFailure(Throwable t) {
}
}
Все кажется очень простым. Мы без особых усилий создали нужные классы, и уже можем делать запросы, получать результат и обрабатывать ошибки, и все это буквально за 10 минут. Что же еще нужно?
Однако такой подход является в корне неверным. Что будет, если во время выполнения запроса пользователь повернет устройство или вообще закроет приложение? С уверенностью можно сказать только то, что нужный результат вам не гарантирован, и мы недалеко ушли от первоначальных проблем. Да и запросы в активити и фрагментах никак не добавляют красоты вашему коду. Поэтому пора, наконец, вернуться к основной теме статьи — построение архитектуры клиент-серверного приложения.
В данной ситуации у нас есть несколько вариантов. Можно воспользоваться любой библиотекой, которая обеспечивает грамотную работу с многопоточностью. Здесь идеально подходит фреймворк Rx, тем более что Retrofit его поддерживает. Однако построить архитектуру с Rx или даже просто использовать функциональное реактивное программирование — это нетривиальные задачи. Мы пойдем по более простому пути: воспользуемся средствами, которые предлагает нам Android из коробки. А именно, лоадерами.
Лоадеры появились в версии API 11 и до сих пор остаются очень мощным средством для параллельного выполнения запросов. Конечно, в лоадерах можно делать вообще что угодно, но обычно их используют либо для чтения данных с базы, либо для выполнения сетевых запросов. И самое важное преимущество лоадеров — через класс LoaderManager они связаны с жизненным циклом Activity и Fragment. Это позволяет использовать их без опасения, что данные будут утрачены при закрытии приложения или результат вернется не в тот коллбэк.
Обычно модель работы с лоадерами подразумевает следующие шаги:
1) Выполняем запрос и получаем результат;
2) Каким-то образом кэшируем результат (чаще всего в базе данных);
3) Возвращаем результат в Activity или Fragment.
Примечание
Такая модель хороша тем, что Activity или Fragment не думают, как именно получаются данные. Например, с сервера может вернуться ошибка, но при этом лоадер вернет закэшированные данные.
Давайте реализуем такую модель. Я опускаю подробности того, как реализована работа с базой данных, при необходимости вы можете посмотреть пример на Github (ссылка в конце статьи). Здесь тоже возможно множество вариаций, и я буду рассматривать их по очереди, все их преимущества и недостатки, пока, наконец, не дойду до модели, которую считаю оптимальной.
Примечание
Все лоадеры должны работать с универсальным типом данных, чтобы можно было использовать интерфейс LoaderCallbacks в одной активити или фрагменте для разных типов загружаемых данных. Первым таким типом, который приходит на ум, является Cursor.
Еще одно примечание
Все модели, связанные с лоадерами, имеют небольшой недостаток: для каждого запроса нужен отдельный лоадер. А это значит, что при изменении архитектуры или, например, переходе на другую базу данных, мы столкнемся с большим рефакторингом, что не слишком хорошо. Чтобы максимально обойти эту проблему, я буду использовать базовый класс для всех лоадеров и именно в нем хранить всю возможную общую логику.
Loader + ContentProvider + асинхронные запросы
Предусловия: есть классы для работы с базой данных SQLite через ContentProvider, есть возможность сохранять сущности в эту базу.
В контексте данной модели крайне сложно вынести какую-то общую логику в базовый класс, поэтому в данном случае это всего лишь лоадер, от которого удобно наследоваться для выполнения асинхронных запросов. Его содержание не относится непосредственно к рассматриваемой архитектуре, поэтому он в спойлере. Однако вы также можете использовать его в своих приложениях:
public class BaseLoader extends Loader<Cursor> {
private Cursor mCursor;
public BaseLoader(Context context) {
super(context);
}
@Override
public void deliverResult(Cursor cursor) {
if (isReset()) {
if (cursor != null) {
cursor.close();
}
return;
}
Cursor oldCursor = mCursor;
mCursor = cursor;
if (isStarted()) {
super.deliverResult(cursor);
}
if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
oldCursor.close();
}
}
@Override
protected void onStartLoading() {
if (mCursor != null) {
deliverResult(mCursor);
} else {
forceLoad();
}
}
@Override
protected void onReset() {
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
mCursor = null;
}
}
Тогда лоадер для загрузки аэропортов может выглядеть следующим образом:
public class AirportsLoader extends BaseLoader {
private final String mGps;
private final AirportsService mAirportsService;
public AirportsLoader(Context context, String gps) {
super(context);
mGps = gps;
mAirportsService = ApiFactory.getAirportsService();
}
@Override
protected void onForceLoad() {
Call<List<Airport>> call = mAirportsService.airports(mGps);
call.enqueue(new RetrofitCallback<List<Airport>>() {
@Override
public void onResponse(Response<List<Airport>> response) {
if (response.isSuccess()) {
AirportsTable.clear(getContext());
AirportsTable.save(getContext(), response.body());
Cursor cursor = getContext().getContentResolver().query(AirportsTable.URI,
null, null, null, null);
deliverResult(cursor);
} else {
deliverResult(null);
}
}
});
}
}
И теперь мы наконец можем использовать его в UI классах:
public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getLoaderManager().initLoader(R.id.airports_loader, Bundle.EMPTY, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch (id) {
case R.id.airports_loader:
return new AirportsLoader(this, "55.749792,37.6324949");
default:
return null;
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
int id = loader.getId();
if (id == R.id.airports_loader) {
if (data != null && data.moveToFirst()) {
List<Airport> airports = AirportsTable.listFromCursor(data);
//do something here
}
}
getLoaderManager().destroyLoader(id);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
}
Как видно, здесь нет ничего сложного. Это абсолютно стандартная работа с лоадерами. На мой взгляд, лоадеры предоставляют идеальный уровень абстракции. Мы загружаем нужные данные, но без лишних знаний о том, как именно они загружаются.
Эта модель стабильная, достаточно удобная для использования, но все же имеет недостатки:
1) Каждый новый лоадер содержит свою логику для работы с результатом. Этот недостаток можно исправить, и частично мы сделаем это в следующей модели и полностью — в последней.
2) Второй недостаток намного серьезнее: все операции с базой данных выполняются в главном потоке приложения, а это может приводить к различным негативным последствиям, даже до остановки приложения при очень большом количестве сохраняемых данных. Да и в конце концов, мы же используем лоадеры. Давайте делать все асинхронно!
Loader + ContentProvider + синхронные запросы
Спрашивается, зачем мы выполняли запрос асинхронно с помощью Retrofit-а, когда лоадеры и так позволяют нам работать в background? Исправим это.
Эта модель упрощенная, но основное отличие заключается в том, что асинхронность запроса достигается за счет лоадеров, и работа с базой уже происходит не в основном потоке. Наследники базового класса должны лишь вернуть нам объект типа Cursor. Теперь базовый класс может выглядеть следующим образом:
public abstract class BaseLoader extends AsyncTaskLoader<Cursor> {
public BaseLoader(Context context) {
super(context);
}
@Override
protected void onStartLoading() {
super.onStartLoading();
forceLoad();
}
@Override
public Cursor loadInBackground() {
try {
return apiCall();
} catch (IOException e) {
return null;
}
}
protected abstract Cursor apiCall() throws IOException;
}
И тогда реализация абстрактного метода может выглядеть следующим образом:
@Override
protected Cursor apiCall() throws IOException {
AirportsService service = ApiFactory.getAirportsService();
Call<List<Airport>> call = service.airports(mGps);
List<Airport> airports = call.execute().body();
AirportsTable.save(getContext(), airports);
return getContext().getContentResolver().query(AirportsTable.URI, null, null, null, null);
}
Работа с лоадером в UI у нас никак не изменилась.
По факту, эта модель является модификацией предыдущей, она частично устраняет ее недостатки. Но на мой взгляд, этого все равно недостаточно. Тут можно снова выделить недостатки:
1) В каждом лоадере присутствует индивидуальная логика сохранения данных.
2) Возможна работа только с базой данных SQLite.
И наконец, давайте полностью устраним эти недостатки и получим универсальную и почти идеальную модель!
Loader + любое хранилище данных + синхронные запросы
Перед рассмотрением конкретных моделей я говорил о том, что для лоадеров мы должны использовать единый тип данных. Кроме Cursor ничего на ум не приходит. Так давайте создадим такой тип! Что должно в нем быть? Естественно, он не должен быть generic-типом (иначе мы не сможем использовать коллбэки лоадера для разных типов данных в одной активити / фрагменте), но в то же время он должен быть контейнером для объекта любого типа. И вот здесь я вижу единственное слабое место в этой модели — мы должны использовать тип Object и выполнять unchecked преобразования. Но все же, это не столь существенный минус. Итоговая версия данного типа выглядит следующим образом:
public class Response {
@Nullable private Object mAnswer;
private RequestResult mRequestResult;
public Response() {
mRequestResult = RequestResult.ERROR;
}
@NonNull
public RequestResult getRequestResult() {
return mRequestResult;
}
public Response setRequestResult(RequestResult requestResult) {
mRequestResult = requestResult;
return this;
}
@Nullable
public <T> T getTypedAnswer() {
if (mAnswer == null) {
return null;
}
//noinspection unchecked
return (T) mAnswer;
}
public Response setAnswer(@Nullable Object answer) {
mAnswer = answer;
return this;
}
public void save(Context context) {
}
}
Данный тип может хранить результат выполнения запроса. Если мы хотим что-то делать для конкретного запроса, нужно унаследоваться от этого класса и переопределить / добавить нужные методы. Например, так:
public class AirportsResponse extends Response {
@Override
public void save(Context context) {
List<Airport> airports = getTypedAnswer();
if (airports != null) {
AirportsTable.save(context, airports);
}
}
}
Отлично! Теперь напишем базовый класс для лоадеров:
public abstract class BaseLoader extends AsyncTaskLoader<Response> {
public BaseLoader(Context context) {
super(context);
}
@Override
protected void onStartLoading() {
super.onStartLoading();
forceLoad();
}
@Override
public Response loadInBackground() {
try {
Response response = apiCall();
if (response.getRequestResult() == RequestResult.SUCCESS) {
response.save(getContext());
onSuccess();
} else {
onError();
}
return response;
} catch (IOException e) {
onError();
return new Response();
}
}
protected void onSuccess() {
}
protected void onError() {
}
protected abstract Response apiCall() throws IOException;
}
Этот класс лоадера является конечной целью данной статьи и, на мой взгляд, отличной, работоспособной и расширяемой моделью. Хотите перейти с SQLite, например, на Realm? Не проблема. Рассмотрим это в качестве следующего примера. Классы лоадеров не изменятся, изменится только модель, которую вы бы в любом случае редактировали. Не удалось выполнить запрос? Не проблема, доработайте в наследнике метод apiCall. Хотите очистить базу данных при ошибке? Переопределите onError и работайте — этот метод выполняется в фоновом потоке.
А любой конкретный лоадер можно представить следующим образом (опять-таки, покажу только реализацию абстрактного метода):
@Override
protected Response apiCall() throws IOException {
AirportsService service = ApiFactory.getAirportsService();
Call<List<Airport>> call = service.airports(mGps);
List<Airport> airports = call.execute().body();
return new AirportsResponse()
.setRequestResult(RequestResult.SUCCESS)
.setAnswer(airports);
}
Примечание
При неудачно выполненном запросе будет выброшен Exception, и мы попадем в catch-ветку базового лоадера.
В итоге мы получили следующие результаты:
1) Каждый лоадер зависит исключительно от своего запроса (от параметров и результата), но при этом он не знает, что он делает с полученными данными. То есть он будет меняться только при изменении параметров конкретного запроса.
2) Базовый лоадер управляет всей логикой выполнения запросов и работы с результатами.
3) Более того, сами классы модели тоже не имеют понятия о том, как устроена работа с базой данных и прочее. Все это вынесено в отдельные классы / методы. Я этого нигде не указывал явно, но это можно посмотреть в примере на Github — ссылка в конце статьи.
Вместо заключения
Чуть выше я обещал показать еще один пример — переход с SQLite на Realm — и убедиться, что мы действительно не затронем лоадеры. Давайте сделаем это. На самом деле, кода здесь совсем чуть-чуть, ведь работа с базой у нас сейчас выполняется лишь в одном методе (я не учитываю изменения, связанные со спецификой Realm, а они есть, в частности, правила именования полей и работа с Gson; их можно посмотреть на Github).
Подключим Realm:
compile 'io.realm:realm-android:0.82.1'
И изменим метод save в AirportsResponse:
public class AirportsResponse extends Response {
@Override
public void save(Context context) {
List<Airport> airports = getTypedAnswer();
if (airports != null) {
AirportsHelper.save(Realm.getInstance(context), airports);
}
}
}
public class AirportsHelper {
public static void save(@NonNull Realm realm, List<Airport> airports) {
realm.beginTransaction();
realm.clear(Airport.class);
realm.copyToRealm(airports);
realm.commitTransaction();
}
@NonNull
public static List<Airport> getAirports(@NonNull Realm realm) {
return realm.allObjects(Airport.class);
}
}
Вот и все! Мы элементарным образом, не затрагивая классы, которые содержат другую логику, изменили способ хранения данных.
Все-таки заключение
Хочу выделить один достаточно важный момент: мы не рассмотрели вопросы, связанные с использованием закэшированных данных, то есть при отстуствии интернета. Однако стратегия использования закэшированных данных в каждом приложении индивидуальна, и навязывать какой-то определенный подход я не считаю правильным.
В итоге мы рассмотрели основные вопросы организации архитектуры клиент-серверных приложений, и я надеюсь, что эта статья помогла вам узнать что-то новое и что вы будете использовать какую-либо из перечисленных моделей в своих проектах. Кроме того, если у вас есть свои идеи, как можно организовать такую архитектуру, — пишите, я буду рад обсудить.
Спасибо, что дочитали до конца. Удачной разработки!
P.S. Обещанная ссылка на код на GitHub.
Комментарии (37)
Valle
31.08.2015 18:24Я так и не понял, зачем нужно каждый ответ писать в базу только для того чтоб сразу из нее прочитать, а при ошибке вернуть null.
Arturka
31.08.2015 18:36+1В лоадере мы только загружаем и сохраняем данные. Если они нам требуются дальше, то, разумеется, мы не ходим каждый раз на сервер, а берем из базы. Например, мы можем на сплеше один раз загрузить все данные в лоадерах и сохранить их в базу (при этом можно вообще всегда возвращать null — это будет лишь означать то, что загрузка данного лоадера завершена, и можно загружать следующий — при последовательных запросах), а после работать только локально.
Если кратко — то лоадеры отдают нам данные один раз и долго, а из базы мы можем их вытащить всегда и сразу.Valle
31.08.2015 19:56Это понятно, но имхо время жизни объектов, равно как и правила их кеширования должны задаваться в HTTP, и, следовательно, обрабатываться на том же уровне. Если ответ сервера еще не протух, то сетевая библиотека сразу же должна возвратить закешированный результат. Конечно, в некоторых случаях оправдано хранить модель данных локально, если есть жесткие требования к работе оффлайн, но это далеко не самый обычный http клиент.
Arturka
31.08.2015 20:17+1Я склонен считать, что всегда есть смысл хранить данные локально. Что будет, если у вас сервер вдруг отвалится? Никакое кэширование в HTTP уже не поможет. И приложение несколько даже не будет открываться.
Все равно не понимаю вашу нелюбовь к локальной базе :) Занимает не так много места, а дает возможность постоянно быстро получать данные из разных мест без всяких запросов и прочем. Не вижу недостатков в таком подходе, а преимущества, как мне кажется, очевидны.Valle
31.08.2015 21:00Нет, никакой нелюбви, просто в приведенной схеме БД только замедляет сетевой стек. А если сервер вдруг отвалится, то все закешированные ответы будут лежать в кеше и отдаваться мгновенно без участия БД.
Arturka
31.08.2015 21:12+1Честно скажу, что не работал с кэшированием в http, так что дальше могу ошибаться. Насколько я понимаю, кэширование тоже выполняется локально в файлах или как-то еще. Скорость, конечно, повыше чем из бд, но не настолько, чтобы это было явным преимуществом. Но если мы говорим о структурированных данных разного типа из разных запросов, то без бд тут не обойтись. Да и когда у нас в приложении 50 разных запросов, использовать кэш для всех тоже вряд ли разумно. Кроме того, может случиться так, что кэш очистится (если закрыл приложение и не использовал его какое-то время), запустил приложение, а сервер лежит. С бд не попадем в такую ситуацию.
Впрочем да, я с вам согласен частично, в каких-то приложениях достаточно будет такого кэширование, но все же такая архитектура менее расширяема, и при росте приложения ее будет сложно поддерживать.
dzigoro
31.08.2015 18:56+1Потому что а) источник данных должен быть один б) активити может быть прибита в любой момент.
Поэтому в ситуации создание активити — запрос — прибитие активити такой подход позволяет все-таки сохранить данные, и на следующем запуске их показать.
Плюс, от запроса не всегда приходят просто данные для показа. Часто это данные бизнес-логики(данные для авторизации н-р).Valle
31.08.2015 19:58Я могу ошибаться, но в статье вроде об этом не сказано и не приведен код который так может работать, так что идет нарушение принципа yagni.
Arturka
31.08.2015 20:14+1Не знаю, правильно ли вас понял, но пример для б) я приводил в самом начале, с запросом в Activity. Там запрос выполнится, но при преждевременной смерти Activity, результат просто потеряется.
По сути база данных нужна для нескольких кейсов:
1) Даже если мы всегда ходим за данными на бэкенд, то все равно в один прекрасный момент интернет может пропасть, и нам придется работать локально.
2) Мы работаем с какой-то сложной структурой данных, и нужно использовать данные от предыдущих запросов (даже из апи примера в статье, получили список аэропортов, потом нужно получить список популярных направлений с учетом этих аэропортов [может, бред говорю, не изучал это апи]. Как их хранить? Напрямую в памяти как-то не очень хорошо. Кэширование запроса тоже не подходит. Вот и используем БД.)Valle
31.08.2015 21:07+1Хранить напрямую в памяти часто это очень хорошо, т.к. очень быстро, а данных обычно не десятки мегабайт. Если процесс прибивается, то все равно нужно все перезагружать путем дерганья сетевых api, не так ли? А где кешировать полученные данные — в сетевой библиотеке или в БД это архитектурный вопрос, который зависит от того, необходима ли гарантированная работа в оффлайн режиме с синхронизацией изменений в каком-то другом сервисе. Если синхронизация происходит прозрачно, то и дергать сеть ручками не нужно, а если в ручную, то и БД особо не нужна наверное. Все зависит от типа приложения, конечно.
Arturka
31.08.2015 21:18В общем, да, я с вами согласен. БДшки на самом деле потихоньку могут становится архаизмом в данных случаях, учитывая постоянно растущую мощность устройств, сейчас уже не страшно хранить все в памяти или в кэше.
Из плюсов все-таки можно считать, что бд обеспечивает большую стабильность.dev_troy
31.08.2015 21:24Ой, ну ладно. Прям уж архаизм? Имхо, гораздо удобнее хранить сложные структуры в БД, нежели в Map<String, Map<String, Map<String… >>>>>>>>>> =) К тому же, :memory: хранилище в БД никто не отменял. Тот же SQLite прекрасно может в память!
Valle
31.08.2015 21:28Зачем же такие страсти с мапами? Ничего удобнее и быстрее чем доступ к POJO нет.
Arturka
31.08.2015 22:11Мы вообще переходим на такой подход, что в БД храним чистый json, который приходит с сервера (то есть один столбец строковый в бд), а при создании объекта получаем эту строку и через Gson конвертируем. Получается вот такая немного странная сериализация. Намного удобнее, на самом деле. В этом случае нельзя сказать, что БД нам для чего-то необходима.
P.S. Упс, не совсем в ту ветку добавил коммент)dev_troy
31.08.2015 22:54Тоже подход, но это уже крайний случай, если уж совсем никак. Хотя, скоро будет праздник для любителей хранить JSON: www.sqlite.org/src/timeline?r=json
dev_troy
31.08.2015 22:51Да, без сомнения POJO лучше, но сути особо не меняет, особенно, если нужны операции фильтрации, сортировки и прочие прелести (а они нужны в большинстве случаев).
belozerow
31.08.2015 19:14Вопрос вне данной архитектуры, но раз уж вы используете realm.io, то как вы сообщаете UI об изменениях в базе? Eventbus?
Всем хорош realm.io, но этот вопрос не дает мне покоя.
Про RX тоже очень интересно было бы почитать.Arturka
31.08.2015 19:33Да, с реалмом вопрос хороший, и в принципе любой bus с этим может справиться, хотя они не добавляют плюсов к карме в архитектуру. Обычно в сочетании с Realm мы используем Rx, и уже соответственно, средства Rx для таких оповещений.
Я не то чтобы сильный фанат реалма на самом деле, его использовал лишь для примера, что можно легко перейти к другой БД.
Хорошо, я всерьез подумаю о том, чтобы в ближайший месяц максимально разобраться с Rx и что-то такое написать :) Хотя не исключено, что это сделает еще кто-нибудь :)belozerow
31.08.2015 19:36А что вы используете в качестве БД в своих проектах?
Голый SQLite это ад. Всякие annotation фрэимворки упрощают конечно это дело, но с реалмом ни в какое сравнение не идут.Arturka
31.08.2015 19:40У нас в легионе внутренняя библиотека для SQLite, которая этот ад сильно уменьшает. Ну и иногда используем Realm, недавно начали.
andrey7mel
01.09.2015 00:04Спасибо за развернутый обзор!
Подскажите пожалуйста, при использовании Rx, как решаете проблему кеширования и оффлайн работы? Например отображать сначала сохраненные данные, проверить необходимость обновления, запустить загрузку новых данных и отобразить.Arturka
01.09.2015 08:33Например отображать сначала сохраненные данные, проверить необходимость обновления, запустить загрузку новых данных и отобразить.
Такой подход можно использовать независимо от того, что вы используете для получения данных с бэкенда. Здесь важно лишь то, является ли критичным показ актуальных данных, или можно сначала показать старые. У нас общая политика обычно такая — всегда сначала ходим на бэкенд, а уже потом достаем из кэша, в случае ошибки. Обычно стараемся подгружать данные за экран-два до того, как они понадобятся, так что такой подход тоже неплох.
Схема с Rx примерно такая (могу немного ошибаться, так как с Rx-проектами не работал особо) — получаем Observable, потом в flatMap сохраняем данные (или повторяем запрос, если ошибка) и прокидываем Observable в UI.
ruslanys
01.09.2015 01:37-2запускать AsyncTask-и и сильно бить в бубен
Я последний раз работал с Android API 4.0.2, но бубнов не помю.
Раньше в Android единственным доступным средством для выполнения сетевых запросов был клиент Apache, который на самом деле далек от идеала, и не зря сейчас Google усиленно старается избавиться от него в новых приложениях.
HTTP Apache Client — прекрасная библиотека, используемая в куче Java проектов. В энтерпрайзе полно.
Позже плодом стараний разработчиков Google стал класс HttpUrlConnection.
Разве HttpUrlConnection не часть Java API? (JavaDoc).
Он ситуацию исправил не сильно. По-прежнему не хватало возможности выполнять асинхронные запросы, хотя модель HttpUrlConnection + Loaders уже является более-менее работоспособной.
А как связана многопоточность(асинхронность) и работа с сетью (HttpUrlConnection)?
А Retrofit библиотека хорошая.Arturka
01.09.2015 08:27Я последний раз работал с Android API 4.0.2, но бубнов не помю.
AsyncTask-и никак не связаны с жизненным циклом Activity / Fragment. Со всеми вытекающими проблемами при уничтожении Activity / закрытии приложения.
HTTP Apache Client — прекрасная библиотека, используемая в куче Java проектов. В энтерпрайзе полно.
Библиотека отличная, без сомнений. Проблема в том, что в Android SDK включена не сама библиотека как зависимость, а ее самая начальная beta-версия. Сейчас в API 23 те, кто хочет продолжать использовать Apache, уже подключают саму библиотеку.
Разве HttpUrlConnection не часть Java API? (JavaDoc).
Да, здесь я погрешил против истины. В свое оправдание могу сказать, что Google все равно переработал этот класс.
А как связана многопоточность(асинхронность) и работа с сетью (HttpUrlConnection)?
Если рассматривать их отдельно, то никак. А если в контексте разработки приложения, то связь самая прямая. Не очень-то возможно работать с сетью без асинхронности / многопоточности.
forgotten
01.09.2015 09:23compile 'com.squareup.retrofit:retrofit:2.0.0-beta1' compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1' compile 'com.squareup.okhttp:okhttp:2.0.0' public class Airport { @SerializedName("iata") private String mIata; @SerializedName("name") private String mName; @SerializedName("airport_name") private String mAirportName; public Airport() { } } public interface AirportsService { @GET("/places/coords_to_places_ru.json") Call<List<Airport>> airports(@Query("coords") String gps); } public class ApiFactory { private static final int CONNECT_TIMEOUT = 15; private static final int WRITE_TIMEOUT = 60; private static final int TIMEOUT = 60; private static final OkHttpClient CLIENT = new OkHttpClient(); static { CLIENT.setConnectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS); CLIENT.setWriteTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS); CLIENT.setReadTimeout(TIMEOUT, TimeUnit.SECONDS); } @NonNull public static AirportsService getAirportsService() { return getRetrofit().create(AirportsService.class); } @NonNull private static Retrofit getRetrofit() { return new Retrofit.Builder() .baseUrl(BuildConfig.API_ENDPOINT) .addConverterFactory(GsonConverterFactory.create()) .client(CLIENT) .build(); } } public class MainActivity extends AppCompatActivity implements Callback<List<Airport>> { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); AirportsService service = ApiFactory.getAirportsService(); Call<List<Airport>> call = service.airports("55.749792,37.6324949"); call.enqueue(this); } @Override public void onResponse(Response<List<Airport>> response) { if (response.isSuccess()) { List<Airport> airports = response.body(); //do something here } } @Override public void onFailure(Throwable t) { } }
> Все кажется очень простым.
То есть получение списка объектов вида «три строки» HTTP-запросом в три экрана кода — это просто и хорошо, что, наконец, появились удобные библиотеки???forgotten
01.09.2015 09:29Так, для сравнения, на некоторых языках этот код выглядит вот так:
fetch('/places/coords_to_places_ru.json').then( function (response) { return response.json(); }, console.error.bind(console) );
psa
01.09.2015 11:06Осталось распарсить json и преобразовать в объект
forgotten
01.09.2015 12:14response.json() именно это и делает.
psa
01.09.2015 15:04Да, верно, но он вернет JSON object, что не совсем похоже на:
List<Airport> airports = response.body();
Да и какой смысл сравнивать кусок javascript'a и часть архитектурного слоя android приложения, который наверняка писался не для того чтобы просто послать HTTP-запрос.
Arturka
01.09.2015 09:57Вот запрос.
@GET("/places/coords_to_places_ru.json") Call<List<Airport>> airports(@Query("coords") String gps);
MainActivity — это UI часть.
ApiFactory — общий класс для всех запросов. Больше он не трогается.
Зависимости считать кодом тоже как-то странно.
Можете как-нибудь сравнить это с кодом, который писался раньше плюс ручной парсинг json-а.Valle
01.09.2015 17:52Справедливости ради в том коде фабрику можно схлопнуть в несколько раз без потери функциональности, и почти в одну строку кода, если таймауты дефолтные подойдут.
senneco
01.09.2015 23:27+2Мало того, решение можно сделать ещё более «простым», подпилив gson, чтоб не нужно было писать аннотации в модели =) Gson умеет «сам» убирать префикс m и переводить CamelCase к lower_case_with_underscores и обратно(когда переводим объект в json):
new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE) .setFieldNamingStrategy(new FieldNamingStrategy { public String translateName(Field field) { String name = FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES.translateName(field); name = name.substring(2, name.length()).toLowerCase(); return name; } }) .create();
atetc
05.09.2015 11:57Кстати в чате Android-разработчиков появился новый канал посвященный обсуждению паттернов и архитектуры приложений gitter.im/rus-speaking/android-patterns
HotIceCream
А жаль, про то как организовывать работу с RxJava было бы интересно почитать. Причем, интересует именно вопрос хранения данных при повороте экрана. В loader'ах об это думает система, а в RxAndroid обычно советуют cache() + какое-нибудь статическое хранилище для обсерверов. А как вы храните их?)
Arturka
Я допускаю, что и об архитектуре с Rx могу написать, но не обещаю :)
Но на самом деле, мы в легионе используем для Rx похожую модель. Такое же использование лоадеров, чтобы система заботилась + всякие фишки и операторы от Rx, довольно неплохо получается. Вот только мне она не очень нравится (сильно больше классов выходит), а ничего получше я пока не придумал. Вот если появятся хорошие идеи, то можно и статью написать.
ptiss
В последнее время обкатываю связку Rx + Retrofit-like транспорт слой (возвращает Observable) + AndroidViewModel (MVVM библиотека с VM, переживающей повороты и пр., на похожей идее основана Chronos от Роботов). Пока полёт нормальный и таких простыней нет.