Comments 35
There are only two hard things in Computer Science: cache invalidation and naming things.
— Phil Karlton
Готов выслушать ваши предложения о именовании класса)
Меня убивает именование в стиле ActivityMain. Не говоря уже о том, что так никто не пишет в коде и это непривычно, это еще и неправильно с точки зрения английского языка. А у вас все классы так обозваны.
AndroidStudio понимает автокомплит и с середины.
Ого, у вас еще и скобки с новой строки… Печаль-беда. Это уже точно конвенции не приветствуют.
Вот андроидовые конвенции неплохо описанные. https://github.com/ribot/android-guidelines/blob/master/project_and_code_guidelines.md
Там сразу же:
the name of the class should end with the name of the component
А скобки… Мне просто удобнее так, тем более, что сменить все скобки можно одним shortCut-ом)
Уровень View просто не должен знать о кэшировании.
Также непонятна причина неиспользования базы данных для хранения результатов запроса.
Получили данные, записали результат в базу, вернули List (который враппер над Cursor).
При повороте или восстановлении данных с экрана смотрим, есть ли закэшированный запрос.
У вас же много ненужного и непонятного кода, про code-style и naming я вообще молчу.
Есть сообщество андроид-разработчиков, которое использует общепринятые стандарты, которые тут игнорируются, потому что «я так привык». Это неуважение.
Извините, но код уровня junior developer, статью я бы советовал только примером — «как усложнить себе жизнь и делать не нужно».
Вот вам некая абстракция, которая куда привлекательнее, уровень View — внутри Fragment:
model
.compose(bindToLifecycle()) // rxLifecycle
.getRecords()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
records -> {
// TODO show result here
},
throwable -> {
// TODO show error here
});
И вот вам абстракция, уровень Model:
class Model {
...
private BehaviorSubject<List<Record>> recordsSubject = BehaviorSubject.create();
public Observable<List<Record>> getRecords() {
return Observable.<List<Record>>create(subscriber -> {
// some synchronization here - begin
if (cacheIsEmpty()) {
List<Record> records = loadRecordsFromWeb();
saveInDatabase(records);
}
// some synchronization here - end
List<Record> cachedRecords = getListFromDatabase();
subscriber.onNext(cachedRecords);
subscriber.onCompleted();
})
.flatMap(records -> {
recordsSubject.onNext(records);
return recordsSubject.asObservable();
});
}
...
}
Бонусом класс — wrapper над объектом Cursor (надеюсь объяснение, почему лучше использовать курсор вместо ArrayList для большого количества входных данных давать не нужно):
public class DbList<T> extends AbstractList<T> {
public interface Factory<T> {
T get(Cursor cursor);
}
public static <T> List<T> create(Cursor cursor, Factory<T> factory) {
return new DbList<>(cursor, factory);
}
private final Cursor cursor;
private final Factory<T> factory;
private DbList(Cursor cursor, Factory<T> factory) {
this.cursor = cursor;
this.factory = factory;
}
@Override
@Nullable
public T get(int location) {
if (cursor != null && cursor.moveToPosition(location)) {
return factory.get(cursor);
}
return null;
}
@Override
public int size() {
return cursor == null ? 0 : cursor.getCount();
}
}
Спасибо за полезную информацию. Отвечаю по пунктам:
- MVP я ещё не пробовал, хотя много слышал. Судя по реакции на эту статью надо изучить. Просто изначально целью статьи было поделиться своим первым опытом с Rx и Retrofit в максимально простом и абстрактном примере.
- В БД ничего не сохранял, т.к. изначально (хоть это и не помянуто в статье по ряду причин) задача стояла в отображении данных с коротким сроком жизни, устаревающих в течении пары часов. Засим желательно при каждом запуске приложения получать данные именно из сети, а не сохранённые ранее, ибо они уже будут нерелевантны. А так — да, в одном из своих приложений так и делал — сначала в БД данные шли, потом уже из неё во View. Но это от конкретной задачи зависит.
- Code style и имена поправил и тут и в репозитории. Судя по реакции в комментариях всё было плохо, так что исправлюсь)
- Про преимущества Cursor над ArrayList бы послушал — могу предположить что он просто быстрее работает?
- Ещё раз спасибо за код в комментарии. Возможно, я попробую переделать в другой ветке проект в MVP стиле и напишу ещё одну статью)
А в каком месте закрывать Cursor? Наверное, нужно добавить метод close к DbList?
В большинстве мест в коде используется вот такая конструкция:
Ссылка для понимания, что именно я использую — LINK
Собственно код:
//composer
class CursorAutoCloser implements Observable.Transformer<Cursor, Cursor> {
private Cursor cursor;
@Override
public Observable<Cursor> call(Observable<Cursor> observable) {
return observable
.map(cursor -> this.cursor = cursor)
.doOnUnsubscribe(() -> CursorUtil.close(this.cursor));
}
}
//part of CursorUtil
class CursorUtil {
...
public static void close(Cursor cursor) {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
...
}
//usage example
cursorObservable
.compose(new CursorAutoCloser())
...
//composer
class CursorUiTransformer implements Observable.Transformer<Cursor, Cursor> {
private Cursor cursor;
@Override
public Observable<Cursor> call(Observable<Cursor> observable) {
return observable
.subscribeOn(RxExecutors.io())
.observeOn(RxExecutors.ui())
.unsubscribeOn(RxExecutors.ui())
.doOnNext(cursor -> {
closeCursor();
this.cursor = cursor;
})
.doOnUnsubscribe(this::closeCursor);
}
private void closeCursor() {
CursorUtil.close(this.cursor);
}
}
//usage example
cursorObservable
...
.compose(new CursorAutoCloser()) // apply at the end
Так делается для того, чтобы старый Cursor закрывался при множественных onNext (обновления данных) и в ui потоке — в таком случае нету конфликтов с адаптером.
Ну а если Rx не используется? Как тогда закрывать без нарушения абстракции?
Ох, как же JavaRX мне режет глаза без лямбд.
В общем странно, что вы не наткнулись на RxLoader, который в полное мере решает описанную задачу, Так же на том же SO я уже отвечал на подобный вопрос .
Должен признаться, что я специально и не искал других решений этой задачи :) Мне просто захотелось взять Rx, Retrofit, их соединить и посмотреть что получится) А ссылка интересная, спасибо. Правда уж очень давно, похоже, не обновляли библиотеку, что некоторые сомнения вызывает...
Используем RxJava и Retrofit на Android, учитывая поворот экрана