Русский
Русский
English
Статистика
Реклама

Jetpack

Евгений Флекс владивостокский пилот-испытатель, инженер и разработчик реактивного ранца-крыла

11.12.2020 18:19:05 | Автор: admin
В мире, где нет ничего невозможного, реальность ограничена лишь вашей фантазией и упорством.

image

Еще в 2016, за 2 года до того, как я стал заниматься своим реактивным ранцем, Евгений Флекс, мой земляк из Владивостока, презентовал миру свой дерзкий проект с обратной стреловидностью крыла.

Я списывался и созванивался с Евгением, консультировался с ним на счет микро-турбореактивных двигателей, мы обсуждали варианты возможного сотрудничества. Сейчас поздно писать про Евгения и его работы, но я всё же хочу немного рассказать про этого человека и его труды по созданию реактивного крыла.




Jetwing IKAR 1st VTOL turbo jetpack. Design. Part 1.
Engine: 2x turbofan with RDK. DIY
Thrust: 2000+ Н (~200kg)
Flight time: 35min max
Wingspan 1.6m

image

image

Совсем рядом с этим импровизированным полигоном расположен многоэтажный строящийся дом Во время испытаний вся стройка замирает всем интересно)
На нижнем слайде просто земля, выжженная и оплавленная реактивной струей раскаленных газов,- она становится похожей на твердый полированый кварц это результат непродолжительной работы двигателей на холостых с высоты 80см.
(Здесь и далее подписи к фотографиям взяты из инстаграма Евгения)

image

image

image

Турбореактивный двигатель


Евгений разрабатывал свой турбореактивный двигатель

image


Турбовентиляторный реактивный двигатель (ТВРД) ТРДД со степенью двухконтурности m=210. Здесь компрессор низкого давления преобразуется в вентилятор, отличающийся от компрессора меньшим числом ступеней и большим диаметром, и горячая струя практически не смешивается с холодной. Обычно применяется в гражданской авиации, двигатель имеет больший назначенный ресурс и малый удельный расход топлива на дозвуковых скоростях.

Проверяем гипотезу применимости гибридной безредукторной схемы на основе ТРДД собственной разработки.

image

image

image

image

Авиационный двигатель принципиально нового поколения для будущих летающих автомобилей движитель на эффекте Коандэ-Бернулли-Флекса. Извините, что приписал свое имя к этим великим людям, но под общепринятые классификации моя конструкция пока не попадает.) Удельная тяга, при одинаковой затрачиваемой энергии, выше, чем у всех известных на сегодняшний день типов винтовых агрегатов.

image

Принцип действия: запуск прозводится нагнетанием воздушного потока через колесо компрессора и далее, за счет эффекта Коандэ, преобразуясь из турбулентного в ламинарный процесс, ускоряющийся воздушный поток устремляется через спрямители ВП и другие аэродинамические элементы, при прохождении сужающегося диффузора возникающая инфузия увлекает за собой приграничные воздушные массы, что, вкупе с нарастающей разницей давлений, приводит к самовозбуждению реактивной тяги (без прямого нагнетания и дополнительных энергетических затрат!). Сравнительные данные, схемы и видео с испытаний позже.

image

У нас свой НИОКР) Экспериментируем с графеном и укладкой слоев. Раньше колесо компрессора разлеталость до 36000об/мин, теперь не выдерживает металлический крепеж статора. Такие разрушения говорят, что мы на верном пути)

image

Испытания



Доброе утро, любимый город! У тебя сегодня день рождения и у нас есть подарок: первый во вселенной гибридный двухконтурник разогнал твой морской воздух до 1400км/ч при etg всего в 615градусов! Спасибо оператору за выдержку сама спряталась, а камеру держала до победного. Увидимся сегодня на мосту, друзья) (1 июля 2017)

image

Турбомангал воплощенная мечта Кирилла Юзова (на самом деле нет).

image

Прощальный слайд перед демонтажем гибридного апофеоза глупости ЖРД+ТРД, и установкой доработанных двигателей для Икара. Они само совершенство!))

Треугольное крыло


image

Сотни часов расчетов, более 200 деталей, десятки пресс-форм, миллионы погибших в бурных обсуждениях нейронов И пора бы уже свихнуться, но на столе лежат ещё два воздушных судна и 16 инициированных проектов. A вдали уже слышен гул реактивных двигателей)

image

Евгений Флекс в работе над реактивным десантным крылом

Испытания крыла без двигателей


Испытания реактивного ранца-крыла #7/19 Падение с 1200м


Первый российский реактивный ранец-крыло JETWING. Взлетаем


Первый российский реактивный ранец-крыло JETWING. Предполётная подготовка


Первый российский реактивный ранец-крыло JETWING. Первые испытания


Первый российский реактивный ранец-крыло JETWING


image

image

image

Реактивное крыло продавалось на Авито:
www.avito.ru/moskva/sport_i_otdyh/reaktivnyy_ranets-krylo_jetwing_1935124941




Интервью


Интервью с Главным конструктором. Жизнь в большом городе


image

В июле 2020 Евгений Флекс утонул, выйдя в шторм на SUP-борде.
Подробнее..

Джетпак вертолетного типа

06.06.2021 14:21:06 | Автор: admin

Австралийская компания CopterPack выложила ролик с успешным отрывом о земли и пилотируемым полетом в несколько секунд своего пропеллерного джетпака.

Каркас сделан из углеволокна. Работает на электричестве. Судя по видео полет может длиться не больше минуты. Система оснащена автоматической стабилизацией. Больше никакой информации нет.

Под катом парочка исторических аналогов вертолетных джетпаков.

Baumgrtl Heliofly I


image


Австрийский инженер Paul Baumgartl в 1941 разработал персональный вертолетный ранец.

image

Hoppi-Copters


В 1940-х Horace T. Pentecost основал Hoppi-Copters Inc.
В 1945 году они предложили устройство с роторами встречного вращения:

image


Радиус 3.66, мощность мотора 20 лс, грузоподъемность 90 кг, скорость 154 км/ч, длительность полета 1 час.

image

Патент

SoloTrek XFV


Был впервые представлен в 2001 году.


Martin Jetpack


Стартовали в 2008, в 2019 обанкротились.



P.S.


Складной вертолёт Ка-56 Оса


Еще:

Подробнее..

Трансформация Android-разработки с Jetpack Compose и Корутинами

06.08.2020 10:11:52 | Автор: admin

Jetpack Compose одна из наиболее обсуждаемых тем из серии видео про Android 11, заменивших собой Google IO. Многие ожидают от библиотеки, что она решит проблемы текущего UI-фреймворка Android, содержащего много легаси-кода и неоднозначных архитектурных решений. Другим не менее популярным фреймворком, о применении которого я расскажу в этой статье является Kotlin Coroutines, а конкретнее входящий в него Flow API, который может помочь избежать оверинжиниринга при использовании RxJava.
Применение этих инструментов я покажу на примере небольшого приложения для контроля за употреблением кофе, написанного с использованием Jetpack Compose для UI и StateFlow как инструмента для управления состоянием. В нем также используется MVI-архитектура.



Хотел бы сразу предупредить, что подходы, использованные в коде ниже, могут немного отличаться от последних рекомендованных. Я продолжаю изучать эти инструменты, поэтому если у вас появятся предложения по улучшению кода напишите про это в комментариях или ишью в репозитории.


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


Jetpack Compose


При первом взгляде на Jetpack Compose после продолжительной работы с версткой в XML вы, вероятно, почувствуете дискомфорт из-за смешения UI-кода с другим в файлах на Kotlin и необходимости постоянного контроля состояний. До этого у меня был опыт работы с Flutter который построен на виджетах и их состояниях. Это помогло мне легче понять концепцию декларативного UI и сделать прототип приложения всего за шесть вечеров после работы. Тот же опыт я использую для сравнения этих двух UI-фреймворков.


Помочь с переходом может сайт, на котором можно найти компонент в Compose по его названию в традиционном UI.


Также как во Flutter, в Compose вы можете использовать MainActivity как точку входа в приложение. Навигация же может быть сделана посредством композиции виджетов без необходимости использовать другие активити или фрагменты. Вы можете также поместить часть, написанную на Flutter или Compose в любую другую новую активити уже существующего приложения. Разработчики Compose планируют также добавить удобный API для навигации, похожий на тот, который есть в Flutter.


Свой проект я начал с шаблона Compose-проекта в Android Studio. Ниже приведен код из MainActivity.kt:


class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContent {            CoffeegramTheme {                Greeting("Android")            }        }    }}@Composablefun Greeting(name: String) {    Text(text = "Hello $name!")}@Preview(showBackground = true)@Composablefun DefaultPreview() {    CoffeegramTheme {        Greeting("Android")    }} 

Если вы уже знакомы с Compose, можете просмотреть эту часть бегло
Весь Compose построен на функциях, помеченных аннотацией @Composable. Она позволяет плагину компилятора Котлина сгенерировать необходимый для работы код.
Вместо обычной функции setContentView(), вызываемой внутри Activity.onCreate() для инфлейта лейаутов, нужно вызвать функцию setContent(), передав ей в качестве параметра Composable-функцию.


Недавно появилась аннотация @Preview для Composable-функций, с помощью которой в новых версиях Android Studio (я использовал 4.2 Canary) можно увидеть превью помеченного компонента. Однако для этого нужно будет пересобрать проект. Этот механизм немного напоминает Hot Reload из Flutter, но работает медленнее из-за необходимости пересборки и не предоставляет синхронного анализатора кода, подсвечивающего ошибки компиляции всего проекта. Так у вас не получится увидеть превью, если вы меняете UI в одном файле, а в других остались ошибки компиляции.
Другая проблема, с которой я столкнулся, возникла после удаления директории .idea из Git и с диска после коммита. Превью перестал работать совсем, из-за чего пришлось начать проект с шаблона заново. Надеюсь, что это поправят в следующих версиях студии.
Тем не менее будет полезным оставлять как минимум одну превью функцию в каждом файле, чтобы отслеживать изменения в текущем.
Можно также аннотировать одну Composable-функцию несколькими превью-аннотациями с разными параметрами, чтобы, например, отслеживать вид компонента сразу в светлой и темной теме, нескольких размерах или с тестовыми данными. Сейчас не буду останавливаться подробнее, но пример можно посмотреть тут.


Давайте создадим первый компонент приложения. Это будет элемент списка с типом кофе, количеством и кнопками для его изменения.


image

data class CoffeeType(    @DrawableRes    val image: Int,    val name: String,    val count: Int = 0)@Composablefun CoffeeTypeItem(type: CoffeeType) {    Row(        modifier = Modifier.padding(16.dp)    ) {        Image(            imageResource(type.image), modifier = Modifier                .preferredHeightIn(maxHeight = 48.dp)                .preferredWidthIn(maxWidth = 48.dp)                .fillMaxWidth()                .clip(shape = RoundedCornerShape(24.dp))                .gravity(Alignment.CenterVertically),            contentScale = ContentScale.Crop        )        Spacer(Modifier.preferredWidth(16.dp))        Text(            type.name, style = typography.body1,            modifier = Modifier.gravity(Alignment.CenterVertically).weight(1f)        )        Row(modifier = Modifier.gravity(Alignment.CenterVertically)) {            val count = state { type.count }            Spacer(Modifier.preferredWidth(16.dp))            val textButtonModifier = Modifier.gravity(Alignment.CenterVertically)                .preferredSizeIn(                    maxWidth = 32.dp,                    maxHeight = 32.dp,                    minWidth = 0.dp,                    minHeight = 0.dp                )            TextButton(                onClick = { count.value-- },                padding = InnerPadding(0.dp),                modifier = textButtonModifier            ) {                Text("-")            }            Text(                "${count.value}", style = typography.body2,                modifier = Modifier.gravity(Alignment.CenterVertically)            )            TextButton(                onClick = { count.value++ },                padding = InnerPadding(0.dp),                modifier = textButtonModifier            ) {                Text("+")            }        }    }}

Элемент списка здесь создаётся с использованием аналога ListView с горизонтальной ориентацией виджета Row. Внутри него помещается иконка (загруженный в виджет Image png-файл из drawable); разделительный виджет Spacer; Text с названием напитка, занимающий всё свободное пространство из-за применения модификатора weight(1f) (принцип похож на веса в ListView); и вложенный Row с двумя кнопками и текстом для отображения количества чашек.


Превью в Android Studio позволяет запустить виджет в интерактивном режиме. Это позволит с помощью тапов и других действий изменить состояние прямо в превью, без запуска приложения целиком на устройстве. Но если виджет получается слишком сложным (и превью в Android Studio не справляется), его можно запустить отдельно на эмуляторе. Так можно увидеть конкретный виджет без необходимости докликивать до него по всему приложению.


State


Виджет, показанный в коде выше, уже запускается в интерактивном режиме с возможностью изменить кнопками количество чашек. Это возможно из-за строки val count = state { type.count }, в которой функция state забирает исходное количество из модели type и оповещает окружающую ее Composable-функцию о каждом изменении этого состояния. Внутренние виджеты могут получить и изменить текущее значение через свойство count.value. Когда ему будет присвоено новое значение, поддерево виджетов, начиная с виджета, вызывающего функцию получения состояния, будет перерисовано (помимо state это может быть также collectAsState и прочие).


В отличие от Flutter, в Compose нет разделения на Stateful (с состоянием) и Stateless (без состояния) виджеты. Каждый виджет, содержащий вызов функции получения состояния может условно считаться Stateful, а остальные Stateless.


Теперь давайте создадим из уже имеющихся элементов весь список. Наиболее простой способ использовать виджет Column со вложенными элементами и добавить скроллинг, если список слишком длинный:


@Composablefun CoffeeList(coffeeTypes: List<CoffeeType>) {    Column {        coffeeTypes.forEach { type ->            CoffeeTypeItem(type)        }    }}@Composablefun ScrollableCoffeeList(coffeeTypes: List<CoffeeType>) {    VerticalScroller(modifier = Modifier.weight(1f)) {        CoffeeList(coffeeTypes: List<CoffeeType>)    }}

Composable функции могут быть вложены внутрь условных операторов и циклов, таких как if, for, when и т.п. Виджет Column представляет собой аналог ListView с вертикальной ориентацией, а VerticalScroller аналог ScrollView.
Проблема в этом коде должна быть очевидной. Список будет не оптимизированным и лагать во время скроллинга. Есть ли в Compose RecyclerView? Да представленный виджетом LazyColumnItems (еще недавно он назывался AdapterList). С ним реализация списка CoffeeList будет выглядеть следующим образом:


@Composablefun CoffeeList( coffeeTypes: List<CoffeeType>, modifier: Modifier = Modifier) {    LazyColumnItems(data = coffeeTypes, modifier = modifier.fillMaxHeight()) { type ->        CoffeeTypeItem(type)    }}

На данный момент нет аналога для RecyclerView с GridLayoutManager (для создания сетки вместо линейного списка). Но для приложения уже сделан один из двух экранов.


image

Перед тем как сделать следующий, нужно подумать о навигации.


Навигационные элементы из Material design реализованы очень похоже во Flutter и Compose. Корневым элементом является виджет Scaffold, обернутый темой. Он может содержать TopAppBar (верхнюю панель меню), BottomAppBar (нижнюю панель меню, с возможностью интегрировать кнопку Floating action button) или Drawer (левое боковое меню). Чтобы реализовать BottomNavigationView из Material я поместил в Scaffold виджет Column с BottomNavigation внутри:


@Composablefun DefaultPreview() {    CoffeegramTheme {        Scaffold() {                Column() {                    var selectedItem by state { 0 }                    when (selectedItem) {                        0 -> {                            Column(modifier = Modifier.weight(1f)){}                        }                        1 -> {                            CoffeeList(listOf(...))                        }                    }                    val items =                        listOf(                            "Calendar" to Icons.Filled.DateRange,                             "Info" to Icons.Filled.Info                        )                    BottomNavigation {                        items.forEachIndexed { index, item ->                            BottomNavigationItem(                                icon = { Icon(item.second) },                                text = { Text(item.first) },                                selected = selectedItem == index,                                onSelected = { selectedItem = index }                            )                        }                    }                }            }    }}

Состояние текущей выбранной вкладки содержится в selectedItem. Его состояние с помощью оператора when формирует контент для вкладки. Изменение вкладки происходит по клику на BottomNavigation с последующим изменением значения selectedItem. Такой способ построения навигации позволяет отказаться от использования фрагментов и активити кроме корневой для дерева Compose.


Реализацию второго экрана с таблицей и покоммитное создание кода приложения можно посмотреть в репозитории. Полезной деталью, которую я встретил тут, стал способ доступа к контексту для получения, например, ресурсов или текущей локали. Для этого нужно вызвать ContextAmbient.current.context внутри любой Composable-функции. Сам же экран с месячной таблицей выглядит так:


image

Во время разработки я заменил используемые png-иконки для типов кофе на векторные. Для этого функцию imageResource внутри виджета Image надо заменить на vectorResource. Можно также попробовать использовать виджет Icon для этой цели (как сделал изначально я), но тогда иконки будут монохромными.


StateFlow


Теперь перейдем ко второй части названия статьи. Flow является аналогом реактивных стримов в составе Корутин. Его можно рассматривать как холодную последовательность данных они начинают поступать только после подписки (вызова терминальной функции). Для передачи состояния между разными компонентами приложения в реактивном стиле нужен аналог BehaviorSubject из RxJava. Таким аналогом является StateFlow. Как и BehaviorSubject, он может иметь несколько подписчиков и должен быть проинициализирован исходным значением.


Для иллюстрации его использования в примере, показанном выше, состояние selectedItem может быть заменено с помощью selectedItemFlow:


val selectedItemFlow = MutableStateFlow(0)@Composablefun DefaultPreview() {    ...    val selectedItem by selectedItemFlow.collectAsState()    when (selectedItem) {        0 -> TablePage()        1 -> CoffeeListPage()    }    ...    BottomNavigationItem(        selected = selectedItem == index,        onSelected = { selectedItemFlow.value = index }    )}

Состояние получается из StateFlow (или другого Flow) с помощью вызова функции collectAsState(). Оно используется для определения необходимости перерисовки, а также получения текущего значения.


Чтобы изменить состояние, нужно присвоить значение свойству selectedItemFlow.value.


Так как текущее значение может быть также получено через это свойство, важно не забыть вызвать collectAsState() внутри виджета. Иначе он не будет обновляться вместе с состоянием. Возможным паттерном тут может быть использование состояния (val selectedItem by selectedItemFlow.collectAsState()) для чтения значения, а свойства MutableStateFlow (selectedItemFlow.value) для изменения.


Для прототипа приложения, который позволяет переключать ежемесячные таблицы, смотреть и изменять количество ежедневых чашек, мне понадобилось три StateFlow:


val yearMonthFlow = MutableStateFlow(YearMonth.now())val dateFlow = MutableStateFlow(-1)val daysCoffeesFlow: DaysCoffeesFlow = MutableStateFlow(mapOf())

yearMonthFlow отвечает за текущий отображаемый месяц.
dateFlow за выбранный день в таблице и навигацию между экранами: если текущее значение равно -1 отображается экран с таблицей TablePage. В другом случае это будет экран со списком CoffeeListPage для конкретного дня месяца.
daysCoffeesFlow прототип репозитория, содержащего все записанные чашки кофе. Его внутренняя структура стала решением следующей проблемы.


Когда пользователь переходит из TablePage в CoffeeListPage, состояние этого экрана должно быть частью (за конкретный день) из общего состояния, представленного в daysCoffeesFlow. Состояние элемента списка CoffeeList внутри должно быть также частью состояния целого списка. Когда изменяется количество чашек внутри, сам элемент не может знать, как изменить общее состояние daysCoffeesFlow. Мы помогаем ему, создавая набор мапперов из более общих Flow в более конкретные и наоборот.


Такое временное решение добавило четыре лишних типа, показанных в файле DayCoffee.kt. Если бы уровней вложенности было больше, то и мапперов было бы ещё больше.


Эти мапперы привели к трудночитаемому коду внутри UI-функций. Поэтому я решил применить MVI-архитектуру. Существующие решения, такие как MVICore, показались слишком завязанными на RxJava или другие асинхронные фреймворки и слишком сложными для текущей задачи. Моё решение базируется на статье Android MVI with Kotlin Coroutines & Flow article. Базовые концепции MVI с диаграммами можно найти там же. Здесь я покажу код базового класса Store:


abstract class Store<Intent : Any, State : Any>(private val initialState: State) {    protected val _intentChannel: Channel<Intent> = Channel(Channel.UNLIMITED)    protected val _state = MutableStateFlow(initialState)    val state: StateFlow<State>        get() = _state    fun newIntent(intent: Intent) {        _intentChannel.offer(intent)    }    init {        GlobalScope.launch {            handleIntents()        }    }    private suspend fun handleIntents() {        _intentChannel.consumeAsFlow().collect { _state.value = handleIntent(it) }    }    protected abstract fun handleIntent(intent: Intent): State}

Store работает на входящих Intent-ах и предоставляет StateFlow<State> подписчикам. Он содержит несколько вспомогательных функций, которые позволяют наследникам реализовать только похожую на Reducer функцию handleIntent() и иерархию собственных интентов и состояний. Пользователи наследников Store могут получить состояние через свойство state, возвращающее StateFlow; или передать новый интент с помощью функции newIntent().


Ниже приведен пример такого наследника NavigationStore, реализующего снова логику навигации:


class NavigationStore : Store<NavigationIntent, NavigationState>(        initialState = NavigationState.TablePage(YearMonth.now())    ) {    override fun handleIntent(intent: NavigationIntent): NavigationState {        return when (intent) {            NavigationIntent.NextMonth -> {                increaseMonth(_state.value.yearMonth)            }            NavigationIntent.PreviousMonth -> {                decreaseMonth(_state.value.yearMonth)            }            is NavigationIntent.OpenCoffeeListPage -> {                NavigationState.CoffeeListPage(                    LocalDate.of(                        _state.value.yearMonth.year,                        _state.value.yearMonth.month,                        intent.dayOfMonth                    )                )            }            NavigationIntent.ReturnToTablePage -> {                NavigationState.TablePage(_state.value.yearMonth)            }        }    }    private fun increaseMonth(yearMonth: YearMonth): NavigationState {        return NavigationState.TablePage(yearMonth.plusMonths(1))    }    private fun decreaseMonth(yearMonth: YearMonth): NavigationState {        return NavigationState.TablePage(yearMonth.minusMonths(1))    }}sealed class NavigationIntent {    object NextMonth : NavigationIntent()    object PreviousMonth : NavigationIntent()    data class OpenCoffeeListPage(val dayOfMonth: Int) : NavigationIntent()    object ReturnToTablePage : NavigationIntent()}sealed class NavigationState(val yearMonth: YearMonth) {    class TablePage(yearMonth: YearMonth) : NavigationState(yearMonth)    data class CoffeeListPage(val date: LocalDate) : NavigationState(        YearMonth.of(date.year, date.month)    )}

Начнем с конца. Здесь можно увидеть два sealed-класса, отвечающих за все возможные интенты и состояния, с которыми работает этот Store. Интенты представляют возможные действия в UI. А состояния навигации соответствующие экраны приложения.


Параметр initialState в NavigationStore представляет собой состояние-экран, которое увидит пользователь, только открыв приложение.


Функция handleIntent() содержит бизнес-логику превращения интентов в состояния.
Второй DaysCoffeesStore, отвечающий непосредственно за кофе, как и весь код приложения, можно найти в репозитории.


Несмотря на детские болезни Jetpack Compose и необходимость поменять мышление для полного погружения, он уже выглядит как готовая для использования в некритичных приложениях технология и будущее Android-разработки. С ним легко применить современные техники, такие как архитектуры с однонаправленным потоком данных и Корутины, решая некоторые задачи (как, например, навигацию) легче. Я думаю, что разработчики должны начать знакомиться с ним, чтобы не пропустить тот момент, когда Compose (вместе с Корутинами) появится в списке обязательных технологий в вакансиях.


С популяризацией декларативных UI-фреймворков, таких как Compose, Flutter и SwiftUI, мобильная разработка становится всё больше похожей на Web. Это может привести, как к унификации используемых архитектур, так и к увеличению переиспользования кода между большинством клиентских платформ.

Подробнее..

Compose. Jetpack Compose

09.10.2020 12:10:51 | Автор: admin
image

Благодаря стремительному развитию мобильной индустрии каждые несколько лет мы наблюдаем появления новых технических решений, призванных усложнить упростить жизнь разработчикам. Некоторые из них, не сыскав популярности у пользователей, остаются лишь частью истории, другие плотно укореняются в повседневной разработке, становясь в определенной области стандартом де-факто.

Пожалуй, главным трендом мобильной разработки за последние несколько лет стал декларативный UI. Такое решение уже давно успешно применяется в веб и кроссплатформенных решениях и, наконец, добралось и до нативной разработки. На iOS существует SwiftUI (представленный на WWDC 2019), а на Android Jetpack Compose (представленный месяцем ранее на Google I/O 2019). И именно о последнем мы сегодня и поговорим.

Примечание: в данной статье мы не будем рассматривать поэтапное создание первого проекта на Compose, так как этот процесс прекрасно описан в других материалах. Моя цель лишь рассказать о преимуществах и недостатках, которые дает android-разработчикам переход на Jetpack Compose, а решение использовать или нет всегда остаётся за вами.

Появление


Официальная история Jetpack Compose начинается с мая 2019, когда он был представлен публике на конференции Google I/O. Простой, реактивный и Kotlin-only новый декларативный фреймворк от Google выглядел как младший брат Flutter (который к тому моменту уже стремительно набирал популярность).

API design is building future regret

О недостатках текущего UI-фреймворка Android было сказано и написано уже достаточно большое количество раз. Проблемы с View-иерархией, зависимость от релизов платформы наличие этих и множества других мелких недостатков в той или иной мере доставляли неудобства разработчикам, что и побудило компанию Google заняться разработкой нового фреймворка, способного решить все эти проблемы.

Преимущества


Итак, чем же хорош Jetpack Compose и, главное, чем он кардинально отличается от существующего на данный момент UI-фреймворка Android?

  • Unbundled toolkit: JC не зависит от конкретных релизов платформы, а значит, забудем уже про Support Library.
  • Kotlin-only: Больше не нужно переключаться между классами и xml-файлами вся работа с UI происходит в одном Kotlin-файле.
  • Композитный подход: Наследованию нет, композиции да. Каждый UI-компонент представляет собой обычную composable-функцию, отвечающую только за ограниченный функционал, т.е. без лишней логики. Никаких больше View.java на 30 тысяч строк кода.
  • Unidirectional Data Flow: Одна из основополагающих концепций Jetpack Compose, о которой будет рассказано подробнее чуть ниже.
  • Обратная совместимость: Для использования Compose не требуется начинать проект с нуля. Имеется возможность как его встраивания (с помощью ComposeView) в имеющуюся xml-вёрстку, так и наоборот.
  • Меньше кода: Тут, как говорится, лучше один раз увидеть, чем сто раз услышать. В качестве примера возьмём классическое сочетание компонентов два поля ввода и кнопка подтверждения:

В реализации текущего UI-фреймворка вёрстка этих компонентов выглядит так:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="wrap_content"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:orientation="vertical"    android:padding="@dimen/padding_16dp">    <com.google.android.material.textfield.TextInputLayout        android:id="@+id/til_login"        android:layout_width="match_parent"        android:layout_height="wrap_content"        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"        android:hint="@string/sign_in_email"        android:layout_marginBottom="@dimen/margin_8dp">        <com.google.android.material.textfield.TextInputEditText            android:id="@+id/et_login"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:inputType="text"/>    </com.google.android.material.textfield.TextInputLayout>    <com.google.android.material.textfield.TextInputLayout        android:id="@+id/til_password"        android:layout_width="match_parent"        android:layout_height="wrap_content"        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"        android:hint="@string/sign_in_password"        android:layout_marginVertical="@dimen/margin_8dp">        <com.google.android.material.textfield.TextInputEditText            android:id="@+id/et_password"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:inputType="textPassword"/>    </com.google.android.material.textfield.TextInputLayout>    <Button        android:id="@+id/btn_confirm"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="@string/sign_in_submit"        android:layout_marginTop="@dimen/margin_8dp"        android:padding="@dimen/padding_8dp"        android:background="@color/purple_700"/></LinearLayout>

В то же время, при использовании Jetpack Compose, решение будет выглядеть следующим образом:

@Preview@Composablefun LoginPage(){    var loginValue by remember { mutableStateOf(TextFieldValue("")) }    var passwordValue by remember { mutableStateOf(TextFieldValue("")) }    Surface(color = Color.White) {        Column(modifier = Modifier.padding(16.dp).fillMaxWidth()) {            Surface(color = Color.White, modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp))) {                OutlinedTextField(                        value = loginValue,                        onValueChange = { loginValue = it },                        label = { Text(text = stringResource(id = R.string.sign_in_email)) },                        placeholder = { Text(text = stringResource(id = R.string.sign_in_email)) },                        modifier = Modifier.fillMaxWidth()                )            }            Surface(color = Color.White, modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp))) {                OutlinedTextField(                        value = passwordValue,                        onValueChange = { passwordValue = it },                        label = { Text(text = stringResource(id = R.string.sign_in_password)) },                        placeholder = { Text(text = stringResource(id = R.string.sign_in_password)) },                        visualTransformation = PasswordVisualTransformation(),                        modifier = Modifier.fillMaxWidth()                )            }            Button(                    onClick = {},                    modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp)).fillMaxWidth(),                    backgroundColor = colorResource(R.color.purple_700)) {                Text(text = stringResource(id = R.string.sign_in_submit), modifier = Modifier.padding(8.dp))            }        }    }}

Ну и напоследок сравнительный результат:

image

Недостатки


  • Alpha-версия: Безусловно, более чем за год разработки фреймворк значительно преобразился и стал гораздо стабильнее. Однако это всё ещё альфа, а поэтому за пределами Pet-проектов использовать его не рекомендуется.

Декларативный стиль


Отдельное внимание стоит уделить главной особенности Jetpack Compose декларативному стилю создания UI. Суть подхода заключается в описании интерфейса как совокупности composable-функций (они же виджеты), которые не используют под капотом view, а напрямую занимаются отрисовкой на canvas. Для кого-то это минус, для других возможность попробовать что-то новое. Так или иначе, к концепции верстать UI кодом нативному разработчику, не работавшему ранее с аналогичными технологиями (к примеру, Flutter или React Native), придётся привыкать.

Что за Unidirectional Data Flow?


В современном android-приложении UI-состояние меняется в зависимости от приходящих событий (нажатие на кнопку, переворот экрана и т.д.). Мы нажимаем на компонент, тем самым формируя событие, а компонент меняет свой state и вызывает callback в ответ. Из-за довольно тесной связи UI-состояния с View это потенциально может привести к усложнению поддержки и тестирования такого кода. К примеру, возможна ситуация, когда помимо внутреннего state компонента, мы можем хранить его состояние в поле (например во viewmodel), что теоретически может привести к бесконечному циклу обновления этого самого state.

Что же касается Jetpack Compose, то здесь все компоненты по умолчанию являются stateless. Благодаря принципу однонаправленности нам достаточно скормить модель данных, а любое изменение состояния фреймворк обработает за нас. Таким образом, логика компонента упрощается, а инкапсуляция состояния позволяет избежать ошибок, связанных с его частичным обновлением. В качестве примера возьмем уже рассмотренный ранее composable-код. Перед описание компонентов были определены две переменные:

    var loginValue by remember { mutableStateOf(TextFieldValue("")) }    var passwordValue by remember { mutableStateOf(TextFieldValue("")) }

Мы создаем два текстовых объекта, значения которых будем устанавливать полям ввода (логина и пароля) в качестве value. А благодаря связке remember { mutableStateOf() } любое изменение значений этих объектов (из других частей кода) уведомит об этом соответствующее поле ввода, которое перерисует только значение value, вместо полной рекомпозиции всего компонента.

Вывод


Какой же вывод можно сделать о Jetpack Compose? По моему мнению, у нового решения от Google имеется огромный потенциал. С момента анонса в 2019 году была проделана огромная работа, и не менее долгий путь до релиза у фреймворка ещё впереди. Однако теперь он публично доступен, и я считаю, что это прекрасная возможность познакомиться с ним поближе. Ну а за чем, по вашему мнению, будущее пишите в комментарии, будет интересно узнать ваше мнение. Любите android!
Подробнее..

Представляем бета-версию Jetpack Compose

04.03.2021 14:05:18 | Автор: admin

Совсем недавно, 24 февраля, мы анонсировали запуск бета-версииJetpack Compose. Этот новый набор инструментов для разработки пользовательского интерфейса позволит легко и быстро создавать оригинальные приложения для всех платформ Android. Jetpack Compose предоставляет современные и декларативные API для языка Kotlin для создания привлекательных и быстрых приложений с меньшим объемом кода. Набор совместим с существующими приложениями для Android и библиотеками Jetpack. Кроме того, его можно использовать вместе с Android Views.

Бета-версия Compose это уже готовый API со всеми основными функциями, необходимыми для комфортной работы. Версия стабильная, поэтому мы не будем изменять или удалять API. Финальная версия 1.0 станет доступна уже в этом году. Сейчас самое время начать знакомство с Compose и запланировать применение новых инструментов в следующих проектах и компонентах.

Возможности бета-версии

При создании Compose нашей команде помогали и другие разработчики, которые оставляли свои отзывы. С момента открытия исходного кода в 2019 году мы выпустили 30 публичных версий продукта, получили более 700 внешних отчетов об ошибках и больше 200 внешних дополнений. Нам нравится наблюдать за результатами вашей работы с Сompose, и мы внимательно изучили все отзывы и предложения, чтобы усовершенствовать API и расставить приоритеты при разработке. Мы значительно доработали альфа-версию продукта, а также добавили и улучшили функционал. Вот некоторые из них:

  • Поддержка сопрограмм (новое)

  • Поддержка специальных возможностей для TalkBack. Другие технологии появятся в финальной версии (новое)

  • Новый API для простого использованияанимации(новое)

  • Совместимостьс Views

  • Компоненты Material UIс примерами кода

  • Ленивые списки аналог RecyclerView

  • РазметкаConstraint Layoutна основе DSL

  • Модификаторы

  • Тестирование

  • Темы и графика для простого добавления тёмных и светлых тем

  • Ввод и жесты

  • Редактируемый и обычный текст

  • Управление окнами

Главная задача этой бета-версии обеспечить работу всех API, необходимых в этой и следующих версиях. Мы будем улучшать их стабильность вплоть до финальной версии и уделять внимание производительности и специальным возможностям приложений.

Бета-версия Compose поддерживается в последней версииAndroid Studio Arctic Fox Canary, в которой тоже многоновых инструментов:

  • Live Literals: обновление литералов в реальном времени при предварительном просмотре, на устройстве и в эмуляторе (новое)

  • Предварительный просмотр анимации(новое)

  • Поддержка Compose в инструменте Layout Inspector(новое)

  • Интерактивный предварительный просмотр: воспроизведение сборки, выполненной с помощью Compose, в изолированной среде и взаимодействие с ней (новое)

  • Предварительный просмотр разметки: разметка сборки, выполненной с помощью Compose, прямо на устройстве даже при отсутствии полного приложения (новое)

Live Literals в Android EmulatorLive Literals в Android EmulatorLayout Inspector для Jetpack ComposeLayout Inspector для Jetpack Compose

Работа с уже созданным приложением

Jetpack Compose безупречно работает с Android Views, и вам не придется менять старые привычки. Интерфейсы из Compose можно встроить в Android Views, и наоборот. Вдокументации по совместимостирассказано обо всех возможностях использования этих наборов инструментов.

Compose работает не только с Views, но и с самымираспространенными библиотеками. Вам не придется переписывать приложение. Вот что мы интегрировали:

  • Navigation

  • ViewModel

  • LiveData/RX/Flow

  • Paging

  • Hilt

БиблиотекиMDC-Android Compose Theme AdapterиAccompanistработают с темамиMaterialиAppCompatXML. Вам не придется повторять определения тем. Accompanist также предлагает оболочки распространеннымбиблиотекам для загрузки изображений.

Легкость работы в Compose

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

Jetpack Compose полностью написан на языке Kotlin и использует все егопреимущества, предлагая мощные, простые и интуитивные API. Например,сопрограммыпозволяют создавать простые асинхронные API для описания жестов, анимации и прокрутки. Это облегчает написание кода, сочетающего асинхронные события, например жесты, передающие анимацию, с отменой и очисткой, обеспечиваемой структурированным параллелизмом.

Знакомство с Compose

Чтобы вы ближе познакомились с Jetpack Compose, мы обновили своймини-курсс видео, практическими работами и полезными документами. Сегодня мы также обновилируководство по документации, добавили несколько видеороликов и представили новуюпрактическую работу по созданию анимации. Все это поможет лучше работать с Compose. Помимо статей обархитектуре,специальных возможностяхитестировании, теперь вы можете изучить информацию обанимации,спискахидекларативности.

А если вам хочется сразу приступить к делу, мы подготовили восемьобразцов официальных предложенийразных уровней сложности. В них демонстрируются все нужные API для разработки. Чтобы узнать больше, ознакомьтесь синформацией.

Заключение

В бета-версии Jetpack Compose все нужные API и функции готовы к выходу версии 1.0. Самое время знакомиться с набором инструментов и думать о том, где реализовать его возможности. Мы рады вашимотзывамоб использовании Compose. Своими впечатлениями можно также поделиться с другими разработчиками на канале #compose вKotlin Slack.


Выражаем благодарность за помощь в подготовке статьи коллегам: Анна-Кьяра Беллини(менеджер по продуктам),Ник Бутчер(подразделение по работе с разработчиками) и Звиад Кардава (подразделение по работе с разработчиками)

Подробнее..

Перевод BSBD погиб пилот реактивного ранца Винс Реффет

17.11.2020 18:17:45 | Автор: admin
image

Француз Винс Реффет (Vince Reffet), входящий в команду JetMan, которая выполняла новаторские трюки над Дубаем на турбореактивных крыльях из углеродного волокна, погиб во вторник в результате несчастного случая на тренировке, сообщил пресс-секретарь.

Пилоты реактивных крыльев Jetman совершили серию впечатляющих полетов над городом Персидского залива, паря в тандеме над самым высоким зданием в мире Бурдж-Халифа и рядом с Emirates Airbus A380, крупнейшим коммерческим авиалайнером в мире.



С невообразимой грустью мы объявляем о кончине пилота реактивного ранца Винсента (Винса) Реффета, который умер сегодня утром, 17 ноября, во время тренировки в Дубае, сообщил агентству AFP пресс-секретарь Jetman Dubai Абдулла Бинхабтур.

В возрасте 36 лет Винс был талантливым спортсменом, а также очень любимым и уважаемым членом нашей команды. Наши мысли и молитвы с его семьей и всеми теми, кто знал и работал с ним.

Инцидент, произошедший с пилотом реактивного крыла Реффетом в пустыне за городом, сейчас расследуется.

Мы тесно сотрудничаем со всеми соответствующими органами, сказал Бинхабтур.

Подвиги Реффета стали вирусными в начале этого года, когда он снялся на видео с земли и поднялся на 1800 метров (почти 6000 футов) над набережной Дубая, что напоминает подвиг Железного Человека Marvel, только это произошло впервые в реальности.



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

До этого летчики поднимались в воздух, спрыгивая с высоких платформ.



Крыло Реффета из углеродного волокна разгоняется четырьмя мини-реактивными двигателями. Оборудование, управляемое движениями пилота, способно развивать скорость до 400 километров в час.

В команде Jetman также француз Fred Fugen и эмиратец Ahmed Alshehhi.

В прошлом году Реффет и Фуген также пролетели через знаменитую арку Небесных врат в горах китайской провинции Хунань.



PS


image

Надпись на крыле Винса Реффета: Ты выйдешь за меня?

image

Agnes Rodriguez ответила: Да!

RIP





К сожалению, мы не выбираем себе смерть, но мы можем встретить её достойно, чтобы нас запомнили как мужчин.
к/ф Гладиатор
Подробнее..

Navigation Component-дзюцу, vol. 3 Corner-кейсы

23.09.2020 10:08:04 | Автор: admin


В этой части трилогии про Navigation Component разберем как организовать навигацию в многомодульных приложениях, как работается с диплинками, а также рассмотрим кейсы со встраиваемыми фрагментами и диалогами.


Это третья и заключительная статья в цикле про различные кейсы навигации с Navigation Component-ом. Вы также можете ознакомиться с первой и второй частями



Если вы работаете с большим приложением, вероятно, вы уже разбили его на модули. Неважно, как именно. Может быть, вы создаёте отдельные модули для логики и UI, а может храните всю логику фичи (от взаимодействия с API до логики presentation-слоя) в одном модуле. Главное у вас могут быть кейсы, когда требуется осуществить навигацию между двумя независимыми модулями.


Где на схеме приложения кейсы с навигацией?


На картинке мы видим, что у нас есть как минимум два модуля: модуль :vacancy с одним экраном и модуль :company с двумя экранами вложенного flow. В рамках моего примера я построил навигацию из модуля :vacancy в модуль :company, которые не связаны друг с другом.


Существует три способа как это сделать, разберём их один за другим.


App-модуль + интерфейсы


Первый способ использовать ваш application-модуль в качестве хранилища всего графа навигации и определить в feature-модулях специальные интерфейсы для роутинга.


Структура вашего приложения в этом способе


Структура приложения будет стандартной: есть app-модуль, который знает обо всех feature-модулях, есть feature-модули, которые не знают друг о друге. В этом способе ваши feature-модули пребывают в священном неведении о Navigation Component, и для навигации они будут определять интерфейсы примерно вот такого вида:


// ::vacancy moduleinterface VacancyRouterSource {    fun openNextVacancy(vacancyId: String)    // For navigation to another module    fun openCompanyFlow()}

А ваш app-модуль будет реализовывать эти интерфейсы, потому что он знает обо всех action-ах и навигации:


fun initVacancyDI(navController: NavController) {  VacancyDI.vacancyRouterSource = object : VacancyRouterSource {      override fun openNextVacancy(vacancyId: String) {          navController.navigate(              VacancyFragmentDirections                .actionVacancyFragmentToVacancyFragment(vacancyId = vacancyId)          )      }      override fun openCompanyFlow() {          initCompanyDI(navController)          navController.navigate(R.id.action__VacancyFragment__to__CompanyFlow)      }  }}

Плюс этого способа отсутствие дополнительных модулей, которые могут повлиять на скорость сборки приложения. Однако в минусах вы получаете:


  • дополнительную работу в виде определения интерфейсов, реализаций, организации DI для проброса этих интерфейсов в ваши feature-модули;
  • отсутствие возможности использовать использовать Safe Args плагин, делегат navArgs, сгенерированные Directions, и другие фишки Navigation Component-а в feature-модулях, потому что эти модули ничего не знают про библиотеку.

Сомнительный, в общем, способ.


Графы навигации в feature-модулях + диплинки


Второй способ вынести отдельные графы навигации в feature-модули и использовать поддержку навигации по диплинкам (она же навигация по URI, которую добавили в Navigation Component 2.1).


Структура вашего приложения в этом способе


Структура приложения будет ровно такой же, как и в первом способе: есть app-модуль, который зависит от всех feature-модулей, и есть feature-модули, которые друг о друге ничего не знают.


Но теперь ваш app-модуль не обязан содержать весь граф навигации приложения, он может содержать только его часть. А остальные кусочки будут содержать именно feature-модули.


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/company_flow__nav_graph"    app:startDestination="@id/CompanyFragment">    <fragment        android:id="@+id/CompanyFragment"        android:name="company.CompanyFragment">        <deepLink app:uri="companyflow://company" />        <!-- Or with arguments -->        <argument android:name="company_id" app:argType="long" />        <deepLink app:uri="companyflow://company" />        <action            android:id="@+id/action__CompanyFragment__to__CompanyDetailsFragment"            app:destination="@id/CompanyDetailsFragment" />    </fragment>    <fragment        android:id="@+id/CompanyDetailsFragment"        android:name="company.CompanyDetailsFragment" /></navigation>

Feature-модули будут определять свои собственные графы навигации для роутинга между экранами, о которых они знают. А ещё они будут объявлять диплинки для тех экранов, на которые можно попасть из других модулей. В примере выше мы добавили тэг deepLink, чтобы на экран CompanyFragment можно было попасть из другого модуля.


После этого мы можем использовать этот диплинк для открытия экрана CompanyFragment из модуля :vacancy :


// ::vacancy modulefragment_vacancy__button__open_company_flow.setOnClickListener {  // Navigation through deep link  val companyFlowUri = "companyflow://company".toUri()  findNavController().navigate(companyFlowUri)}

Плюс этого метода в том, что это самый простой способ навигации между двумя независимыми модулями. А минус что вы не сможете использовать Safe Args, или сложные типы аргументов (Enum, Serializable, Parcelable) при навигации между фичами.


P.S. Есть, конечно, вариант сериализовать ваши сложные структуры в JSON и передавать их в качестве String-аргументов в диплинк, но это как-то Странно.


Общий модуль со всем графом навигации


Третий способ ввести для хранения всего графа навигации специальный модуль, который будет подключаться к каждому feature-модулю.


Структура вашего приложения в этом способе


У нас по-прежнему есть app-модуль, но теперь его задача просто подсоединить к себе все feature-модули; он больше не хранит в себе граф навигации. Весь граф навигации теперь располагается в специальном модуле, который ничего не знает о feature-модулях. Зато каждый feature-модуль знает про common navigation.


В чём соль? Несмотря на то, что common-модуль не знает о реализациях ваших destination-ов (фрагментах, диалогах, activity), он всё равно способен объявить граф навигации в XML-файлах! Да, Android Studio начинает сходить с ума: все имена классов в XML-е горят красным, но, несмотря на это, все нужные классы генерируются, Safe Args плагин работает как нужно. И так как ваши feature-модули подключают к себе common-модуль, они могут свободно использовать все сгенерированные классы и пользоваться любыми action-ами вашего графа навигации.


Плюс этого способа наконец-то можно пользоваться всеми возможностями Navigation Component-а в любом feature-модуле. Из минусов:


  • добавился ещё один модуль в critical path каждого feature-модуля, которому потребовалась навигация;
  • отсутствует автоматический рефакторинг имён: если вы поменяете имя класса какого-нибудь destination-а, вам нужно будет не забыть, что надо поправить его в common-модуле.

Выводы по навигации в многомодульных приложениях


  • У вас на выбор целых три способа организации межмодульной навигации.
  • Выбирайте тот, который вам нравится, понятен и, главное, который вам проще реализовать.

Работа с диплинками


Практически каждое большое приложение должно уметь поддерживать диплинки. И практически каждый Android-разработчик мечтал о простом способе работы с этими глубокими ссылками. Окей, я мечтал. И казалось, что Navigation Component ровно то, что нужно.


По моей задумке кейсы с диплинками должны были занимать довольно большую часть переходов тестового приложения.


Какую именно часть?


У меня было три кейса с диплинками, которые я хотел реализовать с помощью Navigation Component.


  • Открытие определённой вкладки нижней навигации допустим, я хочу через диплинк открыть вторую вкладку на главном экране после Splash-экрана

Посмотреть на картинке

Допустим, я хочу через диплинк открыть вкладку Favorites нижней навигации на главном экране после Splash-экрана:



  • Открытие определённого экрана ViewPager-а внутри конкретной вкладки нижней навигации

Посмотреть на картинке

Пусть я хочу открыть определённую вкладку ViewPager-а внутри вкладки Responses:



  • Открытие экрана, который требует авторизации пользователя пусть теперь для корректной работы вкладки нижней навигации требуется авторизация. И с помощью диплинка я хочу сначала показать Splash-экран, затем открыть флоу авторизации, а когда пользователь его пройдёт, открыть нужную вкладку

Посмотреть на картинке

Пусть вкладка Profile теперь требует авторизации. И с помощью диплинка я хочу сначала показать Splash-экран, затем открыть флоу авторизации, а когда пользователь его пройдёт открыть вкладку Profile.



Сразу скажу, что у меня не получилось нормально реализовать ни один из этих кейсов. И работа с диплинками в Navigation Component стала для меня самым большим разочарованием.



Прежде чем делать диплинк для второй вкладки, я решил настроить ссылку хотя бы для первой. Я решил, что для этого будет достаточно просто добавить диплинк на экран контейнера нижней навигации (не самой вкладки, а контейнера):


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/app_nav_graph"    app:startDestination="@id/SplashFragment">    <fragment        android:id="@+id/SplashFragment"        android:name="ui.splash.SplashFragment" />    <fragment        android:id="@+id/MainFragment"        android:name="ui.main.MainFragment">        <deepLink app:uri="www.example.com/main" />    </fragment></navigation>

Затем я, следуя документации, добавил граф навигации с диплинком в Android Manifest:


<manifest xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    package="com.aaglobal.jnc_playground">    <application android:name=".App">        <activity android:name=".ui.root.RootActivity">            <nav-graph android:value="@navigation/app_nav_graph"/>            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>    </application></manifest>

А потом решил проверить, работает ли то, что я настроил при помощи простой adb-команды:


adb shell am start \  -a android.intent.action.VIEW \  -d "https://www.example.com/main" com.aaglobal.jnc_playground

И-и-и нет. Ничего не завелось. Я получил краш приложения с уже знакомым исключением IllegalStateException: FragmentManager is already executing transactions. Дебаггер указывал на код, связанный с настройкой нижней навигации, поэтому я решил просто обернуть эту настройку в очередной Handler.post:


// MainFragment.kt  fragment with BottomNavigationViewoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {    super.onViewCreated(view, savedInstanceState)    if (savedInstanceState == null) {        safeSetupBottomNavigationBar()    }}private fun safeSetupBottomNavigationBar() {    Handler().post {        setupBottomNavigationBar()    }}

Это исправило краш, но приложение всё равно работало неправильно: запустив диплинк, мы пропустили Splash-экран, он просто не запускался. А это означает, что не отрабатывал код, который отвечал за инициализацию моего приложения.


Это произошло, потому что в нашем случае путь диплинка был таким: мы запустили приложение, запустилась его единственная Activity. В вёрстке этой activity мы инициализировали первый граф навигации. В этом графе оказался элемент, который удовлетворял URI, мы отправили его через adb-команду вуаля, он сразу и открылся, проигнорировав указанный в графе startDestination.


Тогда я решил перенести диплинк в другой граф внутрь вкладки нижней навигации.


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/menu__search"    app:startDestination="@id/SearchContainerFragment">    <fragment        android:id="@+id/SearchContainerFragment"        android:name="tabs.search.SearchContainerFragment">        <deepLink app:uri="www.example.com/main" />        <action            android:id="@+id/action__SearchContainerFragment__to__CompanyFlow"            app:destination="@id/company_flow__nav_graph" />        <action            android:id="@+id/action__SearchContainerFragment__to__VacancyFragment"            app:destination="@id/vacancy_nav_graph" />    </fragment></navigation>

И, запустив приложение, я получил ЭТО:


Посмотреть на ЭТО


На гифке видно, как приложение запустилось, и мы увидели Splash-экран. После этого на мгновение показался экран с нижней навигацией, а затем приложение словно запустилось заново! Мы снова увидели Splash-экран, и только после его повторного прохождения появилась нужная вкладка нижней навигации.


И что самое неприятное во всей этой истории это не баг, а фича.


Если почитать внимательно документацию про работу с диплинками в Navigation Component, можно найти следующий кусочек:


When a user opens your app via an explicit deep link, the task back stack is cleared and replaced with the deep link destination.

То есть наш back stack специально очищается, чтобы Navigation Component-у было удобнее работать с диплинками. Говорят, что когда-то давно, в бета-версии библиотеки всё работало адекватнее.


Мы можем это исправить. Корень проблемы в методе handleDeepLink NavController-а:


Кусочек handleDeepLink
public void handleDeepLink(@Nullable Intent intent) {    // ...    if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {        // Start with a cleared task starting at our root when we're on our own task        if (!mBackStack.isEmpty()) {            popBackStackInternal(mGraph.getId(), true);        }        int index = 0;        while (index < deepLink.length) {            int destinationId = deepLink[index++];            NavDestination node = findDestination(destinationId);            if (node == null) {                final String dest = NavDestination.getDisplayName(mContext, destinationId);                throw new IllegalStateException("Deep Linking failed:"                        + " destination " + dest                        + " cannot be found from the current destination "                        + getCurrentDestination());            }            navigate(node, bundle,                    new NavOptions.Builder().setEnterAnim(0).setExitAnim(0).build(), null);        }        return true;    }}

Чтобы переопределить это поведение, нам потребуется:


  • почти полностью скопировать к себе исходный код Navigation Component;
  • добавить свой собственный NavController с исправленной логикой (добавление исходного кода библиотеки необходимо, так как от NavController-а зависят практически все элементы библиотеки) назовём его FixedNavController;
  • заменить все использования исходного NavController-а на FixedNavController.

Несложно, правда? Но кажется, что это уже слишком. Подобные исправления заставляют нас в будущем тратить всё больше и больше времени на поддержку нашего кастомного решения, ведь нужно будет обновлять свой фикс при каждом обновлении библиотеки. Около года назад коллеги из Яндекса столкнулись с похожей проблемой, и им пришлось делать примерно то же самое. Сочувствую.


В этот невесёлый момент я заметил ещё один баг, который был добавлен при попытке исправить краш с диплинками: сломалась обратная навигация из auth-флоу.


Покажи гифку


На гифке видно, как мы переключаемся на вкладку Profile, затем переходим во флоу авторизации. Потом мы нажимаем на кнопку Back и получаем просто белый экран. Если переключиться на какую-нибудь другую вкладку и обратно, то мы снова увидим контент вкладки Profile.


Для этой проблемы я уже даже не стал искать решение, потому что чертовски устал от этих войн за каждый кейс навигации. Если знаете как исправить, отпишитесь в комментариях, пожалуйста.



Если у вас будет свой собственный NavController, корректно обрабатывающий диплинки, реализовать этот кейс будет просто.


В NavController-е есть специальный булевский флажок isDeepLinkHandled, который говорит нам, что текущий NavController успешно обработал диплинк. Вы могли бы добавить диплинк, ведущий на фрагмент, который содержит в себе ViewPager, затем написать примерно вот такой код, чтобы перейти на нужную вкладку:


if (findMyNavController().isDeepLinkHandled && requireActivity().intent.data != null) {    val uriString = requireActivity().intent.data?.toString()    val selectedPosition = when {        uriString == null -> 0        uriString.endsWith("favorites") -> 0        uriString.endsWith("subscribes") -> 1        else -> 2    }    fragment_favorites_container__view_pager.setCurrentItem(selectedPosition, true)}

Но, опять же, это будет доступно только в случае, если вы уже добавили к себе в кодовую базу свою реализацию NavController-а, ведь флаг isDeepLinkHandled является private-полем. Ок, можно достучаться до него через механизм reflection-а, но это уже другая история.



Navigation Component не поддерживает диплинки с условием из коробки. Если вы хотите поддержать такое поведение, Google предлагает действовать следующим образом:


  • через диплинк открыть экран, который требует авторизацию;
  • на этом экране проверить, авторизован ли пользователь, если нет открыть флоу авторизации поверх нужного экрана;
  • пройти auth flow, вернуть результат из вложенного графа и т.д., и т.п.

Возможности глобально решить мою задачу средствами Navigation Component-а я не нашёл.


Выводы по работе с диплинками в Navigation Component


  • Работать с ними больно, если требуется добавлять дополнительные действия или условия.
  • Объявлять диплинки ближе к месту их назначения классная идея, в разы удобнее AndroidManifest-а со списком поддерживаемых ссылок.

Бонус-секция кейсы БЕЗ проблем


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



Допустим, у вас есть экран вакансий, с которого вы можете перейти на другую вакансию.


Где на схеме приложения этот кейс?


По опыту предыдущих кейсов я ожидал проблем и здесь, но всё оказалось достаточно просто я определил навигацию с экрана на экран:


<fragment  android:id="@+id/VacancyFragment"  android:name="com.aaglobal.jnc_playground.ui.vacancy.VacancyFragment"  android:label="Fragment vacancy"  tools:layout="@layout/fragment_vacancy">  <argument      android:name="vacancyId"      app:argType="string"      app:nullable="false" />  <action      android:id="@+id/action__VacancyFragment__to__VacancyFragment"      app:destination="@id/VacancyFragment" /></fragment>

И этого оказалось достаточно новый экран открывался поверх старого, при нажатии на кнопку Back навигация была корректной. Если бы я захотел, чтобы каждый новый экран открывался вместо текущего, было бы достаточно добавить атрибут popUpTo к моему action-у.



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


Где на схеме приложения этот кейс?


Я добавил контейнер для будущего фрагмента со списком в вёрстку вкладки нижней навигации:


<LinearLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <TextView        android:id="@+id/fragment_favorites_container__text__title"        style="@style/LargeTitle"        android:text="Favorites container" />    <androidx.fragment.app.FragmentContainerView        android:id="@+id/fragment_favorites_container__container__recommend_vacancies"        android:layout_width="match_parent"        android:layout_height="match_parent" /></LinearLayout>

А затем в runtime-е добавил нужный мне фрагмент в этот контейнер:


class FavoritesContainerFragment : Fragment(R.layout.fragment_favorites_container) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        childFragmentManager.attachFragmentInto(          containerId = R.id.fragment_container_view,          fragment = createVacancyListFragment()        )    }}

Метод attachFragmentInfo на childFragmentManager это extension-метод, который просто оборачивает всю работу с транзакциями, не более того.


А вот как я создал фрагмент:


class FavoritesContainerFragment : Fragment(R.layout.fragment_favorites_container) {    // ...    private fun createVacancyListFragment(): Fragment {        return VacancyListFragment.newInstance(          vacancyType = "favorites_container",          vacancyListRouterSource = object : VacancyListRouterSource {              override fun navigateToVacancyScreen(item: VacancyItem) {                  findNavController().navigate(                      R.id.action__FavoritesContainerFragment__to__VacancyFragment,                      VacancyFragmentArgs(vacancyId = "${item.name}|${item.id}").toBundle()                  )              }        }     }}

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



Пусть у меня есть несколько BottomSheetDialog-ов, между которыми я хочу перемещаться с помощью Navigation Component.


Где на схеме приложения этот кейс?


Год назад с таким кейсом были какие-то проблемы, но сейчас всё работает как надо. Можно легко объявить какой-то dialog в качестве destination-а в вашем графе навигации, можно добавить action для открытия диалога из другого диалога.


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/menu__favorites"    app:startDestination="@id/FavoritesContainerFragment">   <dialog        android:id="@+id/ABottomSheet"        android:name="ui.dialogs.dialog_a.ABottomSheetDialog">        <action            android:id="@+id/action__ABottomSheet__to__BBottomSheet"            app:destination="@id/BBottomSheet"            app:popUpTo="@id/ABottomSheet"            app:popUpToInclusive="true" />    </dialog>    <dialog        android:id="@+id/BBottomSheet"        android:name="ui.dialogs.dialog_b.BBottomSheetDialog">        <action            android:id="@+id/action__BBottomSheet__to__ABottomSheet"            app:destination="@id/ABottomSheet"            app:popUpTo="@id/BBottomSheet"            app:popUpToInclusive="true" />    </dialog></navigation>

Диалоги создавались как нужно, закрывались вовремя, навигация по кнопке Back отрабатывала как ожидалось.


Выводы по бонус-секции


Кейсы без проблем существуют.


Подведём итоги


На данный момент нет никакой причины переводить большое приложение на Navigation Component. Слишком много проблем, слишком много костылей, постоянно нужно выдумывать что-то для осуществления не самых сложных кейсов навигации. Сам факт, что я ухитрился написать так много текста про проблемы с Navigation Component-ом, что-то да говорит.


Если у вас маленькое приложение, надо внимательно посмотреть на то, какие кейсы навигации вы хотите поддержать. Если задумываетесь про нижнюю навигацию, про диплинки пожалуй, лучше реализовать всё по-старинке: либо руками, либо на Cicerone. В остальных случаях, если вас не пугает необходимость постоянно искать фиксы, можно воспользоваться Navigation Component-ом.


Пример приложения на Github-е лежит здесь.


Полезные ссылки по теме


Подробнее..

Перевод Погружение в JetPack Compose. Часть 12

05.01.2021 16:09:52 | Автор: admin

Минутка рекламы

Собрал здесь лучшие статьи, библиотеки и проекты на Jetpack Compose:
Jetpack Compose Awesome

Ожидания по поводу разработки пользовательского интерфейса выросли. Сегодня мы не можем создать приложение и удовлетворить потребности пользователя, не имея отточенного пользовательского интерфейса, включая анимацию и движение UI-элементов. Этих требований не существовало при создании текущего UI Toolkit-а системы Android. Чтобы решить технические проблемы быстрого и эффективного создания безупречного пользовательского интерфейса, мы представили Jetpack Compose - современный набор инструментов для создания UI, который помогает разработчикам приложений добиться успеха на этом новом поприще.

В двух статьях мы расскажем о преимуществах Compose и посмотрим, как это работает "под капотом". Для начала в этом посте я расскажу о проблемах, которые решает Compose, о причинах некоторых наших дизайнерских решений и о том, как они помогают разработчикам приложений. Кроме того, я расскажу о ментальной модели Compose, о том, как вы должны думать о коде, который вы пишете в Compose, и о том, как вы должны формировать свой API.

Какие проблемы решает Compose?

Разделение ответственности (Separation of concerns) - это хорошо известный принцип разработки программного обеспечения. Это одна из фундаментальных вещей, которую мы, как разработчики приложений, узнаем. Несмотря на то, что этот принцип хорошо известен, часто трудно понять, соблюдается ли этот принцип на практике. Может быть полезно думать об этом принципе как о термине типа сцепление или связанность.

Когда мы пишем код, мы создаем модули, которые состоят из нескольких сущностей (unit-тов). Связанность (Coupling) - это зависимость между сущностями в разных модулях, которая отражает то, ка части одного модуля влияют на части других модулей. Целостность (Cohesion)- это, наоборот, взаимосвязь между сущностями (юнитами) в модуле и показывает, насколько хорошо сгруппированы юниты в модуле.

При написании поддерживаемого программного обеспечения наша цель - минимизировать связанность и максимизировать целостность.

Когда у нас есть сильно связанные модули, внесение изменений в код в одном месте означает необходимость внесения множества изменений в другие модули. Что еще хуже, связь часто может быть неявной, так что вещи ломаются в неожиданных местах из-за изменения, которое кажется совершенно не связанным.

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

Давайте перейдем к практике, и рассмотрим современные подходы решения этого вопроса в мире Android-разработки. Возьмем для примера ViewModel и XML-лейаут.

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

Использование таких API требует знания того, как устроен XML-макет, и создает взаимосвязь между ними. Поскольку наше приложение со временем растет, мы должны следить за тем, чтобы ни одна из этих зависимостей не устарела.

Большинство современных приложений отображают пользовательский интерфейс динамически и меняются в процессе выполнения. В результате необходимо не только проверить, что эти зависимости (т. е. View-элементы) предоставляются XML-макетом, но также и то, что они будут предоставляться во время работы программы. Если элемент покидает иерархию View во время выполнения, некоторые из этих зависимостей могут быть нарушены и могут привести к таким проблемам, как NulReferenceExceptions.

Примечание переводчика:

Под "предоставлением зависимостей" имеется в виду наличие вьюшки в самом лейауте и возможность найти её через findViewById.

Обычно ViewModel определяется языке программирования Kotlin, а макет - в XML. Из-за этой разницы в языке существует принудительное разделение, хотя ViewModel и XML-макет иногда могут быть тесно связаны. Другими словами, они очень тесно связаны.

Возникает вопрос: что, если бы мы начали определять лейаут, т. е. структуру нашего пользовательского интерфейса на одном языке? Что, если мы выберем Kotlin?

Поскольку в этом случае мы будем работать на одном языке, некоторые из зависимостей, которые ранее были неявными, могут стать более явными. Мы также можем провести рефакторинг кода и переместить вещи туда, где они уменьшат взаимосвязь и увеличат согласованность.

Теперь вы можете подумать, что вы смешиваете логику с пользовательским интерфейсом. Реальность такова, что у вас будет логика, связанная с пользовательским интерфейсом, в вашем приложении, независимо от того, как оно структурировано. Сама структура не может этого изменить.

Но то, что может сделать фреймворк, - это предоставить вам инструменты, упрощающие разделение: этим инструментом является функция Composable. Функции - это то, что вы, вероятно, использовали в течение долгого времени для разделения задач в других местах вашего кода. Навыки, которые вы приобрели для такого рода рефакторинга и написания надежного, поддерживаемого, чистого кода, - те же навыки применимы и к Composable -функциям.

Устройство Composable-функции

Это пример Composable-функции.

@Composablefun App(appData: AppData) {  val derivedData = compute(appData)  Header()  if (appData.isOwner) {    EditButton()  }  Body {    for (item in derivedData.items) {      Item(item)    }  }}

Здесь функция получает данные как параметры из класса appData. В идеале это неизменяемые данные, которые Composable-функция не меняет: Composable-функция должна быть функцией преобразования для этих данных. Следовательно, мы можем использовать любой код на Kotlin, чтобы взять эти данные и использовать их для описания нашей иерархии, например вызвав функции Header() и Body().

Это означает, что мы вызываем другие Composable-функции, и эти вызовы отражают структуру нашего UI. Мы можем использовать все примитивы предоставляемые Kotlin-ом. Мы можем включить операторы if и циклы for для управления структурой UI, чтобы справиться с более сложной логикой пользовательского интерфейса.

Composable-функции часто используют конечный лямбда-синтаксис Kotlin, поэтому Body() - это Сomposable-функция, которая принимает composable-лямбду в качестве параметра. Это подразумевает иерархию или структуру, поэтому Body() обертывает здесь набор элементов.

Декларативный UI

Декларативный - это модное, но важное слово. Когда мы говорим о декларативном программировании, мы говорим об его отличии от императивного программирования. Давайте рассмотрим пример.

Рассмотрим почтовое приложение со значком непрочитанных сообщений. Если сообщений нет, приложение отображает пустой конверт. Если есть какие-то сообщения, мы визуализируем бумагу в конверте, а если есть 100 сообщений, мы визуализируем значок, как будто он горит.

С императивным интерфейсом нам, возможно, придется написать такую функцию подсчета обновлений:

fun updateCount(count: Int) {  if (count > 0 && !hasBadge()) {    addBadge()  } else if (count == 0 && hasBadge()) {    removeBadge()  }  if (count > 99 && !hasFire()) {    addFire()    setBadgeText("99+")  } else if (count <= 99 && hasFire()) {    removeFire()  }  if (count > 0 && !hasPaper()) {   addPaper()  } else if (count == 0 && hasPaper()) {   removePaper()  }  if (count <= 99) {    setBadgeText("$count")  }}

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

Если мы перепишем эту логику в декларативном стиле, мы получим нечто подобное:

@Composablefun BadgedEnvelope(count: Int) {  Envelope(fire=count > 99, paper=count > 0) {    if (count > 0) {      Badge(text="$count")    }  }}

Здесь мы говорим:

  • Если счет больше 99, покажи огонь

Подробнее..

Навигация в многомодульном приложении на Jetpack без магии и DI

22.04.2021 20:09:52 | Автор: admin

Когда вы начинаете создавать приложение, в котором хотя бы несколько экранов, всегда встает вопрос - как лучше реализовать навигацию. Вопрос становится интереснее и сложнее, когда вы собираетесь делать многомодульное приложение. Примерно полтора года назад я рассказывал как можно реализовать навигацию c помощью Jetpack в многомодульном проекте. И вот спустя время, я наткнулся на свою реализацию и понял, что можно на том же Jetpack летать по модулям проще: без магии и DI.


Архитектура проекта

Чтобы покрыть основные кейсы я покажу как реализовать навигацию на многомодульном проекте такой структуры:

Типичная архитектура Android проекта: feature-модули c реализацией экранов зависят от shared-модулей с общей логикой. И app модуль, который зависит от feature и shared.

Сейчас довольно популярен подход Single Activity, поэтому в моем примере будет всего одна Activity с глобальным хостом, в котором будут переключаться фрагменты

Подготовка

От модуля shared:navigation зависят почти все модули проекта не просто так. В этом модуле реализована функция расширения фрагмента для реализации переходов.

fun Fragment.navigate(actionId: Int, hostId: Int? = null, data: Serializable? = null) {val navController = if (hostId == null) {findNavController()} else {Navigation.findNavController(requireActivity(), hostId)}val bundle = Bundle().apply { putSerializable("navigation data", data) }navController.navigate(actionId, bundle)}

У функции есть параметры:

  • actionId - id действия графа навигации

  • hostId - id хоста графа навигации. Если не будет передан, то будет использован текущий хост

  • data - объект с данными типа Serializable

В этом же модуле реализована функция расширения фрагмента для получения данных, которые были переданы при выполнении действия навигации.

val Fragment.navigationData: Serializable?get() = arguments?.getSerializable("navigation data")

Также в этом модуле надо описать id хостов навигации, чтобы к ним был доступ из feature модулей. Для этого в директории ресурсов надо создать файл res/value/ids.xml

<?xml version="1.0" encoding="utf-8"?><resources><item name="host_global" type="id"/><item name="host_main" type="id"/></resources>

Отлично! Подготовка завершена, можно приступать к самой реализации навигации.

Простые переходы в feature-модулях

Сэмулируем типичное поведение экрана splash. Обычно с этого экрана идет переход либо к онбордингу, либо к главному экрану приложения, либо к экрану авторизации. Реализуем нечто похожее: пусть фрагмент фичи splash будет уметь переходить на экран онбординга и на главный экран по нажатию кнопки.

Для начала создади id для этих действий: запишем их в res/value/ids.xml модуля splash

<?xml version="1.0" encoding="utf-8"?><resources><item name="action_splashFragment_to_mainFragment" type="id"/><item name="action_splashFragment_to_onboardingFragment" type="id"/></resources>

Id для действий переходов я рекомендую создавать именно в модулях фич, которые будут использовать эти действия, а не в модуле shared:navigation. Это позволяет модулю знать только о необходимых действиях.

Теперь можно использовать созданные id для выполнения переходов.

import com.example.smmn.shared.navigation.navigateclass SplashFragment : Fragment(R.layout.fragment_splash) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        buttonToOnboarding.setOnClickListener {            navigate(R.id.action_splashFragment_to_onboardingFragment)        }        buttonToMain.setOnClickListener {            navigate(R.id.action_splashFragment_to_mainFragment)        }    }}

Обратите внимание, что для выполнения перехода используется функция расширения из модуля shared:navigation.

Но чтобы этот переход заработал надо настроить глобальный хост и реализовать глобальную навигацию.

Глобальный хост

В нашей архитектуре всего одна Activity. Она будет содержать глобальный хост для фрагментов. Для этого нам ничего не потребуется реализовывать в самом коде Activity.

class MainActivity : AppCompatActivity(R.layout.activity_main)

Хост добавить надо в ее разметке activity_main.xml

<?xml version="1.0" encoding="utf-8"?><androidx.fragment.app.FragmentContainerView xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:id="@id/host_global"    android:name="androidx.navigation.fragment.NavHostFragment"    android:layout_width="match_parent"    android:layout_height="match_parent"    app:defaultNavHost="true"    app:navGraph="@navigation/navigation_global"    tools:ignore="FragmentTagUsage" />

Глобальная навигация

Это навигация, которая происходит в глобальном хосте. Для ее реализации надо реализовать в модуле app граф навигации res/navigation/navigation_global.xml

<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:id="@+id/navigation_global"    app:startDestination="@id/splashFragment">    <fragment        android:id="@+id/splashFragment"        android:name="com.example.smmn.feature.splash.SplashFragment"        android:label="SplashFragment">        <action            android:id="@id/action_splashFragment_to_mainFragment"            app:destination="@id/mainFragment"            app:popUpTo="@id/navigation_global" />        <action            android:id="@id/action_splashFragment_to_onboardingFragment"            app:destination="@id/onboardingFragment"            app:popUpTo="@id/navigation_global" />    </fragment>    <fragment        android:id="@+id/mainFragment"        android:name="com.example.smmn.feature.main.MainFragment"        android:label="MainFragment" >        <action            android:id="@id/action_mainFragment_to_splashFragment"            app:popUpTo="@id/navigation_global"            app:destination="@id/splashFragment" />    </fragment>    <fragment        android:id="@+id/onboardingFragment"        android:name="com.example.smmn.feature.onboarding.OnboardingFragment"        android:label="OnboardingFragment">        <action            android:id="@id/action_onboardingFragment_to_mainFragment"            app:destination="@id/mainFragment"            app:popUpTo="@id/navigation_global" />    </fragment></navigation>

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

И очень важно отметить, что id действий прописаны без знака +, то есть мы не создаем id в этом графе, а используем id, прописанные в feature модуле.

Прописанные id в модуле splash

<item name="action_splashFragment_to_mainFragment" type="id"/><item name="action_splashFragment_to_onboardingFragment" type="id"/>

Использование их в действиях глобального графа

        <action            android:id="@id/action_splashFragment_to_mainFragment"            app:destination="@id/mainFragment"            app:popUpTo="@id/navigation_global" />        <action            android:id="@id/action_splashFragment_to_onboardingFragment"            app:destination="@id/onboardingFragment"            app:popUpTo="@id/navigation_global" />

Вложенный хост

В Jetpack навигации есть возможность использовать вложенный хост. Это очень полезно, когда мы хотим сделать меню типа BottomNavigation и использовать для этого меню отдельный граф навигации.

В нашем примере во вложенном хосте будут фичи профиля и настроек.

Благодаря библиотеке navigation-ui, реализовать вложенную навигацию довольно просто.

В модуле main создадим меню для BottomNavigation в res/menu/menu_main.xml

<?xml version="1.0" encoding="utf-8"?><menu xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android">    <item        android:id="@+id/profileFragment"        android:icon="@drawable/ic_baseline_account_circle_24"        android:title="@string/main_menu_title_profile" />    <item        android:id="@+id/settingsFragment"        android:icon="@drawable/ic_baseline_settings_24"        android:title="@string/main_menu_title_settings" /></menu>

Создадим граф навигации в res/navigation/navigation_main.xml

<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:id="@+id/navigation_main"    app:startDestination="@id/profileFragment">    <fragment        android:id="@+id/profileFragment"        android:name="com.example.smmn.feature.profile.ProfileFragment"        android:label="ProfileFragment">        <action            android:id="@id/action_profileFragment_to_infoFragment"            app:destination="@id/infoFragment" />    </fragment>    <fragment        android:id="@+id/settingsFragment"        android:name="com.example.smmn.feature.settings.SettingsFragment"        android:label="SettingsFragment" />    <fragment        android:id="@+id/infoFragment"        android:name="com.example.smmn.feature.info.InfoFragment"        android:label="InfoFragment" /></navigation>

Здесь важно указать у фрагментов те же id что указаны в файле меню res/menu/menu_main.xml. И не забывать, что id действий брать из модулей фич.

Осталось добавить хост и меню в разметку фрагмента res/layout/fragment_main.xml

<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent">    <fragment        android:id="@id/host_main"        android:name="androidx.navigation.fragment.NavHostFragment"        android:layout_width="match_parent"        android:layout_height="0dp"        app:defaultNavHost="true"        app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent"        app:navGraph="@navigation/navigation_main" />    <com.google.android.material.bottomnavigation.BottomNavigationView        android:id="@+id/bottomNavigationView"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:background="@android:color/white"        app:elevation="8dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:menu="@menu/menu_main" /></androidx.constraintlayout.widget.ConstraintLayout>

И в самом фрагменте настроить bottomNavigationView

class MainFragment : Fragment(R.layout.fragment_main) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        NavigationUI.setupWithNavController(            bottomNavigationView,            Navigation.findNavController(requireActivity(), R.id.host_main)        )    }}

Переходы между фрагментами из разных хостов

Довольно частый случай, когда надо перейти с экрана, который находится внутри вложенного хоста, на экран глобального хоста. Например, у нас есть главный экран c хостом для экранов главных фич: настроек и профиля.

И на экране настроек, который находится внутри хоста главного экрана (не глобальный хост, а глубже) надо выполнить переход на экран сплэша, который находится в глобальном хосте. Например это может понадобиться, если надо разлогинить текущего пользователя.

В этом случае также воспользуемся функцией расширения фрагмента, но укажем id глобального хоста. Мы имеем к нему доступ из фичи, так как он прописан в модуле shared:navigation.

class SettingsFragment : Fragment(R.layout.fragment_settings) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        buttonToSplash.setOnClickListener {            navigate(R.id.action_mainFragment_to_splashFragment, R.id.host_global)        }    }}

Id действия по аналогии с предыдущим переходом прописан в самом модуле фичи res/values/ids.xml

<?xml version="1.0" encoding="utf-8"?><resources><item name="action_mainFragment_to_splashFragment" type="id"/></resources>

Переходы между фрагментами с передачей и получением данных

Чтобы выполнить переход с передачей данных необходимо, чтобы данные можно было положить в bundle. Это могуг быть какие-то примитивные типы или объекты Serializable классов.

Выше я уже реализовал функцию расширения фрагмента для выполнения перехода, в которую можно передать объект Serializable класса. Аналогично вы можете реализовать передачу примитивных типов.

Чтобы передать объект Serializable класса надо чтобы модуль фичи, с которой происходит переход, и модуль фичи, на которую происходит переход, имели доступ к модулю с таким классом. В нашем случае создадим модуль shared:model где будет лежать Serializable класс Info.

data class Info(    val name: String,    val surname: String) : Serializable

Переход будет происходить с экрана profile на экран info. Создадим объект Info и передадим его в функцию расширения фрагмента.

class ProfileFragment : Fragment(R.layout.fragment_profile) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        buttonToInfo.setOnClickListener {            navigate(R.id.action_profileFragment_to_infoFragment, data = Info("name", "surname"))        }    }}

И получим данные используя другую функцию расширения фрагмента, созданную ранее.

class InfoFragment : Fragment(R.layout.fragment_info) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        val info = navigationData as? Info ?: return        textView.text = info.toString()    }}

Так это будет выглядеть в приложении

Заметьте, что мы не указывали в каком хосте выполнить переход, и переход произошел в текущем хосте.

Заключение

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

Оставляю ссылку на код примера приложения.

Буду рад обратной связи!

Подробнее..

Из песочницы Hilt еще один DI?

11.08.2020 20:13:10 | Автор: admin

Встречайте Hilt Dependency Injection (DI) в JetPack, но это не правда, так как Hilt это просто обертка для Dagger2. Для небольших проектов сможет встать более удобным инструментом и хорошо интегрируется с остальными продуктами в JetPack.


Не буду описывать как добавить в проект, все хорошо описано в статье


Зачем?


Dagger2 довольно сложный DI, можно легко запутаться, что и куда поместил. Придумывать бесконечное количество компонентов и увеличивается количество абстракций. И чем больше проект, тем больше увеличивается путаница.


И если хочется использовать Dagger2, но с минимальными усилиями, то для этого как раз и был придуман Hilt.


Что упростили для нас:


  • Готовые компоненты (из названий понятно к чему относятся)
    • ApplicationComponent
    • ActivityRetainedComponent
    • ActivityComponent
    • FragmentComponent
    • ViewComponent
    • ViewWithFragmentComponent
    • ServiceComponent
  • В модуле указываешь в какой компонент добавить
  • Через @AndroidEntryPoint Hilt компилятор генерирует весь bolierplate для создания компонента и хранения (например, ActivityRetainedComponent сохранит сущность после поворота экрана, ActivityComponent пересоздаст заново).

Такой код выглядит довольно элегантно (весь boilerplate за нас сгенерируется)


@AndroidEntryPointclass ExampleActivity : AppCompatActivity() {     @Inject lateinit var testService: TestService}

Особенности


Application обязателен


Необходимо объявить Application и пометить @HiltAndroidApp, без него Hilt не заработает.


@HiltAndroidAppclass App : Application() { }

Иерархическая зависимость


Если хотите использовать Hilt в фрагментах, то Activity которая содержит эти фрагменты обязательно помечать аннотацией @AndroidEntryPoint


Если View пометить @WithFragmentBindings то Fragment должен быть с аннотацией @AndroidEntryPoint, а без этой аннотации зависит от того куда инжектимся Activity или Fragment


Объявление модулей


Все как в Dagger2, но нет необходимости добавлять модуль в компонент, а достаточно использовать аннотацию @InstallIn. Это и понятно, так как компоненты не доступны для редактирования.


@InstallIn(ApplicationComponent::class)@Moduleclass NetworkModule {    @Singleton    @Provides    fun provideHttpService(): HttpService {        return object : HttpService {            init {                Log.e("Tester", "HttpService initialized")            }            override fun request() {                Log.e("Tester", "HttpService::request")            }        }    }}

При добавления Hilt, все модули должны быть с @InstallIn, либо компилятор ругнется, что аннотация отсутствует.


Кастомные Component и Subcomponent


Создать их конечно можно, так как там Dagger2, но теряется тогда весь смысл Hilt и все придется писать вручную. Вот что предлагается в документации:


DaggerLoginComponent.builder()        .context(this)        .appDependencies(          EntryPointsAccessors.fromApplication(            applicationContext,            LoginModuleDependencies::class.java          )        )        .build()        .inject(this)

Поддержка многомодульности


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


Ограничения для @AndroidEntryPoint


  • Поддерживаются Activity наследуемые от ComponentActivity и AppCompatActivity
  • Поддерживаются Fragment наследуемые от androidx.Fragment
  • Не поддерживаются Retain фрагменты

Что внутри


Hilt работает следующим образом:


  • Генерируются Dagger Component-ы
  • Генерируются базовые классы для Application, Activity, Fragment, View и т.д, которые помечены аннотацией @AndroidEntryPoint
  • Dagger компилятор генерирует статический коды

Как устроено хранение ActivityRetainedComponent


Не стали усложнять и просто поместили компонент в ViewModel из arch библиотеки:


this.viewModelProvider =        new ViewModelProvider(            activity,            new ViewModelProvider.Factory() {              @NonNull              @Override              @SuppressWarnings("unchecked")              public <T extends ViewModel> T create(@NonNull Class<T> aClass) {                ActivityRetainedComponent component =                    ((GeneratedComponentManager<LifecycleComponentBuilderEntryPoint>)                            activity.getApplication())                        .generatedComponent()                        .retainedComponentBuilder()                        .build();                return (T) new ActivityRetainedComponentViewModel(component);              }            });

Итог


Плюсы:


  • Более простое использование чем Dagger2
  • Добавление модулей через аннотацию выглядит довольно удобно (в несколько уже не поместишь)
  • Код чище и много boilerpate спрятано.
  • Все плюсы от Dagger2 (генерация статического кода, валидация зависимостей и т.д.)
  • Удобен для небольших проектов

Минусы:


  • Тяжело избавится, не только захочется убрать или заменить, но и просто перейти на Dagger2
  • Тяжело добавить кастомные компоненты, что ограничивает использование с крупных проектах
  • Наследует минусы Dagger2 и еще больше увеличивает время сборки
  • Иерархическая зависимость, например, нельзя использовать в Fragment без Activity c @AndroidEntryPoint

Полезные ссылки:


Подробнее..

Navigation Component-дзюцу, vol. 1 BottomNavigationView

09.09.2020 12:15:30 | Автор: admin


Два года назад на Google I/O Android-разработчикам представили новое решение для навигации в приложениях библиотеку Jetpack Navigation Component. Про маленькие приложения уже было сказано достаточно, а вот о том, с какими проблемами можно столкнуться при переводе большого приложения на Navigation Component, информации практически нет.


В этой и следующих двух статьях я расскажу о кейсах, с которыми может встретиться разработчик, желающий опробовать Navigation Component в большом Android-приложении.


Это текстовая версия моего выступления в рамках серии митапов по Android 11 в Android Academy. Само выступление было на английском, статью пишу на русском. Кому удобнее смотреть велкам.


В первой статье я расскажу о кейсах, связанных с BottomNavigationView, во второй о кейсах с вложенными графами навигации, в третьей про навигацию в многомодульных приложениях, дип линки, встраиваемые фрагменты и диалоги. Все три статьи лонгриды, которые, однако, способны сэкономить много времени и вам, и вашей команде.


Disclaimer


Я сделал пример, по структуре навигации повторяющий основные моменты навигации соискательского приложения hh.ru, и выхватил ряд проблем, о которых и собираюсь рассказать. Я основательно поресёрчил практическую сторону вопроса, но, разумеется, рассмотрел далеко не все возможные кейсы.


Схема моего тестового приложения выглядит так:



В цикле статей мы разберём каждый переход, который описан на этой схеме, а также несколько кейсов, которые не поместились на картинку.


Кейсы с BottomNavigationView


Когда я только-только услышал про Navigation Component, мне стало интересно: как будет работать BottomNavigationView и как Google подружит несколько отдельных back stack-ов в разных вкладках. Два года назад с этим кейсом были некоторые проблемы, и я решил проверить, как там обстоят дела сегодня.


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


Где на схеме приложения кейсы с навигацией?


Первый опыт


Я установил Android Studio 4.1 Beta (последнюю более-менее стабильную версию на тот момент) и попробовал шаблон приложения с нижней навигацией. Начало было многообещающим.


  • Мне сгенерировали Activity в качестве контейнера для хоста навигации и нижней навигации

Вёрстка Activity из шаблона
<androidx.constraintlayout.widget.ConstraintLayout    android:id="@+id/container">    <com.google.android.material.bottomnavigation.BottomNavigationView        android:id="@+id/nav_view"        app:menu="@menu/bottom_nav_menu" />    <fragment        android:id="@+id/nav_host_fragment"        android:name="androidx.navigation.fragment.NavHostFragment"        app:defaultNavHost="true"        app:navGraph="@navigation/mobile_navigation" /></androidx.constraintlayout.widget.ConstraintLayout>

Я убрал шумовые атрибуты, чтобы было проще читать.


Стандартный ConstraintLayout, в который добавили BottomNavigationView и тэг <fragment> для инициализации NavHostFragment-а (Android Studio, кстати, подсвечивает, что вместо фрагмента лучше использовать FragmentContainerView).


  • Для каждой вкладки BottomNavigationView был создан отдельный фрагмент

Граф навигации из шаблона
<navigation    android:id="@+id/mobile_navigation"    app:startDestination="@+id/navigation_home">    <fragment        android:id="@+id/navigation_home"        android:name="com.aaglobal.graph_example.ui.home.HomeFragment"/>    <fragment        android:id="@+id/navigation_dashboard"        android:name="com.aaglobal.graph_example.ui.dashboard.DashboardFragment"/>    <fragment        android:id="@+id/navigation_notifications"        android:name="com.aaglobal.graph_example.ui.notifications.NotificationsFragment"/></navigation>

Все фрагменты были добавлены в качестве отдельных destination-ов в общий граф навигации.


  • А ещё в проект был добавлен файл-ресурс для описания меню BottomNavigationView

@menu-ресурс для описания табов
<menu xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android">    <item        android:id="@+id/navigation_home"        android:icon="@drawable/ic_home_black_24dp"        android:title="@string/title_home" />    <item        android:id="@+id/navigation_dashboard"        android:icon="@drawable/ic_dashboard_black_24dp"        android:title="@string/title_dashboard" />    <item        android:id="@+id/navigation_notifications"        android:icon="@drawable/ic_notifications_black_24dp"        android:title="@string/title_notifications" /></menu>

При этом я заметил, что идентификаторы элементов меню должны совпадать с идентификаторами destination-ов в графе навигации. Не самая очевидная связь между табами BottomNavigationView и фрагментами, но работаем с тем, что есть.


Пора запускать приложение


После создания приложения из шаблона я запустил его и сразу столкнулся с двумя проблемами.


Первая проблема: при переключении между вкладками их состояние не сохранялось.


А ну-ка покажи


Для проверки я добавил во вкладку Dashboard простенькую ViewModel со счётчиком. На гифке видно, как я переключаюсь со вкладки Home на вкладку Dashboard, увеличиваю счётчик до четырёх. После этого я переключился обратно на вкладку Home и вновь вернулся на Dashboard. Счётчик сбросился.


Баг с описанием этой проблемы уже два года висит в Issue Tracker-е. Чтобы решить её, Google-у потребовалось серьёзно переработать внутренности фреймворка Fragment-ов, чтобы поддержать возможность работать с несколькими back stack-ами одному FragmentManager-у. Недавно на Medium вышла статья Ian Lake, в которой он рассказывает, что Google серьёзно продвинулись в этом вопросе, так что, возможно, фикс проблемы с BottomNavigationView не за горами.


Вторая проблема следствие первой. По принятым принципам навигации, когда вы нажимаете на уже выбранную вкладку BottomNavigationView, вы должны вернуться на первый фрагмент в стеке текущей вкладки. Но когда это происходит, состояние этого первого фрагмента сбрасывается, так как сам фрагмент пересоздаётся.


А ну-ка покажи


Для демонстрации этой проблемы я добавил на вкладку Dashboard кнопку, которая ведёт на следующий экран. На гифке видно, как я переключаюсь на вкладку Dashboard, увеличиваю счётчик до трёх, а затем перехожу на экран Graphic. Если я нажимаю на кнопку Back то всё работает как надо, состояние вкладки не сбрасывается. Но если, находясь на экране Graphic, я ещё раз нажму на вкладку Dashboard, то после возврата на первый экран в стеке увижу, что его состояние сброшено.


Не самое лучшее первое впечатление, подумал я. И начал искать фикс.


У нас есть workaround


Решение этих проблем живёт в специальном репозитории Google-а с примерами работы с Architecture Components, в проекте NavigationAdvancedSample.


Большая часть фикса расположена в файле NavigationExtensions.kt. В самом проекте довольно много кода, поэтому я не буду его разбирать подробно, а вместо этого подсвечу основные моменты, которые относятся к решению проблем.


  • Во-первых, для каждой вкладки вводится отдельный, независимый граф навигации

Граф навигации для одной из вкладок
<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:id="@+id/navigation_home"    app:startDestination="@id/HomeFragment">    <fragment        android:id="@+id/HomeFragment"        android:name="com.aaglobal.jnc_playground.ui.home.HomeFragment"        android:label="@string/title_home"        tools:layout="@layout/fragment_home" /></navigation>

Соответственно, для примера BottomNavigationView с тремя вкладками у нас получится три отдельных файла навигации XML, в которых в качестве startDestination будут указаны первые фрагменты вкладок.


  • Во-вторых, для каждой вкладки под капотом создаётся отдельный NavHostFragment, который будет связан с графом навигации этой вкладки

Создание NavHostFragment-а для графа вкладки BottomNavigationView
private fun obtainNavHostFragment(    fragmentManager: FragmentManager,    fragmentTag: String,    navGraphId: Int,    containerId: Int): NavHostFragment {    // If the Nav Host fragment exists, return itval existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?    existingFragment?.let { return it }    // Otherwise, create it and return it.    val navHostFragment = NavHostFragment.create(navGraphId)    fragmentManager.beginTransaction()        .add(containerId, navHostFragment, fragmentTag)        .commitNow()    return navHostFragment}

FragmentManager пока что не поддерживает работу с множеством back stack-ов одновременно, поэтому пришлось придумать альтернативное решение, которое позволило ассоциировать с каждым графом свой back stack. Им стало создание отдельного NavHostFragment-а для каждого графа. Из этого следует, что с каждой вкладкой BottomNavigationView у нас будет связан отдельный NavController.


  • В-третьих, мы устанавливаем в BottomNavigationView специальный listener, который будет заниматься переключением между back stack-ами фрагментов

Listener для переключения между вкладками BottomNavigationView
setOnNavigationItemSelectedListener { item ->  val newlySelectedItemTag = graphIdToTagMap[item.itemId]  if (selectedItemTag != newlySelectedItemTag) {    fragmentManager.popBackStack(firstFragmentTag, FragmentManager.POP_BACK_STACK_INCLUSIVE)    val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)        as NavHostFragment    if (firstFragmentTag != newlySelectedItemTag) {      fragmentManager.beginTransaction()        .attach(selectedFragment)        .setPrimaryNavigationFragment(selectedFragment).apply {          graphIdToTagMap.forEach { _, fragmentTagIter ->            if (fragmentTagIter != newlySelectedItemTag) {              detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)            }          }        }        .addToBackStack(firstFragmentTag)        .setReorderingAllowed(true)        .commit()    }    selectedNavController.value = selectedFragment.navController    true  } else {    false  }}

В прикреплённом кусочке кода мы видим, как при переключении между вкладками BottomNavigationView выполняется специальная транзакция в FragmentManager-е, которая прикрепляет фрагмент выбранной вкладки и отцепляет все остальные фрагменты. По сути, так мы и переключаемся между различными back stack-ами.


  • В итоге метод настройки BottomNavigationView возвращает разработчику специальную LiveData, которая содержит в себе NavController выбранной вкладки. Этот NavController можно использовать, например, для обновления надписи на ActionBar

Настраиваем BottomNavigationView в Activity
class RootActivity : AppCompatActivity(R.layout.activity_root) {  private var currentNavController: LiveData<NavController>? = null  private fun setupBottomNavigationBar() {      // Setup the bottom navigation view with a list of navigation graphs      val liveData = bottom_nav.setupWithNavController(          navGraphIds = listOf(            R.navigation.home_nav_graph,            R.navigation.dashboard_nav_graph,            R.navigation.notifications_nav_graph          ),          fragmentManager = supportFragmentManager,          containerId = R.id.nav_host_container,          intent = intent      )      // Whenever the selected controller changes, setup the action bar.      liveData.observe(this, Observer { ctrl -> setupActionBarWithNavController(ctrl) })      currentNavController = liveData  }}

Метод для настройки BottomNavigationView вызывают в onCreate-е, когда Activity создаётся в первый раз, затем в методе onRestoreInstanceState, когда Activity пересоздаётся с помощью сохранённого состояния.


Кроме того, для работы фикса нужно, чтобы идентификаторы элементов меню, которое используется для инициализации табов BottomNavigationView, совпадали с идентификаторами графов навигации.


Посмотреть, как это выглядит в коде


Опять же, не самая очевидная связь между этими элементами, зато работает.


После применения этого workaround-а первые две проблемы исчезли теперь состояние вкладки сохраняется между переключениями вкладок.


А ну-ка покажи

Первая проблема решилась:



И вторая тоже:



Адаптация workaround-а для фрагментов


Очень здорово, что workaround помог решить основную проблему с сохранением состояний, но этих фиксов мне было недостаточно. Пример из проекта NavigationAdvancedSample использовал в качестве контейнера нижней навигации Activity, мне же нужен был фрагмент.


Почему тебе нужен фрагмент?

Посмотрите внимательно на эту схему:



На ней можно увидеть, что пользователь начинает свой путь в приложении со Splash-экрана:



Google говорит, что Splash-экраны зло, ухудшающее UX приложения. Тем не менее, Splash-экраны суровая реальность большинства крупных Android-приложений. И если мы хотим использовать в нашем приложении Single Activity-архитектуру, то в качестве контейнера нижней навигации придётся использовать Fragment, а не Activity:



Я добавил вёрстку для фрагмента с нижней навигацией и перенёс настройку BottomNavigationView во фрагмент:


Посмотреть код
class MainFragment : Fragment(R.layout.fragment_main) {    private var currentNavController: LiveData<NavController>? = null    override fun onViewStateRestored(savedInstanceState: Bundle?) {        super.onViewStateRestored(savedInstanceState)        setupBottomNavigationBar()    }    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        if (savedInstanceState == null) {            setupBottomNavigationBar()        }    }}

Я добавил в свой пример Splash-экран и дополнительную вкладку для BottomNavigationView. А чтобы пример стал ещё более походить на приложение для соискателей hh.ru, я также убрал из него ActionBar.


Для этого я поменял тему приложения с Theme.MaterialComponents.DayNight.DarkActionBar на Theme.MaterialComponents.DayNight.NoActionBar и убрал код для связки NavController-а с ActionBar-ом:


Код настройки BottomNavigationView выглядел так
class MainFragment : Fragment(R.layout.fragment_main) {    private var currentNavController: LiveData<NavController>? = null    private fun setupBottomNavigationBar() {        val navGraphIds = listOf(            R.navigation.search__nav_graph,            R.navigation.favorites__nav_graph,            R.navigation.responses__nav_graph,            R.navigation.profile__nav_graph        )        val controller = bottom_navigation.setupWithNavController(            navGraphIds = navGraphIds,            fragmentManager = requireActivity().supportFragmentManager,            containerId = R.id.fragment_main__nav_host_container,            intent = requireActivity().intent        )        currentNavController = controller    }}

После всех манипуляций я включил режим Don't keep activities, запустил свой пример и получил краш при сворачивании приложения.


А ну-ка покажи


На гифке видно, как я запустил приложение, и после Splash-экрана показывается экран с нижней навигацией. После этого мы сворачиваем приложение и получаем краш.


В чём была причина? При вызове onDestroyView активный NavHostFragment пытается отвязаться от NavController-а. Так как мой фрагмент-контейнер с нижней навигацией никак не привязывал к себе NavController, который он получил из LiveData, метод Navigation.findNavController из onDestroyView крашил приложение.


Добавляем привязку NavController-а к фрагменту с нижней навигацией (для этого в Navigation Component-е есть утилитный метод Navigation.setViewNavController), и проблема исчезает.


Кусочек кода с фиксом
class MainFragment : Fragment(R.layout.fragment_main) {    private var currentNavController: LiveData<NavController>? = null    private fun setupBottomNavigationBar() {        ...        currentNavController?.observe(            viewLifecycleOwner,            Observer { liveDataController ->                Navigation.setViewNavController(requireView(), liveDataController)            }        )    }}

Но это ещё не всё. Не выключая режим Don't keep activities, я попробовал свернуть, а затем развернуть приложение. Оно снова упало, но с другим неприятным исключением IllegalStateException в FragmentManager FragmentManager already executing transactions.


А ну-ка покажи


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


Краш происходит в методах, которые прикрепляют NavHostFragment к FragmentManager-у после их создания. Это исключение можно исправить при помощи костыля: обернуть методы attach-detach в Handler.post {}.


Фиксим IllegalStateException
// NavigationExtensions.ktprivate fun attachNavHostFragment(    fragmentManager: FragmentManager,    navHostFragment: NavHostFragment,    isPrimaryNavFragment: Boolean) {  Handler().post {    fragmentManager.beginTransaction()    .attach(navHostFragment)    .apply {      if (isPrimaryNavFragment) {        setPrimaryNavigationFragment(navHostFragment)      }    }    .commitNow()  }}

После добавления Handler.post приложение заработало, как надо.


Выводы по работе с BottomNavigationView


  • Использовать BottomNavigationView в связке с Navigation Component можно, если знать, где искать workaround-ы.
  • Если вы захотите иметь фрагмент в качестве контейнера нижней навигации BottomNavigationView, будьте готовы искать дополнительные фиксы для ваших проблем, так как скорее всего я поймал не все возможные краши.

На этом с BottomNavigationView всё, на следующей неделе расскажу про кейсы с вложенными графами навигации.

Подробнее..

Перевод Как использовать Android Data Binding в пользовательских представлениях?

16.06.2021 20:20:54 | Автор: admin

. . .

Как вы знаете, Data Binding Library - это отличная часть библиотеки Android Jetpack, позволяющая сократить количество шаблонного кода и связать представления с данными более эффективным способом, чем это было возможно ранее. В этой статье я собираюсь объяснить, как можно использовать привязку данных в наших пользовательских представлениях.

. . .

Начало работы

Чаще всего мы создаем пользовательские представления для повышения производительности, настройки и т.д., но если вы используете привязку данных в своих проектах, или напротив не используете ее в своих пользовательских представлениях, это не очень хорошо. Возможно, это выглядит примерно так:

class MyCustomView @JvmOverloads constructor(    context: Context,    attrs: AttributeSet? = null,    defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {    init {        attrs?.let {            val typedArray =                context.obtainStyledAttributes(it, R.styleable.MyCustomView)            // some attr handling stuffs...            typedArray.recycle()        }    }}

Но если вы хотите использовать пользовательское представление с компонентом привязки данных, то нет необходимости обрабатывать эти атрибуты.

Автоматический выбор метода

Когда вы определяете атрибут следующим образом:

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="MyCustomView">        <attr name="currencyCode" format="string" />    </declare-styleable></resources>

библиотека привязки данных имеет возможность автоматического выбора метода, то есть для атрибута с именем currencyCode библиотека автоматически пытается найти метод setCurrencyCode(arg), принимающий в качестве аргумента совместимые типы. Пространство имен атрибута не учитывается, при поиске метода используется только имя атрибута и тип. С другой стороны, если автоматический выбор метода не работает для имени вашего атрибута или вы хотите изменить метод сеттера для вашего атрибута, вы можете использовать методы привязки.

Методы привязки

Методы привязки дают вам возможность изменить сеттер для атрибута вашего пользовательского представления. Вы можете поместить эти методы над своим классом, используя аннотацию @BindingMethods, или создать пустой класс с этой аннотацией.

class MyCustomView @JvmOverloads constructor(    context: Context,    attrs: AttributeSet? = null,    defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {    private val currencyFormatter = NumberFormat.getCurrencyInstance(Locale.getDefault())        //..    fun setCurrency(currencyCode: String?) {        if (currencyCode.isNullOrEmpty())            return        currencyFormatter.currency = Currency.getInstance(currencyCode)    }    //..}

определите функцию, названную как угодно, затем просто поместите ее с атрибутом в методы привязки.

@BindingMethods(    value = [        BindingMethod(            type = MyCustomView::class,            attribute = "currencyCode",            method = "setCurrency"        )    ])class BindingMethods

с помощью этого способа вы можете задать различные имена сеттеров для вашего атрибута.

Адаптеры привязки

Если у вас есть функции с несколькими параметрами в вашем пользовательском представлении, и необходимо использовать эти функции с привязкой данных, то в этом случае следует использовать адаптеры привязки.

@BindingAdapter(    value = ["paddingEnd", "paddingTop", "paddingStart", "paddingBottom"],    requireAll = false)fun MyCustomView.setPaddingRelative(    paddingEnd: Int = 0,    paddingTop: Int = 0,    paddingStart: Int = 0,    paddingBottom: Int = 0) {    this.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom)}

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

. . .

Вот и все, спасибо за чтение! Если вам понравилась эта история, пожалуйста, нажмите на стрелочку вверх и поделитесь ею, чтобы помочь другим!

Если у вас есть какие-либо комментарии, не стесняйтесь связаться со мной в Twitter.


Перевод материала подготовлен в рамках запуска курса "Android Developer. Professional".

Всех желающих приглашаем на двухдневный интенсив по теме: "Полный coverage. Покрываем Android приложение юнит/интеграционными/UI тестами"


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru