Pull to refresh

Comments 35

Бегло почитал документацию, с ходу не нашел, может подскажите:
— в каком контексте выполняется операция? сервис?
— есть ли доступ к Context внутри операции?
— если захватить этот контекст ничего не потечет?
— очередь выполнения операций последовательная или параллельная? есть ли возможность влиять на параллельность? что-нибудь вроде тредпулов? Если запустить 100 операций — очередь не забьется?
— можно ли задавать операциям приоритет?
— как я понимаю, никакого IPC?
Спасибо
P.S. написал подобную штуку за день, используя EventBus и обычный сервис, а это оказывается библиотека :)
На эту тему, совсем недавно была отличная статья содержащая ответ Синдре Сорхуса о объеме модулей/библиотек http://habrahabr.ru/post/262681/
Ога, аналогичное решение пользуем. Только даже без сервиса, thread результат постит.
Кого напрягает потенциальное количество одновременных потоков — есть заклинание «ExecutorService».
Про контекст: в самих операциях ссылка на Context не содержится, ей владеет посредник – объект класса ChronosConnector, но он ее теряет после вызова onPause(). Если вам нужен контекст в операции, то можно брать, например, контекст приложения.

Про режим выполнения: под капотом на данный момент находится ExecutorService в лице CachedThreadPool, в будущем, возможно, вынесем реализацию в конфиг. Возможности задать приоритет нет, но это тоже в планах на расширение. IPC нет.
Я бы добавил в README инструкций как завести вашу библиотеку под proguard, а то он выпиливает все ваши контрактные public void onOperationFinished(ResultType) методы как неиспользуемые
Как принято, прошу прощения если что не так прочитал, но из статьи я не совсем понял следующее:
1. Можно ли использовать Chronos во View а не в Activity.
2. Что будет при смене ориентации (конфигурации)
3. Что будет, если система захочет убить приложение (если Activity ушла в бекграунд)
4. Можно ли запускать подтаски

И еще коммент: мне было бы удобно использовать фоновую задачу непосредственно во View. Скажем, написал View, которая грузит картинку из сети, положил ее в Layout и забыл — она сама себя грузит. При смене ориентации сама восстанавливает свое состояние не стартуя загрузку заново. Когда таких View десяток и больше, то выносить фоновые таски в Activity, ну очень не хочется.

Именно поэтому, некоторе время назад, я написал для своих нужд видоизмененный велосипед на основе AsyncTask, которй хранит пул таск в статик переменной.
При смене конфигурации View отписывается от AsyncTask и при пересоздании заново подписывается на ту же AsyncTask.

В данной реализации есть свои ограничения, но они несущественны на фоне возможности перенести фоновые таски во View.

Если в Chronos это возможно, то наверное стоит его опробовать.
Про View: в качестве «UI-клиента» можно использовать любой класс, в том числе и View. Однако, основная особенность библиотеки – привязка к жизненному циклу не сработает, поскольку у View нет как такового однозначного определения жизненного цикла. Вот здесь можете почитать рассуждения на этот счет: plus.google.com/u/0/+ArpitMathur/posts/cT1EuBbxEgN. Если придумаете, как правильно подключать View, то не стесняйтесь делать Pull Request. С другой стороны, выносить логику получения данных во View не рекомендуется, но это уже вопрос архитектуры.

Про смену ориентации и уход в бэкграунд: если не был убит сам процесс, то после пересоздания Activity, Chronos вернет ей все результаты операций, которые скопились за время, пока она была неактивна. Также есть очень простой способ подхватывать запущенные таски, чтобы, например, не начинать грузить данные два раза при повороте экрана: github.com/RedMadRobot/Chronos/wiki/Single-launch-mode.
Спасибо, теперь понятно.
Касательно View и загрузки данных, да, конечно это вопрос архитектуры. В моем случае, этот подход кажется мне оправданым — в данном контексте View мало чем отличается от Activity — тот же элемент UI.

Интересно, а можно ли Chronos точно так же заточить для использования во View — вместо onResume и onPause можно наврное использовать onAttachedToWindow и onDetachedFromWindow?
Прошу прощения — посмотрел сорцы и сам ответил на свой вопрос.
В целом, концепция у нас с вами похожа.
Разве нельзя извлечь активити из вью?
            Activity activity = null;
            try {
                activity = (Activity) view.getContext();
            } catch (Exception e) {
               //
            }
            if (activity!=null && !activity.isFinishing()) {
            }
Извлечь можно, но я не очень понимаю, как это поможет в данном случае. Нужно определить две точки, между которыми мы полагаем валидным оперирование компонентом UI, например – onResume() и onPause(). Для View выше предложили onAttachedToWindow() и onDetachedFromWindow(), возможно, это сработает.
RxJava позволяет делать это и многое другое более элегантно.
RxJava, безусловно, очень крутая библиотека, но, к сожалению, в ней нет легкой привязки к жизненному циклу. Если верить самим авторам библиотеки, то привязку сделать, действительно, можно, но это требует написания своего способа хранить Subscription при пересоздании Activity: github.com/ReactiveX/RxJava/wiki/The-RxJava-Android-Module#fragment-and-activity-life-cycle. В Chronos фокус направлен именно на легкую и бесшовную интеграцию фоновых задач с жизненным циклом UI-классов.
Это так, но написать такую привязку не сложно, её код будет зависеть от ваших потребностей, но в целом всё решается Fragment + setRetainInstanceState().
Если нужно самому привязываться через setRetainInstanceState(), то это поддержка жизненного цикла на уровне AsyncTask'ов, их так же можно «завернуть» во фрагмент. Кстати, именно так и работают Loader'ы, только у них свой менеджер, а не фрагментный, но суть там абсолютно та же.
Из готовых оберток над RxJava мне, к сожалению, не попадались достаточно качественные – в основном везде нужно или руками как-то сохранять подписки (особенно если их много), или при каждом запуске явно лезть в кеш и смотреть, пришли ли данные, или грузятся, или загрузка даже не запускалась.
не забывайте что вложенные фрагменты не могут быть retain)
Приятно читать стройную документацию. Несколько вопросов по функционалу:
«в системе закончилась память, в этом случае для предотвращения OutOfMemory Chronos может стереть старые данные результатов»
Что произойдет в этом случае с запросом? При возвращении в activity автоматом будет запущен новый или его нужно будет запускать явно?

«Чтобы не потерять уже запущенный запрос при повороте экрана, Chronos предоставляет возможность “именовать” запуски операций»
Предположим операция — это часть какой-то большой операции по обновлению инфы для пользователя. Как тогда быть с проблемой устаревания инфы? (Часть данных прогрузили с прошлого раза, потом приложение ушло в фон на недельку, вернулось в foreground и получается часть данных старая, а часть новая)

«Есть возможность отмены операций»
Круто, что есть возможность interrupt'ить поток загрузки. Насколько помню, в известном robospice cancel поток не interrupt'ит.
Про освобождение памяти: GC соберет результаты операций, чтобы освободить память, и, когда Activity восстановится, они не будут доставлены. Повторно операция сама по себе не запустится, поскольку это, в общем случае, противоречит бизнес-логике – например, если это была операция включающая POST запрос на перевод денег в банковском приложении. Однако, вот здесь можете посмотреть, как написать код автоматически перезапускающихся операций: github.com/RedMadRobot/Chronos/wiki/Single-launch-mode, это очень просто.

Про сложное обновление данных: в этом случае вам лучше будет в одной операции агрегировать несколько. Таким образом получится, что до UI слоя дойдет только информация о полной загрузке, либо ошибке обновления данных. Также в этом случае логично описывать что делать при неполной загрузке (откат транзакции БД, инвалидация кеша, нотификация об ошибке и тому подобное) по ходу выполнения агрегированных операций. Мы используем такой подход в ряде приложений, которые должны работать в offline, и при этом синхронизироваться с сервером по возможности.
Привет, можно пожелание?
Не всегда удобно экстендить уже существующие компоненты. Довольно элегантно эта проблема решалась через рефлекшен в android query

public void asyncJson(){
        
        //perform a Google search in just a few lines of code
        
        String url = "http://www.google.com/uds/GnewsSearch?q=Obama&v=1.0";     
       //мы передаем класс (this) и имя метода (jsonCallback), в который придет результат
        aq.ajax(url, JSONObject.class, this, "jsonCallback");
        }

public void jsonCallback(String url, JSONObject json, AjaxStatus status){
        
        if(json != null){               
                //successful ajax call          
        }else{          
                //ajax error
        }
        
}


Обратите внмание также на то, что Статус обработки приходит в тот же обработчик, что и в саксесс, что уменьшает количество кода и повышает читаемость.

code.google.com/p/android-query/wiki/AsyncAPI
Если я вас правильно понял, то предлагается сделать изменяемое имя метода-обработчика. В принципе, такую опцию можно добавить, но я не уверен, что этот функционал будет часто востребован, поэтому не могу обещать, что сделаем это в ближайшее время. Впрочем вы сами можете дописать эту фичу, с точки зрения архитектуры это будет несложно.

Наследовать классы вовсе не обязательно, вот здесь пример, как «дружить» Chronos с обычной Activity: github.com/RedMadRobot/Chronos/wiki/How-to-use-Chronos#step-one--setting-up-gui. И у нас тоже в один и тот же метод-обработчик приходит как успешный результат, так и ошибка: github.com/RedMadRobot/Chronos/wiki/How-to-use-Chronos#step-four--handling-the-result.
" вот здесь пример, как «дружить» Chronos с обычной Activity" —
сейчас я асинктаск с тем же успехом могу допилить.

Идея у Вас хорошая, но нужно же допилить ее до более удобного вида.

1. Можно добавить в код проверки на то, что активити сдохло, например. И тогда не нужно плясать вокруг onResume onPause
if (activity.isFinishing()) {
    AQUtility.warn("Warning", "Possible memory leak. Calling ajax with a terminated activity.");
    //какой ть интерфейс, например тут может теребится
}
this.activity  = new WeakReference<Activity>(activity); 


2. Обработчики можно было бы сделать гибчее, например одинаковые для разных задач, передавать в них какие то произвольные таги, как во вьюхах или в okHttp или array params как в асинктасках
class MyActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Button startButton = (Button) findViewById(R.id.button_start);
        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                runOperation(new MyOperation(),MyActivity.this,"onOperationFinished",v);
            }
        });
    }

    public void onOperationFinished(final MyOperation.Result result, final Object tag) {
        if (result.isSuccessful()) {
            if (tag instanceоf Button) {...}
            showData(result.getOutput());
        } else {
            showDataLoadError(result.getError());
        }
    }

}

Я немножко скомкано объясняю наверно, простите нет времени подробно расписать. Сейчас это не выглядит как суперское универсальное и малобуквенное решение. Но может таким стать.
Боюсь, что если прикручивать AsyncTask'и, то кода будет поболее. То, что я привел в примере достаточно один раз в базовом классе написать.

Завязываться на isFinishing() получится только у Acitivity, ближайший родственник этого метода во Fragment – isRemoving(), но они не идентичны. Получается, что экономим две строчки кода, а придется придумывать новый способ привязки, который не факт, что будет проще и короче.

Насчет тагов подумаем. Лично я очень не люблю instanceof и явные касты, поскольку это рушит ООП-дизайн. В качестве какого-то параметра можно всегда использовать любой объект, передаваемый в Operation при создании, и заключаемый в шаблонный Output при возвращении результата.
И еще про isFinishing() – этот метод не позволяет отловить поворот экрана, так что остается потенциальная уязвимость краша при завершении операции во врем пересоздания UI.
Спасибо за развернутый ответ. Согласен, что все непросто. В примере, который я привел после проверки на isFinishing — следующей строкой идет создание WeakReference:
this.activity  = new WeakReference<Activity>(activity);

Получается что можно однозначно определить из кода — живо ли еще активити и стоит ли слать результат — без перекрытия onPause в Активити при помощи isfinishing + weakreference. Мне кажется это немножко упростило бы внедрение. Потому что в крупном проекте — Ваша библиотека будет не единственной либой, которая захочет подписаться на onPause. Как то так.

А хочется волшебную либу — чтоб все само работало — и прямо, и из коробки и подключалось парой строчек)
У вас все операции работают внутри ChronosService, который на самом деле не сервис в терминах андроида, а просто сингтон, который содержит ExecutorService, стало быть если в процессе приложения не останется ни одной активити/сервиса, он (процесс) будет первоочередным кандидатом на удаление при чистке ресурсов системой и не важно, что ExecutorService что-то выполняется. Так и было задумано?
Да, задача повышения выживаемости процесса нами не ставилась, это немного другой аспект.
Заметил одну проблему: нельзя самому бросить исключение в методе

@Nullable
public abstract Output run();

в наследниках класса ChronosOperation. Допустим, я вызываю метод, который может бросить IOException и я хочу вернуть это исключение в UI поток. Как быть?
Оборачивать все исключения в RuntimeException не комильфо, на мой взгляд.
Это проблема с любыми checked исключениями в реализациях любых интерфейсов – нельзя менять сигнатуру метода, но иначе выбросить такое исключение тоже нельзя. Опять же, поскольку checked – это не наследники какого-нибудь AbstractCheckedException, а лишь подмножество наследников Throwable, то мы даже не можем обобщить их и добавить в сигнатуру абстрактного метода. Можно, конечно, написать «throws Throwable» у базового метода, но это совсем уж ужасный дизайн.
Поэтому единственный нормальный способ выбросить checked исключение из переопределенного метода – завернуть его в любое unchecked исключение.
Спасибо за ответ.
Заметил еще одну интересную штуку — подключенный Chronos при сборке генерирует в ресурсах файл values.xml в котором создает строку <string name="app_name">Chronos</string>, тем самым зачастую переопределяя имя приложения в манифесте. Это такая пасхалка или что?
Вообще, такого, конечно, не должно происходить. Спасибо, что подсветили проблему, будем разбираться.
Я правильно понимаю, что при загрузки активити считывая файл с диска в текстовую переменную (эта операция происходит при помощи Хроноса), мы спокойно можем вывести ее (значение переменной), например, в WebView после оканчания работы в Хроносе? При этом пользователь может спокойно работать с остальными элементами активити?
Sign up to leave a comment.