20 июля 2015 в 16:51

Библиотека Chronos: облегчаем написание долгих операций

Привет, Хабр! Хочу рассказать вам о библиотеке Chronos для Android (API level >= 9), цель которой – облегчить написание долгих операций, например, сетевых запросов, или обращений к БД.

Какую проблему решаем?

Не секрет, что для Android задача выполнения асинхронных операций всегда была одной из самых частовстречающихся. Действительно, крайне мало приложений работают исключительно в оффлайн, и где можно обойтись без сетевого взаимодействия. И уж совсем крохотная их часть обходится без обращения к постоянной памяти устройства, будь то база данных, Preferences или обычный файл. Однако, на протяжении истории развития системы нам так и не было предложено ни одного достаточно удобного решения “из коробки”.


Чем решали проблему – краткая история
Давайте взглянем на имеющийся инструментарий в контексте задачи “отработать клик по кнопке “авторизация” ”. Собственно, чем мы располагаем?

1. Стандартные потоки

Button signInButton = (Button) findViewById(R.id.button_auth);
signInButton.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(final View v) {
       final Activity activity = AuthActivity.this;
       showProgress();
       new Thread(new Runnable() {
           @Override
           public void run() {
               APIFactory.getApi().signIn();
               activity.runOnUiThread(new Runnable() {
                   @Override
                   public void run() {
                       goToMainContent();
                   }
               });
           }
       }).start();

   }
});

В этом коде плохо буквально все. Он сложночитаемый, в нем протекает память, его нельзя отменить, в нем не обрабатывается поворот экрана, как и любые ошибки вызова API (а если их обрабатывать, то выглядеть все станет совсем уж неудобоваримо).

2. AsynkTask

Button signInButton = (Button) findViewById(R.id.button_auth);
signInButton.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(final View v) {
      new AuthTask().execute();
   }
});

private class AuthTask extends AsyncTask<Void, Void, Boolean>{

   @Override
   protected void onPreExecute() {
       showProgress();
   }

   @Override
   protected Boolean doInBackground(final Void... params) {
       try {
           APIFactory.getApi().signIn();
       }catch (Exception e){
           return false;
       }
       return true;
   }

   @Override
   protected void onPostExecute(final Boolean result) {
       if(!isCancelled() && result) {
           goToMainContent();
       }
   }
}

Уже чуть лучше, но все еще недостаточно. Появилась читаемая обработка ошибок, возможность отмены. Однако до сих пор этот код не способен правильно отработать при повороте экрана в момент выполнения запроса к API – утекает ссылка на Activity, в которой определен класс.

3. Loader
Когда Google представил Loader’ы, то казалось, что они станут Silver bullet для асинхронных запросов, сместив классические на тот момент AsyncTask’и. К сожалению, чуда не произошло. На данный момент Loader’ы – редкий гость в коммерческий проектах, поскольку очень уж они оказались неудобны в использовании. В этом разделе я не буду приводить код по аналогии с предыдущими двумя. Вместо этого рекомендую любопытному читателю ознакомиться с официальным гайдом по этой технологии, чтобы оценить объем кода, требующегося Loader’ам: developer.android.com/reference/android/content/AsyncTaskLoader.html

4. Service
Сервисы хороши для выполнения долгих задач, которые «висят» в фоне на протяжении использования приложения. Однако для запуска операций, результат которых нужен здесь и сейчас, структура сервисов не идеальна. Главным образом, ограничение накладывает методика передачи данных через Intent, который, во-первых, вмещает только ограниченное количество данных, а во-вторых, требует чтобы передаваемые данные были тем или иным способом сериализуемы. На этой технологии работает популярная библиотека Robospice.

Что предлагает Chronos?



Chronos делает за вас всю работу по выполнению задачи в параллельном потоке и доставке результата или ошибки выполнения в основной поток. Грубо говоря, эта библиотека предоставляет контейнер для любого рода долгих операций.
В проекте есть полноценная wiki, часть кода оттуда будет использоваться в статье, однако для более полного руководства обращайтесь на github.com/RedMadRobot/Chronos/wiki.

Пример

Давайте решим типовую задачу, используя Chronos: в Activity нужно запросить какой-то объект у некоего хранилища, доступ к которому достаточно долго, чтобы не делать запрос в UI потоке. Сначала напишем код, а потом разберем, что у нас получилось.

1. Первым делом нужно подключить Chronos к проекту. Для этого достаточно прописать зависимость в gradle:

compile 'com.redmadrobot:chronos:1.0.5'

2. Теперь опишем Activity. Базовый класс ChronosActivity– это одна из компонент библиотеки, однако вы легко можете написать его аналог, примеры этого есть в документации. Так же Chronos можно использовать во фрагментах, код не будет отличаться.

class MyActivity extends ChronosActivity {
    @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());
            }
        });
    }

    public void onOperationFinished(final MyOperation.Result result) {
        if (result.isSuccessful()) {
            showData(result.getOutput());
        } else {
            showDataLoadError(result.getError());
        }
    }

    private void showData(BusinessObject data){
        //...
    }

    private void showDataLoadError(Exception exception){
        //...
    }
}


3. И, наконец, опишем бизнес-логику получения данных в классе MyOperation:

class MyOperation extends ChronosOperation<BusinessObject> {

    @Nullable
    @Override
    public BusinessObject run() {
        final BusinessObject result ;
        // here you should write what you do to get the BusinessObject
        return result;
    }

    @NonNull
    @Override
    public Class<? extends ChronosOperationResult<BusinessObject>> getResultClass(){
        return Result.class;
    }

    public final static class Result extends ChronosOperationResult<BusinessObject> {
    }
}

Вот, собственно, и все. Давайте разберемся подробно, что же происходит в этом коде. Начнем с начала.

Настройка класса UI
class MyActivity extends ChronosActivity {

Чтобы работать с Chronos, базовый класс Acvitity или фрагмента должен либо наследоваться от предложенных в библиотеке, либо содержать определенный код в методах жизненного цикла, примеры можно увидеть в документации.

Запуск операции
runOperation(new MyOperation());

Здесь вызывается базовый метод класса ChronosActivity, в который передается только что созданная операция. Сразу после вызова этого метода Chronos заберет операцию в очередь и начнет ее выполнение в параллельном потоке.

Обработка результата операции
public void onOperationFinished(final MyOperation.Result result) {
        if (result.isSuccessful()) {
            showData(result.getOutput());
        } else {
            showDataLoadError(result.getError());
        }
    }

Этот метод будет вызван после того, как операция будет выполнена, либо в ходе выполнения выбросится исключение. Такие методы-обработчики обязательно должны иметь сигнатуру public void onOperationFinished(ResultType). Важный момент: метод вызовется только между вызовами onResume() и onPause(), то есть в нем вы спокойно можете изменять UI, не боясь, что он к тому моменту уже стал невалидным. Более того, если Activity была пересоздана из-за поворота, ухода в бэкграунд, или других причин – Chronos вернет результат в любом случае (единственное исключение – в системе закончилась память, в этом случае для предотвращения OutOfMemory Chronos может стереть старые данные результатов).
“откуда идет вызов?”
Внимательный читатель заметит, что Activity не реализует никаких специфических интерфейсов, так откуда же вызовется именно этот метод? Ответ – из кода, содержащего рефлексию. Решение делать рефлексию вместо интерфейса было принято из-за TypeErasure в Java, который делает невозможным одновременную реализацию одного и того же шаблонного интерфейса с разными параметрами. То есть это сделано, чтобы в одной Activity можно было обработать результат скольких угодно типов операций.

Настройка класса операции
class MyOperation extends ChronosOperation<BusinessObject> {

Класс ChronosOperation инкапсулирует в себе бизнес-логику получения объекта определенного типа, в данном случае – BusinessObject. Все пользовательские операции должны наследоваться от ChronosOperation.

Бизнес-логика
    @Nullable
    @Override
    public BusinessObject run() {
        final BusinessObject result ;
        // here you should write what you do to get the BusinessObject
        return result;
    }

Этот абстрактный метод класса ChronosOperation отвечает, собственно, за бизнес-логику получения объекта. Он выполняется в параллельном потоке, поэтому в нем можно делать сколь угодно долгие действия, это не вызовет лагов в интерфейсе приложения. Также любые исключения, выброшенные в нем, будут заботливо переданы в вызывающий объект, не приводя к крашу приложения.

Именование результата
    @NonNull
    @Override
    public Class<? extends ChronosOperationResult<BusinessObject>> getResultClass(){
        return Result.class;
    }

    public final static class Result extends ChronosOperationResult<BusinessObject> {
    }

Следующие метод и класс призваны дать возможность в коде Activity прописать обработчик результатов для каждой конкретной операции, указывая класс в качестве типа параметра метода onOperationFinished. Допускается использование одного класса результата для разных операций, если вы хотите, чтобы их результат обрабатывался одинаково.

Резюмирую: соберем минимальный набор кодовых участков, нужных для работы с Chronos.

  • Класс операции
  • Код вызова операции в UI объекте
  • Код обработки результата в UI объекте

Что здесь есть еще?


Итак, почему и зачем можно использовать Chronos?

  • Chronos берет на себя передачу данных между потоками, оставляя вам заботы только о бизнес-логике.
  • Chronos учитывает все нюансы жизненного цикла Activity и фрагментов, доставляя результат только тогда, когда они готовы его обработать, сохраняя данные до тех пор.
  • В Chronos не течет память. Вы больше не рискуете поймать краш, потому что утекло слишком много объектов Activity.
  • Chronos покрыт unit-тестами.
  • И наконец, Chronos – open-source проект. Вы всегда можете взять код и переписать его под свои нужды. Благодаря тестам, вам будет легко валидировать изменения кода.

Ссылка на проект в GitHub. Там вы найдете полное руководство по библиотеке, примеры использования и, конечно, исходный код.

Читайте также:
Сажаем контроллеры на диету: Android
Архитектурный дизайн мобильных приложений: часть 1
Архитектурный дизайн мобильных приложений: часть 2
Автор: @MaximEfimov
REDMADROBOT
рейтинг 91,86
№1 в разработке мобильных решений для бизнеса
Похожие публикации

Комментарии (35)

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

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

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

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

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

    Если в Chronos это возможно, то наверное стоит его опробовать.
    • +1
      Про 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.
      • 0
        Спасибо, теперь понятно.
        Касательно View и загрузки данных, да, конечно это вопрос архитектуры. В моем случае, этот подход кажется мне оправданым — в данном контексте View мало чем отличается от Activity — тот же элемент UI.

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

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

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

      Про сложное обновление данных: в этом случае вам лучше будет в одной операции агрегировать несколько. Таким образом получится, что до UI слоя дойдет только информация о полной загрузке, либо ошибке обновления данных. Также в этом случае логично описывать что делать при неполной загрузке (откат транзакции БД, инвалидация кеша, нотификация об ошибке и тому подобное) по ходу выполнения агрегированных операций. Мы используем такой подход в ряде приложений, которые должны работать в offline, и при этом синхронизироваться с сервером по возможности.
  • 0
    Привет, можно пожелание?
    Не всегда удобно экстендить уже существующие компоненты. Довольно элегантно эта проблема решалась через рефлекшен в 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
    • 0
      Если я вас правильно понял, то предлагается сделать изменяемое имя метода-обработчика. В принципе, такую опцию можно добавить, но я не уверен, что этот функционал будет часто востребован, поэтому не могу обещать, что сделаем это в ближайшее время. Впрочем вы сами можете дописать эту фичу, с точки зрения архитектуры это будет несложно.

      Наследовать классы вовсе не обязательно, вот здесь пример, как «дружить» 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.
      • 0
        " вот здесь пример, как «дружить» 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());
                }
            }
        
        }
        

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

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

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

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

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

    @Nullable
    public abstract Output run();
    

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

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

Самое читаемое Разработка