Как стать автором
Обновить

Комментарии 36

Мы пойдем по более простому пути...

А жаль, про то как организовывать работу с RxJava было бы интересно почитать. Причем, интересует именно вопрос хранения данных при повороте экрана. В loader'ах об это думает система, а в RxAndroid обычно советуют cache() + какое-нибудь статическое хранилище для обсерверов. А как вы храните их?)
Я допускаю, что и об архитектуре с Rx могу написать, но не обещаю :)
Но на самом деле, мы в легионе используем для Rx похожую модель. Такое же использование лоадеров, чтобы система заботилась + всякие фишки и операторы от Rx, довольно неплохо получается. Вот только мне она не очень нравится (сильно больше классов выходит), а ничего получше я пока не придумал. Вот если появятся хорошие идеи, то можно и статью написать.
В последнее время обкатываю связку Rx + Retrofit-like транспорт слой (возвращает Observable) + AndroidViewModel (MVVM библиотека с VM, переживающей повороты и пр., на похожей идее основана Chronos от Роботов). Пока полёт нормальный и таких простыней нет.
Речь идет об архитектуре, а в статье нет ни одной диаграммы
Я так и не понял, зачем нужно каждый ответ писать в базу только для того чтоб сразу из нее прочитать, а при ошибке вернуть null.
В лоадере мы только загружаем и сохраняем данные. Если они нам требуются дальше, то, разумеется, мы не ходим каждый раз на сервер, а берем из базы. Например, мы можем на сплеше один раз загрузить все данные в лоадерах и сохранить их в базу (при этом можно вообще всегда возвращать null — это будет лишь означать то, что загрузка данного лоадера завершена, и можно загружать следующий — при последовательных запросах), а после работать только локально.
Если кратко — то лоадеры отдают нам данные один раз и долго, а из базы мы можем их вытащить всегда и сразу.
Это понятно, но имхо время жизни объектов, равно как и правила их кеширования должны задаваться в HTTP, и, следовательно, обрабатываться на том же уровне. Если ответ сервера еще не протух, то сетевая библиотека сразу же должна возвратить закешированный результат. Конечно, в некоторых случаях оправдано хранить модель данных локально, если есть жесткие требования к работе оффлайн, но это далеко не самый обычный http клиент.
Я склонен считать, что всегда есть смысл хранить данные локально. Что будет, если у вас сервер вдруг отвалится? Никакое кэширование в HTTP уже не поможет. И приложение несколько даже не будет открываться.
Все равно не понимаю вашу нелюбовь к локальной базе :) Занимает не так много места, а дает возможность постоянно быстро получать данные из разных мест без всяких запросов и прочем. Не вижу недостатков в таком подходе, а преимущества, как мне кажется, очевидны.
Нет, никакой нелюбви, просто в приведенной схеме БД только замедляет сетевой стек. А если сервер вдруг отвалится, то все закешированные ответы будут лежать в кеше и отдаваться мгновенно без участия БД.
Честно скажу, что не работал с кэшированием в http, так что дальше могу ошибаться. Насколько я понимаю, кэширование тоже выполняется локально в файлах или как-то еще. Скорость, конечно, повыше чем из бд, но не настолько, чтобы это было явным преимуществом. Но если мы говорим о структурированных данных разного типа из разных запросов, то без бд тут не обойтись. Да и когда у нас в приложении 50 разных запросов, использовать кэш для всех тоже вряд ли разумно. Кроме того, может случиться так, что кэш очистится (если закрыл приложение и не использовал его какое-то время), запустил приложение, а сервер лежит. С бд не попадем в такую ситуацию.
Впрочем да, я с вам согласен частично, в каких-то приложениях достаточно будет такого кэширование, но все же такая архитектура менее расширяема, и при росте приложения ее будет сложно поддерживать.
Потому что а) источник данных должен быть один б) активити может быть прибита в любой момент.

Поэтому в ситуации создание активити — запрос — прибитие активити такой подход позволяет все-таки сохранить данные, и на следующем запуске их показать.

Плюс, от запроса не всегда приходят просто данные для показа. Часто это данные бизнес-логики(данные для авторизации н-р).
Я могу ошибаться, но в статье вроде об этом не сказано и не приведен код который так может работать, так что идет нарушение принципа yagni.
Не знаю, правильно ли вас понял, но пример для б) я приводил в самом начале, с запросом в Activity. Там запрос выполнится, но при преждевременной смерти Activity, результат просто потеряется.
По сути база данных нужна для нескольких кейсов:
1) Даже если мы всегда ходим за данными на бэкенд, то все равно в один прекрасный момент интернет может пропасть, и нам придется работать локально.
2) Мы работаем с какой-то сложной структурой данных, и нужно использовать данные от предыдущих запросов (даже из апи примера в статье, получили список аэропортов, потом нужно получить список популярных направлений с учетом этих аэропортов [может, бред говорю, не изучал это апи]. Как их хранить? Напрямую в памяти как-то не очень хорошо. Кэширование запроса тоже не подходит. Вот и используем БД.)
Хранить напрямую в памяти часто это очень хорошо, т.к. очень быстро, а данных обычно не десятки мегабайт. Если процесс прибивается, то все равно нужно все перезагружать путем дерганья сетевых api, не так ли? А где кешировать полученные данные — в сетевой библиотеке или в БД это архитектурный вопрос, который зависит от того, необходима ли гарантированная работа в оффлайн режиме с синхронизацией изменений в каком-то другом сервисе. Если синхронизация происходит прозрачно, то и дергать сеть ручками не нужно, а если в ручную, то и БД особо не нужна наверное. Все зависит от типа приложения, конечно.
В общем, да, я с вами согласен. БДшки на самом деле потихоньку могут становится архаизмом в данных случаях, учитывая постоянно растущую мощность устройств, сейчас уже не страшно хранить все в памяти или в кэше.
Из плюсов все-таки можно считать, что бд обеспечивает большую стабильность.
Ой, ну ладно. Прям уж архаизм? Имхо, гораздо удобнее хранить сложные структуры в БД, нежели в Map<String, Map<String, Map<String… >>>>>>>>>> =) К тому же, :memory: хранилище в БД никто не отменял. Тот же SQLite прекрасно может в память!
Зачем же такие страсти с мапами? Ничего удобнее и быстрее чем доступ к POJO нет.
Мы вообще переходим на такой подход, что в БД храним чистый json, который приходит с сервера (то есть один столбец строковый в бд), а при создании объекта получаем эту строку и через Gson конвертируем. Получается вот такая немного странная сериализация. Намного удобнее, на самом деле. В этом случае нельзя сказать, что БД нам для чего-то необходима.

P.S. Упс, не совсем в ту ветку добавил коммент)
Да, без сомнения POJO лучше, но сути особо не меняет, особенно, если нужны операции фильтрации, сортировки и прочие прелести (а они нужны в большинстве случаев).
Вопрос вне данной архитектуры, но раз уж вы используете realm.io, то как вы сообщаете UI об изменениях в базе? Eventbus?
Всем хорош realm.io, но этот вопрос не дает мне покоя.

Про RX тоже очень интересно было бы почитать.
Да, с реалмом вопрос хороший, и в принципе любой bus с этим может справиться, хотя они не добавляют плюсов к карме в архитектуру. Обычно в сочетании с Realm мы используем Rx, и уже соответственно, средства Rx для таких оповещений.
Я не то чтобы сильный фанат реалма на самом деле, его использовал лишь для примера, что можно легко перейти к другой БД.
Хорошо, я всерьез подумаю о том, чтобы в ближайший месяц максимально разобраться с Rx и что-то такое написать :) Хотя не исключено, что это сделает еще кто-нибудь :)
А что вы используете в качестве БД в своих проектах?
Голый SQLite это ад. Всякие annotation фрэимворки упрощают конечно это дело, но с реалмом ни в какое сравнение не идут.
У нас в легионе внутренняя библиотека для SQLite, которая этот ад сильно уменьшает. Ну и иногда используем Realm, недавно начали.
Спасибо за развернутый обзор!
Подскажите пожалуйста, при использовании Rx, как решаете проблему кеширования и оффлайн работы? Например отображать сначала сохраненные данные, проверить необходимость обновления, запустить загрузку новых данных и отобразить.
Например отображать сначала сохраненные данные, проверить необходимость обновления, запустить загрузку новых данных и отобразить.

Такой подход можно использовать независимо от того, что вы используете для получения данных с бэкенда. Здесь важно лишь то, является ли критичным показ актуальных данных, или можно сначала показать старые. У нас общая политика обычно такая — всегда сначала ходим на бэкенд, а уже потом достаем из кэша, в случае ошибки. Обычно стараемся подгружать данные за экран-два до того, как они понадобятся, так что такой подход тоже неплох.

Схема с Rx примерно такая (могу немного ошибаться, так как с Rx-проектами не работал особо) — получаем Observable, потом в flatMap сохраняем данные (или повторяем запрос, если ошибка) и прокидываем Observable в UI.
запускать AsyncTask-и и сильно бить в бубен

Я последний раз работал с Android API 4.0.2, но бубнов не помю.


Раньше в Android единственным доступным средством для выполнения сетевых запросов был клиент Apache, который на самом деле далек от идеала, и не зря сейчас Google усиленно старается избавиться от него в новых приложениях.

HTTP Apache Client — прекрасная библиотека, используемая в куче Java проектов. В энтерпрайзе полно.


Позже плодом стараний разработчиков Google стал класс HttpUrlConnection.

Разве HttpUrlConnection не часть Java API? (JavaDoc).


Он ситуацию исправил не сильно. По-прежнему не хватало возможности выполнять асинхронные запросы, хотя модель HttpUrlConnection + Loaders уже является более-менее работоспособной.

А как связана многопоточность(асинхронность) и работа с сетью (HttpUrlConnection)?


А Retrofit библиотека хорошая.
Я последний раз работал с 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)?

Если рассматривать их отдельно, то никак. А если в контексте разработки приложения, то связь самая прямая. Не очень-то возможно работать с сетью без асинхронности / многопоточности.
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);

}

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-запросом в три экрана кода — это просто и хорошо, что, наконец, появились удобные библиотеки???
Так, для сравнения, на некоторых языках этот код выглядит вот так:
fetch('/places/coords_to_places_ru.json').then(
    function (response) {
        return response.json();
    },
    console.error.bind(console)
);
Осталось распарсить json и преобразовать в объект
response.json() именно это и делает.
Да, верно, но он вернет JSON object, что не совсем похоже на:
List<Airport> airports = response.body();


Да и какой смысл сравнивать кусок javascript'a и часть архитектурного слоя android приложения, который наверняка писался не для того чтобы просто послать HTTP-запрос.
Вот запрос.
@GET("/places/coords_to_places_ru.json")
Call<List<Airport>> airports(@Query("coords") String gps);

MainActivity — это UI часть.
ApiFactory — общий класс для всех запросов. Больше он не трогается.
Зависимости считать кодом тоже как-то странно.
Можете как-нибудь сравнить это с кодом, который писался раньше плюс ручной парсинг json-а.
Справедливости ради в том коде фабрику можно схлопнуть в несколько раз без потери функциональности, и почти в одну строку кода, если таймауты дефолтные подойдут.
Мало того, решение можно сделать ещё более «простым», подпилив 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();
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.