<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>SkillBranch – образовательное IT-сообщество</title><generator>teletype.in</generator><description><![CDATA[Это онлайн-платформа, призванная помочь тебе освоить престижную профессию в сфере IT.]]></description><image><url>https://teletype.in/files/db/db790265-9c51-412a-83c7-311365379f58.png</url><title>SkillBranch – образовательное IT-сообщество</title><link>https://teletype.in/@skillbranch</link></image><link>https://teletype.in/@skillbranch?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/skillbranch?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/skillbranch?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Tue, 14 Apr 2026 11:46:05 GMT</pubDate><lastBuildDate>Tue, 14 Apr 2026 11:46:05 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@skillbranch/PN2iJGYbY</guid><link>https://teletype.in/@skillbranch/PN2iJGYbY?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/PN2iJGYbY?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>Паттерны разработки: MVC vs MVP vs MVVM vs MVI</title><pubDate>Sat, 15 Aug 2020 12:16:39 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/04/71/04715901-e222-4a2e-a607-3c7ac97b6173.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/5e/ee/5eee7472-21ac-4b8f-8b1e-36d7650acf94.png"></img>От переводчика: данная статья является переработкой английской статьи по паттернам разработки. В процессе адаптации на русский немало пришлось изменить. Оригинал]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://teletype.in/files/5e/ee/5eee7472-21ac-4b8f-8b1e-36d7650acf94.png" width="1024" />
  </figure>
  <p><em>От переводчика: данная статья является переработкой английской статьи по паттернам разработки. В процессе адаптации на русский немало пришлось изменить. <a href="https://academy.realm.io/posts/mvc-vs-mvp-vs-mvvm-vs-mvi-mobilization-moskala/" target="_blank">Оригинал</a></em></p>
  <p><em>От редактора: ссылка на источник – в конце статьи</em></p>
  <hr />
  <p>Выбор между различными паттернами разработки всегда сопровождается рядом споров и дискуссий, а разные взгляды разработчиков на это еще больше усложняют задачу. Существует ли решение этой идеологической проблемы? Давайте поговорим о MVC, MVP, MVVM и MVI прагматично. Давайте ответим на вопросы: “Почему?”, “Как найти консенсус?”</p>
  <h2>Вступление</h2>
  <p>Вопрос выбора между MVC, MVP, MVVM и MVI коснулся меня, когда я делал приложение для Warta Mobile вместе с моей командой. Нам было необходимо продвинуться от минимально жизнеспособного продукта к проверенному и полностью укомплектованному приложению, и мы знали, что необходимо будет ввести какую-либо архитектуру.</p>
  <p>У многих есть непоколебимое мнение насчет различных паттернов разработки. Но когда вы рассматриваете архитектуру, такую как MVC, то, казалось бы, полностью стандартные и определенные элементы: модель (Model), представление (View) и контроллер (Controller), разные люди описывают по-разному.</p>
  <p>Трюгве Реенскауг (Trygve Reenskaug) — изобрел и определил MVC. Через 24 года после этого он описал это не как архитектуру, а как набор реальных моделей, которые были основаны на идее MVC.</p>
  <p>Я пришел к выводу, что поскольку каждый проект — уникален, то нет идеальной архитектуры.</p>
  <p>Необходимо детально рассмотреть различные способы реализации и подумать над преимуществами и недостатками каждой.</p>
  <h2>Чего мы хотим добиться?</h2>
  <h4>Масштабируемость, сопровождаемость, надежность</h4>
  <p>Очевидно, что <strong>масштабируемость (scalability)</strong> — возможность расширять проект, реализовывать новые функции.</p>
  <p><strong>Сопровождаемость (maintainability)</strong> — можно определить как необходимость небольших, атомарных изменений после того, как все функции реализованы. Например, это может быть изменение цвета в пользовательском интерфейсе. Чем лучше сопровождаемость проекта, тем легче новым разработчикам поддерживать проект.</p>
  <p><strong>Надежность (reliability)</strong> — понятно, что никто не станет тратить нервы на нестабильные приложения!</p>
  <h4>Разделение ответственности, повторное использование кода, тестируемость</h4>
  <p>Важнейшим элементом здесь является разделение ответственности (Separation of Concerns): <strong>различные идеи должны быть разделены</strong>. Если мы хотим изменить что-то, мы не должны ходить по разным участкам кода.</p>
  <p>Без разделения ответственности ни <strong>повторное использование кода (Code Reusability)</strong>, ни <strong>тестируемость (Testability)</strong> практически невозможно реализовать.</p>
  <p>Ключ в <strong>независимости</strong>, как заметил Uncle Bob в Clean Architecture. К примеру, если вы используете библиотеку для загрузки изображений, вы не захотите использовать другую библиотеку для решения проблем, созданных первой! Независимость в архитектуре приложения частично реализует масштабируемость и сопровождаемость.</p>
  <h2>Model View Controller</h2>
  <p>У архитектуры MVC есть два варианта: <strong>контроллер-супервизор (supervising controller)</strong> и <strong>пассивное представление (passive view)</strong>.</p>
  <h3>MVC с контроллером-супервизором</h3>
  <p>В мобильной экосистеме практически никогда не встречается реализация контроллера-супервизора.</p>
  <p>Архитектуру MVC можно охарактеризовать двумя пунктами:</p>
  <ul>
    <li>Представление — это <strong>визуальная проекция модели</strong></li>
    <li>Контроллер — это <strong>соединение между пользователем и системой</strong></li>
  </ul>
  <figure class="m_column">
    <img src="https://habrastorage.org/getpro/habr/post_images/21c/f57/390/21cf573907dcd9a3109974ac18dc1a7c.png" width="1568" />
  </figure>
  <p>Диаграмма иллюстрирует идеологию паттерна. Здесь представление определяет как слушателей, так и обратные вызовы; представление передает вход в контроллер.</p>
  <p>Контроллер принимает входные данные, а представление — выходные, однако большое число операций происходит и между ними. Данная архитектура хорошо подходит только для небольших проектов.</p>
  <h3>Пассивное представление MVC</h3>
  <figure class="m_column">
    <img src="https://habrastorage.org/getpro/habr/post_images/7c8/498/36a/7c849836a7235e37a463e18b74460a21.png" width="1502" />
  </figure>
  <p>Главная идея пассивного представления MVC — это то, что представление полностью управляется контроллером. Помимо этого, код четко разделен на два уровня: бизнес логику и логику отображения:</p>
  <ul>
    <li>Бизнес логика — то, как работает приложение</li>
    <li>Логика отображения — то, как выглядит приложение</li>
  </ul>
  <h3>Massive View Controller</h3>
  <p><strong>Нельзя трактовать Activity как представление (view)</strong>. Необходимо рассматривать его как слой отображения, а сам контроллер выносить в отдельный класс.</p>
  <p>А чтобы уменьшить код контроллеров представлений, можно разделить представления или определить субпредставления (subviews) с их собственными контроллерами. Реализация MVC паттерна таким образом позволяет легко разбивать код на модули.</p>
  <p>Однако, при таком подходе появляются некоторые проблемы:</p>
  <ul>
    <li>Объединение логики отображения и бизнес логики</li>
    <li>Трудности при тестировании</li>
  </ul>
  <p>Решение этих проблем кроется за созданием абстрактного интерфейса для представления. Таким образом, презентер будет работать только с этой абстракцией, а не самим представлением. Тесты станут простыми, а проблемы решенными.</p>
  <p>Все это — и есть главная идея MVP.</p>
  <h2>Model View Presenter</h2>
  <p>Данная архитектура облегчает unit-тестирование, <strong>презентер (presenter)</strong> прост для написание тестов, а также может многократно использоваться, потому что представление может реализовать несколько интерфейсов.</p>
  <p>С точки зрения того, как лучше и корректней создавать интерфейсы, необходимо рассматривать MVP и MVC только как основные идеи, а не паттерны разработки.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/getpro/habr/post_images/c99/af4/d93/c99af4d9379048b44ae882d34c2a01f3.png" width="1580" />
  </figure>
  <h2>Use cases</h2>
  <p>Создание <strong>use cases</strong> — это процесс выноса бизнес логики в отдельные классы, делая их частью модели. Они независимы от контроллера, и каждый содержит в себе одно бизнес-правило. Это повышает возможность многократного использования и упрощает написание тестов.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/getpro/habr/post_images/267/894/dbf/267894dbfa91527050ffe3a59d8af241.png" width="1538" />
  </figure>
  <p>В примере на <a href="https://github.com/MarcinMoskala/MVTest/tree/MVC-UseCases/app/src/main/java/com/mvtest/marcinmoskala/mvtest" target="_blank">GitHub</a>, в login controller вынесен use case валидации и use case логина. Логин производит соединение с сетью. Если есть общие бизнес правила в других контроллерах или презентерах, можно будет переиспользовать эти use case’ы.</p>
  <h2>Привязывание представления (View Bindings)</h2>
  <p>В <a href="https://github.com/MarcinMoskala/MVTest/blob/MVP/app/src/main/java/com/mvtest/marcinmoskala/mvtest/LoginPresenter.kt" target="_blank">реализации MVP</a> есть четыре линейные функции, которые ничего не делают, кроме небольших изменений в пользовательском интерфейсе. Можно избежать этого лишнего кода, использовав <strong>view binding</strong>.</p>
  <p>Все способы биндинга можно найти <a href="https://github.com/MarcinMoskala/KotlinAndroidViewBindings" target="_blank">здесь</a>.</p>
  <p>Здесь простой подход: легко тестировать и еще легче представить элементы представления как параметры через интерфейс, а не функции.</p>
  <p>Стоит отметить, что с точки зрения презентера ничего не изменилось.</p>
  <h2>Model View View-Model</h2>
  <p>Существует другой способ биндинга: вместо привязывания представления к интерфейсу мы привязываем элементы представления к параметрам view-модели — такая архитектура называется MVVM. В нашем примере поля email, password и разметка определены с помощью связываний. Когда мы меняем параметры в нашей модели, в разметку тоже вносятся изменения.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/getpro/habr/post_images/7e0/ab4/4cd/7e0ab44cd6c4fea03809bf4ec031ac48.png" width="1518" />
  </figure>
  <p>ViewModel’и просты для написания тестов, потому что они не требуют написания mock-объектов — потому что вы меняете свой собственный элемент, а потом проверяете, как он изменился.</p>
  <h2>Model View Intent</h2>
  <p>Еще один элемент, который можно ввести в архитектуре, обычно называется MVI.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/getpro/habr/post_images/042/4db/582/0424db5822508cb000eda3038610f6d3.png" width="1504" />
  </figure>
  <p>Если взять какой-либо элемент разметки, например кнопку, то можно сказать, что кнопка ничего не делает кроме того, что производит какие-либо данные, в частности посылает сведения о том, что она нажата или нет.</p>
  <p>В библиотеке <strong>RxJava</strong> то, что создает события, называется observable, то есть кнопка будет являться <strong>observable</strong> в парадигме реактивного программирования.</p>
  <p>А вот TextView только отображает какой-либо текст и никаких данных не создает. В RxJava такие элементы, которые только принимают данные, называются <strong>consumer</strong>.</p>
  <p>Также существуют элементы, которые делают и то, и то, т. е. и принимают, и отправляют информацию, например TextEdit. Такой элемент одновременно является и создателем (producer), и приемником (receiver), а в RxJava он называется <strong>subject</strong>.</p>
  <p>При таком подходе все есть поток, и каждый поток начинается с того момента, как какой-либо producer начинает испускать информацию, а заканчивается на каком-либо receiver, который, в свою очередь, информацию принимает. Как результат, приложение можно рассматривать как потоки данных. <strong>Потоки данных — главная идея RxJava</strong>.</p>
  <h2>Заключение</h2>
  <p>Несмотря на то, что внедрение разделения ответственности требует усилий, это хороший способ повысить качество кода в целом, сделать его масштабируемым, легким в понимании и надежным.</p>
  <p>Другие паттерны, такие как MVVM, удаление шаблонного кода, MVI могут еще сильнее улучшить масштабируемость, но сделают проект зависимым от RxJava.</p>
  <p>Также следует помнить, что можно выбрать всего лишь часть из этих элементов и сконфигурировать для конечного приложения в зависимости от задач.</p>
  <p>Все исходники можно найти <a href="https://github.com/MarcinMoskala/MVTest" target="_blank">здесь</a>.</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/post/344184/" target="_blank">Паттерны разработки: MVC vs MVP vs MVVM vs MVI</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@skillbranch/LOeNULQ8y</guid><link>https://teletype.in/@skillbranch/LOeNULQ8y?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/LOeNULQ8y?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>Редактор кода на Android: часть 2</title><pubDate>Sun, 09 Aug 2020 09:29:40 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/05/a6/05a67155-8a83-42b2-970d-750b8b11207b.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/33/22/3322baf5-1551-45c7-83f0-46368c9cb0fb.png"></img>Во второй части мы продолжим разрабатывать наш редактор кода и добавим в него автодополнение и подсветку ошибок, а также поговорим, почему любой редактор кода на EditText будет лагать.]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://teletype.in/files/33/22/3322baf5-1551-45c7-83f0-46368c9cb0fb.png" width="1920" />
  </figure>
  <p>Во второй части мы продолжим разрабатывать наш редактор кода и добавим в него автодополнение и подсветку ошибок, а также поговорим, почему <strong>любой</strong> редактор кода на <code>EditText</code> будет лагать.</p>
  <p>Перед дальнейшим прочтением настоятельно рекомендую ознакомиться с <a href="https://teletype.in/@skillbranch/ganUh-JNj" target="_blank">первой частью</a>.</p>
  <h2>Вступление</h2>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/k7/jb/80/k7jb80b1ecbhvyvfzihzwa3ixkq.jpeg" width="280" />
  </figure>
  <p>Для начала давайте вспомним, на чем мы остановились в <a href="https://habr.com/ru/post/509300/" target="_blank">прошлой части</a>. Мы написали оптимизированную подсветку синтаксиса, которая парсит текст в фоне и раскрашивает только его видимую часть, а также добавили нумерацию строк (хоть и без андройдовских переносов на новую строку, но всё же).</p>
  <p>В этой части мы добавим автодополнение кода и подсветку ошибок.</p>
  <h2>Автодополнение кода</h2>
  <p>Для начала представим, как это должно работать:</p>
  <ol>
    <li>Пользователь пишет слово.</li>
    <li>После ввода <strong>N</strong> первых символов появляется окошко с подсказками.</li>
    <li>При нажатии на подсказку слово автоматически «допечатывается».</li>
    <li>Окошко с подсказками закрывается, и курсор переносится в конец слова.</li>
    <li>Если пользователь сам ввел слово, отображаемое в подсказке, то окошко с подсказками должно автоматически закрыться.</li>
  </ol>
  <p>Ничего не напоминает? В андроиде уже есть компонент с точно такой же логикой — <a href="https://developer.android.com/reference/android/widget/MultiAutoCompleteTextView" target="_blank"><code>MultiAutoCompleteTextView</code></a>, поэтому писать костыли с <a href="https://developer.android.com/reference/android/widget/PopupWindow" target="_blank"><code>PopupWindow</code></a> нам не придется (их уже написали за нас).</p>
  <p>Первым шагом поменяем родителя у нашего класса:</p>
  <pre>class TextProcessor @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.autoCompleteTextViewStyle
) : MultiAutoCompleteTextView(context, attrs, defStyleAttr)</pre>
  <p>Теперь нам нужно написать <a href="https://developer.android.com/reference/android/widget/ArrayAdapter" target="_blank"><code>ArrayAdapter</code></a>, который будет отображать найденные результаты. Полного кода адаптера не будет, примеры реализации можно найти в интернете. Но на моменте с фильтрацией я всё-таки остановлюсь.</p>
  <p>Чтобы <code>ArrayAdapter</code> мог понимать, какие подсказки нужно отобразить, нам нужно переопределить метод <code>getFilter</code>:</p>
  <pre>override fun getFilter(): Filter {
    return object : Filter() {

        private val suggestions = mutableListOf&lt;String&gt;()

        override fun performFiltering(constraint: CharSequence?): FilterResults {
            //...
        }

        override fun publishResults(constraint: CharSequence?, results: FilterResults) {
            clear() // необходимо очистить старый список
            addAll(suggestions)
            notifyDataSetChanged()
        }
    }
}</pre>
  <p>И в методе <code>performFiltering</code> наполнить список <code>suggestions</code> из слов, основываясь на слове, которое начал вводить пользователь (содержится в переменной <code>constraint</code>).</p>
  <h3>Откуда взять данные перед фильтрацией?</h3>
  <p>Тут всё зависит от вас — можно использовать какой-нибудь интерпретатор для подбора только валидных вариантов, либо сканировать весь текст при открытии файла. Для простоты примера я буду использовать уже готовый список вариантов автодополнения:</p>
  <pre>private val staticSuggestions = mutableListOf(
    &quot;function&quot;,
    &quot;return&quot;,
    &quot;var&quot;,
    &quot;const&quot;,
    &quot;let&quot;,
    &quot;null&quot;
    ...
)

...

override fun performFiltering(constraint: CharSequence?): FilterResults {
    val filterResults = FilterResults()
    val input = constraint.toString()
    suggestions.clear() // очищаем старый список
    for (suggestion in staticSuggestions) {
        if (suggestion.startsWith(input, ignoreCase = true) &amp;&amp; 
            !suggestion.equals(input, ignoreCase = true)) {
            suggestions.add(suggestion)
        }
    }
    filterResults.values = suggestions
    filterResults.count = suggestions.size
    return filterResults
}</pre>
  <p>Логика фильтрации тут довольно примитивная — проходимся по всему списку и, игнорируя регистр, сравниваем начало строки.</p>
  <p>Установили адаптер, пишем текст — не работает. Что не так? По первой ссылке в гугле натыкаемся на ответ, в котором говорится, что мы забыли установить <a href="https://developer.android.com/reference/android/widget/MultiAutoCompleteTextView.Tokenizer" target="_blank"><code>Tokenizer</code></a>.</p>
  <h3>Для чего нужен Tokenizer?</h3>
  <p>Говоря простым языком, <code>Tokenizer</code> помогает <code>MultiAutoCompleteTextView</code> понять, после какого введенного символа можно считать ввод слова завершенным. Также у него есть готовая реализация в виде <a href="https://developer.android.com/reference/android/widget/MultiAutoCompleteTextView.CommaTokenizer" target="_blank"><code>CommaTokenizer</code></a> с разделением слов на запятые, что в данном случае нам не подходит.</p>
  <p>Что ж, раз <code>CommaTokenizer</code> нас не устраивает, тогда напишем свой:</p>
  <pre>class SymbolsTokenizer : MultiAutoCompleteTextView.Tokenizer {

    companion object {
        private const val TOKEN = &quot;!@#$%^&amp;*()_+-={}|[]:;&#x27;&lt;&gt;/&lt;.? \r\n\t&quot;
    }

    override fun findTokenStart(text: CharSequence, cursor: Int): Int {
        var i = cursor
        while (i &gt; 0 &amp;&amp; !TOKEN.contains(text[i - 1])) {
            i--
        }
        while (i &lt; cursor &amp;&amp; text[i] == &#x27; &#x27;) {
            i++
        }
        return i
    }

    override fun findTokenEnd(text: CharSequence, cursor: Int): Int {
        var i = cursor
        while (i &lt; text.length) {
            if (TOKEN.contains(text[i - 1])) {
                return i
            } else {
                i++
            }
        }
        return text.length
    }

    override fun terminateToken(text: CharSequence): CharSequence = text
}</pre>
  <p><strong>Разбираемся:</strong><br /><code>TOKEN</code> — строка с символами, которые отделяют одно слово от другого. В методах <code>findTokenStart</code> и <code>findTokenEnd</code> мы проходимся по тексту в поисках этих самых отделяющих символов. Метод <code>terminateToken</code> позволяет вернуть измененный результат, но нам он не нужен, поэтому просто возвращаем текст без изменений.</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/5a/jm/ds/5ajmds7nuntnzejvhl62fjjldpg.jpeg" width="311" />
  </figure>
  <p>Ещё я предпочитаю добавлять задержку на ввод в 2 символа перед отображением списка:</p>
  <pre>textProcessor.threshold = 2</pre>
  <p>Устанавливаем, запускаем, пишем текст — работает! Вот только почему-то окошко с подсказками странно себя ведет — отображается во всю ширину, высота у него маленькая, да и по идее оно ведь должно появляться под курсором. Как будем фиксить?</p>
  <h4>Исправляем визуальные недостатки</h4>
  <p>Вот тут и начинается самое интересное, ведь API позволяет нам изменять не только размеры окна, но и его положение.</p>
  <p>Для начала определимся с размерами. На мой взгляд, наиболее удобным вариантом будет окошко размером с половину от высоты и ширины экрана, но т. к. размер нашей <code>View</code> изменяется в зависимости от состояния клавиатуры, подбирать размеры будем в методе <code>onSizeChanged</code>:</p>
  <pre>override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    updateSyntaxHighlighting()
    dropDownWidth = w * 1 / 2
    dropDownHeight = h * 1 / 2
}</pre>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/8r/cy/xd/8rcyxdh5sunmxjrtmntjb_90oam.png" width="295.24355300859594" />
  </figure>
  <p>Выглядеть стало лучше, но не сильно. Мы хотим добиться, чтобы окошко появлялось под курсором и перемещалось вместе с ним во время редактирования.</p>
  <p>Если с перемещением по <strong>X</strong> всё довольно просто — берем координату начала буквы и устанавливаем это значение в <code>dropDownHorizontalOffset</code>, то с подбором высоты будет сложнее.</p>
  <p>Гугля про свойства шрифтов, можно наткнуться на <a href="https://stackoverflow.com/a/27631737/4405457" target="_blank">вот такой пост</a>. Картинка, которую прикрепил автор, наглядно показывает, какими свойствами мы можем воспользоваться для вычисления вертикальной координаты.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/br/zj/ep/brzjepbqxlsdbtcrrh-z58hkc7q.png" width="1543" />
  </figure>
  <p>Судя по картинке, <strong>Baseline</strong> — это то, что нам нужно. Именно на этом уровне и должно появляться окошко с вариантами автодополнения.</p>
  <p>Теперь напишем метод, который будем вызывать при изменении текста в <code>onTextChanged</code>:</p>
  <pre>private fun onPopupChangePosition() {
    val line = layout.getLineForOffset(selectionStart) // строка с курсором
    val x = layout.getPrimaryHorizontal(selectionStart) // координата курсора
    val y = layout.getLineBaseline(line) // тот самый baseline

    val offsetHorizontal = x + gutterWidth // нумерация строк - тоже часть отступа
    dropDownHorizontalOffset = offsetHorizontal.toInt()

    val offsetVertical = y - scrollY // -scrollY чтобы не &quot;заезжать&quot; за экран
    dropDownVerticalOffset = offsetVertical
}</pre>
  <p>Вроде ничего не забыли — смещение по <strong>X</strong> работает, но смещение по <strong>Y</strong> рассчитывается неправильно. Это потому что мы не указали <a href="https://developer.android.com/reference/android/widget/AutoCompleteTextView#attr_android:dropDownAnchor" target="_blank"><code>dropDownAnchor</code></a> в разметке:</p>
  <pre>android:dropDownAnchor=&quot;@id/toolbar&quot;</pre>
  <p>Указав <code>Toolbar</code> в качестве <code>dropDownAnchor</code>, мы даём виджету понять, что выпадающий список будет отображаться <strong>под</strong> ним.</p>
  <p>Теперь если мы начнем редактировать текст, то всё будет работать, но со временем мы заметим — если окошко не помещается под курсором, оно переносится вверх с огромным отступом, что выглядит некрасиво. Самое время написать костыль:</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/oc/0p/kn/oc0pkntbfin2nlh284dfzjbyzzo.gif" width="257" />
  </figure>
  <pre>val offset = offsetVertical + dropDownHeight
if (offset &lt; getVisibleHeight()) {
    dropDownVerticalOffset = offsetVertical
} else {
    dropDownVerticalOffset = offsetVertical - dropDownHeight
}

...

private fun getVisibleHeight(): Int {
    val rect = Rect()
    getWindowVisibleDisplayFrame(rect)
    return rect.bottom - rect.top
}</pre>
  <p>Нам не нужно изменять отступ, если сумма <code>offsetVertical + dropDownHeight</code> меньше видимой высоты экрана, ведь в таком случае окошко помещается <strong>под</strong> курсором. Но если всё-таки больше, то вычитаем из отступа <code>dropDownHeight</code> — так оно поместится <strong>над</strong> курсором без огромного отступа, который добавляет сам виджет.</p>
  <p><strong>P.S.</strong> На гифке можно заметить промаргивания клавиатуры, и, честно говоря, я не знаю, как это исправить, поэтому если у вас есть решение — пишите.</p>
  <h2>Подсветка ошибок</h2>
  <p>С подсветкой ошибок всё гораздо проще, чем кажется. Т.к. сами мы напрямую не можем определять синтаксические ошибки в коде — будем использовать стороннюю библиотеку-парсер. Т.к. я пишу редактор для JavaScript, мой выбор пал на <a href="https://github.com/mozilla/rhino" target="_blank">Rhino</a> — популярный JavaScript-движок, который проверен временем и всё ещё поддерживается.</p>
  <h4>Как парсить будем?</h4>
  <p>Запуск Rhino — довольно тяжелая операция, поэтому запускать парсер после каждого введенного символа (как мы делали с подсветкой) — вообще не вариант. Для решения этой проблемы я буду использовать библиотеку <a href="https://github.com/JakeWharton/RxBinding" target="_blank">RxBinding</a>, а для тех, кто не хочет тащить в проект RxJava, можно попробовать <a href="https://gist.github.com/demixdn/1267fa215824e2d5111e5321a7184721" target="_blank">подобные</a> варианты.</p>
  <p>Оператор <a href="http://reactivex.io/documentation/operators/debounce.html" target="_blank"><code>debounce</code></a> поможет нам добиться желаемого, а если вы с ним не знакомы, то советую почитать вот <a href="https://habr.com/ru/post/345278/" target="_blank">эту статью</a>.</p>
  <pre>textProcessor.textChangeEvents()
    .skipInitialValue()
    .debounce(1500, TimeUnit.MILLISECONDS)
    .filter { it.text.isNotEmpty() }
    .distinctUntilChanged()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribeBy {
        // Запуск парсера будет тут
    }
    .disposeOnFragmentDestroyView()</pre>
  <p>Теперь напишем модель, которую нам будет возвращать парсер:</p>
  <pre>data class ParseResult(val exception: RhinoException?)</pre>
  <p>Предлагаю использовать такую логику: если ошибок не найдено, то <code>exception</code> будет <code>null</code>. В противном случае мы получим объект <a href="https://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/rhinoexception" target="_blank"><code>RhinoException</code></a>, который содержит в себе всю необходимую информацию — номер строки, сообщение об ошибке, StackTrace и т. д.</p>
  <p>Ну и собственно, сам парсинг:</p>
  <pre>// Это должно выполняться в фоне!
val context = Context.enter() // org.mozilla.javascript.Context
context.optimizationLevel = -1
context.maximumInterpreterStackDepth = 1
try {
    val scope = context.initStandardObjects()

    context.evaluateString(scope, sourceCode, fileName, 1, null)
    return ParseResult(null)
} catch (e: RhinoException) {
    return ParseResult(e)
} finally {
    Context.exit()
}</pre>
  <p><strong>Разбираемся:</strong><br />Самое главное тут — это метод <code>evaluateString</code>. Он позволяет запустить код, который мы передали в качестве строки <code>sourceCode</code>. В <code>fileName</code> указывается имя файла — оно будет отображаться в ошибках, единица — номер строки для начала отсчета, последний аргумент — это security domain, но он нам не нужен, поэтому ставим <code>null</code>.</p>
  <h3>optimizationLevel и maximumInterpreterStackDepth</h3>
  <p>Параметр <code>optimizationLevel</code> со значением от <strong>1</strong> до <strong>9</strong> позволяет включить определенные «оптимизации» кода (data flow analysis, type flow analysis и т. д.), что превратит простую проверку синтаксических ошибок в очень длительную операцию, а нам это ни к чему.</p>
  <p>Если же использовать его со значением <strong>0</strong>, то все эти «оптимизации» применяться не будут. Однако, если я правильно понял, Rhino по-прежнему будет использовать часть ресурсов, ненужных для простой проверки ошибок, а значит, нам это не подходит.</p>
  <p>Остаётся только отрицательное значение — указав <strong>-1</strong>, мы активируем режим «интерпретатора», а это именно то, что нам нужно. В <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Optimization" target="_blank">документации</a> сказано, что это самый быстрый и экономичный вариант работы Rhino.</p>
  <p>Параметр <code>maximumInterpreterStackDepth</code> позволяет ограничить количество рекурсивных вызовов.</p>
  <p>Представим, что будет, если не указать этот параметр:</p>
  <p>1. Пользователь напишет следующий код:</p>
  <pre>function recurse() { recurse(); } recurse();</pre>
  <p>2. Rhino запустит код, и через секунду наше приложение вылетит с <code>OutOfMemoryError</code>. Конец.</p>
  <h4>Отображение ошибок</h4>
  <p>Как я говорил ранее, как только мы получим <code>ParseResult</code>, содержащий <code>RhinoException</code>, у нас появится весь необходимый набор данных для отображения, в том числе и номер строки — нужно лишь вызвать метод <a href="https://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/rhinoexception#lineNumber()" target="_blank"><code>lineNumber()</code></a>.</p>
  <p>Теперь напишем спан с красной волнистой линией, который я скопировал на <a href="https://stackoverflow.com/a/36029433/4405457" target="_blank">StackOverflow</a>. Кода много, но логика простая — рисуем две короткие красные линии под разным углом.</p>
  <pre>class ErrorSpan(
    private val lineWidth: Float = 1 * Resources.getSystem().displayMetrics.density + 0.5f,
    private val waveSize: Float = 3 * Resources.getSystem().displayMetrics.density + 0.5f,
    private val color: Int = Color.RED
) : LineBackgroundSpan {

    override fun drawBackground(
        canvas: Canvas,
        paint: Paint,
        left: Int,
        right: Int,
        top: Int,
        baseline: Int,
        bottom: Int,
        text: CharSequence,
        start: Int,
        end: Int,
        lineNumber: Int
    ) {
        val width = paint.measureText(text, start, end)
        val linePaint = Paint(paint)
        linePaint.color = color
        linePaint.strokeWidth = lineWidth

        val doubleWaveSize = waveSize * 2
        var i = left.toFloat()
        while (i &lt; left + width) {
            canvas.drawLine(i, bottom.toFloat(), i + waveSize, bottom - waveSize, linePaint)
            canvas.drawLine(i + waveSize, bottom - waveSize, i + doubleWaveSize, bottom.toFloat(), linePaint)
            i += doubleWaveSize
        }
    }
}</pre>
  <p>Теперь можно написать метод установки спана на проблемную строку:</p>
  <pre>fun setErrorLine(lineNumber: Int) {
    if (lineNumber in 0 until lineCount) {
        val lineStart = layout.getLineStart(lineNumber)
        val lineEnd = layout.getLineEnd(lineNumber)
        text.setSpan(ErrorSpan(), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    }
}</pre>
  <p>Важно помнить, что т. к. результат приходит с задержкой, пользователь может успеть стереть пару строк кода, и тогда <code>lineNumber</code> может оказаться невалидным.</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/dt/ca/u_/dtcau_aadqxl5tbptmvzj3woqx4.png" width="325" />
  </figure>
  <p>Поэтому, чтобы не получить <code>IndexOutOfBoundsException</code>, мы добавляем проверку в самом начале. Ну а дальше по знакомой схеме вычисляем первый и последний символ строки, после чего устанавливаем спан.</p>
  <p>Главное — не забыть очистить текст от уже установленных спанов в <code>afterTextChanged</code>:</p>
  <pre>fun clearErrorSpans() {
    val spans = text.getSpans&lt;ErrorSpan&gt;(0, text.length)
    for (span in spans) {
        text.removeSpan(span)
    }
}</pre>
  <h2>Почему редакторы кода лагают?</h2>
  <p>За две статьи мы написали неплохой редактор кода, наследуясь от <code>EditText</code> и <code>MultiAutoCompleteTextView</code>, но производительностью при работе с большими файлами похвастаться не можем.</p>
  <p>Если открыть тот же <a href="https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/TextView.java" target="_blank">TextView.java</a> на <strong>9к+</strong> строк кода, то <strong>любой</strong> текстовый редактор, написанный по такому же принципу, как наш, будет лагать.</p>
  <p><strong>Q:</strong> А почему QuickEdit тогда не лагает?<br /><strong>A:</strong> Потому что под капотом он не использует ни <code>EditText</code>, ни <code>TextView</code>.</p>
  <p>В последнее время набирают популярность редакторы кода на CustomView (<a href="https://github.com/Rose2073/CodeEditor" target="_blank">вот</a> и <a href="https://github.com/fengdeyingzi/CodeEditText" target="_blank">вот</a>, ну или <a href="https://github.com/TIIEHenry/CodeEditor" target="_blank">вот</a> и <a href="https://github.com/Lzhiyong/TextEditor" target="_blank">вот</a>, их очень много). Исторически так сложилось, что TextView имеет слишком много лишней логики, которая не нужна редакторам кода. Первое, что приходит на ум, — <a href="https://developer.android.com/guide/topics/text/autofill-optimize" target="_blank">Autofill</a>, <a href="https://developer.android.com/guide/topics/ui/look-and-feel/emoji-compat" target="_blank">Emoji</a>, <a href="https://developer.android.com/reference/android/widget/TextView.html#setCompoundDrawablesWithIntrinsicBounds(int,%20int,%20int,%20int)" target="_blank">Compound Drawables</a>, <a href="https://developer.android.com/reference/android/widget/TextView#attr_android:autoLink" target="_blank">кликабельные ссылки</a> и т. д.</p>
  <p>Если я правильно понял, авторы библиотек просто избавились от всего этого, в следствие чего получили текстовый редактор, способный работать с файлами в миллион строк без особой нагрузки на UI Thread. (Хотя частично могу ошибаться, в исходниках не сильно разобрался)</p>
  <p>Есть ещё один вариант, но на мой взгляд менее привлекательный — редакторы кода на <a href="https://developer.android.com/reference/android/webkit/WebView" target="_blank">WebView</a> (<a href="https://github.com/jecelyin/920-text-editor-v2" target="_blank">вот</a> и <a href="https://github.com/deadlyjack/code-editor" target="_blank">вот</a>, их тоже очень много). Мне они не нравятся, потому что UI на WebView выглядит хуже, чем нативный, да и редакторам на CustomView они также проигрывают по производительности.</p>
  <h2>Заключение</h2>
  <p>Если ваша задача — написать редактор кода и выйти в топ Google Play, то не тратьте время и возьмите готовую библиотеку на CustomView. Если же вы хотите получить уникальный опыт — пишите всё сами, используя нативные виджеты.</p>
  <p>Также оставлю ссылку на исходники моего редактора кода на <a href="https://github.com/massivemadness/ModPE-IDE" target="_blank">GitHub</a>, там вы найдёте не только те фичи, о которых я рассказал за эти две статьи, но и много других, которые остались без внимания.</p>
  <p>Спасибо!</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/post/509468/" target="_blank">Редактор кода на Android: часть 2</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@skillbranch/ganUh-JNj</guid><link>https://teletype.in/@skillbranch/ganUh-JNj?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/ganUh-JNj?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>Редактор кода на Android: часть 1</title><pubDate>Tue, 04 Aug 2020 10:59:49 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/8a/62/8a62cf58-86cc-4d3e-ac16-c9a74b16f22b.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/c5/e6/c5e6bc22-6438-47db-b8b2-984d52e74e2f.png"></img>Перед тем как закончить работу над своим редактором кода я много раз наступал на грабли, наверное декомпилировал десятки похожих приложений, и в данной серии статей я расскажу о том, чему научился, каких ошибок можно избежать и много других интересных вещей. (Источник указан с конце статьи)]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://teletype.in/files/c5/e6/c5e6bc22-6438-47db-b8b2-984d52e74e2f.png" width="1920" />
  </figure>
  <p>Перед тем как закончить работу над своим редактором кода я много раз наступал на грабли, наверное декомпилировал десятки похожих приложений, и в данной серии статей я расскажу о том, чему научился, каких ошибок можно избежать и много других интересных вещей. <em>(Источник указан с конце статьи)</em></p>
  <h2>Вступление</h2>
  <p>Привет всем! Судя из названия, вполне понятно, о чем будет идти речь, но всё же я должен вставить свои пару слов перед тем, как перейти к коду.</p>
  <p>Я решил разделить статью на 2 части, в первой мы поэтапно напишем оптимизированную подсветку синтаксиса и нумерацию строк, а во второй добавим автодополнение кода и подсветку ошибок.</p>
  <p>Для начала составим список того, что наш редактор должен уметь:</p>
  <ul>
    <li>Подсвечивать синтаксис</li>
    <li>Отображать нумерацию строк</li>
    <li>Показывать варианты автодополнения <em>(расскажу во второй части)</em></li>
    <li>Подсвечивать синтаксические ошибки <em>(расскажу во второй части)</em></li>
  </ul>
  <p>Это далеко не весь список того, какими свойствами должен обладать современный редактор кода, но именно об этом я хочу рассказать в этой небольшой серии статей.</p>
  <h2>MVP — простой текстовый редактор</h2>
  <p>На данном этапе проблем возникнуть не должно — растягиваем <code>EditText</code> на весь экран, указываем <code>gravity</code>, прозрачный <code>background</code>, чтобы убрать полосу снизу, размер шрифта, цвет текста и т.д. Я люблю начинать с визуальной части, так мне становится проще понять, чего не хватает в приложении и над какими деталями ещё стоит поработать.</p>
  <p>На этом этапе я также сделал загрузку/сохранение файлов в память. Код приводить не буду, в интернете переизбыток примеров работы с файлами.</p>
  <h2>Подсветка синтаксиса</h2>
  <p>Как только мы ознакомились с требованиями к редактору, пора переходить к самому интересному.</p>
  <p>Очевидно, чтобы контролировать весь процесс — реагировать на ввод, отрисовывать номера строк, нам придется писать <code>CustomView</code>, наследуясь от <code>EditText</code>. Накидываем <a href="https://developer.android.com/reference/android/text/TextWatcher" target="_blank"><code>TextWatcher</code></a>, чтобы слушать изменения в тексте и переопределяем метод <code>afterTextChanged</code>, в котором и будем вызывать метод, отвечающий за подсветку:</p>
  <pre>class TextProcessor @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.editTextStyle
) : EditText(context, attrs, defStyleAttr) {

    private val textWatcher = object : TextWatcher {
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
        override fun afterTextChanged(s: Editable?) {
            syntaxHighlight()
        }
    }

    private fun syntaxHighlight() {
        // Тут будем подсвечивать текст
    }
}</pre>
  <p><strong>Q:</strong> Почему мы используем <code>TextWatcher</code> как переменную, ведь можно реализовать интерфейс прямо в классе?<br /><strong>A:</strong> Так уж получилось, что у <code>TextWatcher</code> есть метод, который конфликтует c уже существующим методом у <code>TextView</code>:</p>
  <pre>// Метод TextWatcher
fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int)

// Метод TextView
fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int)</pre>
  <p>Оба этих метода имеют одинаковое название и одинаковые аргументы, да и смысл вроде у них тот же, но проблема в том, что метод <code>onTextChanged</code> у <code>TextView</code> вызовется вместе с <code>onTextChanged</code> у <code>TextWatcher</code>. Если проставим логи в теле метода, то увидим, что <code>onTextChanged</code> вызовется дважды:</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/tg/px/px/tgpxpxtz_820e_dtyojkly27ukc.png" width="1078.1851851851852" />
  </figure>
  <p>Это очень критично, если мы планируем добавлять функционал Undo/Redo. Также нам может понадобится момент, в котором не будут работать слушатели, в котором мы сможем очищать стэк с изменениями текста. Мы ведь не хотим, чтобы после открытия нового файла можно было нажать Undo и получить совершенно другой текст. Хоть об Undo/Redo в этой статье говориться не будет, важно учитывать этот момент.</p>
  <p>Соответственно, чтобы избежать такой ситуации, можно использовать свой метод установки текста вместо стандартного <code>setText</code>:</p>
  <pre>fun processText(newText: String) {
    removeTextChangedListener(textWatcher)
    // undoStack.clear()
    // redoStack.clear()
    setText(newText)
    addTextChangedListener(textWatcher)
}</pre>
  <p>Но вернёмся к подсветке.</p>
  <p>Во многих языках программирования есть такая замечательная штука как <a href="https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%B3%D1%83%D0%BB%D1%8F%D1%80%D0%BD%D1%8B%D0%B5_%D0%B2%D1%8B%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F" target="_blank">RegEx</a> – это инструмент, позволяющий искать совпадения текста в строке. Рекомендую как минимум ознакомится с его базовыми возможностями, потому что рано или поздно любому программисту может понадобиться «вытащить» какой-либо кусочек информации из текста.</p>
  <p>Сейчас нам важно знать только две вещи:</p>
  <ol>
    <li><strong>Pattern</strong> определяет, что конкретно нам нужно найти в тексте.</li>
    <li><strong>Matcher</strong> будет пробегать по всему тексту в попытках найти то, что мы указали в <strong>Pattern</strong>.</li>
  </ol>
  <p>Может не совсем корректно описал, но принцип работы такой.</p>
  <p>Т.к. я пишу редактор для JavaScript, вот небольшой паттерн с ключевыми словами языка:</p>
  <pre>private val KEYWORDS = Pattern.compile(
    &quot;\\b(function|var|this|if|else|break|case|try|catch|while|return|switch)\\b&quot;
)</pre>
  <p>Конечно, слов тут должно быть гораздо больше, а ещё нужны паттерны для комментариев, строк, чисел и т.д. но моя задача заключается в демонстрации принципа, по которому можно найти нужный контент в тексте.</p>
  <p>Далее с помощью <strong>Matcher</strong> мы пройдёмся по всему тексту и установим спаны:</p>
  <pre>private fun syntaxHighlight() {
    val matcher = KEYWORDS.matcher(text)
    matcher.region(0, text.length)
    while (matcher.find()) {
        text.setSpan(
            ForegroundColorSpan(Color.parseColor(&quot;#7F0055&quot;)),
            matcher.start(),
            matcher.end(),
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )
    }
}</pre>
  <p><strong>Поясню:</strong> мы получаем объект <strong>Matcher</strong> у <strong>Pattern</strong> и указываем ему область для поиска в символах (соответственно, с 0 по <code>text.length</code> – это весь текст). Далее вызов <code>matcher.find()</code> вернёт <code>true</code>, если в тексте было найдено совпадение, а с помощью вызовов <code>matcher.start()</code> и <code>matcher.end()</code> мы получим позиции начала и конца совпадения в тексте. Зная эти данные, мы можем использовать метод <a href="https://developer.android.com/guide/topics/text/spans" target="_blank"><code>setSpan</code></a> для раскраски определённых участков текста.</p>
  <p>Существует много видов спанов, но для перекраски текста обычно используется <a href="https://developer.android.com/reference/android/text/style/ForegroundColorSpan" target="_blank"><code>ForegroundColorSpan</code></a>.</p>
  <h3>Итак, запускаем!</h3>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/ok/8l/ah/ok8lahswjxkfp1co-lmh4_gr1xi.jpeg" width="280" />
  </figure>
  <p>Результат соответствует ожиданиям ровно до того момента, пока мы не начнём редактировать большой файл (на скриншоте файл в ~1000 строк).</p>
  <p>Дело в том, что метод <code>setSpan</code> работает медленно, сильно нагружая UI Thread, а учитывая, что метод <code>afterTextChanged</code> вызывается после каждого введенного символа, писать код становится одним мучением.</p>
  <h3>Поиск решения</h3>
  <p>Первое, что приходит в голову, – вынести тяжелую операцию в фоновый поток. Но тяжелая операция тут – это <code>setSpan</code> по всему тексту, а не регулярки. (Думаю, можно не объяснять, почему нельзя вызывать <code>setSpan</code> из фонового потока).</p>
  <p>Немного поискав тематические статьи, узнаем, что если мы хотим добиться плавности, придётся подсвечивать <strong>только видимую часть</strong> текста.</p>
  <p>Точно! Так и сделаем! Вот только… как?</p>
  <h3>Оптимизация</h3>
  <p>Хоть я и упомянул, что нас заботит только производительность метода <code>setSpan</code>, всё же рекомендую выносить работу RegEx в фоновой поток, чтобы добиться максимальной плавности.</p>
  <p>Нам нужен класс, который будет в фоне обрабатывать весь текст и возвращать список спанов.<br />Конкретной реализации приводить не буду, но если кому интересно, то я использую <a href="https://developer.android.com/reference/android/os/AsyncTask" target="_blank"><code>AsyncTask</code></a>, работающий на <a href="https://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor" target="_blank"><code>ThreadPoolExecutor</code></a>. (Да-да, AsyncTask в 2020!)</p>
  <p>Нам главное, чтобы выполнялась такая логика:</p>
  <ol>
    <li>В <code>beforeTextChanged</code> <strong>останавливаем</strong> Task, который парсит текст.</li>
    <li>В <code>afterTextChanged</code> <strong>запускаем</strong> Task, который парсит текст.</li>
    <li>По окончании своей работы Task должен вернуть список спанов в <code>TextProcessor</code>, который в свою очередь подсветит только видимую часть.</li>
  </ol>
  <p>И да, спаны тоже будем писать свои собственные:</p>
  <pre>data class SyntaxHighlightSpan(
    private val color: Int,
    val start: Int,
    val end: Int
) : CharacterStyle() {

    // можно заморочиться и добавить italic, например, только для комментариев
    override fun updateDrawState(textPaint: TextPaint?) {
        textPaint?.color = color
    }
}</pre>
  <p>Таким образом, код редактора превращается в нечто подобное:</p>
  <pre>class TextProcessor @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.editTextStyle
) : EditText(context, attrs, defStyleAttr) {

    private val textWatcher = object : TextWatcher {
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            cancelSyntaxHighlighting()
        }
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
        override fun afterTextChanged(s: Editable?) {
            syntaxHighlight()
        }
    }

    private var syntaxHighlightSpans: List&lt;SyntaxHighlightSpan&gt; = emptyList()

    private var javaScriptStyler: JavaScriptStyler? = null

    fun processText(newText: String) {
        removeTextChangedListener(textWatcher)
        // undoStack.clear()
        // redoStack.clear()
        setText(newText)
        addTextChangedListener(textWatcher)
        // syntaxHighlight()
    }

    private fun syntaxHighlight() {
        javaScriptStyler = JavaScriptStyler()
        javaScriptStyler?.setSpansCallback { spans -&gt;
            syntaxHighlightSpans = spans
            updateSyntaxHighlighting()
        }
        javaScriptStyler?.runTask(text.toString())
    }

    private fun cancelSyntaxHighlighting() {
        javaScriptStyler?.cancelTask()
    }

    private fun updateSyntaxHighlighting() {
        // подсветка видимой части будет тут
    }
}</pre>
  <p>Т.к. конкретной реализации обработки в фоне я не показал, представим, что мы написали некий <code>JavaScriptStyler</code>, который в фоне будет делать всё то же самое, что мы делали до этого в UI Thread — пробегать по всему тексту в поисках совпадений и заполнять список спанов, а в конце своей работы возвращать результат в <code>setSpansCallback</code>. В этот момент запустится метод <code>updateSyntaxHighlighting</code>, который пройдётся по списку спанов и отобразит только те, что видны в данный момент на экране.</p>
  <h3>Как понять, какой текст попадает в видимую область?</h3>
  <p>Буду ссылаться на <a href="https://habr.com/ru/post/204248/" target="_blank">эту статью</a>, там автор предлагает использовать примерно такой способ:</p>
  <pre>val topVisibleLine = scrollY / lineHeight
val bottomVisibleLine = topVisibleLine + height / lineHeight + 1 // height - высота View
val lineStart = layout.getLineStart(topVisibleLine)
val lineEnd = layout.getLineEnd(bottomVisibleLine)</pre>
  <p>И он работает! Теперь вынесем <code>topVisibleLine</code> и <code>bottomVisibleLine</code> в отдельные методы и добавим пару дополнительных проверок, на случай если что-то пойдёт не так:</p>
  <pre>private fun getTopVisibleLine(): Int {
    if (lineHeight == 0) {
        return 0
    }
    val line = scrollY / lineHeight
    if (line &lt; 0) {
        return 0
    }
    return if (line &gt;= lineCount) {
        lineCount - 1
    } else line
}

private fun getBottomVisibleLine(): Int {
    if (lineHeight == 0) {
        return 0
    }
    val line = getTopVisibleLine() + height / lineHeight + 1
    if (line &lt; 0) {
        return 0
    }
    return if (line &gt;= lineCount) {
        lineCount - 1
    } else line
}</pre>
  <p>Последнее, что остаётся сделать, — пройтись по полученному списку спанов и раскрасить текст:</p>
  <pre>for (span in syntaxHighlightSpans) {
    val isInText = span.start &gt;= 0 &amp;&amp; span.end &lt;= text.length
    val isValid = span.start &lt;= span.end
    val isVisible = span.start in lineStart..lineEnd
            || span.start &lt;= lineEnd &amp;&amp; span.end &gt;= lineStart
    if (isInText &amp;&amp; isValid &amp;&amp; isVisible)) {
        text.setSpan(
            span,
            if (span.start &lt; lineStart) lineStart else span.start,
            if (span.end &gt; lineEnd) lineEnd else span.end,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )
    }
}</pre>
  <p>Не пугайтесь страшного <code>if</code>&#x27;а, он всего лишь проверяет, попадает ли спан из списка в видимую область.</p>
  <h4>Ну что, работает?</h4>
  <p>Работает, вот только при редактировании текста спаны не обновляются, исправить ситуацию можно, очистив текст от всех спанов перед наложением новых:</p>
  <pre>// Примечание: метод getSpans из библиотеки core-ktx
val textSpans = text.getSpans&lt;SyntaxHighlightSpan&gt;(0, text.length)
for (span in textSpans) {
    text.removeSpan(span)
}</pre>
  <p>Ещё один косяк — после закрытия клавиатуры кусок текста остаётся неподсвеченным. Исправляем:</p>
  <pre>override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    updateSyntaxHighlighting()
}</pre>
  <p>Главное – не забыть указать <a href="https://developer.android.com/guide/topics/manifest/activity-element#wsoft" target="_blank"><code>adjustResize</code></a> в манифесте.</p>
  <h4>Скроллинг</h4>
  <p>Говоря про скроллинг, снова буду ссылаться на <a href="https://habr.com/ru/post/204248/" target="_blank">эту статью</a>. Автор предлагает ждать 500 мс после окончания скроллинга, что противоречит моему чувству прекрасного. Я не хочу дожидаться, пока прогрузится подсветка, я хочу видеть результат моментально.</p>
  <p>Также автор приводит аргумент, что запускать парсер после каждого «проскролленного» пикселя затратно, и я полностью с этим согласен (вообще рекомендую полностью ознакомится с его статьей, она небольшая, но там много интересного). Но дело в том, что у нас <strong>уже</strong> есть готовый список спанов и нам <strong>не нужно</strong> запускать парсер.</p>
  <p>Достаточно вызывать метод, отвечающий за обновление подсветки:</p>
  <pre>override fun onScrollChanged(horiz: Int, vert: Int, oldHoriz: Int, oldVert: Int) {
    super.onScrollChanged(horiz, vert, oldHoriz, oldVert)
    updateSyntaxHighlighting()
}</pre>
  <h2>Нумерация строк</h2>
  <p>Если мы добавим в разметку ещё один <code>TextView</code>, то будет проблематично их между собой связать (например, синхронно обновлять размер текста), да и если у нас большой файл, то придется полностью обновлять текст с номерами после каждой введенной буквы, что не очень круто. Поэтому будем использовать стандартные средства любой <code>CustomView</code> — рисование на <code>Canvas</code> в <code>onDraw</code>, это и быстро, и несложно.</p>
  <p>Для начала определим, что будем рисовать:</p>
  <ul>
    <li>Номера строк</li>
    <li>Вертикальную линию, отделяющую поле ввода от номеров строк</li>
  </ul>
  <p>Предварительно необходимо вычислить и установить <code>padding</code> слева от редактора, чтобы не было конфликтов с напечатанным текстом.</p>
  <p>Для этого напишем функцию, которая будет обновлять отступ перед отрисовкой:</p>
  <pre>private var gutterWidth = 0
private var gutterDigitCount = 0
private var gutterMargin = 4.dpToPx() // отступ от разделителя в пикселях

...

private fun updateGutter() {
    var count = 3
    var widestNumber = 0
    var widestWidth = 0f

    gutterDigitCount = lineCount.toString().length
    for (i in 0..9) {
        val width = paint.measureText(i.toString())
        if (width &gt; widestWidth) {
            widestNumber = i
            widestWidth = width
        }
    }
    if (gutterDigitCount &gt;= count) {
        count = gutterDigitCount
    }
    val builder = StringBuilder()
    for (i in 0 until count) {
        builder.append(widestNumber.toString())
    }
    gutterWidth = paint.measureText(builder.toString()).toInt()
    gutterWidth += gutterMargin
    if (paddingLeft != gutterWidth + gutterMargin) {
        setPadding(gutterWidth + gutterMargin, gutterMargin, paddingRight, 0)
    }
}</pre>
  <p><strong>Пояснение:</strong></p>
  <p>Для начала мы узнаем кол-во строк в <code>EditText</code> (не путать с кол-вом &quot;<code>\n</code>&quot; в тексте) и берем кол-во символов от этого числа. Например, если у нас 100 строк, то переменная <code>gutterDigitCount</code> будет равна 3, потому что в числе 100 ровно 3 символа. Но допустим, у нас всего 1 строка – а значит отступ в 1 символ будет визуально казаться маленьким, и для этого мы используем переменную count, чтобы задать минимально отображаемый отступ в 3 символа, даже если у нас меньше 100 строк кода.</p>
  <p>Эта часть была самой запутанной из всех, но если вдумчиво прочитать несколько раз (поглядывая на код), то всё станет понятно.</p>
  <p>Далее устанавливаем отступ, предварительно вычислив <code>widestNumber</code> и <code>widestWidth</code>.</p>
  <h4>Приступим к рисованию</h4>
  <p>К сожалению, если мы хотим использовать стандартный андроидовский перенос текста на новую строку, то придется поколдовать, что займет у нас много времени и ещё больше кода, которого хватит на целую статью, поэтому, дабы сократить ваше время (и время модератора хабра), мы включим горизонтальный скроллинг, чтобы все строки шли одна за другой:</p>
  <pre>setHorizontallyScrolling(true)</pre>
  <p>Ну а теперь можно приступать к рисованию. Объявим переменные с типом <code>Paint</code>:</p>
  <pre>private val gutterTextPaint = Paint() // Нумерация строк
private val gutterDividerPaint = Paint() // Отделяющая линия</pre>
  <p>Где-нибудь в <code>init</code> блоке установим цвет текста и цвет разделителя. Важно помнить, что если вы поменяете шрифт текста, то шрифт <code>Paint</code>&#x27;а придется применять вручную. Для этого советую переопределить метод <a href="https://developer.android.com/reference/android/widget/TextView#setTypeface(android.graphics.Typeface)" target="_blank"><code>setTypeface</code></a>. Аналогично и с размером текста.</p>
  <p>После чего переопределяем метод <code>onDraw</code>:</p>
  <pre>override fun onDraw(canvas: Canvas?) {
    updateGutter()
    super.onDraw(canvas)
    var topVisibleLine = getTopVisibleLine()
    val bottomVisibleLine = getBottomVisibleLine()
    val textRight = (gutterWidth - gutterMargin / 2) + scrollX
    while (topVisibleLine &lt;= bottomVisibleLine) {
        canvas?.drawText(
            (topVisibleLine + 1).toString(),
            textRight.toFloat(),
            (layout.getLineBaseline(topVisibleLine) + paddingTop).toFloat(),
            gutterTextPaint
        )
        topVisibleLine++
    }
    canvas?.drawLine(
        (gutterWidth + scrollX).toFloat(),
        scrollY.toFloat(),
        (gutterWidth + scrollX).toFloat(),
        (scrollY + height).toFloat(),
        gutterDividerPaint
    )
}</pre>
  <h4>Смотрим на результат</h4>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/k7/jb/80/k7jb80b1ecbhvyvfzihzwa3ixkq.jpeg" width="280" />
  </figure>
  <p>Выглядит круто.</p>
  <p>Что же мы сделали в <code>onDraw</code>? Перед вызовом <code>super</code>-метода мы обновили отступ, после чего отрисовали номера только в видимой области. Ну и под конец провели вертикальную линию, визуально отделяющую нумерацию строк от редактора кода.</p>
  <p>Для красоты можно ещё перекрасить отступ в другой цвет, визуально выделить строку, на которой находится курсор, но это я уже оставлю на ваше усмотрение.</p>
  <h2>Заключение</h2>
  <p>В этой статье мы написали отзывчивый редактор кода с подсветкой синтаксиса и нумерацией строк, а в следующей части добавим удобное автодополнение кода и подсветку синтаксических ошибок прямо во время редактирования.</p>
  <p>Также оставлю ссылку на исходники моего редактора кода на <a href="https://github.com/massivemadness/ModPE-IDE" target="_blank">GitHub</a>. Там вы найдёте не только те фичи, о которых я рассказал в этой статье, но и много других, которые остались без внимания.</p>
  <p>Задавайте вопросы и предлагайте темы для обсуждения <em>(см. источник)</em>, ведь я вполне мог что-то упустить.</p>
  <p>Спасибо!</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/post/509300/" target="_blank">Редактор кода на Android: часть 1</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@skillbranch/mFoiHUuDp</guid><link>https://teletype.in/@skillbranch/mFoiHUuDp?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/mFoiHUuDp?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>Энергопотребление Android-приложений</title><pubDate>Wed, 29 Jul 2020 11:56:26 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/8a/63/8a6329b6-2a9a-4523-a4f3-8cff4884478e.png"></media:content><description><![CDATA[<img src="https://habrastorage.org/webt/eg/4m/nk/eg4mnk3iupcn5osj1rlqypcye7u.jpeg"></img>Ваши пользователи жалуются на то, что приложение очень быстро сажает заряд телефона? Запущенный фоновый сервис внезапно останавливается? Сообщения от FCM не доходят до пользователя? Что связывает эти три серьезных вопроса? Ответ прост — неверно выстроенная работа с энергопотреблением приложения.]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/eg/4m/nk/eg4mnk3iupcn5osj1rlqypcye7u.jpeg" width="1920" />
  </figure>
  <p>Ваши пользователи жалуются на то, что приложение очень быстро сажает заряд телефона? Запущенный фоновый сервис внезапно останавливается? Сообщения от FCM не доходят до пользователя? Что связывает эти три серьезных вопроса? Ответ прост — неверно выстроенная работа с энергопотреблением приложения.</p>
  <p>Давайте разберемся в основных моментах, связанных с этой темой. Возможно, это позволит вам в будущем избежать ошибок, с которыми сталкивалось большинство разработчиков мобильных приложений.</p>
  <p>В интернете огромное количество разрозненной информации, собрать которую в единое общее руководство было одной из основных целей этой статьи <em>(см. источник)</em>.</p>
  <h2>Общая информация</h2>
  <p>В Android есть следующие платформенные фичи для оптимизации энергопотребления:</p>
  <ul>
    <li><a href="https://developer.android.com/training/monitoring-device-state/doze-standby" target="_blank">Doze and App Standby</a></li>
    <li><a href="https://developer.android.com/topic/performance/appstandby" target="_blank">App Standby Buckets</a></li>
    <li><a href="https://developer.android.com/topic/performance/background-optimization#bg-restrict" target="_blank">Background restrictions</a></li>
    <li><a href="https://developer.android.com/topic/performance/power/power-details" target="_blank">Power management restrictions</a></li>
    <li><a href="https://developer.android.com/topic/performance/power/test-power" target="_blank">Testing and troubleshooting</a></li>
  </ul>
  <p>В Android 6 появились две фичи для сохранения заряда батареи за счет управления поведением приложений, когда устройство не на зарядке:</p>
  <ul>
    <li>Doze Mode.</li>
    <li>App Standby.</li>
  </ul>
  <h2>Doze Mode</h2>
  <p>Когда устройство находится в режиме Doze, доступ приложений к определенным ресурсам откладывается до появления окна обслуживания (maintenance window). <a href="https://developer.android.com/topic/performance/power/power-details" target="_blank">Список конкретных ограничений</a>.</p>
  <p>Если пользователь оставляет на какое-то время устройство отключенным от зарядки и с выключенным экраном, то оно переходит в режим Doze. В этом режиме система пытается сохранить заряд батареи, ограничивая доступ приложений к сетевым и ресурсоемким службам, откладывает Jobs, синхронизацию и Alarms.</p>
  <p>Периодически система выходит из режима Doze, чтобы приложения могли выполнить отложенные действия. Во время этого окна обслуживания (maintenance window) система запускает все отложенные синхронизации, Jobs, Alarms и позволяет приложениям получить доступ к сети.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/getpro/habr/post_images/13a/135/bc7/13a135bc7d64042a79cc5e1f50ced5c6.png" width="1600" />
  </figure>
  <p>Со временем система все реже и реже планирует maintenance windows, что помогает снизить расход энергии, когда устройство не на зарядке.</p>
  <p><strong>В режиме Doze к приложениям применяются следующие ограничения:</strong></p>
  <ul>
    <li>Доступ в сеть приостановлен.</li>
    <li>Стандартные <a href="https://developer.android.com/reference/android/app/AlarmManager" target="_blank">AlarmManager</a> откладываются до следующего окна обслуживания.</li>
    <li>Система не сканирует Wi-Fi.</li>
    <li>Система не позволяет запускаться <a href="https://developer.android.com/reference/android/content/AbstractThreadedSyncAdapter" target="_blank">sync adapters</a>.</li>
    <li>Система не позволяет запускаться <a href="https://developer.android.com/reference/android/app/job/JobScheduler" target="_blank">JobScheduler</a><u>.</u></li>
  </ul>
  <p><strong>Чеклист для приложения в режиме Doze:</strong></p>
  <ul>
    <li>Использовать FCM для <a href="https://firebase.google.com/docs/cloud-messaging/android/receive" target="_blank">обмена сообщениями</a>.</li>
    <li>Если пользователь должен сразу увидеть уведомление, то нужно использовать <a href="https://firebase.google.com/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message" target="_blank">FCM с высоким приоритетом</a>.</li>
    <li>Предоставлять достаточное количество информации в сообщении, чтобы избежать последующих запросов в сеть.</li>
    <li>Установить критически оповещения с <a href="https://developer.android.com/reference/android/app/AlarmManager#setAndAllowWhileIdle(int,%20long,%20android.app.PendingIntent)" target="_blank">setAndAllowWhileIdle()</a> and <a href="https://developer.android.com/reference/android/app/AlarmManager#setExactAndAllowWhileIdle(int,%20long,%20android.app.PendingIntent)" target="_blank">setExactAndAllowWhileIdle()</a>.</li>
    <li>Протестировать приложение в режиме Doze.</li>
  </ul>
  <h2>App StandBy, App StandBy Buckets</h2>
  <p>App StandBy позволяет системе определить, что приложение простаивает, когда пользователь не пользуется им активно. App StandBy запускается, когда не выполняется ни одно из следующих условий:</p>
  <ul>
    <li>Пользователь явно запускает приложение.</li>
    <li>Приложение находится на переднем плане (явно или в качестве Foreground service, либо используется другой Activity).</li>
    <li>Приложение генерирует уведомления, которые пользователь видит на экране блокировки или в области уведомлений.</li>
    <li>Приложение является активным приложением администратора устройств.</li>
  </ul>
  <p>Когда устройство подключается к зарядке, система выпускает приложения из режима Standby, что позволяет им выполнять любые задачи. Если устройство не используется в течение длительного периода времени, система предоставляет бездействующим приложениям доступ в сеть примерно раз в день.</p>
  <p>Определение частоты использования отличается у разных производителей, особенно «жестит» Samsung.</p>
  <p>В Android 9 появились новые фичи для управления питанием устройства. Они делятся на две категории:</p>
  <ul>
    <li><a href="https://developer.android.com/about/versions/pie/power#buckets" target="_blank">App standby buckets</a>. Система ограничивает доступ приложения к ресурсам устройства в зависимости от модели поведения пользователя.</li>
    <li><a href="https://developer.android.com/about/versions/pie/power#battery-saver" target="_blank">Battery Saver Improvements</a>. Когда включена функция экономии заряда батареи, система накладывает ограничения на все приложения.</li>
  </ul>
  <p>Эти ограничения применяются ко всем приложениям независимо от их <code>targetSdk</code>.</p>
  <p>App StandBy Buckets помогает системе приоритизировать запросы приложений к ресурсам на основании того, как давно и как часто использовалось приложение. На основе шаблонов использования приложение помещается в один из пяти сегментов. Система ограничивает ресурсы устройства, доступные для каждого приложения, в зависимости от того, в каком сегменте находится приложение.</p>
  <p><strong>Пять сегментов, назначаемые приложениям в зависимости от приоритета:</strong></p>
  <ul>
    <li><strong>Active</strong>. Приложение находится в активном сегменте, если пользователь в настоящий момент использует приложение. Т.е. если видна Activity, или запущен Foreground service, или есть synchronized adapter, связанный с приложением на переднем плане, или пользователь кликнул на уведомление. Если приложение в активном сегменте, то никакие ограничения на использование ресурсов устройства не накладываются.</li>
    <li><strong>Working set.</strong> Приложение находится в этом сегменте, если часто запускается, но в данный момент не активно. Система накладывает <a href="https://developer.android.com/topic/performance/power/power-details" target="_blank">умеренные ограничения</a> на действия этого приложения.</li>
    <li><strong>Frequent</strong>. Приложение находится в этом сегменте, если используется часто, но не каждый день. Система накладывает больше <a href="https://developer.android.com/topic/performance/power/power-details" target="_blank">ограничений</a>, также накладываются ограничения на количество сообщений FCM с высоким приоритетом.</li>
    <li><strong>Rare.</strong> Приложение находится в этом сегменте, если оно редко используется. В этом случае система накладывает строгие ограничения и на получение сообщений FCM с высоким приоритетом. Система также ограничивает возможность приложения подключаться к интернету.</li>
    <li><strong>Never</strong>. Это сегмент для приложений, которые были установлены, но никогда не запускались. Система накладывает жесткие ограничения.</li>
  </ul>
  <p>Каждый производитель может установить свои критерии присвоения неактивных приложений к сегментам.</p>
  <p>Для определения сегмента, в который система поместит приложение, используется машинное обучение. С его помощью прогнозируется поведение пользователя. Например, если приложение из сегмента Rare было только что использовано и перешло в Active, то это не означает, что после использования приложение поднимется в более приоритетный сегмент. Сегменты определяются на основе прогнозов будущих действий пользователя, а не на основе недавнего использования.</p>
  <p>Полезная информация по работе с App StandBy Buckets:</p>
  <ul>
    <li>НЕ пытаться манипулировать тем, к какому сегменту система отнесет приложение.</li>
    <li>Создать Launcher Activity, если ее нет.</li>
    <li>Создавать обработчик нажатий на уведомления. Если с ними нельзя взаимодействовать, то приложение не сможет перейти в активный сегмент.</li>
    <li>Если приложение не показывает пользователю уведомление при получении high-priority FCM-уведомления, то пользователь не сможет взаимодействовать с приложением и оно не перейдет в активный сегмент. Если многие сообщения будут помечены как high-priority, то приложение исчерпает свою <a href="https://developer.android.com/topic/performance/power/power-details" target="_blank">квоту на такие сообщения</a>, и все последующие будут иметь normal-priority.</li>
  </ul>
  <h2>Firebase Cloud Messaging с App StandBy и режимом Doze</h2>
  <p>Необходимо использовать FCM для взаимодействия с приложением во время простоя устройства. FCM оптимизирован для работы в режимах ожидания Doze и App StandBy с помощью <a href="https://firebase.google.com/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message" target="_blank">высокоприоритетных FCM-сообщений</a>. Высокоприоритетные сообщения позволяют разбудить приложение для доступа к сети, даже если устройство находится в режиме Doze или приложение в режиме App StandBy. В обоих режимах система доставляет сообщение и дает приложению временный доступ к сетевым сервисам, а затем возвращает устройство или приложение в режим ожидания.</p>
  <h3>Как протестировать приложение с различными ограничениями системы</h3>
  <p><strong>Тестирование Doze Mode</strong></p>
  <ul>
    <li>Получить доступ к ADB (android device bridge) в текущей сессии:</li>
  </ul>
  <pre>export PATH=«~/Library/Android/sdk/platform-tools»:$PATH</pre>
  <ul>
    <li>Перевести систему в режим ожидания:</li>
  </ul>
  <pre>adb shell dumpsys deviceidle force-idle</pre>
  <ul>
    <li>Выйти из режима ожидания:</li>
  </ul>
  <pre>adb shell dumpsys deviceidle unforce</pre>
  <ul>
    <li>Активировать устройство:</li>
  </ul>
  <pre>adb shell dumpsys battery reset</pre>
  <ul>
    <li>Проверить поведение приложения.</li>
  </ul>
  <p><strong>Тестирование приложения с App StandBy для Android &lt; 9</strong></p>
  <ul>
    <li>Перевести приложение в App StandBy:</li>
  </ul>
  <pre>$ adb shell dumpsys battery unplug
$ adb shell am set-inactive &lt;package_name&gt; true</pre>
  <ul>
    <li>Пробудить приложение:</li>
  </ul>
  <pre>$ adb shell am set-inactive &lt;package_name&gt; false
$ adb shell am get-inactive &lt;package_name&gt;</pre>
  <ul>
    <li>Проверить работу приложения. Убедиться, что восстанавливается корректно. Проверить, продолжают ли работать уведомления и фоновые процессы.</li>
  </ul>
  <p><strong>Тестирование App Standby Buckets</strong></p>
  <p>Можно вручную переместить приложение в определенный App StandBy bucket с помощью команды:</p>
  <pre>adb shell am set-standby-bucket &lt;package_name&gt; active|working_set|frequent|rare</pre>
  <p>Команда проверки, в каком сегменте сейчас приложение:</p>
  <pre>adb shell am get-standby-bucket &lt;package_name&gt;</pre>
  <p><strong>Тестирование ограничений на фоновые процессы</strong></p>
  <ul>
    <li>Вручную применить ограничения на выполнение фоновых задач:</li>
  </ul>
  <pre>adb shell cmd appops set &lt;package_name&gt; RUN_ANY_IN_BACKGROUND ignore</pre>
  <ul>
    <li>Убрать ограничения на выполнение фоновых процессов:</li>
  </ul>
  <pre>adb shell cmd appops set &lt;package_name&gt; RUN_ANY_IN_BACKGROUND allow</pre>
  <p><strong>Тестирование режима Battery safety</strong></p>
  <ul>
    <li>Отключить устройство от ПК:</li>
    <li>Проверить поведение устройства в условиях экономии энергии:</li>
  </ul>
  <pre>adb shell settings put global low_power 1</pre>
  <ul>
    <li>Отменить ручную настройку:</li>
  </ul>
  <pre>adb shell dumpsys battery reset</pre>
  <h2><a href="https://developer.android.com/topic/performance/background-optimization#bg-restrict" target="_blank">Фоновые оптимизации</a></h2>
  <p>Ограничения, начиная с Android 7:</p>
  <ul>
    <li>Не отправляются широковещательные сообщения &#x60;CONNECTIVITY_ACTION&#x60;, если receiver объявлен в манифесте. Если receiver зарегистрирован динамически, то сообщение будет получено.</li>
    <li>Приложения не могут получать или отправлять &#x60;ACTION_NEW_PICTURE&#x60; или &#x60;ACTION_NEW_VIDEO&#x60;.</li>
  </ul>
  <p>Ограничения, начиная с Android 9:</p>
  <p>Если система замечает, что приложение потребляет чрезмерное количество ресурсов, она уведомляет пользователя и дает ему возможность ограничить действия приложения. Это поведение включает в себя:</p>
  <ul>
    <li>Чрезмерные wake locks.</li>
    <li>Избыточное количество фоновых сервисов.</li>
  </ul>
  <p>Точные ограничения определяются производителем устройства.</p>
  <h2>Battery Historian</h2>
  <p>Инструмент Battery Historian дает представление о расходе заряда батареи. Инструмент визуализирует связанные с энергопотреблением события и предоставляет разнообразные данные, которые могут помочь вам определить поведение приложения, разряжающего батарею.</p>
  <h4>Анализ приложения с помощью Battery Historian</h4>
  <p>Предварительно необходимо установить <a href="https://www.docker.com/community-edition" target="_blank">Docker</a>.</p>
  <ul>
    <li>Получить доступ к ADB (android device bridge) в текущей сессии:</li>
  </ul>
  <pre>export PATH=«~/Library/Android/sdk/platform-tools»:$PATH</pre>
  <ul>
    <li>Подключить устройство к ПК.</li>
    <li>Убить текущий ADB-сервер.</li>
  </ul>
  <pre>adb kill-server</pre>
  <ul>
    <li>Проверить доступные устройства:</li>
  </ul>
  <pre>adb devices</pre>
  <ul>
    <li>Сбросить данные о батарее:</li>
  </ul>
  <pre>adb shell dumpsys batterystats --reset</pre>
  <ul>
    <li>Отключить устройство и пройти по выбранному вами сценарию использования приложения.</li>
    <li>Подключить устройство.</li>
    <li>Проверить, что устройство подключено:</li>
  </ul>
  <pre>adb devices</pre>
  <ul>
    <li>Сделать дамп данных батареи:</li>
  </ul>
  <pre>adb shell dumpsys batterystats &gt; [path/b]batterystats.txt</pre>
  <ul>
    <li>Создать отчет для данных:</li>
  </ul>
  <pre>adb bugreport [path/]bugreport.zip</pre>
  <ul>
    <li>Запустить (порт можно указать любой):</li>
  </ul>
  <pre>docker run -p 5554:5554 gcr.io/android-battery-historian/stable:3.0 --port 5554</pre>
  <ul>
    <li>В браузере перейти по ссылке <a href="http://localhost:5554/" target="_blank">http://localhost:5554</a> и открыть ZIP файл.</li>
    <li>Вот так примерно будет выглядеть график BatteryHistorian:</li>
  </ul>
  <figure class="m_original">
    <img src="https://habrastorage.org/getpro/habr/post_images/3f5/78e/7ed/3f578e7edd5e7ad2221d9874f2b671e9.png" width="1585" />
  </figure>
  <p>На этом графике вы можете узнать, в какой момент запустился ваш сервис, установились wake locks, был запущен JobScheduler и другую информацию. Возможно, вы даже узнаете о своем приложении то, чего еще не знали и о чем не подозревали. Уделите этому инструменту пару-тройку свободных часов и, гарантирую, вы не пожалеете.</p>
  <h2>Energy Profiler</h2>
  <p>Energy Profiler — встроенный в Android Studio анализатор энергопотребления. Думаю, тут не стоит задерживаться. Этот инструмент довольно хорошо описан, и каждый может оценить его в действии.</p>
  <h2>BatteryStats + UI-тесты</h2>
  <p>В этой главе мы разберем, как можно использовать связку из BatteryStats и UI-тестирования.</p>
  <ul>
    <li>Перед запуском теста я написал bash-скрипт:</li>
  </ul>
  <pre>echo Write test class path e.g. &lt;путь_к_классу_с_тестом&gt;
read testName
export PATH=«~/Library/Android/sdk/platform-tools»:$PATH
adb shell dumpsys battery unplug
adb shell dumpsys batterystats --reset
adb shell am instrument -w  \ -e class $testName \ com.myapp.test/androidx.test.runner.AndroidJUnitRunner
adb shell dumpsys batterystats | awk -f BatteryStatsParseScript.awk &gt; BatteryTestsResult.txt
adb shell dumpsys batterystats &gt; BatteryTestsResultFull.txt
adb shell dumpsys batterystats reset
echo You can find the output file in the parent directory named BatteryTestsResult.txt</pre>
  <ul>
    <li>Для начала нужно ввести расположение класса с тестом. Например, класс &#x60;com.myApp.MyTestEspressoTest&#x60;.</li>
    <li>Далее подключается ADB.</li>
    <li>Устройство отключается от ПК.</li>
    <li>Сбрасывается статистика BatteryStats.</li>
    <li>Запускается тест, подставляет класс, введенный нами ранее, и используемый фреймворк для тестирования.</li>
    <li>Выгружается информация об энергопотреблении и парсится в более читаемый формат с помощью .awk-файла. Далее этот файл сохраняется под именем BatteryTestsResultFull.txt в главной папке приложения (или в любой другой, которую вы выберете).</li>
    <li>Выводится сообщение с расположением файла с результатом.</li>
    <li>Сбрасывается статистика BatteryStats.</li>
    <li>Вы восхитительны!</li>
  </ul>
  <p>Для парсинга файла, получившегося после теста, применяется .awk-файл. Сам файл я решил не прикладывать, т.к. он получился огромным и не все будут использовать те же поля, что использовал я. В результате получаем текстовый BatteryTestsResult.txt такого содержания:</p>
  <pre>Estimated battery capacity: 3700 mAh

Time on battery: 32s 609ms (100.0%) realtime, 32s 610ms (100.0%) uptime

App Uid u0a358
Cpu Usage: 1.56 mAh
Radio Usage:  mAh
WiFi Usage: 0.0476 mAh
Wake Usage:  mAh
Sensor Usage:  mAh
GPS Usage: 0.0417 mAh
Total App Usage: 1.65mAh

Total time in seconds: 32 seconds
Usage per second: 0.0515625 mAh/seconds

User activity: 14 touch

Wi-Fi network: 335.22KB received, 342.84KB sent (packets 745 received, 758 sent)</pre>
  <p>Результат более удобочитаемый, что стандартный файл BatteryStats. При желании вы можете добавить необходимые поля для анализа, либо вместо .awk-файла использовать регулярные выражения.</p>
  <h2>P.S. Проблемы с Samsung</h2>
  <p>При написании статьи я наткнулся на полезный сайт <a href="https://dontkillmyapp.com/" target="_blank">https://dontkillmyapp.com</a>, на котором можно узнать, какие ограничения накладывают различные производители на энергопотребление устройств. Самой частой проблемой, с которой я сталкивался, была жалоба пользователей Samsung на высокое энергопотребление различными приложениями. И на этом ресурсе я нашел ответ на свой вопрос.</p>
  <p>Вместе с релизом Samsung S8 была представлена утилита для увеличения времени работы батареи под названием App Power Monitor. И чтобы приложения работали корректно, их нужно вносить в whitelist. Также Samsung — рекордсмен по убийству приложений благодаря его «Адаптивной батарее».</p>
  <p>На сайте есть рекомендации для разработчиков по обходу ограничений, но в случае с данным производителем:</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/getpro/habr/post_images/1ff/48c/d93/1ff48cd93eb2d656e5ee12e0397b4a37.png" width="736" />
  </figure>
  <p>Чтобы до конца понять, как работает и от каких факторов зависит энергопотребление Android-приложения, одной статьи, конечно, недостаточно. Но надеюсь, что я выполнил свою главную цель — заинтересовать вас этой темой, и вы сможете оптимизировать работу с энергопотреблением. На этом у меня все.</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/company/citymobil/blog/512668/" target="_blank">Энергопотребление Android-приложений</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@skillbranch/BGa8wZ-9r</guid><link>https://teletype.in/@skillbranch/BGa8wZ-9r?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/BGa8wZ-9r?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>Анимация в Android: плавные переходы фрагментов внутри Bottom Sheet</title><pubDate>Fri, 24 Jul 2020 11:25:54 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/db/88/db88fbd3-3a08-4d6a-b830-cb048ae30c82.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/36/d3/36d3c979-00ee-4367-b990-c5a4ecc7651f.png"></img>Написано огромное количество документации и статей о важной визуальной составляющей приложений — анимации. Несмотря на это мы (Dodo Pizza – см. источник) смогли вляпаться в проблемы столкнулись с загвоздками при её реализации.]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://teletype.in/files/36/d3/36d3c979-00ee-4367-b990-c5a4ecc7651f.png" width="2292" />
  </figure>
  <p>Написано огромное количество документации и статей о важной визуальной составляющей приложений — анимации. Несмотря на это мы <em>(Dodo Pizza – см. источник)</em> <s>смогли вляпаться в проблемы</s> столкнулись с загвоздками при её реализации.</p>
  <p>Данная статья о проблеме и анализе вариантов её решения. Я не дам вам серебряную пулю против всех монстров, но покажу, как можно изучить конкретного, чтобы создать пулю специально для него. Разберу это на примере того, как мы подружили анимацию смены фрагментов с Bottom Sheet.</p>
  <h2>Бриллиантовый чекаут: предыстория</h2>
  <p>Бриллиантовый чекаут — кодовое название нашего проекта. Смысл его очень прост — сократить время, затрачиваемое клиентом на последнем этапе оформления заказа. Если в старой версии для оформления заказа требовалось минимум четыре клика на двух экранах (а каждый новый экран — это потенциальная потеря контекста пользователем), «бриллиантовый чекаут» в идеальном случае требует всего один клик на одном экране.</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/nd/4r/bb/nd4rbbvjxxjk2rdqt7ljik6o5c4.png" width="861.305334846765" />
    <figcaption>Сравнение старого и нового чекаута</figcaption>
  </figure>
  <p>Между собой мы называем новый экран «шторка». На рисунке вы видите, в каком виде мы получили задание от дизайнеров. Данное дизайнерское решение является стандартным, известно оно под именем Bottom Sheet, <a href="https://material.io/components/sheets-bottom" target="_blank">описано в Material Design</a> (в том числе <a href="https://material.io/develop/android/components/bottom-sheet-behavior/" target="_blank">для Android</a>) и в разных вариациях используется во многих приложениях. Google предлагает нам два готовых варианта реализации: модальный (Modal) и постоянный (Persistent). Разница между этими подходами описана во <a href="https://medium.com/@mhrpatel12/making-most-out-of-android-bottom-sheet-352c04551fb4" target="_blank">многих</a> и <a href="https://medium.com/@kosta.palash/using-bottomsheetdialogfragment-with-material-design-guideline-f9814c39b9fc" target="_blank">многих</a> статьях.</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/7g/2k/c1/7g2kc1rwisqpr--m2rdjbt9xvws.png" width="574.0350877192983" />
  </figure>
  <p>Мы решили, что наша шторка будет модальной и были близки к хэппи энду, но команда дизайнеров была настороже и не дала этому так просто свершиться.</p>
  <h2>Смотри, какая <a href="https://habr.com/ru/company/dodopizzadev/blog/463527/" target="_blank">классная анимация на iOS</a>. Давай так же сделаем?</h2>
  <p>Такой вызов не принять мы не могли! Ладно, шучу по поводу «дизайнеры неожиданно пришли с предложением сделать анимацию», но часть про iOS — чистая правда.</p>
  <p>Стандартные переходы между экранами (то есть отсутствие переходов) выглядели хоть и не слишком коряво, но до соответствия званию «бриллиантовый чекаут» не дотягивали. Хотя, кого я обманываю, это действительно было ужасно:</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/mc/7m/h4/mc7mh4sdl8-6mwlzp3lx-b7vig4.gif" width="320.4301075268818" />
    <figcaption>Что имеем «из коробки»</figcaption>
  </figure>
  <p>Прежде чем перейти к описанию реализации анимации, расскажу, как выглядели переходы раньше.</p>
  <ol>
    <li>Клиент нажимал на поле адреса пиццерии – в ответ открывался фрагмент «Самовывоз». Открывался он на весь экран (так было задумано) с резким скачком, при этом список пиццерий появлялся с небольшой задержкой.</li>
    <li>Когда клиент нажимал «Назад» – возврат на предыдущий экран происходил с резким скачком.</li>
    <li>При нажатии на поле способа оплаты – снизу с резким скачком открывался фрагмент «Способ оплаты». Список способов оплаты появлялся с задержкой, при их появлении экран увеличивался со скачком.</li>
    <li>При нажатии «Назад» – возврат обратно с резким скачком.</li>
  </ol>
  <p>Задержка в отображении данных вызвана тем, что они подгружаются на экран асинхронно. Нужно будет учесть это в дальнейшем.</p>
  <h2>В чём, собственно, проблема: где клиенту хорошо, там у нас ограничения</h2>
  <p>Пользователям не нравится, когда на экране происходит слишком много резких движений. Это отвлекает и смущает. Кроме того, всегда хочется видеть плавный отклик на своё действие, а не судороги.</p>
  <p>Это привело нас к техническому ограничению: мы решили, что нам нельзя на каждую смену экрана закрывать текущий bottom sheet и показывать новый, а также будет плохо показывать несколько bottom sheet один над другим. Так, в рамках нашей реализации (каждый экран — новый фрагмент), можно сделать только один bottom sheet, который должен двигаться максимально плавно в ответ на действия пользователей.</p>
  <p>Это означает, что у нас будет контейнер для фрагментов, который будет динамическим по высоте (поскольку все фрагменты имеют разную высоту), и мы должны анимировать изменение его высоты.</p>
  <h4>Предварительная разметка</h4>
  <p>Корневой элемент «шторки» очень простой — это всего лишь прямоугольный фон с закруглёнными сверху углами и контейнер, в который помещаются фрагменты.</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;FrameLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:background=&quot;@drawable/dialog_gray200_background&quot;&gt;
 
    &lt;androidx.fragment.app.FragmentContainerView
        android:id=&quot;@+id/container&quot;
        android:layout_width=&quot;match_parent&quot;
       android:layout_height=&quot;match_parent&quot; /&gt;
 
&lt;/FrameLayout&gt;</pre>
  <p>И файл dialog_gray200_background.xml выглядит так:</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;selector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
  &lt;item&gt;
    &lt;shape android:shape=&quot;rectangle&quot;&gt;
      &lt;solid android:color=&quot;@color/gray200&quot; /&gt;
      &lt;corners
          android:bottomLeftRadius=&quot;0dp&quot;
          android:bottomRightRadius=&quot;0dp&quot;
          android:topLeftRadius=&quot;10dp&quot;
          android:topRightRadius=&quot;10dp&quot; /&gt;
    &lt;/shape&gt;
  &lt;/item&gt;
&lt;/selector&gt;</pre>
  <p>Каждый новый экран представляет собой отдельный фрагмент, фрагменты сменяются с помощью метода replace, тут всё стандартно.</p>
  <h2>Первые попытки реализовать анимацию</h2>
  <h4>animateLayoutChanges</h4>
  <p>Вспоминаем о древней эльфийской магии <a href="https://developer.android.com/training/animation/layout" target="_blank">animateLayoutChanges</a>, которая на самом деле представляет собой дефолтный LayoutTransition. Хотя animateLayoutChanges совершенно не рассчитан на смену фрагментов, есть надежда, что это поможет с анимацией высоты. Также FragmentContainerView не поддерживает animateLayoutChanges, поэтому меняем его на старый добрый FrameLayout.</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;FrameLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:background=&quot;@drawable/dialog_gray200_background&quot;&gt;
 
  &lt;FrameLayout
      android:id=&quot;@+id/container&quot;
      android:layout_width=&quot;match_parent&quot;
      android:layout_height=&quot;match_parent&quot;
      android:animateLayoutChanges=&quot;true&quot; /&gt;
 
&lt;/FrameLayout&gt;</pre>
  <p>Запускаем:</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/dw/dj/su/dwdjsu4dkihuiwhywjcejdi-wpy.gif" width="319.6451612903226" />
    <figcaption>animateLayoutChanges</figcaption>
  </figure>
  <p>Как видим, изменение высоты контейнера действительно анимируется при смене фрагментов. Переход на экран «Самовывоз» выглядит нормально, но остальное оставляет желать лучшего.</p>
  <p>Интуиция подсказывает, что данный путь приведёт к нервно подёргивающемуся глазу дизайнера, поэтому откатываем наши изменения и пробуем что-то другое.</p>
  <h4>setCustomAnimations</h4>
  <p>FragmentTransaction позволяет задать анимацию, описанную в xml-формате с помощью метода <a href="https://developer.android.com/reference/android/support/v4/app/FragmentTransaction.html#setcustomanimations%D0%BC" target="_blank">setCustomAnimation</a>. Для этого в ресурсах создаём папку с названием «anim» и складываем туда четыре файла анимации:</p>
  <p>to_right_out.xml</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:duration=&quot;500&quot;
    android:interpolator=&quot;@android:anim/accelerate_interpolator&quot;&gt;
  &lt;translate android:toXDelta=&quot;100%&quot; /&gt;
&lt;/set&gt;</pre>
  <p>to_right_in.xml</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:duration=&quot;500&quot;
    android:interpolator=&quot;@android:anim/accelerate_interpolator&quot;&gt;
  &lt;translate android:fromXDelta=&quot;-100%&quot; /&gt;
&lt;/set&gt;</pre>
  <p>to_left_out.xml</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:duration=&quot;500&quot;
    android:interpolator=&quot;@android:anim/accelerate_interpolator&quot;&gt;
  &lt;translate android:toXDelta=&quot;-100%&quot; /&gt;
&lt;/set&gt;</pre>
  <p>to_left_in.xml</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:duration=&quot;500&quot;
    android:interpolator=&quot;@android:anim/accelerate_interpolator&quot;&gt;
  &lt;translate android:fromXDelta=&quot;100%&quot; /&gt;
&lt;/set&gt;</pre>
  <p>И затем устанавливаем эти анимации в транзакции:</p>
  <pre>fragmentManager
    .beginTransaction()
    .setCustomAnimations(R.anim.to_left_in, R.anim.to_left_out, R.anim.to_right_in, R.anim.to_right_out)
    .replace(containerId, newFragment)
    .addToBackStack(newFragment.tag)
    .commit()</pre>
  <p>Получаем вот такой результат:</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/t7/w-/zl/t7w-zlk3lbkzacrjslvddfqjjy8.gif" width="320" />
    <figcaption>setCustomAnimation</figcaption>
  </figure>
  <p>Что мы имеем при такой реализации:</p>
  <ul>
    <li>Уже стало лучше — видно, как экраны сменяют друг друга в ответ на действие пользователя.</li>
    <li>Но всё равно есть скачок из-за разной высоты фрагментов. Так происходит из-за того, что при переходе фрагментов в иерархии есть только один фрагмент. Именно он подстраивает высоту контейнера под себя, а второй отображается «как получилось».</li>
    <li>Всё ещё есть проблема с асинхронной загрузкой данных о способах оплаты — экран появляется сначала пустым, а потом со скачком наполняется контентом.</li>
  </ul>
  <p>Это никуда не годится. Вывод: нужно что-то другое.</p>
  <h2>А может попробуем что-то внезапное: Shared Element Transition</h2>
  <p>Большинство Android-разработчиков знает про Shared Element Transition. Однако, хотя этот инструмент очень гибкий, многие сталкиваются с проблемами при его использовании и поэтому не очень любят применять его.</p>
  <p>Суть его довольно проста — мы можем анимировать переход элементов одного фрагмента в другой. Например, можем элемент на первом фрагменте (назовём его «начальным элементом») с анимацией переместить на место элемента на втором фрагменте (этот элемент назовём «конечным элементом»), при этом с фэйдом скрыть остальные элементы первого фрагмента и с фэйдом показать второй фрагмент. Элемент, который должен анимироваться с одного фрагмента на другой, называется Shared Element.</p>
  <p>Чтобы задать Shared Element, нам нужно:</p>
  <ul>
    <li>пометить начальный элемент и конечный элемент атрибутом transitionName с одинаковым значением;</li>
    <li>указать <a href="https://developer.android.com/reference/android/support/v4/app/Fragment.html#setsharedelemententertransition" target="_blank">sharedElementEnterTransition</a> для второго фрагмента.</li>
  </ul>
  <p>А что если использовать корневую View фрагмента в качестве Shared Element? Возможно, Shared Element Transition придумывали не для этого. Хотя, если подумать, сложно найти аргумент, почему это решение не подойдёт. Мы хотим анимировать начальный элемент в конечный элемент между двумя фрагментами. Не вижу идеологического противоречия. Давайте попробуем сделать так!</p>
  <p>Для каждого фрагмента, который находится внутри «шторки», для корневой View указываем атрибут transitionName с одинаковым значением:</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:transitionName=&quot;checkoutTransition&quot;&gt;</pre>
  <p><em>Важно: это будет работать, поскольку мы используем REPLACE в транзакции фрагментов. Если вы используете ADD (или используете ADD и скрываете предыдущий фрагмент с помощью previousFragment.hide() [не надо так делать]), то transitionName придётся задавать динамически и очищать после завершения анимации. Так приходится делать, потому что в один момент времени в текущей иерархии View не может быть две View с одинаковым transitionName. Осуществить это можно, но будет лучше, если вы сможете обойтись без такого хака. Если вам всё-таки очень нужно использовать ADD, вдохновение для реализации можно найти <a href="https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html" target="_blank">в этой статье.</a></em></p>
  <p>Далее нужно указать класс Transition&#x27;а, который будет отвечать за то, как будет протекать наш переход. Для начала проверим, что есть «из коробки» — используем <a href="https://developer.android.com/reference/android/transition/AutoTransition" target="_blank">AutoTransition</a>.</p>
  <pre>newFragment.sharedElementEnterTransition = AutoTransition()</pre>
  <p>И мы должны задать Shared Element, который хотим анимировать, в транзакции фрагментов. В нашем случае это будет корневая View фрагмента:</p>
  <pre>fragmentManager
    .beginTransaction()
    .apply {
        if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.LOLLIPOP) {
            addSharedElement(
                currentFragment.requireView(),
                currentFragment.requireView().transitionName
            )
            setReorderingAllowed(true)
        }
    }
    .replace(containerId, newFragment)
    .addToBackStack(newFragment.tag)
    .commit()</pre>
  <p><em>Важно: обратите внимание, что transitionName (как и весь Transition API) доступен, начиная с версии Android Lollipop.</em></p>
  <p>Посмотрим, что получилось:</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/6k/gp/ck/6kgpckqxr9lmg5ysr_px0yacexa.gif" width="319.96774193548396" />
    <figcaption>AutoTransition</figcaption>
  </figure>
  <p>Транзишн сработал, но выглядит так себе. Так происходит, потому что во время транзакции фрагментов в иерархии View находится только новый фрагмент. Этот фрагмент растягивает или сжимает контейнер под свой размер и только после этого начинает анимироваться с помощью транзишна. Именно по этой причине мы видим анимацию, только когда новый фрагмент больше по высоте, чем предыдущий.</p>
  <p>Раз стандартная реализация нам не подошла, что нужно сделать? Конечно же, нужно <s>переписать всё на Flutter</s> написать свой Transition!</p>
  <h4>Пишем свой Transition</h4>
  <p>Transition — это класс из <a href="https://developer.android.com/training/transitions" target="_blank">Transition API</a>, который отвечает за создание анимации между двумя сценами (Scene). Основные элементы этого API:</p>
  <ul>
    <li>Scene — это расположение элементов на экране в определённый момент времени (layout) и ViewGroup, в которой происходит анимация (sceneRoot).</li>
    <li>Начальная сцена (Start Scene) — это Scene в начальный момент времени.</li>
    <li>Конечная сцена (End Scene) — это Scene в конечный момент времени.</li>
    <li>Transition — класс, который собирает свойства начальной и конечной сцены и создаёт аниматор для анимации между ними.</li>
  </ul>
  <p>В классе Transition мы будем использовать четыре метода:</p>
  <ul>
    <li><strong>fun getTransitionProperties(): Array. </strong>Данный метод должен вернуть набор свойств, которые будут анимироваться. Из этого метода нужно вернуть массив строк (ключей) в свободном виде, главное, чтобы методы captureStartValues и captureEndValues (описанные далее) записали свойства с этими ключами. Пример будет далее.</li>
    <li><strong>fun captureStartValues(transitionValues: TransitionValues).</strong> В данном методе мы получаем нужные свойства layout&#x27;а начальной сцены. Например, мы можем получить начальное расположение элементов, высоту, прозрачность и так далее.</li>
    <li><strong>fun captureEndValues(transitionValues: TransitionValues).</strong> Такой же метод, только для получения свойств layout&#x27;а конечной сцены.</li>
    <li><strong>fun createAnimator(sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues?): Animator?.</strong> Этот метод должен использовать свойства начальной и конечной сцены, собранные ранее, чтобы создать анимацию между этими свойствами. Обратите внимание, что если свойства между начальной и конечной сценой не поменялись, то данный метод не вызовется вовсе.</li>
  </ul>
  <h4>Реализуем свой Transition за девять шагов</h4>
  <p>1. Создаём класс, который представляет Transition.</p>
  <pre>@TargetApi(VERSION_CODES.LOLLIPOP)
class BottomSheetSharedTransition : Transition {
    @Suppress(&quot;unused&quot;)
    constructor() : super()

    @Suppress(&quot;unused&quot;)
    constructor(
        context: Context?,
        attrs: AttributeSet?
    ) : super(context, attrs)
}</pre>
  <p><em>Напоминаю, что Transition API доступен с версии Android Lollipop.</em></p>
  <p>2. Реализуем getTransitionProperties.</p>
  <p>Поскольку мы хотим анимировать высоту View, заведём константу PROP_HEIGHT, соответствующую этому свойству (значение может быть любым) и вернём массив с этой константой:</p>
  <pre>companion object {
    private const val PROP_HEIGHT = &quot;heightTransition:height&quot;
    
    private val TransitionProperties = arrayOf(PROP_HEIGHT)
}

override fun getTransitionProperties(): Array&lt;String&gt; = TransitionProperties</pre>
  <p>3. Реализуем captureStartValues.</p>
  <p>Нам нужно запомнить высоту той View, которая хранится в параметре transitionValues. Значение высоты нам нужно записать в поле transitionValues.values (он имеет тип Map) c ключом PROP_HEIGHT:</p>
  <pre>override fun captureStartValues(transitionValues: TransitionValues) {
    transitionValues.values[PROP_HEIGHT] = transitionValues.view.height
}</pre>
  <p>Всё просто, но есть нюанс. Вспомните, что во всех случаях ранее высота контейнера резко менялась в соответствии с высотой нового фрагмента. Чтобы такого не происходило, придётся что-то придумать. Проще всего просто «прибить гвоздями» контейнер фрагментов, то есть просто задать ему константную высоту, равную текущей высоте. При этом на экране ничего не произойдёт, но при смене фрагмента высота останется той же. В итоге метод будет выглядеть следующим образом:</p>
  <pre>override fun captureStartValues(transitionValues: TransitionValues) {
    // Запоминаем начальную высоту View...
    transitionValues.values[PROP_HEIGHT] = transitionValues.view.height

    //...и затем закрепляем высоту контейнера фрагмента
    transitionValues.view.parent
        .let { it as? View }
        ?.also { view -&gt;
            view.updateLayoutParams&lt;ViewGroup.LayoutParams&gt; {
                height = view.height
            }
        }
}</pre>
  <p>4. Реализуем captureEndValues.</p>
  <p>Аналогично предыдущему методу, нужно запомнить высоту View. Но не всё так просто. На предыдущем шаге мы зафиксировали высоту контейнера. Новый фрагмент по высоте может быть меньше, равным или больше предыдущего фрагмента. В первых двух случаях мы можем просто взять высоту нового фрагмента. Однако, в случае когда новый фрагмент должен занять больше места, чем старый, значение высоты будет ограничено высотой контейнера. Поэтому придётся пойти на небольшую хитрость — мы просто измерим view, чтобы определить, сколько места на самом деле ей требуется. Реализация будет выглядеть так:</p>
  <pre>override fun captureEndValues(transitionValues: TransitionValues) {
    // Измеряем и запоминаем высоту View
    transitionValues.values[PROP_HEIGHT] =
        getViewHeight(transitionValues.view.parent as View)
}</pre>
  <p>И метод getViewHeight:</p>
  <pre>private fun getViewHeight(view: View): Int {
    // Получаем ширину экрана
    val deviceWidth = getScreenWidth(view)

    // Попросим View измерить себя при указанной ширине экрана
    val widthMeasureSpec = MeasureSpec.makeMeasureSpec(deviceWidth, MeasureSpec.EXACTLY)
    val heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)

    return view
        // измеряем
        .apply { measure(widthMeasureSpec, heightMeasureSpec) }
        // получаем измеренную высоту
        .measuredHeight
        // если View хочет занять высоту больше доступной высоты экрана,
        // мы должны вернуть высоту экрана
        .coerceAtMost(getScreenHeight(view))
}

private fun getScreenHeight(view: View) =
    getDisplaySize(view).y - getStatusBarHeight(view.context)
    
private fun getScreenWidth(view: View) =
    getDisplaySize(view).x private

fun getDisplaySize(view: View) = Point().also {
    val wm = view.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
    vm.defaultDisplay.getSize(it)
}

private fun getStatusBarHeight(context: Context): Int = 
    context.resources
        .getIdentifier(&quot;status_bar_height&quot;, &quot;dimen&quot;, &quot;android&quot;)
        .takeIf { resourceId -&gt; resourceId &gt; 0 }
        ?.let { resourceId -&gt; context.resources.getDimensionPixelSize(resourceId) }
        ?: 0</pre>
  <p>Таким образом, мы знаем начальную и конечную высоту контейнера, и теперь дело за малым — создать анимацию.</p>
  <p>5. Реализация анимации. Fade in.</p>
  <p>Начальный фрагмент нам анимировать не нужно, так как при старте транзакции он удалится из иерархии. Будем показывать конечный фрагмент с фэйдом. Добавляем метод в класс «BottomSheetSharedTransition», ничего хитрого:</p>
  <pre>private fun prepareFadeInAnimator(view: View): Animator =
    ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f) </pre>
  <p>6. Реализация анимации. Анимация высоты.</p>
  <p>Ранее мы запомнили начальную и конечную высоту, теперь мы можем анимировать высоту контейнера фрагментов:</p>
  <pre>private fun prepareHeightAnimator(
    startHeight: Int,
    endHeight: Int,
    view: View
) = ValueAnimator.ofInt(startHeight, endHeight)
    .apply {
        val container = view.parent.let { it as View }

        // изменяем высоту контейнера фрагментов
        addUpdateListener { animation -&gt;
            container.updateLayoutParams&lt;ViewGroup.LayoutParams&gt; {
                height = animation.animatedValue as Int
            }
        }
    }</pre>
  <p>Создаём ValueAnimator и обновляем высоту конечного фрагмента. Снова ничего сложного, но есть нюанс. Поскольку мы меняем высоту контейнера, после анимации его высота будет фиксированной. Это означает, что если фрагмент в ходе своей работы будет менять высоту, то контейнер не будет подстраиваться под это изменение. Чтобы этого избежать, по окончании анимации нужно установить высоту контейнера в значение WRAP_CONTENT. Таким образом, метод для анимации высоты контейнера будет выглядеть так:</p>
  <pre>private fun prepareHeightAnimator(
    startHeight: Int,
    endHeight: Int,
    view: View
) = ValueAnimator.ofInt(startHeight, endHeight)
    .apply {
        val container = view.parent.let { it as View }

        // изменяем высоту контейнера фрагментов
        addUpdateListener { animation -&gt;
            container.updateLayoutParams&lt;ViewGroup.LayoutParams&gt; {
                height = animation.animatedValue as Int
            }
        }

        // окончании анимации устанавливаем высоту контейнера WRAP_CONTENT
        doOnEnd {
            container.updateLayoutParams&lt;ViewGroup.LayoutParams&gt; {
                height = ViewGroup.LayoutParams.WRAP_CONTENT
            }
        }
    }</pre>
  <p>Теперь всего лишь нужно использовать аниматоры, созданные этими функциями.</p>
  <p>7. Реализация анимации. createAnimator.</p>
  <pre>override fun createAnimator(
    sceneRoot: ViewGroup?,
    startValues: TransitionValues?,
    endValues: TransitionValues?
): Animator? {
    if (startValues == null || endValues == null) {
        return null
    }

    val animators = listOf&lt;Animator&gt;(
        prepareHeightAnimator(
            startValues.values[PROP_HEIGHT] as Int,
            endValues.values[PROP_HEIGHT] as Int,
            endValues.view
        ),
        prepareFadeInAnimator(endValues.view)
    )

    return AnimatorSet().apply {
        interpolator = FastOutSlowInInterpolator()
        duration = ANIMATION_DURATION
        playTogether(animators)
    }
}</pre>
  <p>8. Всегда анимируем переход.</p>
  <p>Последний нюанс касательно реализации данного Transititon&#x27;а. Звёзды могут сойтись таким образом, что высота начального фрагмента будет точно равна высоте конечного фрагмента. Такое вполне может быть, если оба фрагмента занимают всю высоту экрана. В таком случае метод «createAnimator» не будет вызван совсем. Что же произойдёт?</p>
  <ul>
    <li>Не будет Fade&#x27;а нового фрагмента, он просто резко появится на экране.</li>
    <li>Поскольку в методе «captureStartValues» мы зафиксировали высоту контейнера, а анимации не произойдёт, высота контейнера никогда не станет равной WRAP_CONTENT.</li>
  </ul>
  <p>Неприятно, но не смертельно. Можно обойти это поведение простым трюком: нужно добавить любое значение, которое будет отличаться для начальной сцены и конечной сцены, в список свойств Transition&#x27;а. Можно просто добавить строки с разными значениями:</p>
  <pre>companion object {
    private const val PROP_HEIGHT = &quot;heightTransition:height&quot;
    private const val PROP_VIEW_TYPE = &quot;heightTransition:viewType&quot;

    private val TransitionProperties = arrayOf(PROP_HEIGHT, PROP_VIEW_TYPE)
}

override fun getTransitionProperties(): Array&lt;String&gt; = TransitionProperties

override fun captureStartValues(transitionValues: TransitionValues) {
    // Запоминаем начальную высоту View...
    transitionValues.values[PROP_HEIGHT] = transitionValues.view.height
    transitionValues.values[PROP_VIEW_TYPE] = &quot;start&quot;

    //...и затем закрепляем высоту контейнера фрагмента
    transitionValues.view.parent
        .let { it as? View }
        ?.also { view -&gt;
            view.updateLayoutParams&lt;ViewGroup.LayoutParams&gt; {
            height = view.height
        }
    }
}

override fun captureEndValues(transitionValues: TransitionValues) {
    // Измеряем и запоминаем высоту View
    transitionValues.values[PROP_HEIGHT] =
        getViewHeight(transitionValues.view.parent as View)
    transitionValues.values[PROP_VIEW_TYPE] = &quot;end&quot;
} </pre>
  <p>Обратите внимание, добавилось свойство «PROP<em>VIEW</em>TYPE», и в методах «captureStartValues» и «captureEndValues» записываем разные значения этого свойства. Всё, транзишн готов!</p>
  <p>9. Применяем Transition.</p>
  <pre>newFragment.sharedElementEnterTransition = BottomSheetSharedTransition()</pre>
  <h2>Асинхронная загрузка данных</h2>
  <p>Чтобы анимация началась вовремя и выглядела хорошо, нужно просто отложить переход между фрагментами (и, соответственно, анимацию) до момента, пока данные не будут загружены. Для этого внутри фрагмента нужно вызвать метод <a href="https://developer.android.com/reference/android/support/v4/app/Fragment.html#postponeentertransition" target="_blank">postponeEnterTransition</a>. По окончании долгих задач по загрузке данных не забудьте вызвать <a href="https://developer.android.com/reference/android/support/v4/app/Fragment.html#startpostponedentertransition" target="_blank">startPostponedEnterTransition</a>. Я уверен, вы знали об этом приёме, но напомнить лишний раз не помешает.</p>
  <h2>Всё вместе: что в итоге получилось</h2>
  <p>С новым BottomSheetSharedTransition и использованием postponeEnterTransition при асинхронной загрузке данных у нас получилась такая анимация:</p>
  <figure class="m_custom">
    <img src="https://habrastorage.org/webt/jt/-m/qq/jt-mqqvbvd9pnibbgp1gimzcplk.gif" width="450" />
    <figcaption>Готовый transition</figcaption>
  </figure>
  <p><strong><em>В источнике в этом месте приводится готовый класс BottomSheetSharedTransition</em></strong></p>
  <p>Когда у нас есть готовый класс Transition&#x27;а, его применение сводится к простым шагам:</p>
  <p>Шаг 1. При транзакции фрагмента добавляем Shared Element и устанавливаем Transition:</p>
  <pre>private fun transitToFragment(newFragment: Fragment) {
    val currentFragmentRoot = childFragmentManager.fragments[0].requireView()
 
    childFragmentManager
        .beginTransaction()
        .apply {
            if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.LOLLIPOP) {
                addSharedElement(currentFragmentRoot, currentFragmentRoot.transitionName)
                setReorderingAllowed(true)
 
                newFragment.sharedElementEnterTransition = BottomSheetSharedTransition()
            }
        }
        .replace(R.id.container, newFragment)
        .addToBackStack(newFragment.javaClass.name)
        .commit()
}</pre>
  <p>Шаг 2. В разметке фрагментов (текущего фрагмента и следующего), которые должны анимироваться внутри BottomSheetDialogFragment, устанавливаем transitionName:</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:transitionName=&quot;checkoutTransition&quot;&gt;</pre>
  <p>На этом всё, конец.</p>
  <h2>А можно было сделать всё иначе?</h2>
  <p>Всегда есть несколько вариантов решения проблемы. Хочу упомянуть другие возможные подходы, которые мы не попробовали:</p>
  <ul>
    <li>Отказаться от фрагментов, использовать один фрагмент с множеством View и анимировать конкретные View. Так вы получите больший контроль над анимацией, но потеряете преимущества фрагментов: нативную поддержку навигации и готовую обработку жизненного цикла (придётся реализовывать это самостоятельно).</li>
    <li>Использовать MotionLayout. Технология MotionLayout на данный момент всё ещё находится на стадии бета, но выглядит очень многообещающе, и уже есть <a href="https://developer.android.com/training/constraint-layout/motionlayout/examples#fragment_transition_12" target="_blank">официальные примеры</a>, демонстрирующие красивые переходы между фрагментами.</li>
    <li>Не использовать анимацию. Да, наш дизайн является частным случаем, и вы вполне можете счесть анимацию в данном случае избыточной. Вместо этого можно показывать один Bottom Sheet поверх другого или скрывать один Bottom Sheet и следом показывать другой.</li>
    <li>Отказаться от Bottom Sheet совсем. Нет изменения высоты контейнера фрагментов — нет проблем.</li>
  </ul>
  <p>Демо проект можно найти <a href="https://github.com/keymusicman/Bottom-Sheet-Transition-Demo" target="_blank">вот тут на GitHub</a>.</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/company/dodopizzadev/blog/510066/%5C" target="_blank">Анимация в Android: плавные переходы фрагментов внутри Bottom Sheet</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@skillbranch/otuh9-g0b</guid><link>https://teletype.in/@skillbranch/otuh9-g0b?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/otuh9-g0b?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>Кодовая база. Расширяем RecyclerView</title><pubDate>Tue, 14 Jul 2020 12:57:17 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/f6/6e/f66e27da-028a-4dd8-a1d0-cc4e6a762bb9.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/0d/3f/0d3f9cf9-06da-4ca0-9dd7-8af58e2de9c2.png"></img>Всем привет!]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://teletype.in/files/0d/3f/0d3f9cf9-06da-4ca0-9dd7-8af58e2de9c2.png" width="1340" />
  </figure>
  <p>Всем привет!</p>
  <p>Меня зовут <a href="https://vk.com/anton_knjazev" target="_blank">Антон Князев</a>, senior Android-разработчик компании Omega-R <em>(см. источник)</em>. В течение последних семи лет я профессионально занимаюсь разработкой мобильных приложений и решаю сложные проблемы нативной разработки.</p>
  <p>Хочу поделиться способами расширения RecyclerView, наработанными нашей командой и мной. Они станут надежной базой для создания нестандартных списков в приложениях.</p>
  <p>Каждое приложение по-своему уникально благодаря своей идее, дизайну и команде специалистов. Удачные решения часто хочется перенести из одного проекта в другой. Поэтому вместо простого копирования логично создать отдельную библиотеку, которую бы использовала и совершенствовала вся команда.</p>
  <p>Команда решила создавать маленькие библиотеки, которые улучшают и ускоряют разработку приложений, и выкладывать их в <a href="https://github.com/Omega-R" target="_blank">публичный репозиторий GitHub</a>. Это позволяет легко подключать библиотеку в проектах через JitPack и дает заказчикам гарантию, что в коде нет ничего “криминального”.</p>
  <p><a href="https://github.com/Omega-R/OmegaRecyclerView" target="_blank">Первая библиотека</a>, которую мы выложили на GitHub, является простым расширением RecyclerView.</p>
  <p>Начнем с проблем, которые она решала:</p>
  <ol>
    <li>Нет дефолтного layoutManager – это неудобно, так как часто приходится выбирать один и тот же LinearLayoutManager.</li>
    <li>Нет возможности добавлять divider и item space через xml – тоже неудобно, так как необходимо добавлять либо в item layout, либо через ItemDecorator.</li>
    <li>Нельзя просто добавить header и footer через xml – это возможно только через отдельный ViewHolder.</li>
  </ol>
  <p>Проблемы некритичные, но создают неудобства и увеличивают время разработки.</p>
  <h2>1. Проблема: нет дефолтного layoutManager</h2>
  <p>Разработчики RecyclerView не предусмотрели возможности выбора LayoutManager по умолчанию. Задать layoutManager можно следующими способами:</p>
  <p>1. Через XML в атрибуте app:layoutManager=”LinearLayoutManager”:</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.recyclerview.widget.RecyclerView
    ...
     app:layoutManager=&quot;LinearLayoutManager&quot;/&gt;</pre>
  <p>2. Через код:</p>
  <pre>recyclerView.layoutManager = LinearLayoutManager(this)</pre>
  <p>По нашему опыту, в большинстве случаев нужен именно LinearLayoutManager.</p>
  <p>Вот несколько примеров таких списков из наших приложений ITProTV, Простой Мир и Dexen:</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/de/xi/o9/dexio9sfso2phof1n2mbejebdzk.png" width="1560" />
  </figure>
  <h3>Решение: добавим дефолтный layoutManager</h3>
  <p>В OmegaRecyclerView добавляется лишь 3 строчки:</p>
  <pre>if (layoutManager == null)  {
    layoutManager = LinearLayoutManager(context, attrs, defStyleAttr, 0)
}</pre>
  <p>Таким образом, когда требуется LinearLayoutManager, то ничего добавлять не надо, то есть про layoutManager можно забыть.</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;com.omega_r.libs.omegarecyclerview.OmegaRecyclerView
    android:id=&quot;@+id/recyclerview&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot; /&gt;</pre>
  <h2>2. Проблема: нет возможности добавлять divider и item space через xml</h2>
  <p>Добавлять divider приходится довольно часто при использовании RecyclerView. Например, в проекте “Простой Мир” один из экранов был с таким нестандартным divider (горизонтальная линия):</p>
  <figure class="m_column">
    <img src="https://teletype.in/files/e6/94/e6949217-e7ce-4b8d-92ed-e827b8452fe6.png" width="1560" />
  </figure>
  <p>Из этого макета видно, что:</p>
  <ul>
    <li>используется divider между элементами и в самом конце;</li>
    <li>используется item space.</li>
  </ul>
  <p>Каким образом это можно реализовать в Android стандартным путем?</p>
  <h3>Способ 1</h3>
  <p>Самый очевидный способ – включить divider как элемент ImageView:</p>
  <pre> &lt;RelativeLayout
   ...
   android:paddingStart=&quot;20dp&quot;
   android:paddingTop=&quot;12dp&quot;
   android:paddingEnd=&quot;20dp&quot;
   android:paddingBottom=&quot;12dp&quot;&gt;

   ...

   &lt;ImageView
       ...
       android:layout_alignParentBottom=&quot;true&quot;
       android:src=&quot;@drawable/divider&quot;/&gt;

&lt;/RelativeLayout&gt;</pre>
  <p>Может случиться так, что необходимо делать divider только между элементами. В таком случае придется убрать последний divider и дописать в адаптере код его скрытия.</p>
  <h3>Способ 2</h3>
  <p>Другим способом является использование DividerItemDecoration, который может нарисовать этот divider. Для него необходимо дополнительно создать drawable:</p>
  <pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;layer-list xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
    &lt;item android:left=&quot;32dp&quot;&gt;
        &lt;shape android:shape=&quot;rectangle&quot;&gt;
            &lt;size
                android:width=&quot;1dp&quot;
                android:height=&quot;1dp&quot; /&gt;
            &lt;solid android:color=&quot;@color/gray_dark&quot; /&gt;
        &lt;/shape&gt;
    &lt;/item&gt;
&lt;/layer-list&gt;</pre>
  <p>Для добавления отступа требуется написать свой ItemDecoration:</p>
  <pre>class VerticalSpaceItemDecoration(
    private val verticalSpaceHeight: Int
): RecyclerView.ItemDecoration {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        outRect.bottom = verticalSpaceHeight
    }
}
</pre>
  <p><a href="https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.java" target="_blank">DividerItemDecoration</a> прост: он рисует divider всегда под каждым элементом списка.<br />Но в случае изменения требований придется искать другое решение.</p>
  <h3>Решение: дополним возможностью добавлять divider и item space через xml</h3>
  <p>Итак, наш OmegaRecyclerView должен уметь добавлять divider с помощью следующих атрибутов:</p>
  <ol>
    <li>divider – определяет drawable, может быть назначен и цвет напрямую;</li>
    <li>dividerShow (beginning, middle, end) – флаги, которые определяют, где рисовать;</li>
    <li>dividerHeight – задает высоту divider, в случае с цветом становится особенно нужным;</li>
    <li>dividerPadding, dividerPaddingStart, dividerPaddingEnd – отступы: общий, с начала, с конца;</li>
    <li>dividerAlpha – определяет прозрачность;</li>
    <li>itemSpace – отступы между элементами списка.</li>
  </ol>
  <p>Все эти атрибуты применяются непосредственно к нашему OmegaRecyclerView с помощью двух специальных ItemDecoration.</p>
  <p>Один из ItemDecoration добавляет отступы между элементами, второй – рисует сам divider. Необходимо учитывать, что при наличии отступа между элементами второй ItemDecoration рисует divider посередине отступа. Возможности поддерживать все возможные варианты LayoutManager нет, поэтому поддерживаем только LinearLayoutManager и GridLayoutManager. Также следует учитывать ориентацию списка для LinearLayoutManager.</p>
  <p>Для того чтобы упростить код, выделим специальный DividerDecorationHelper, который будет читать и записывать относительные данные в Rect, зависящие от ориентации и порядка следования (обратный или прямой). В нем будут методы setStart, setEnd, getStart, getEnd.</p>
  <p>Создадим базовый ItemDecoration для двух классов, в котором будет содержаться общая логика, а именно:</p>
  <ol>
    <li>проверка, что layoutManager является наследником LinearLayoutManager;</li>
    <li>вычисление текущей ориентации и порядка следования;</li>
    <li>определение подходящего DividerDecorationHelper.</li>
  </ol>
  <p>В SpaceItemDecoration переопределим только один метод getItemOffset, который будет добавлять отступы:</p>
  <pre>override fun getItemOffset(
    outRect: Rect,
    parent: RecyclerView,
    helper: DividerDecorationHelper,
    position: Int,
    itemCount: Int
) {
    if (isShowBeginDivider() || countBeginEndPositions &lt;= position) {
        helper.setStart(outRect, space)
    }
    if (isShowEndDivider() &amp;&amp; position == itemCount - countBeginEndPositions) {
        helper.setEnd(outRect, space)
    }
}</pre>
  <p>Следующий DividerItemDecoration будет непосредственно рисовать divider. Он должен учитывать отступ между элементами и рисовать divider посередине. Для начала переопределим метод getItemOffset для случая, когда отступ не задан, но divider требуется для рисования.</p>
  <pre>override fun getItemOffset(
    outRect: Rect,
    parent: RecyclerView,
    helper: DividerDecorationHelper,
    position: Int,
    itemCount: Int
) {
    if (position == 0 &amp;&amp; isShowBeginDivider()) {
        helper.setStart(outRect, dviderSize)
    }
    if (position != 0 &amp;&amp; isShowMiddleDivider()) {
        helper.setStart(outRect, dividerSize)
    }
    if (position == itemCount - 1 &amp;&amp; isShowEndDivider()) {
        helper.setEnd(outRect, dividerSize)
    }
}</pre>
  <p>Также добавим опцию, которая позволит DividerItemDecoration спрашивать adapter, можно ли рисовать выше или ниже выбранного элемента. Для реализации такой возможности создадим свой адаптер, наследуемый от стандартного со следующими методами:</p>
  <pre>open fun isDividerAllowedAbove(position: Int): Boolean {
    return true
}

open fun isDividerAllowedBelow(position: Int): Boolean {
    return true
}</pre>
  <p>Далее переопределим метод onDrawOver, чтобы рисовать divider поверх нарисованных элементов. В этом методе надо пройтись по всем элементам, видимым на экране (через getChildAt), и при необходимости нарисовать этот divider. Также надо учесть, что из атрибута dividerDrawable может прийти и цвет, у которого нет высоты. Для такого случая высоту можно взять из атрибута dividerHeight.</p>
  <h2>3. Проблема: нельзя напрямую добавить header и footer через xml</h2>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/tk/og/x3/tkogx3rc9ufy8ld3ucow9kxb6ss.png" width="1560" />
  </figure>
  <p>В RecyclerView невозможно добавлять view через xml, но есть другие способы сделать это.</p>
  <h3>Способ 1</h3>
  <p>Один из очевидных способ добавлении view – через adapter. Причем необходимо отличать header и footer в adapter при введении своего идентификатора для viewType.</p>
  <pre>fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder? {
    val inflater = LayoutInflater.from(parent.context)
    return when (viewType) {
        TYPE_HEADER -&gt; {
            val headerView: View = inflater.inflate(R.layout.item_header, parent, false)
            HeaderViewHolder(itemView)
        }
        TYPE_ITEM -&gt; {
            val itemView: View = inflater.inflate(R.layout.item_view, parent, false)
            ItemViewHolder(itemView)
        }
        else -&gt; null
    }
}</pre>
  <h3>Способ 2</h3>
  <p>Немного другой способ, но тоже через adapter. Начиная с <em>recyclerview:1.2.0-alpha02</em>, появился MergeAdapter, который позволяет соединять несколько адаптеров в один, делая код чище.</p>
  <pre>val mergeAdapter = MergeAdapter(headerAdapter, itemAdapter,  footerAdapter)
recyclerView.adapter = mergeAdapter</pre>
  <h3>Решение: дополним возможностью простого добавления header и footer через xml</h3>
  <p>Первое, что нужно сделать, – перехватить добавление view в нашем OmegaRecyclerView, когда идет процесс inflate. Для этого следует переопределить метод addView и добавить себе header и footer view. Этот метод используется самим RecyclerView для дополнения видимыми элементами списка. Но view, добавленные через xml, не будут иметь ViewHolder, что в конечном итоге вызовет NullPointerException.</p>
  <p>Итак, нам надо определить, когда view добавляется во время inflate. К счастью, существует protected метод onFinishInflate, который вызывается при завершении процесса inflate. Поэтому при вызове этого метода помечаем, что процесс inflate завершен.</p>
  <pre>protected override fun onFinishInflate() {
    super.onFinishInflate()
    finishedInflate = true
}</pre>
  <p>Таким образом, метод addView будет выглядеть следующим образом:</p>
  <pre>override fun addView(view: View, index: Int, params: ViewGroup.LayoutParams) {
    if (finishedInflate) {
        super.addView(view, index, params)
    } else {
        // save header and footer views
    }
}</pre>
  <p>Далее необходимо запомнить все эти добавочные view и передать в специальный адаптер по типу MergeAdapter.</p>
  <p>Также нам удалось решить еще одну проблему: при вызове метода findViewById наши view возвращаться не будут. Для решения этой проблемы переопределим метод findViewTraversal: в нем необходимо сравнить id найденных нами view и вернуть view при совпадении. Поскольку этот метод скрыт, просто пишем его, не указывая, что он override.</p>
  <p>С этими и другими полезными фичами с подробным описанием вы можете познакомиться в нашей библиотеке <a href="https://github.com/Omega-R/OmegaRecyclerView" target="_blank">OmegaRecyclerView</a>:</p>
  <ol>
    <li>Мы уже в 2018 году создали свой ViewPager из RecyclerView. Причем в нашем ViewPager есть бесконечный скролл.</li>
    <li>ExpandableRecyclerView – специальный класс для добавления раскрывающегося списка с возможностью выбора анимации раскрытия.</li>
    <li>StickyHeader – специфический элемент списка, который можно добавлять через адаптер.</li>
  </ol>
  <p>Всё это является результатом наработанного опыта Omega-R. Эволюция мастерства разработчиков проходит через несколько стадий. Сначала появляется желание скопировать код из другого проекта или сделать что-то похожее на него. Потом приходит стадия, когда необходимо зафиксировать накопленный опыт и создать отдельный репозиторий.</p>
  <p>На следующей стадии начинаешь целенаправленно создавать фичи, которые не решался делать в проектах. Это может занять значительное время, но позволяет создать задел на будущее и ускорить разработку в новых проектах. Приглашаю каждого, кто сталкивается с трудностями в разработке, познакомиться с нашими решениями в <a href="https://github.com/Omega-R" target="_blank">GitHub-репозитории</a> Omega-R.</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/post/508454/" target="_blank">Кодовая база. Расширяем RecyclerView</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@skillbranch/CoTwlYYhI</guid><link>https://teletype.in/@skillbranch/CoTwlYYhI?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/CoTwlYYhI?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>Приручая MVI</title><pubDate>Wed, 08 Jul 2020 11:29:39 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/e6/4f/e64f6f38-acdf-4520-89e0-297baf5a55a1.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/36/85/368552c0-44e8-401d-864d-db07703ec4f6.jpeg"></img>О том, как распутать джунгли MVI, используя Джунгли собственного производства, и получить простое и структурированное архитектурное решение.]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://teletype.in/files/36/85/368552c0-44e8-401d-864d-db07703ec4f6.jpeg" width="1350" />
  </figure>
  <p>О том, как распутать джунгли MVI, используя Джунгли собственного производства, и получить простое и структурированное архитектурное решение.</p>
  <p><em>Автор: <a href="https://habr.com/users/c5fr7q/" target="_blank">Вячеслав Ворожейкин</a>. Ссылка на источник – в конце статьи.</em></p>
  <h2>Предисловие</h2>
  <p>Впервые наткнувшись на статью о Model-View-Intent (MVI) под Android, я даже не открыл ее. &quot;Серьезно!? Архитектура на Android Intents?&quot;</p>
  <p>Это была глупая идея. Намного позже я прочел про MVI и узнал, что главным образом данная архитектура сосредоточена на однонаправленных потоках данных и управлении состояния.</p>
  <p>Изучая MVI, я невольно столкнулся с проблемой, что весь подход выглядит как-то запутанно, как какие-то дебри. Да, на выходе получается решение с плюсами по отношению к MVP и MVVM, но, смотря на эту всю комплексность, задаешься вопросом: &quot;А стоило ли?&quot;.</p>
  <p>Просмотрев несколько популярных библиотек на эту тему, у меня так и не появилось фаворита из существующих решений, так как что-нибудь мне да не нравилось. Появлялись различные вопросы, на которые не всегда можно было найти однозначные ответы.</p>
  <p>Так я решил написать свое решение. Основные требования (по значимости):</p>
  <ol>
    <li>Простое;</li>
    <li>Покрывает все UI кейсы, которые я только могу придумать;</li>
    <li>Структурированное.</li>
  </ol>
  <h2>И что это?</h2>
  <p>Позвольте представить — Джунгли (<a href="https://github.com/c5fr7q/jungle" target="_blank">Jungle</a>). Под капотом — только <a href="https://github.com/ReactiveX/RxJava" target="_blank">RxJava</a> с ее реактивным подходом.</p>
  <h3>База</h3>
  <ul>
    <li><strong>State </strong>— &quot;устойчивые&quot; данные о UI, которые должны быть показаны даже после пересоздания View;</li>
    <li><strong>Action </strong>— &quot;неустойчивые&quot; данные об UI, которые не должны быть показаны после пересоздания View (например, данные о Snackbar и Toast);</li>
    <li><strong>Event </strong>— <em>Intent</em> из Model-View-Intent;</li>
    <li><strong>MviView </strong>— интерфейс, через который поставляются новые <strong>Actions</strong> и обновления <strong>State</strong>;</li>
    <li><strong>Middleware </strong>— посредник между одной функциональностью бизнес логики и UI;</li>
    <li><strong>Store </strong>— посредник между Model и View, который решает, как обрабатывать <strong>Events</strong>, поставлять обновления <strong>State</strong> и новые <strong>Actions</strong>.</li>
  </ul>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/qb/if/xs/qbifxsnk80w1hoogcmaezxwks1e.png" width="700" />
  </figure>
  <p><em>Все отношения, показанные на картинке, опциональны</em></p>
  <h3>Как это работает?</h3>
  <p>Как мне кажется, лучший способ понять это — разобрать пример. Представим, нам нужен экран со списком стран, который должен быть загружен из Интернета. Также существуют следующие условия:</p>
  <ol>
    <li>Показывать ProgressBar во время загрузки.</li>
    <li>Отображать Button для перезагрузки списка и Toast с сообщением об ошибке в случае ошибки.</li>
    <li>Если страны были успешно загружены, отображать список стран.</li>
    <li>Попробовать загрузить страны на открытии окна автоматически, без каких-либо действий пользователя.</li>
  </ol>
  <p>Давайте напишем нашу UI часть:</p>
  <pre>sealed class DemoEvent {
   object Load : DemoEvent()
}</pre>
  <pre>sealed class DemoAction {
   data class ShowError(val error: String) : DemoAction()
}</pre>
  <pre>data class DemoState(
   val loading: Boolean = false,
   val countries: List&lt;Country&gt; = emptyList()
)</pre>
  <pre>class DemoFragment : Fragment, MviView&lt;DemoState, DemoAction&gt; {

   private lateinit var demoStore: DemoStore
   private var adapter: DemoAdapter? = null

   /*Initializations are skipped*/

   override fun onViewCreated(view: View, bundle: Bundle?) {
      super.onViewCreated(view, bundle)
      demoStore.run {
         attach(this@DemoFragment)
         dispatchEventSource(
            RxView.clicks(demo_load)
               .map { DemoEvent.Load }
         )
      }
   }

   override fun onDestroyView() {
      super.onDestroyView()
      demoStore.detach()
   }

   override fun render(state: DemoState) {
      val showReload = state.run {
         !loading &amp;&amp; countries.isEmpty()
      }
      demo_load.visibility = if (showReload)
         View.GONE else
         View.VISIBLE
      demo_progress.visibility = if (state.loading)
         View.VISIBLE else
         View.GONE
      demo_recycler.visibility = if (state.countries.isEmpty())
         View.GONE else
         View.VISIBLE
      adapter?.apply {
         setItems(state.countries)
         notifyDataSetChanged()
      }
   }

   override fun processAction(action: DemoAction) {
      when (action) {
         is DemoAction.ShowError -&gt;
            Toast.makeText(
               requireContext(),
               action.error,
               Toast.LENGTH_SHORT
            ).show()
      }
   }
}</pre>
  <p>Что из этого (пока) можно понять? Мы можем послать <strong>DemoEvent.Load</strong> нашему <strong>DemoStore</strong> (по клику на кнопку <em>Reload</em>), получить <strong>DemoAction.ShowError</strong> (с данными об ошибке) и отобразить Toast, получить обновление по <strong>DemoState</strong> (с данными о странах и состоянии загрузки) и отобразить UI компоненты в соответствии с требованиями. Вроде бы не так уж и сложно.</p>
  <p>Теперь приступим к нашему <strong>DemoStore</strong>. В первую очередь, унаследуем его от <strong>Store</strong>, разрешим получать <strong>DemoEvent</strong>, производить <strong>DemoAction</strong> и изменять <strong>DemoState</strong>:</p>
  <pre>class DemoStore(
   foregroundScheduler: Scheduler,
   backgroundScheduler: Scheduler
) : Store&lt;DemoEvent, DemoState, DemoAction&gt;(
   foregroundScheduler = foregroundScheduler,
   backgroundScheduler = backgroundScheduler
)</pre>
  <p>Затем создадим <strong>CountryMiddleware</strong>, который будет ответственным за предоставление данных о загрузке стран:</p>
  <pre>class CountryMiddleware(
   private val getCountriesInteractor: GetCountriesInteractor
) : Middleware&lt;CountryMiddleware.Input&gt;() {

   override val inputType = Input::class.java

   override fun transform(upstream: Observable&lt;Input&gt;) =
      upstream.switchMap&lt;CommandResult&gt; {
         getCountriesInteractor.execute()
            .map&lt;Output&gt; { Output.Loaded(it) }
            .onErrorReturn {
               Output.Failed(it.message ?: &quot;Can&#x27;t load countries&quot;)
            }
            .startWith(Output.Loading)
      }

   object Input : Command

   sealed class Output : CommandResult {
      object Loading : Output()
      data class Loaded(val countries: List&lt;Country&gt;) : Output()
      data class Failed(val error: String) : Output()
   }
}</pre>
  <p>Что такое <strong>Command</strong>? Это специфичный сигнал, который побуждает &quot;что-то&quot; сделать. А <strong>CommandResult</strong>? Это результат выполнения этого &quot;чего-то&quot;.</p>
  <p>В нашем случае <strong>CountryMiddleware.Input</strong> сигнализирует, что логика <strong>CountryMiddleware</strong> должна быть выполнена. Каждое выполнение логики <strong>Middleware</strong> возвращает <strong>CommandResult</strong>; для лучшей структуры приложения можно хранить этот результат внутри <code>sealed</code> класса (<strong>CountryMiddleware.Output</strong>).</p>
  <p>В нашем случае мы попросту возвращаем Observable, который испустит <strong>Output.Loading</strong> во время загрузки, <strong>Output.Loaded</strong> с данными на успешную загрузку, <strong>Output.Failed</strong> с информацией об ошибке на ошибку.</p>
  <p>Давайте вернемся к <strong>DemoStore</strong> и заставим обработать <strong>CountryMiddleware</strong> при нажатии кнопки <em>Reload</em>:</p>
  <pre>class DemoStore(..., countryMiddleware: CountryMiddleware)... {

   override val middlewares = listOf(countryMiddleware)

   override fun convertEvent(event: DemoEvent) = when (event) {
      is DemoEvent.Load -&gt; CountryMiddleware.Input
   }
}</pre>
  <p>Переопределяя поле <code>middlewares</code>, мы указываем, какие <strong>Middlewares</strong> наш <strong>DemoStore</strong> может обработать. Под капотом <strong>Store</strong> использует <strong>Commands</strong>. Поэтому нам следует сконвертировать наш <strong>DemoEvent.Load</strong> в <strong>CountryMiddleware.Input</strong> (для того чтобы принудить перезагрузку).</p>
  <p>Итак, теперь мы можем получать результат от <strong>CountryMiddleware</strong>. Давайте позволим последнему изменять наш <strong>DemoState</strong>:</p>
  <pre>class DemoStore ... {
   ...

   override val initialState = DemoState()

   override fun reduceCommandResult(
      state: DemoState,
      result: CommandResult
   ) = when (result) {
      is CountryMiddleware.Output.Loading -&gt;
         state.copy(loading = true)
      is CountryMiddleware.Output.Loaded -&gt;
         state.copy(loading = false, countries = result.countries)
      is CountryMiddleware.Output.Failed -&gt;
         state.copy(loading = false)
      else -&gt; state
   }
}</pre>
  <p>Прежде чем изменять <strong>State</strong>, необходимо указать его начальное состояние в <code>initialState</code>. После этого в методе <code>reduceCommandResult</code> описывается логика того, как каждый <strong>CommandResult</strong> изменяет <strong>State</strong>.</p>
  <p>Для отображения ошибки загрузки используется <strong>DemoAction.ShowError</strong>. Чтобы сгенерировать последний, необходимо предоставить новую <strong>Command</strong> (из <strong>CommandResult</strong>) и связать ее с нашим <strong>Action</strong>:</p>
  <pre>class DemoStore ... {
   ...

   override fun produceCommand(commandResult: CommandResult) =
      when (commandResult) {
         is CountryMiddleware.Output.Failed -&gt;
            ProduceActionCommand.Error(commandResult.error)
         else -&gt; null
      }

   override fun produceAction(command: Command) =
      when (command) {
         is ProduceActionCommand.Error -&gt;
            DemoAction.ShowError(command.error)
         else -&gt; null
      }

   sealed class ProduceActionCommand : Command {
      data class Error(val error: String) : ProduceActionCommand()
   }
}</pre>
  <p>Последнее, что осталось сделать, — привязать автоматический запуск выполнения <strong>CountryMiddleware</strong>. Все, что нужно сделать, это добавить его <strong>Command</strong> в <code>bootstrapCommands</code>:</p>
  <pre>class DemoStore ... {
   ...

   override val bootstrapCommands = listOf(CountryMiddleware.Input)
}</pre>
  <p>Сделано!</p>
  <h3>Просто?</h3>
  <p>Можно использовать конкретно только то, что вам нужно, без какой-либо лишней логики. Несколько классов и щепотка магии под капотом. Один <strong>Store</strong>, опционально несколько <strong>Middlewares</strong>, опционально имплементация <strong>MviView</strong>.</p>
  <p>Ваша View должна только отображать обновления какой-то функциональности бизнес логики? Вам даже не нужны <strong>Events</strong>, только <strong>Store</strong>, <strong>Middleware</strong> и переопределение метода <code>render</code> функции в <strong>MviView</strong>.</p>
  <p>Только кнопка, по клику которой происходит какая-то навигация? Окей, стоит только поиграться с <strong>Event</strong> внутри <strong>Store</strong> и ничего больше.</p>
  <p>Как мне кажется, это простое решение, так как требует небольших усилий как для понимания, так и для использования.</p>
  <h3>Структурировано?</h3>
  <p>Для того чтобы поддерживать структурированность, необходимо:</p>
  <ul>
    <li>Хранить <strong>Commands</strong> в <code>sealed</code> классах внутри <strong>Store</strong>, группируя их по назначения: генерирующие <strong>Actions</strong> или напрямую изменяющие <strong>State</strong>.</li>
    <li>Хранить <strong>Commands</strong>, относящихся к <strong>Middlewares</strong>, внутри последних.</li>
  </ul>
  <p>Также стоит помнить, что <strong>Middleware</strong> — про <em>одну</em> функциональность, что делает его похожим на UseCase (Interactor). На мой взгляд, присутствие последнего (и, как следствие, какого-то domain layer) говорит о хорошо структурированном проекте. По этой же аналогии, я считаю, что использование <strong>Middleware</strong> способствует улучшению структуры проекта.</p>
  <h2>Заключение</h2>
  <p>С использованием Джунгей у меня есть четкое представление того, как организуется навигация внутри подхода. Я также уверен, что проблема <a href="http://hannesdorfmann.com/android/mosby3-mvi-7" target="_blank">SingleLiveEvent</a> может быть легко разрешена с использованием <strong>Actions</strong>.</p>
  <p>Более подробные разборы работы можно найти в <a href="https://github.com/c5fr7q/jungle/wiki" target="_blank">wiki</a>. Отвечу на любые вопросы. Буду рад, если вам данное решение покажется полезным!</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/post/509378/" target="_blank">Приручая MVI</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@skillbranch/CnPIsE7gi</guid><link>https://teletype.in/@skillbranch/CnPIsE7gi?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/CnPIsE7gi?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>Раскладываем на части FragmentLifecycleCallbacks</title><pubDate>Fri, 03 Jul 2020 10:42:55 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/11/7a/117a916e-0e21-4fde-b258-2f7c673f1634.png"></media:content><description><![CDATA[<img src="https://habrastorage.org/webt/py/v_/s4/pyv_s4vjq97ehajg713vrm_oodm.jpeg"></img>В этой статье автор продолжает рассказывать про инструменты, которые почему-то обделили вниманием. В своей предыдущей статье он написал про возможности ActivityLifecycleCallbacks и как их можно применять не только для логирования жизненного цикла. Но кроме Activity есть еще и Fragment, и ему хотелось получить для них подобное поведение. Далее рассказ ведется от лица автора.]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/py/v_/s4/pyv_s4vjq97ehajg713vrm_oodm.jpeg" width="1100" />
  </figure>
  <p>В этой статье автор продолжает рассказывать про инструменты, которые почему-то обделили вниманием. В своей <a href="https://teletype.in/@skillbranch/NlwwZ8u7J" target="_blank">предыдущей статье</a> он написал про возможности ActivityLifecycleCallbacks и как их можно применять не только для логирования жизненного цикла. Но кроме Activity есть еще и Fragment, и ему хотелось получить для них подобное поведение. <em>Далее рассказ ведется от лица автора.</em></p>
  <p>Не долго думая, я открыл поиск по классам в AndroidStudio (Cmd/Ctrl + O) и ввел туда FragmentLifecycleCallbacks. И каково же было мое удивление, когда поиск показал мне FragmentManager.FragmentLifecycleCallbacks. Самые нетерпеливые читатели писали про это в комментариях, поэтому вот продолжение всей этой истории. Скорее под кат!</p>
  <h2>Что это такое</h2>
  <p>Интерфейс наподобие ActivityLifecycleCallbacks, только для Fragment.</p>
  <h3><strong>FragmentLifecycleCallbacks</strong></h3>
  <p>В отличие от ActivityLifecycleCallbacks он управляется не самим Fragment, а FragmentManager, что дает ряд преимуществ. Например, у этого интерфейса методы с приставкой Pre-, которые вызываются до соответствующих методов Fragment. А методы без приставки вызываются после того, как сработают эти же методы Fragment.</p>
  <p>К тому же FragmentLifecycleCallbacks — это абстрактный класс, а не интерфейс. Думаю, это для того, чтобы у методов была реализация по умолчанию.</p>
  <p>Но перейдем к интересному — как это запустить.</p>
  <h2>Как зарегистрировать</h2>
  <p>Чтобы заставить FragmentLifecycleCallbacks работать, его нужно зарегистрировать на FragmentManager. Для этого надо вызвать FragmentManager.registerFragmentLifecycleCallback(), передав в него два параметра: сам callback и флаг — recursive. Флаг показывает, нужно ли применить этот callback только к этому FragmentManager или его надо рекурсивно прокидывать во все childFragmentManager’ы этого FragmentManager&#x27;а и дальше по иерархии.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/an/pk/pk/anpkpk0jafe_6tt-xo-zgd72ape.jpeg" width="1101" />
  </figure>
  <p>FragmentLifecycleCallback стоит регистрировать до Activity.onCreate(), иначе мы можем получить не все события, например, при восстановлении состояния.</p>
  <pre>class FlcExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager
            .registerFragmentLifecycleCallbacks(
                ExampleFragmentLifecycleCallback(),
                true
            )

        super.onCreate(savedInstanceState)
    }
}

class ExampleFragmentLifecycleCallback : FragmentManager.FragmentLifecycleCallbacks()</pre>
  <p>Выглядит не очень красиво, и в некоторых ситуациях потребует заводить что-то вроде базовой Activity. Но если ты уже прочитал мою <a href="https://habr.com/ru/company/yamoney/blog/482476/" target="_blank">статью про ActivityLifecycleCallbacks</a>, то знаешь, на что базовые Activity отлично заменяются =).</p>
  <pre>class ActivityFragmentLifecycleCallbacks :
    Application.ActivityLifecycleCallbacks,
    FragmentManager.FragmentLifecycleCallbacks() {

    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        (activity as? FragmentActivity)
            ?.supportFragmentManager
            ?.registerFragmentLifecycleCallbacks(this, true)
    }
}</pre>
  <p>И тут мы получаем потрясающую синергию callback’ов. Благодаря этому решению мы теперь можем дотянуться почти до любого объекта Activity и Fragment, создаваемых в системе. И теперь, когда мы видим все как на ладони, можно заставить всю эту систему работать на нас.</p>
  <h2>Примеры использования</h2>
  <p>Сразу про dependency injection: да, теперь можно распространять зависимости по всему приложению, даже если у вас Single Activity Application. Помнишь пример из <a href="https://habr.com/ru/company/yamoney/blog/482476/" target="_blank">предыдущей статьи</a>, про RequireCoolTool? То же самое можно сделать для всех Activity и Fragment в приложении. И ты уже догадался как, да? Но я все равно покажу пример.</p>
  <h3><strong>Dependency injection</strong></h3>
  <pre>interface CoolTool {
    val extraInfo: String
}</pre>
  <pre>class CoolToolImpl : CoolTool {
    override val extraInfo = &quot;i am dependency&quot;
}</pre>
  <pre>interface RequireCoolTool {
    var coolTool: CoolTool
}</pre>
  <pre>class InjectingLifecycleCallbacks :
    Application.ActivityLifecycleCallbacks,
    FragmentManager.FragmentLifecycleCallbacks() {

    private val coolToolImpl = CoolToolImpl()

    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        (activity as? RequireCoolTool)?.coolTool = coolToolImpl
        (activity as? FragmentActivity)
            ?.supportFragmentManager
            ?.registerFragmentLifecycleCallbacks(this, true)
    }

    override fun onFragmentPreCreated(
        fm: FragmentManager,
        f: Fragment,
        savedInstanceState: Bundle?
    ) {
        (f as? RequireCoolTool)?.coolTool = coolToolImpl
    }
}</pre>
  <pre>class DIActivity : AppCompatActivity(), RequireCoolTool {

    override lateinit var coolTool: CoolTool

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(LinearLayout {
            orientation = LinearLayout.VERTICAL
            FrameLayout {
                layoutParams = LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)
                Text(
                    &quot;&quot;&quot;
                    DI example activity
                    CoolTool.extraInfo=&quot;${coolTool.extraInfo}&quot;
                    &quot;&quot;&quot;.trimIndent()
                )
            }
            FrameLayout {
                layoutParams = LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)
                id = R.id.container
            }
        })

        supportFragmentManager.findFragmentById(R.id.container) ?: run {
            supportFragmentManager
                .beginTransaction()
                .add(R.id.container, DIFragment())
                .commit()
        }
    }
}</pre>
  <pre>class DIFragment : Fragment(), RequireCoolTool {

    override lateinit var coolTool: CoolTool

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? =
        inflater.context.FrameLayout {
            setBackgroundColor(Color.LTGRAY)
            Text(
                &quot;&quot;&quot;
                    DI example fragment
                    CoolTool.extraInfo=&quot;${coolTool.extraInfo}&quot;
                    &quot;&quot;&quot;.trimIndent()
            )
        }

}</pre>
  <p>И конечно же с Dagger’ом все тоже идеально работает.</p>
  <h3><strong>Dagger</strong></h3>
  <pre>interface DaggerTool {
    val extraInfo: String
}</pre>
  <pre>class DaggerToolImpl : DaggerTool {
    override val extraInfo = &quot;i am dependency&quot;
}</pre>
  <pre>class DaggerInjectingLifecycleCallbacks(
    val dispatchingAndroidInjector: DispatchingAndroidInjector&lt;Any&gt;
) : Application.ActivityLifecycleCallbacks,
    FragmentManager.FragmentLifecycleCallbacks() {

    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        dispatchingAndroidInjector.maybeInject(activity)
        (activity as? FragmentActivity)
            ?.supportFragmentManager
            ?.registerFragmentLifecycleCallbacks(this, true)
    }

    override fun onFragmentPreCreated(
        fm: FragmentManager,
        f: Fragment,
        savedInstanceState: Bundle?
    ) {
        dispatchingAndroidInjector.maybeInject(f)
    }
}</pre>
  <pre>class DaggerActivity : AppCompatActivity() {

    @Inject
    lateinit var daggerTool: DaggerTool

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(LinearLayout {
            orientation = LinearLayout.VERTICAL
            FrameLayout {
                layoutParams = LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)
                Text(
                    &quot;&quot;&quot;
                    Dagger example activity
                    CoolTool.extraInfo=&quot;${daggerTool.extraInfo}&quot;
                    &quot;&quot;&quot;.trimIndent()
                )
            }
            FrameLayout {
                layoutParams = LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)
                id = R.id.container
            }
        })

        supportFragmentManager.findFragmentById(R.id.container) ?: run {
            supportFragmentManager
                .beginTransaction()
                .add(R.id.container, DIFragment())
                .commit()
        }
    }
}</pre>
  <pre>class DaggerFragment : Fragment() {

    @Inject
    lateinit var daggerTool: DaggerTool

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? =
        inflater.context.FrameLayout {
            Text(
                &quot;&quot;&quot;
                Dagger example fragment
                DaggerTool.extraInfo=&quot;${daggerTool.extraInfo}&quot;
                &quot;&quot;&quot;.trimIndent()
            )
        }
}</pre>
  <pre>@Module
class DaggerModule {
    @Provides
    fun provideDaggerTool(): DaggerTool {
        return DaggerToolImpl()
    }
}</pre>
  <pre>@Module
abstract class DaggerAndroidModule {
    @ContributesAndroidInjector(modules = [DaggerModule::class])
    abstract fun contributeDaggerActivity(): DaggerActivity

    @ContributesAndroidInjector(modules = [DaggerModule::class])
    abstract fun contributeDaggerFragment(): DaggerFragment
}</pre>
  <p>Я думаю, что ты вполне справишься с другими DI-фреймворками, но если не получится, то давай обсудим это в комментариях.</p>
  <p>Конечно, можно делать все то же самое, что и с Activity, например, отправлять аналитику.</p>
  <h3><strong>Analytics</strong></h3>
  <pre>interface Screen {
    val screenName: String
}</pre>
  <pre>interface ScreenWithParameters : Screen {
    val parameters: Map&lt;String, String&gt;
}</pre>
  <pre>class AnalyticsCallback(
    val sendAnalytics: (String, Map&lt;String, String&gt;?) -&gt; Unit
) : Application.ActivityLifecycleCallbacks, 
    FragmentManager.FragmentLifecycleCallbacks() {

    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        if (savedInstanceState == null) {
            (activity as? Screen)?.screenName?.let {
                sendAnalytics(
                    it,
                    (activity as? ScreenWithParameters)?.parameters
                )
            }
        }
    }
}</pre>
  <pre>class AnalyticsActivity : AppCompatActivity(), ScreenWithParameters {

    override val screenName: String = &quot;First screen&quot;

    override val parameters: Map&lt;String, String&gt; = mapOf(&quot;key&quot; to &quot;value&quot;)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(LinearLayout {
            orientation = android.widget.LinearLayout.VERTICAL
            FrameLayout {
                layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)
                Text(
                    &quot;&quot;&quot;
                    Analytics example
                    see output in Logcat by &quot;Analytics&quot; tag
                    &quot;&quot;&quot;.trimIndent()
                )
            }
            FrameLayout {
                layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)
                id = R.id.container
            }
        })

        with(supportFragmentManager) {
            findFragmentById(R.id.container) ?: commit {
                add(R.id.container, AnalyticsFragment())
            }
        }
    }
}</pre>
  <pre>class AnalyticsFragment : Fragment(), ScreenWithParameters {

    override val screenName: String = &quot;Fragment screen&quot;

    override val parameters: Map&lt;String, String&gt; = mapOf(&quot;key&quot; to &quot;value&quot;)

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? =
        inflater.context.FrameLayout {
            setBackgroundColor(Color.LTGRAY)
            Text(
                &quot;&quot;&quot;
                Analytics example
                see output in Logcat by &quot;Analytics&quot; tag
                &quot;&quot;&quot;.trimIndent()
            )
        }
}</pre>
  <p>А какие варианты использования знаешь ты?</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/company/yamoney/blog/492272/" target="_blank">Раскладываем на части FragmentLifecycleCallbacks</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@skillbranch/NlwwZ8u7J</guid><link>https://teletype.in/@skillbranch/NlwwZ8u7J?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/NlwwZ8u7J?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>ActivityLifecycleCallbacks — слепое пятно в публичном API</title><pubDate>Sun, 28 Jun 2020 09:39:41 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/de/fa/defa9117-1bc5-4d6d-8df1-7bfe70366b9b.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/56/fe/56fe60ac-3883-41c3-a25a-bd84941d1a9f.png"></img>С детства я люблю читать инструкции (автор статьи – Владимир Генович). Я вырос, но меня до сих пор удивляет то, как взрослые люди безалаберно относятся к инструкциям: многие из них считают, что все знают, и при этом пользуются одной-двумя функциями, в то время как их намного больше! Кто из вас пользовался функцией поддержания температуры в микроволновке? А она есть почти в каждой.]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://teletype.in/files/56/fe/56fe60ac-3883-41c3-a25a-bd84941d1a9f.png" width="1100" />
  </figure>
  <p>С детства я люблю читать инструкции <em>(автор статьи – <a href="https://habr.com/users/Lynnfield/" target="_blank">Владимир Генович</a>)</em>. Я вырос, но меня до сих пор удивляет то, как взрослые люди безалаберно относятся к инструкциям: многие из них считают, что все знают, и при этом пользуются одной-двумя функциями, в то время как их намного больше! Кто из вас пользовался функцией поддержания температуры в микроволновке? А она есть почти в каждой.</p>
  <p>Однажды я решил почитать документацию к различным классам Android framework. Пробежался по основным классам: View, Activity, Fragment, Application, — и меня очень заинтересовал метод <a href="https://developer.android.com/reference/android/app/Application.html#registerActivityLifecycleCallbacks(android.app.Application.ActivityLifecycleCallbacks)" target="_blank">Application.registerActivityLifecycleCallbacks()</a> и интерфейс <a href="https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks.html" target="_blank">ActivityLifecycleCallbacks</a>. Из примеров его использования в интернете не нашлось ничего лучше, чем логирование жизненного цикла Activity. Тогда я начал сам экспериментировать с ним, и теперь мы в Яндекс.Деньгах активно используем его при решении целого спектра задач, связанных с воздействием на объекты Activity снаружи.</p>
  <h3>Что такое ActivityLifecycleCallbacks?</h3>
  <p>Посмотрите на этот интерфейс, вот как он выглядел, когда появился в API 14:</p>
  <pre>public interface ActivityLifecycleCallbacks {
    void onActivityCreated(Activity activity, Bundle savedInstanceState);
    void onActivityStarted(Activity activity);
    void onActivityResumed(Activity activity);
    void onActivityPaused(Activity activity);
    void onActivityStopped(Activity activity);
    void onActivitySaveInstanceState(Activity activity, Bundle outState);
    void onActivityDestroyed(Activity activity);
}</pre>
  <p>Возможно, этому интерфейсу уделяют так мало внимания, потому что он появился только в Android 4.0 ICS. А зря, ведь он позволяет нативно делать очень интересную вещь: воздействовать на все объекты Activity снаружи. Но об этом позже, а сначала внимательнее посмотрим на методы.</p>
  <p>Каждый метод отображает аналогичный метод жизненного цикла Activity и вызывается в тот момент, когда метод срабатывает на какой-либо Activity в приложении. То есть если приложение запускается с MainActivity, то первым мы получим вызов <code>ActivityLifecycleCallback.onActivityCreated(MainActivity, null)</code>.</p>
  <p>Отлично, но как это работает? Тут никакой магии: Activity сами сообщают о том, в каком они состоянии. Вот кусочек кода из Activity.onCreate():</p>
  <pre>    mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
            ? mLastNonConfigurationInstances.fragments : null);
}
mFragments.dispatchCreate();
getApplication().dispatchActivityCreated(this, savedInstanceState);
if (mVoiceInteractor != null) {</pre>
  <p>Это выглядит так, как если бы мы сами сделали BaseActivity. Только коллеги из Android сделали это за нас, еще и обязали всех этим пользоваться. И это очень даже хорошо!</p>
  <p>В API 29 эти методы работают почти так же, но их Pre- и Post-копии честно вызываются до и после конкретных методов. Вероятно, теперь этим управляет ActivityManager, но это только мои догадки, потому что я не углублялся в исходники достаточно, чтобы это выяснить.</p>
  <h3>Как заставить ActivityLifecycleCallbacks работать?</h3>
  <p>Как и все callbacks, сначала их надо зарегистрировать. Мы регистрируем все ActivityLifecycleCallbacks в Application.onCreate(), таким образом получаем информацию обо всех Activity и возможность ими управлять.</p>
  <pre>class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(MyCallbacks())
    }
}</pre>
  <p>Небольшое отступление: начиная с API 29 ActivityLifecycleCallbacks можно зарегистрировать еще и изнутри Activity. Это будет <a href="https://developer.android.com/reference/android/app/Activity.html#registerActivityLifecycleCallbacks(android.app.Application.ActivityLifecycleCallbacks)" target="_blank">локальный callback</a>, который работает только для этой Activity.</p>
  <p>Вот и все. Но это вы можете найти, просто введя название ActivityLifecycleCallbacks в строку поисковика. Там будет много примеров про логирование жизненного цикла Activity, но разве это интересно? У Activity много публичных методов (около 400), и все это можно использовать для того, чтобы делать много интересных и полезных вещей.</p>
  <h2>Что с этим можно сделать?</h2>
  <p>А что вы хотите? Хотите динамически менять тему во всех Activity в приложении? Пожалуйста: метод setTheme() — публичный, а значит, его можно вызывать из ActivityLifecycleCallback:</p>
  <pre>class ThemeCallback(
    @StyleRes val myTheme: Int
) : ActivityLifecycleCallbacks {
    override fun onActivityCreated(
        activity: Activity, 
        savedInstanceState: Bundle?
    ) {
        activity.setTheme(myTheme)
    }
}</pre>
  <p><strong>Повторяйте этот трюк ТОЛЬКО дома</strong><br />Какие-то Activity из подключенных библиотек могут использовать свои кастомные темы. Поэтому проверьте пакет или любой другой признак, по которому можно определить, что тему этой Activity можно безопасно менять. Например, проверяем пакет так (по-котлиновски =)):</p>
  <pre>class ThemeCallback(
    @StyleRes val myTheme: Int
) : ActivityLifecycleCallbacks {
    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        val myPackage = &quot;my.cool.application&quot;
        activity
            .takeIf { it.javaClass.name.startsWith(myPackage) }
            ?.setTheme(myTheme)
    }
}</pre>
  <p>Пример не работает? Возможно, вы забыли зарегистрировать ThemeCallback в Application или Application в AndroidManifest.</p>
  <p>Хотите еще интересный пример? Можно показывать диалоги на любой Activity в приложении.</p>
  <pre>class DialogCallback(
    val dialogFragment: DialogFragment
) : Application.ActivityLifecycleCallbacks {
    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        if (savedInstanceState == null) {
            val tag = dialogFragment.javaClass.name
            (activity as? AppCompatActivity)
                ?.supportFragmentManager
                ?.also { fragmentManager -&gt;
                    if (fragmentManager.findFragmentByTag(tag) == null) {
                        dialogFragment.show(fragmentManager, tag)
                    }
                }
        }
    }
}</pre>
  <p><strong>Повторяйте этот трюк ТОЛЬКО дома</strong><br />Конечно же, не стоит показывать диалог на каждом экране — наши пользователи не будут нас любить за такое. Но иногда может быть полезно показать что-то такое на каких-то конкретных экранах.</p>
  <p>А вот еще кейс: что если нам надо запустить Activity? Тут все просто: Activity.startActivity() — и погнали. Но что делать, если нам надо дождаться результата после вызова Activity.startActivityForResult()? У меня есть один рецепт:</p>
  <pre>class StartingActivityCallback : Application.ActivityLifecycleCallbacks {
    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        if (savedInstanceState == null) {
            (activity as? AppCompatActivity)
                ?.supportFragmentManager
                ?.also { fragmentManager -&gt;
                    val startingFragment = findOrCreateFragment(fragmentManager)

                    startingFragment.listener = { resultCode, data -&gt;
                        // handle response here
                    }

                    // start Activity inside StartingFragment
                }
        }
    }

    private fun findOrCreateFragment(
        fragmentManager: FragmentManager
    ): StartingFragment {
        val tag = StartingFragment::class.java.name
        return fragmentManager
            .findFragmentByTag(tag) as StartingFragment?
                ?: StartingFragment().apply {
                    fragmentManager
                        .beginTransaction()
                        .add(this, tag)
                        .commit()
                }
    }
}</pre>
  <p>В примере мы просто закидываем Fragment, который запускает Activity и получает результат, а потом делегирует его обработку нам. Будьте осторожны: тут мы проверяем, что наша Activity является AppCompatActivity, что может привести к бесконечному циклу. Используйте другие условия.</p>
  <p>Усложним примеры. До этого момента мы использовали только те методы, которые уже есть в Activity. Как насчет того, чтобы добавить свои? Допустим, мы хотим отправлять аналитику об открытии экрана. При этом у наших экранов свои имена. Как решить эту задачу? Очень просто. Создадим интерфейс Screen, который сможет отдавать имя экрана:</p>
  <pre>interface Screen {
    val screenName: String
}</pre>
  <p>Теперь имплементируем его в нужных Activity:</p>
  <pre>class NamedActivity : Activity(), Screen {
    override val screenName: String  = &quot;First screen&quot;
}</pre>
  <p>После этого натравим на такие Activity специальные ActivityLifecycleCallback’и:</p>
  <pre>class AnalyticsActivityCallback(
    val sendAnalytics: (String) -&gt; Unit
) : Application.ActivityLifecycleCallbacks {
    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        if (savedInstanceState == null) {
            (activity as? Screen)?.screenName?.let(sendAnalytics)
        }
    }
}</pre>
  <p>Видите? Мы просто проверяем интерфейс и, если он реализован, отправляем аналитику.</p>
  <p>Повторим для закрепления. Что делать, если надо прокидывать еще и какие-то параметры? Расширим интерфейс:</p>
  <pre>interface ScreenWithParameters : Screen {
    val parameters: Map&lt;String, String&gt;
}</pre>
  <p>Имплементируем:</p>
  <pre>class NamedActivity : Activity(), ScreenWithParameters {
    override val screenName: String = &quot;First screen&quot;
    override val parameters: Map&lt;String, String&gt; = mapOf(&quot;key&quot; to &quot;value&quot;)
}</pre>
  <p>Отправляем:</p>
  <pre>class AnalyticsActivityCallback(
    val sendAnalytics: (String, Map&lt;String, String&gt;?) -&gt; Unit
) : Application.ActivityLifecycleCallbacks {
    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        if (savedInstanceState == null) {
            (activity as? Screen)?.screenName?.let { name -&gt;
                sendAnalytics(
                    name,
                    (activity as? ScreenWithParameters)?.parameters
                )
            }
        }
    }
}</pre>
  <p>Но это все еще легко. Все это было только ради того, чтобы подвести вас к по-настоящему интересной теме: нативное внедрение зависимостей. Да, у нас есть Dagger, Koin, Guice, Kodein и прочее. Но на небольших проектах они избыточны. Но у меня есть решение… Угадайте какое?</p>
  <p>Допустим, у нас есть некоторый инструмент, вроде такого:</p>
  <pre>class CoolToolImpl {
    val extraInfo = &quot;i am dependency&quot;
}</pre>
  <p>Закроем его интерфейсом, как взрослые программисты:</p>
  <pre>interface CoolTool {
    val extraInfo: String
}

class CoolToolImpl : CoolTool {
    override val extraInfo = &quot;i am dependency&quot;
}</pre>
  <p>А теперь немного уличной магии от ActivityLifecycleCallbacks: мы создадим интерфейс для внедрения этой зависимости, реализуем его в нужных Activity, а с помощью ActivityLifecycleCallbacks найдем его и внедрим реализацию CoolToolImpl.</p>
  <pre>interface RequireCoolTool {
    var coolTool: CoolTool
}

class CoolToolActivity : Activity(), RequireCoolTool {
    override lateinit var coolTool: CoolTool
}

class InjectingLifecycleCallbacks : ActivityLifecycleCallbacks {
    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
        (activity as? RequireCoolTool)?.coolTool = CoolToolImpl()
    }
}</pre>
  <p>Не забудьте зарегистрировать InjectingLifecycleCallbacks в вашем Application, запускайте — и все работает.</p>
  <p>И не забудьте протестировать:</p>
  <pre>@RunWith(AndroidJUnit4::class)
class DIActivityTest {

    @Test
    fun &#x60;should access extraInfo when created&#x60;() {
        // prepare
        val mockTool: CoolTool = mock()
        val application = getApplicationContext&lt;android.app.Application&gt;()
        application.registerActivityLifecycleCallbacks(
            object : Application.ActivityLifecycleCallbacks {
                override fun onActivityCreated(
                    activity: Activity,
                    savedInstanceState: Bundle?
                ) {
                    (activity as? RequireCoolTool)?.coolTool = mockTool
                }
            })

        // invoke
        launch&lt;DIActivity&gt;(Intent(application, DIActivity::class.java))

        // assert
        verify(mockTool).extraInfo
    }
}</pre>
  <p>Но на больших проектах такой подход будет плохо масштабироваться, поэтому я не собираюсь отбирать ни у кого DI-фреймворки. Куда лучше объединить усилия и достигнуть синергии. Покажу на примере Dagger 2. Если у вас в проекте есть какая-то базовая Activity, которая делает что-то вроде AndroidInjection.inject(this), то пора ее выкинуть. Вместо этого сделаем следующее:</p>
  <ol>
    <li>по инструкции внедряем DispatchingAndroidInjector в Application;</li>
    <li>создаем ActivityLifecycleCallbacks, который вызывает DispatchingAndroidInjector.maybeInject() на каждой Activity;</li>
    <li>регистрируем ActivityLifecycleCallbacks в Application.</li>
  </ol>
  <pre>class MyApplication : Application() {

    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector&lt;Any&gt;

    override fun onCreate() {
        super.onCreate()
        DaggerYourApplicationComponent.create().inject(this);
        registerActivityLifecycleCallbacks(
            InjectingLifecycleCallbacks(
                dispatchingAndroidInjector
            )
        )
    }
}

class InjectingLifecycleCallbacks(
    val dispatchingAndroidInjector: DispatchingAndroidInjector&lt;Any&gt;
) : ActivityLifecycleCallbacks {

    override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
    ) {
       dispatchingAndroidInjector.maybeInject(activity)
    }
}</pre>
  <p>И такого же эффекта можно добиться с другими DI-фреймворками. Попробуйте и напишите в комментариях, что получилось.</p>
  <h2>Подведем итоги</h2>
  <p>ActivityLifecycleCallbacks — это недооцененный, мощный инструмент. Попробуйте какой-нибудь из <a href="https://github.com/yandex-money/android-activitylifecyclecallbacks-example" target="_blank">этих примеров</a>, и пусть они помогут вам в ваших проектах так же, как помогают Яндекс.Деньгам делать наши приложения лучше.</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/company/yamoney/blog/482476/" target="_blank">ActivityLifecycleCallbacks — слепое пятно в публичном API</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@skillbranch/RccEV3Fzt</guid><link>https://teletype.in/@skillbranch/RccEV3Fzt?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch</link><comments>https://teletype.in/@skillbranch/RccEV3Fzt?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=skillbranch#comments</comments><dc:creator>skillbranch</dc:creator><title>Разбираемся с launchMode Android Activity: standard, singleTop, singleTask и singleInstance</title><pubDate>Wed, 24 Jun 2020 10:31:19 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/e3/2a/e32a5d21-e4fb-4899-85fc-207085dd1d39.png"></media:content><description><![CDATA[<img src="https://teletype.in/files/8e/67/8e679938-774f-41d7-95d1-111cc4ee7a6e.jpeg"></img>Activity — это одна из самых ярких концепций в Android (самой популярной мобильной операционной системе с хорошо продуманной архитектурой управления памятью, которая отлично реализует многозадачность).]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://teletype.in/files/8e/67/8e679938-774f-41d7-95d1-111cc4ee7a6e.jpeg" width="1080" />
  </figure>
  <p>Activity — это одна из самых ярких концепций в Android (самой популярной мобильной операционной системе с хорошо продуманной архитектурой управления памятью, которая отлично реализует многозадачность).</p>
  <p>Так или иначе, с запуском Activity на экране не все так однозначно. Способ, которым оно было запущено, также важен. Нюансов в этой теме очень много. Одним из действительно важных является <strong>launchMode</strong>, о котором мы и поговорим в этой статье.</p>
  <p>Каждая Activity создается для работы с разными целями. Некоторые из них предназначены для работы отдельно с каждым Intent, например, отправленным Activity для составления электронной почты в почтовом клиенте. В то время как другие предназначены для работы в качестве синглтона, например, Activity почтового ящика.</p>
  <p>Вот почему важно указывать, нужно ли создавать новую Activity или использовать существующую, иначе это может привести к плохому UX или сбоям. Благодаря разработчикам ядра Android, это легко сделать с помощью <strong>launchMode</strong>, который и был специально для этого создан.</p>
  <h2>Определение launchMode</h2>
  <p>По сути, мы можем определить <strong>launchMode </strong>напрямую в качестве атрибута тега <code>&lt;activity&gt;</code> в <code>AndroidManifest.xml</code>:</p>
  <pre>&lt;activity
    android:name=&quot;.SingleTaskActivity&quot;
    android:label=&quot;singleTask launchMode&quot;
    android:launchMode=&quot;singleTask&quot;&gt;</pre>
  <p>Доступно 4 типа launchMode. Давайте рассмотрим их по очереди.</p>
  <h3><strong>standard</strong></h3>
  <p>Это режим «по умолчанию».</p>
  <p>Поведение Activity, установленной в этот режим, будет всегда создавать новую Activity, чтобы работать отдельно с каждым отправленным Intent. По сути, если для составления электронного письма отправлено 10 Intent-ов, должно быть запущено 10 Activity, чтобы обслуживать каждый Intent отдельно. В результате на устройстве может быть запущено неограниченное количество таких Activity.</p>
  <p><em>Поведение на пре-Lollipop Android</em></p>
  <p>Этот вид Activity будет создан и помещен в верх стека в той же задаче, которая и отправила Intent.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/ne/n2/2u/nen22uybqupb_f6jnw5hvjz90rc.jpeg" width="1080" />
  </figure>
  <p>На рисунке ниже показано, что произойдет, когда мы поделимся изображением со стандартной Activity. Оно будет расположено в стеке в той же задаче, как описано выше, хотя они из разных приложений.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/wp/t5/fz/wpt5fzwohvycuouxans3y_t11vu.jpeg" width="1080" />
  </figure>
  <p>А это то, что вы увидите в диспетчере задач (может показаться немного странным):</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/s8/u_/ii/s8u_iioeu_3jyfu9e2va7xypr54.jpeg" width="360" />
  </figure>
  <p>Если мы переключим приложение на другую задачу, а затем переключимся обратно в Галерею, мы все равно увидим, что Activity со стандартным launchMode помещается поверх задачи Галереи. В результате, если нам нужно что-то сделать в Галерее, мы должны сначала закончить нашу работу в этой дополнительной Activity.</p>
  <p><em>Поведение на Lollipop Android</em></p>
  <p>Если эти Activity относятся к одному и тому же приложению, поведение будет таким же, как и в пре-Lollipop реализации — размещение в стеке поверх задачи.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/lj/pm/90/ljpm90f0v8oh12ygrs0hrqxnmxa.jpeg" width="1080" />
  </figure>
  <p>Но в случае если Intent отправлен из другого приложения, будет создана новая задача и вновь созданное Activity будет размещено в качестве корневого, как показано ниже.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/re/bn/ci/rebncivt1yw-otyjkghmjmevxss.jpeg" width="1080" />
  </figure>
  <p>Это то, что вы увидите в диспетчере задач.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/st/vh/us/stvhusvjhnp8tmqogzkrenle3am.jpeg" width="360" />
  </figure>
  <p>Это происходит потому, что в Lollipop модифицирована система управления задачами — она стала лучше и понятнее. В Lollipop вы можете просто переключиться обратно в Галерею, поскольку она находится в другой задаче. Вы можете отправить другой Intent, будет создана новая задача, которая будет обслуживать Intent так же, как и предыдущая.</p>
  <figure class="m_original">
    <img src="https://habrastorage.org/webt/ia/iz/p_/iaizp_8fdhzzyft95zupxt89q54.jpeg" width="360" />
  </figure>
  <p>Примером такого вида Activity является <strong>Compose Email Activity</strong> (составление письма) или <strong>Social Network&#x27;s Status Posting Activity</strong> (обновление статуса в соцсети). Если у вас на уме Activity, которое отдельно обрабатывает каждый Intent, то вы думаете именно о <strong>standard Activity</strong>.</p>
  <h3>singleTop</h3>
  <p>Следующий режим — <strong>singleTop</strong>. Он ведет себя почти так же, как и <strong>standard</strong>, – экземпляров singleTop Activity можно создать столько, сколько мы захотим. Единственное отличие состоит в том, что если уже есть экземпляр Activity с таким же типом наверху стека в вызывающей задаче, то никакого нового Activity создано не будет, вместо этого Intent будет отправлен существующему экземпляру Activity через метод <code>onNewIntent()</code>.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/kh/uq/dj/khuqdjmchshqsq4cvxsybngidg8.jpeg" width="1080" />
  </figure>
  <p>В режиме singleTop вы должны предусмотреть обработку входящего Intent в <code>onCreate()</code> и <code>onNewIntent()</code>, чтобы он работал во всех случаях.</p>
  <p>Пример использования этого режима — функция поиска. Давайте подумаем о создании окна поиска, которое направляет вас к SearchActivity, чтобы увидеть результаты поиска. Для лучшего UX обычно мы всегда помещаем окно поиска на страницу результатов поиска, чтобы позволить пользователю выполнить следующий поиск, не возвращаясь назад.</p>
  <p>А теперь представьте, что если мы всегда запускаем новое SearchActivity, чтобы обслуживать новый результат поиска, то мы получим 10 новых Activity для 10 итераций поиска. Было бы очень странно возвращаться назад, так как вам нужно было бы нажимать назад 10 раз, чтобы пройти через все результаты поиска, чтобы вернуться к корневой Activity.</p>
  <p>Вместо этого, если SearchActivity уже находится наверху стека, лучше отправить Intent в существующий экземпляр Activity и позволить ему обновить результат поиска. Теперь будет только одна SearchActivity, размещенная наверху стека, и вы можете просто нажать кнопку «Назад» один раз, чтобы вернуться к предыдущей Activity. В этом больше смысла.</p>
  <p>В любом случае singleTop работает в той же задаче, что и вызывающая сторона. Если вы ожидаете, что Intent будет отправлен в существующую Activity, помещенную поверх любой другой задачи, я должен вас разочаровать, сказав, что там это так уже не работает. В случае если Intent отправлен из другого приложения в singleTop Activity, новая Activity будет запущена в том же аспекте, что и для standard launchMode (<em>пре-Lollipop: помещено поверх вызывающей задачи, Lollipop: будет создана новая задача)</em>.</p>
  <h3>singleTask</h3>
  <p>Этот режим сильно отличается от standard и singleTop. <strong>Activity с singleTask launchMode разрешено иметь только один экземпляр в системе (а-ля синглтон)</strong>. Если в системе уже существует экземпляр Activity, вся задача, удерживающая экземпляр, будет перемещена наверх, а Intent будет предоставлен через метод <code>onNewIntent()</code>. В противном случае будет создана новая Activity и помещена в соответствующую задачу.</p>
  <p><em>Работая в одном приложении</em></p>
  <p>Если в системе еще не было экземпляра singleTask Activity, будет создан новый, и он будет просто помещен вверх стека в той же задаче.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/tl/gd/v5/tlgdv5pp1aup44lacg-a39l4qtg.jpeg" width="1080" />
  </figure>
  <p><em>Но если он существует, все Activity, расположенные над этим singleTask Activity, автоматически будут жестоко уничтожены надлежащим образом (жизненный цикл закончен), чтобы отобразить на вершине стека нужную нам Activity.</em> В то же время Intent будет отправлен в singleTask Activity через прекрасный метод <code>onNewIntent()</code>.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/pr/7l/e9/pr7le9ygampc5au3-slsqydvakq.jpeg" width="1080" />
  </figure>
  <p>Это не имеет смысла с точки зрения пользовательского опыта, но оно разработано именно таким образом…</p>
  <p>Вы можете заметить один нюанс, который упоминается в <a href="https://developer.android.com/guide/components/tasks-and-back-stack.html" target="_blank">документации</a>:</p>
  <p><em>Система создает новую задачу и создает экземпляр activity в корне новой задачи.</em></p>
  <p><strong>Но на практике похоже, что это работает не так, как описано</strong>. SingleTask Activity по-прежнему помещается наверх стека Activity задачи, как видно из результата команды <code>dumpsys activity</code>.</p>
  <pre>sk id #239
  TaskRecord{428efe30 #239 A=com.thecheesefactory.lab.launchmode U=0 sz=2}
  Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
    Hist #1: ActivityRecord{429a88d0 u0 com.thecheesefactory.lab.launchmode/.SingleTaskActivity t239}
      Intent { cmp=com.thecheesefactory.lab.launchmode/.SingleTaskActivity }
      ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}
    Hist #0: ActivityRecord{425fec98 u0 com.thecheesefactory.lab.launchmode/.StandardActivity t239}
      Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
      ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}</pre>
  <p>Если вы хотите, чтобы singleTask Activity вело себя так, как описано в документации: создайте новую задачу и поместите Activity в качестве корневого Activity. Вам нужно определить атрибут <code>taskAffinity</code> для singleTask Activity следующим образом.</p>
  <pre>&lt;activity
    android:name=&quot;.SingleTaskActivity&quot;
    android:label=&quot;singleTask launchMode&quot;
    android:launchMode=&quot;singleTask&quot;
    android:taskAffinity=&quot;&quot;&gt;</pre>
  <p>Таким будет результат, когда мы попытаемся запустить <code>SingleTaskActivity</code>.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/qn/au/9f/qnau9ftfyqxjtq8yty5cjlfaxiq.jpeg" width="1080" />
  </figure>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/ln/pk/d4/lnpkd4alkah9kvmiphylvk8mx6a.jpeg" width="360" />
  </figure>
  <p>Ваша задача – решить, использовать <code>taskAffinity</code> или нет в зависимости от желаемого поведения Activity.</p>
  <p><em>Взаимодействуя с другим приложением</em></p>
  <p>Если Intent отправлен из другого приложения и в системе еще не создано ни одного экземпляра Activity, <u>будет создана новая задача</u> с новой Activity, размещенной в качестве корневой Activity.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/be/wv/9p/bewv9pxsnqrp9rj2is8sfs1ka1m.jpeg" width="1080" />
  </figure>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/7b/wc/_b/7bwc_bbpbwsbw0man-59yupltyw.jpeg" width="960" />
  </figure>
  <p>Если не существует задачи, которая бы являлась владельцем вызывающей singleTask Activity, вместо нее будет выведена наверх новая Activity.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/0e/eo/j5/0eeoj5h6thjdff3izlqcttpphde.jpeg" width="1080" />
  </figure>
  <p><em>В случае если в какой-либо задаче существует экземпляр Activity, вся задача будет перемещена вверх и для каждого отдельного Activity, расположенного над singleTask Activity, будет завершен жизненный цикл</em>. Если нажата кнопка «Назад», пользователь должен пройти через Activity в стеке, прежде чем вернуться к вызывающей задаче.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/dy/if/9d/dyif9dvekt6pictwq7znk7bqb-i.jpeg" width="1080" />
  </figure>
  <p>Примером использования этого режима является любое Entry Point Activity, например, страница «Входящие» почтового клиента или таймлайн соцсети. Эти Activity не предполагают более чем одного экземпляра, поэтому singleTask отлично справится со своей задачей. В любом случае, вы должны использовать этот режим с умом, так как в этом режиме Activity могут быть уничтожены без подтверждения пользователя, как описано выше.</p>
  <h3>singleInstance</h3>
  <p>Этот режим очень похож на singleTask, где в системе мог существовать только один экземпляр Activity. <strong>Разница в том, что задача, которая располагает этой Activity, может иметь только одну Activity — ту, у которой атрибут singleInstance</strong>. Если из этого вида Activity вызывается другая Activity, автоматически создается новое задание для размещения этой новой Activity. Аналогичным образом, если вызывается singleInstance Activity, будет создана новая задача для размещения этой Activity.</p>
  <p>В любом случае результат довольно странный. Из информации, предоставленной <code>dumpsys</code>, видно, что в системе есть две задачи, но в диспетчере задач появляется только одна, в зависимости от того, какая из них находится сверху. В результате, хотя есть задача, которая все еще работает в фоновом режиме, мы не можем переключить ее обратно на передний план. Это не имеет вообще никакого смысла.</p>
  <p>Вот что происходит, когда вызывается singleInstance Activity, в то время как в стеке уже существует какая-либо Activity.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/ea/2v/qy/ea2vqyvflpzuia5elbfl7cd_fzm.jpeg" width="1080" />
  </figure>
  <p>А вот что мы видим в диспетчере задач.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/5a/lr/7s/5alr7sfn0d253r5-wi90zpmaoc4.jpeg" width="360" />
  </figure>
  <p>Поскольку эта задача может иметь только одну Activity, мы больше не можем переключаться обратно на задачу № 1. Единственный способ сделать это — перезапустить приложение из лаунчера, но, как в итоге получится, singleInstance задача будет скрыта в фоновом режиме.</p>
  <p>Во всяком случае, есть некоторые обходные пути для этой проблемы. Как и в случае с singleTask Activity, просто назначьте атрибут <code>taskAffinity</code> для singleInstance Activity, разрешающим существование нескольких задач в диспетчере задач.</p>
  <pre>&lt;activity
    android:name=&quot;.SingleInstanceActivity&quot;
    android:label=&quot;singleInstance launchMode&quot;
    android:launchMode=&quot;singleInstance&quot;
    android:taskAffinity=&quot;&quot;&gt;</pre>
  <p>Теперь картина имеет больше смысла.</p>
  <figure class="m_column">
    <img src="https://habrastorage.org/webt/4g/g7/sr/4gg7srssanlpsftak21yt7ge4o4.jpeg" width="360" />
  </figure>
  <p>Этот режим используется редко. Некоторые из вариантов использования на практике — это лаунчер-Activity или приложение, для которого вы на 100% уверены, что там должна быть только одна Activity. В любом случае, я предлагаю вам не использовать этот режим, если на то нет крайней необходимости.</p>
  <h2>Intent-флаги</h2>
  <p>Помимо назначения режима запуска непосредственно в <code>AndroidManifest.xml</code>, мы также можем регулировать поведение с помощью инструмента, называемого <strong>Intent-флагами</strong>, например:</p>
  <pre>Intent intent = new Intent(StandardActivity.this, StandardActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);</pre>
  <p>запустит <code>StandardActivity</code> с условием singleTop launchMode.</p>
  <p>Есть довольно много флагов, с которыми вы можете работать. Вы можете найти больше информации об этом <a href="https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_BROUGHT_TO_FRONT" target="_blank">здесь</a>.</p>
  <p>Надеюсь, вы нашли эту статью полезной =)</p>
  <p></p>
  <p>Источник: <a href="https://habr.com/ru/company/otus/blog/493802/" target="_blank">Разбираемся с launchMode Android Activity: standard, singleTop, singleTask и singleInstance</a></p>

]]></content:encoded></item></channel></rss>