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

Боль

Всего лишь царапина рыжие волосы и болевой порог

09.04.2021 10:09:31 | Автор: admin


Физическую боль сложно назвать чем-то приятным, однако это ощущение имеет крайне важное значение в работоспособности нашего организма. Боль является своеобразной сигнализацией, оповещающей о наличии некоего раздражителя, от которого необходимо избавиться. Причиной боли могут быть как физические повреждения тканей или органов (переломы, ушибы, порезы, удар мизинцем об комод и т.д.), так и патологические процессы в организме (инфекции, онкология, врожденные дефекты и т.д.). В любом случае боль помогает как можно раньше и точнее локализовать проблему, требующую внимания. При этом разные люди могут ощущать боль от одинакового источника по-разному, что обусловлено разным болевым порогом. Для одних людей снятия пластыря настоящая агония. А другие спокойно могут уснуть во время лечения зубного канала без анестезии. Боль крайне индивидуальна, но даже тут есть скрытая логика. Ученые из MGH (Массачусетская больница общего профиля, США) установили причину, почему люди с рыжими волосами обладают более высоким болевым порогом. Как цвет волос влияет на восприятие боли, как это связано с кожей, и как можно на практике применить полученные сведения? Ответы на эти вопросы мы найдем в докладе ученых. Поехали.

Основа исследования


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

Ранее было установлено, что люди (и мыши) с рыжими волосами обладают отличным от других болевым порогом, повышенной потребностью в неопиоидных анальгетиках и усиленной реакцией на опиоидные анальгетики. Факт отличий был установлен, но причина его существования так и не была определена.

Чтобы понять причину отличия болевого порога у рыжих стоит сначала обратить внимание на причину визуального отличия, т.е. на причину рыжего цвета волосяного покрова. Рыжий цвет обусловлен вариантными аллелями с потерей функции рецептора меланокортина 1 (MC1R) Gs-связанного рецептора, экспрессируемого на меланоцитах (клетках кожи, продуцирующих пигмент). Эти отличительные черты могут каким-то образом влиять и на болевой порог. Чтобы это выяснить, ученые использовали в ходе опытов мышей Mc1re/e, которые демонстрируют многие черты рыжих людей: рыжие волосы, синтез красного/светлого пигмента феомеланина, неспособность загорать после воздействия УФ-излучения и увеличение риска рака кожи из-за УФ воздействия.

Результаты исследования


Чтобы первоначально оценить пороги ноцицепции у мышей Mc1re/e и проверить роль пигмента в ноцицепции, ученые скрестили мышей Mc1re/e с видом-альбиносом, содержащим инактивирующую мутацию гена тирозиназы (Tyrc/c). У этих мышей количество меланоцитов не изменено, но они не пигментированы. Полученный скрещенный вид (Mc1re/e + Tyrc/c) не отличался от Tyrc/c вида, поскольку также имел белый окрас.


Изображение 1

Сравнение мышей с разными генетическими изменениями показало, что особи Mc1re/e обладают значительно более высоким болевым порогом (давление и температура), чем мыши Mc1rE/E с генетическим фоном альбиносов (1A и 1B). При этом повышенный болевой порог наблюдался у мышей Mc1re/e и в сравнении с Mc1rE/E без генетичсекого фона альбиносов (1C и 1D). Из этого следует, что MC1R играет важную роль в регуляции ноцицепции, но не зависит от пигментации.

Далее ученые решили проверить, вызваны ли повышенные пороги ноцицепции у рыжеволосых мышей потерей функции MC1R в меланоцитах или в других типах клеток. Для этого было проведено сравнение трех генетически согласованных (C57BL/6J) моделей мышей, которые различаются по количеству меланоцитов.

Мыши с повышенным количеством эпидермальных меланоцитов показали значительно более низкие пороги ноцицепции (1E и 1F), в то время как мыши, лишенные меланоцитов, показали более высокие пороги ноцицепции по сравнению с мышами из контрольной группы (без каких-либо генетичсеких манипуляций; 1G и 1H). Эти данные свидетельствуют о том, что количество эпидермальных меланоцитов (независимо от функции MC1R) действительно может модулировать пороги ноцицепции.

Далее ученые скрестили рыжих мышей с мышами без меланоцитов. Анализ полученных гибридов позволял оценить функцию MC1R в немеланоцитарных клетках. На графиках 1G и 1H видно, что генетическое отсутствие меланоцитов сводит на нет способность MC1R влиять на пороги ноцицепции. Следовательно, влияние на болевой порог осуществляется MC1R именно в меланоцитах, а не в других клетках.

Одним из гипотетических модуляторов повышенного порога ноцицепции у рыжеволосых мышей является -эндорфин продукт посттрансляционного расщепления проопиомеланокортина (ПОМК), который экспрессируется в меланоцитах. ПОМК индуцируется аденозин 3', 5'-циклическим монофосфатом (ациклический АМР или цАМР) в других типах клеток. Следовательно, низкие уровни цАМР в мутантных меланоцитах MC1R может повлиять на экспрессию ПОМК.

У рыжих мышей уровень -эндорфина в плазме был значительно ниже, чем у черных мышей (1I). Однако это еще не означает, что изменения -эндорфина влияют на болевой порог, поскольку направление изменения противоположно фенотипическому изменению, поскольку передача сигналов опиоидов способствует, а не уменьшает анальгезию (уменьшение болевой чувствительности). Более того, уровни -эндорфина в плазме также были обратно пропорциональны порогам ноцицепции у мышей K14-SCF (более высокие числа меланоцитов и более низкие пороги ноцицепции) и мышей Mitfmi-wh/mi-wh (отсутствие меланоцитов и более высокие пороги ноцицепции).

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

Для оценки роли меланоцитов в модуляции экспрессии ПОМК относительно MC1R были произведены два дополнительных изменения в меланоцитах мышей: подавление мРНК Mc1r (1J) и стимуляция MC1R с помощью -MSH, т.е. -меланоцит-стимулирующего гормона (1K). В первом случае наблюдалось снижение продукции мРНК ПОМК, а во втором, наоборот, увеличение.

Экспрессия ПОМК не показала статистически значимого снижения в надпочечниках и гипофизе мышей Mc1re/e. Это позволяет предположить, что изменения уровня ПОМК в плазме связаны с уменьшением продукции меланоцитов ПОМК, вызванным потерей функции MC1R.

Далее необходимо было установить, является ли повышенный болевой порог результатом адаптации к низкому уровню -эндорфина. Для этого использовалось два типа мышей: у одного была гомозиготная мутация ПОМК, которая экспрессирует все пептиды меланокортина, но не имеет концевой последовательности -эндорфина (нокаут -эндорфин), а у второго типа был дефицит гена рецептора -эндорфина, т.е. -опиоидного рецептора (Oprm1/).


Изображение 2

Анализ не показал каких-либо значимых эффектов нокаута -эндорфина на болевые пороги у черных и рыжих мышей (2A и 2B). Делеция (потеря участка хромосомы) Oprm1 не влияла на пороги ноцицепции у черных мышей, но устраняла повышенные болевые пороги у рыжих (2C и 2D).

Подобный эффект имели и налоксон (антагонист* широкого опиоидного рецептора), и ципродим (антагонист, специфичный для -опиоидного рецептора). Они оба снижали болевые пороги у рыжих мышей до уровня порогов черных мышей (2E и 2F).
Антагонист* лиганд, который блокирует, снижает или предотвращает физиологические эффекты, вызываемые связыванием агониста с рецептором.

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

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


Изображение 3

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

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

После проведения фармакологических манипуляций с меланокортином, которые привели к снижению некоторых эффектов морфина, ученые решили исследовать уровни -MSH (агониста меланокортина) в плазме. MSH кодируется в ПОМК, как и -эндорфин.

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


Изображение 4

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

Чтобы функционально оценить, может ли пропорционально низкий уровень -MSH способствовать повышенным порогам ноцицепции для рыжих мышей, было выполнено фармакологическое исследование. Меланотан II (пептидный имитатор -MSH) снижал пороги ноцицепции дозозависимым образом у самцов рыжих, но не у черных мышей (4B).

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

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

Сначала был проверен пептид SHU 9119, противодействующий MC4R и MC3R. SHU 9119 вызывал уменьшение болевой чувствительности (т.е. увеличение болевого порога) при введении самцам черных мышей (4C). Из этого можно сделать вывод, что обезболивающие эффекты SHU 9119 не зависят от Mc1r и, вероятно, связаны с эффектами лиганда на MC4R или MC3R.

Мыши, которые были лишены MC4R, демонстрировали повышенные пороги ноцицепции (4D и 4E). Отсутствие MC4R у черных мышей также привело к повышению чувствительности к опиоидному антагонизму (4F), что наблюдается у рыжих мышей. Значит, порог ноцицепции может определяться балансом между OPRM1 и сигналами MC4R. Фармакологическое замедление OPRM1 привело к восстановлению порогов ноцицепции как у черных, так и у рыжих мышей (4G). При этом применение агониста меланокортина снижало повышенные пороги ноцицепции у рыжих мышей (4B), но никак не влияло на мышей без MC4R (4H). Это наталкивает на мысль, что именно MC4R является ключевым рецептором меланокортина, на который MSH действует как лиганд для снижения порога ноцицепции.

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

Изображение 5

Однако периферическое введение метилированного налтрексона, непроницаемого для ГЭБ опиоидного антагониста, не уменьшило ноцицептивных различий между черными и рыжими мышами (), что свидетельствует о минимальном периферическом влиянии.

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

Ранее сообщалось, что передача сигналов цАМР играет важную роль в модуляции опиоидного снижения боли. Посему было решено измерить влияние антагонизма на содержание цАМР в первичных нейронах гипоталамуса крыс (RPHN от rat primary hypothalamic neurons). Было обнаружено, что агонист меланокортина увеличивал содержание цАМР, но опиоидный агонист морфин значительно уменьшал вызванное меланокортином повышение цАМР (5C). Следовательно, передача сигналов меланокортином и опиоидами может противодействовать друг другу.

Анализ также показал возможное наличие нейронов в периакведуктальной серой зоне (PAG от periaqueductal gray area), экспрессирующих оба типа рецепторов. Сравнение уровней мРНК опиоидных рецепторов в PAG у разных мышей не показо особых отличий.

Исследование роли PAG в модулировании ноцицепции показало, что местный антагонизм опиоидных рецепторов или агонизм рецепторов меланокортина значительно снижают болевые пороги (5D).

Совокупность всех вышеописанных данных позволяет подытожить: повышенные болевые пороги у рыжих мышей возникают из-за снижения уровней -MSH, вызванного снижением продукции проопиомеланокортина (ПОМК) в меланоцитах, что приводит к снижению передачи сигналов MC4R.

Снижение передачи сигналов MC4R, в свою очередь, снижает его антагонизм по отношению к передаче сигналов опиоидов в ЦНС, которая, несмотря на снижение продукции -эндорфина, не обнаруживает заметных различий в других эндогенных опиоидных лигандах. В совокупности это вызывает дефицит меланокортина, что изменяет баланс в пользу анальгезии (обезболивания), индуцированной -опиоидными рецепторами (5E).

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

Эпилог


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

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

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

Естественно, главным применением новообретенных знаний является медицина и фармакология. Понимая индивидуальную природу боли и ее ощущения у разных пациентов, можно более точно применять обезболивающие. Что касается лекарств, то знания о том, какие именно механизмы участвуют в восприятии/блокировки боли, можно разработать препараты, ингибирующие (подавляющие) рецепторы меланокортина, тем самым повышая болевой порог.

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

Пятничный офф-топ:

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

Благодарю за внимание, оставайтесь любопытствующими и отличных всем выходных, ребята! :)

Немного рекламы


Спасибо, что остаётесь с нами. Вам нравятся наши статьи? Хотите видеть больше интересных материалов? Поддержите нас, оформив заказ или порекомендовав знакомым, облачные VPS для разработчиков от $4.99, уникальный аналог entry-level серверов, который был придуман нами для Вас: Вся правда о VPS (KVM) E5-2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps от $19 или как правильно делить сервер? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40GB DDR4).

Dell R730xd в 2 раза дешевле в дата-центре Maincubes Tier IV в Амстердаме? Только у нас 2 х Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 ТВ от $199 в Нидерландах! Dell R420 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB от $99! Читайте о том Как построить инфраструктуру корп. класса c применением серверов Dell R730xd Е5-2650 v4 стоимостью 9000 евро за копейки?
Подробнее..

Нет реальности без боли электронный эквивалент рецепторов кожи человека

11.09.2020 10:13:26 | Автор: admin


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

Основа исследования


Сенсорная система кожи человека может быть разделена на несколько подсистем, каждая из которых отвечает за определенные раздражители. Самыми распространенными и важными считаются рецепторы давления (тельца Пачини), температуры (терморецепторы) и боли (ноцицепторы).

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

Логично, что подобная биологическая система крайне сложна для воспроизведения, даже при учете современных тактильных сенсоров и КМОП (комплементарная структура металл-оксид-полупроводник).

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

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

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

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

  • мемристор для принятия решений на основе титаната стронция SrTiO3 (STO) с дефицитом кислорода;
  • датчик давления на основе золота на растяжимом эластомере (полидиметилсилоксан, т.е. PDMS);
  • температурный триггер на основе оксида ванадия (VO2) с фазовым переходом.

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

Результаты исследования


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


Изображение 1

Человеческие соматосенсоры через спинные рога (выступы серого вещества) соединены со спинным мозгом, который передает информацию в мозг (). Существуют специальные пути для определения давления (синий цвет на 1a) и температуры (красный цвет на 1a).

Тельца Пачини это слои мембран, заполненных жидкостью. Отпечатки пальцев являются ярким примером тельца Пачини. Когда на тело оказывается местное давление, часть тельца деформируется, вызывая сдвиг химических ионов (например, натрия или калия) и, как следствие, возникает рецепторный потенциал на нервном окончании кожи. Этот рецепторный потенциал при достижении достаточной энергии (порог) генерирует электрический импульс внутри тельца, который проходит через центральную нервную систему, чтобы активировать двигательный ответ через нервные волокна (синий цвет на 1a).

Когда температура кожи поднимается выше 30 C, терморецептор определяет тепло и запускает потенциалы действия. Частота возбуждения увеличивается с увеличением температуры стимула, пока не достигнет значения насыщения. Кроме того, тепловые ноцицепторы, которые улавливают болевые сигналы, начинают срабатывать при температуре около 45 C. Эти клетки специализируются на обнаружении вредоносного тепла и ожогов.

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

Чтобы создать аналогичные искусственные рецепторы, были использованы датчики давления на основе золота и PDMS, которые переключаются между состоянием низкого сопротивления (LRS) и состоянием высокого сопротивления (HRS) без и с приложенным давлением для имитации тельца Пачини (1b и 1c).

Чтобы воспроизвести поведение терморецепторов и ноцицепторов, использовался фазовый переход VO2, который может переходить от HRS при комнатной температуре к LRS при температуре выше температуры перехода (68 C).

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

Для искусственного тельца Пачини, когда нет определяемого давления, ток через мемристор принятия решения (I1) недостаточен из-за напряжения смещения, чтобы инициировать двигательную реакцию (1b). При приложении давления датчик переходит в режим HRS, блокирующий I2, что позволяет максимальному току проходить через мемристор. Из-за более высокого I1 мемристор на основе STO переключается на LRS. Следовательно, через тельце протекает более высокий ток, вызывающий двигательную реакцию ().

В случае терморецептора и ноцицептора VO2 может демонстрировать изменение сопротивления на три-четыре порядка при температуре перехода. Если же температура ниже температуры перехода, то VO2 является изолятором.

Таким образом, через рецептор протекает незначительное количество тока, а напряжение, которое появляется на мемристоре, недостаточно для его включения (1d). По достижении температуры перехода VO2 переключается на LRS, в результате чего на мемристоре появляется более высокий потенциал, что вызывает его переключение на LRS. Когда и VO2, и STO находятся в LRS, через рецептор протекает повышенный ток (1e).

Искусственное тельце Пачини


После создания концепции будущего устройства ученые приступили к поэтапной реализации. На первом этапе было создание искусственного тельца Пачини, для чего использовался мемристор на основе кислорододефицитного STO со стековой структурой: Pt (100 нм) / Ti (10 нм) / STO (55 нм) / Pt (25 нм) / Ti (7 нм) и подложка SiO2.

Нижний слой Ti используется как адгезионный слой нижнего слоя Pt, а верхний слой Ti используется как резервуар для кислорода, а также как адгезионный слой верхнего слоя Pt. Нижний слой Pt принимает участие в процессе переключения, а вот верхний служит в качестве инертного материала, предотвращающего TiO2 из-за воздействия кислорода окружающей среды.

Архитектура датчика давления вдохновлена биологическим тельцем Пачини, которое имеет спиральную форму с шириной дорожки и зазором 100 мкм. Диаметр всей спирали целиком составляет 7.8 мм. Для создания сенсора на PDMS толщиной 300 мкм был нанесен Au (200 нм) / Cr (20 нм).


Изображение 2

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

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

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

Фиксированное сопротивление в 100 кОм было выбрано для ограничения тока, проходящего через сеть датчика давления, которая имеет сопротивление всего 0.6 кОм. Это гарантирует, что система показывает очень низкий ток при отсутствии давления. На изображении 2b показан отклик и повторяемость автономного датчика давления.

При приложении давления датчик давления переходит в очень HRS с сопротивлением около 1 ГОм из-за деформации и трещин, которые очень часто встречаются для датчиков на основе PDMS. Из-за деформаций и трещин характеристики датчика давления могут ухудшиться после многократных циклов, однако это не мешает демонстрации самой концепции искусственного датчика. Когда давление сбрасывается, зазоры из-за трещин снова закрываются, создавая LRS, в результате чего датчик возвращается в исходное состояние.

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

Следует отметить, что компонент принятия решения, состоящий из мемристорного элемента на основе STO, должен быть первоначально подвергнут гальванопластике путем приложения напряжения смещения при очень низком токе 1 мкА к верхнему и нижнему электродам. Этот этап создает локализованный канал для образования проводящих нитей через STO. После этого требуется развертка напряжения для переключения устройства между состояниями HRS и LRS.

Важно и то, что без приложенного давления ток, протекающий через мемристор, недостаточен для его переключения. Однако при приложении давления датчик, содержащий ответвление, переходит в состояние HRS, что приводит к максимальному потенциалу рецептора на мемристоре (). При достижении порога рецепторного потенциала мемристор, принимающий решение, переключается из состояния HRS в состояние LRS (). В этом состоянии примененная последовательность 0 +0.85 В 0 1.12 В 0 переключает устройство в состояние LRS для положительного цикла и в состояние HRS для отрицательного полупериода (2d и ).

Чтобы перевести устройство в LRS, учитывается только положительный полупериод. В соответствии с изображением 2d, когда давление не подается, цепь датчика давления имеет общее сопротивление 100.6 кОм, тогда как параллельный компонент принятия решения (мемристор) имеет сопротивление 70 кОм. Таким образом, эквивалентное сопротивление всего тельца Пачини составляет 41.2 кОм.

Это эквивалентное сопротивление пропускает ток всего 0.02 мА через всю цепь, что можно рассматривать как расслабленное состояние. Приложение давления преобразует цепь датчика давления в состояние с чрезвычайно высоким сопротивлением 1 ГОм, в то время как сопротивление мемристора составляет всего около 2.5 кОм, изменяя эквивалентное сопротивление всего тельца Пачини примерно на 2.5 кОм. Это низкоомное состояние допускает ток 0.35 мА по всей цепи.

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

Искусственный терморецептор


Для создания терморецептора в основе мемристора была использована такая же стек-структура, как и для тельца Пачини, т.е. металл-изолятор-металл (МИМ).


Изображение 3

Часть верхнего электрона использовалась совместно с поверхностью VO2 (3а и 3b) для последовательного подключения теплового датчика. Для смещения всего устройства этот электродный слой, состоящий из Pt (100 нм) / Ti (10 нм), был нанесен на поверхность VO2. Между исходным электродом и верхним электродом мемристора поддерживалось существенное расстояние в 100 мкм.

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

График 3d показывает кривую зависимости сопротивления от температуры для перехода диэлектрикметалл на тонкой пленки VO2. Очевидно, что при достижении температуры перехода наблюдается падение удельного сопротивления на четыре порядка. Очевидный тепловой гистерезис также наблюдается в циклах нагрева и охлаждения. Также было установлено отсутствие какого-либо заметного влияния температуры на резистивное переключение ().

Мемристор может показывать изменение сопротивления от 100 кОм до 2 кОм в процессе переключения. Однако для более понятного анализа было решено рассматривать сопротивление состояния HRS как 93 кОм, а сопротивление состояния LRS как 9 кОм при 80 мВ напряжения считывания (VREAD), так как при этом напряжении наблюдается максимальное отношение переключения ROFF / RON. Последовательность переключения напряжения готового автономного устройства составила: 0 +0.65 В 0 0.80 В 0. Когда та же последовательность применяется ко всему терморецептору, сопротивление уменьшается, и, следовательно, ток рецептора увеличивается с повышением температуры (3f).

Чтобы обеспечить необходимое коммутируемое напряжение, температура приемника поддерживалась на уровне 70 C. Это необходимо для гарантии того, что VO2 находится в состоянии LRS. Затем приложение напряжения смещения от 0 до 2 В полностью устанавливает и сбрасывает устройство ().


Изображение 4

Для мемристора, принимающего решение, исходное сопротивление 93 кОм намного ниже, чем HRS теплового датчика, которое составляет 11 МОм. Таким образом, частичное напряжение, которое появляется на мемристоре, принимающем решение, не может достичь порогового значения VSET для преобразования его из HRS в LRS. Следовательно, и термодатчик, и мемристор находятся в состоянии HRS, что позволяет минимальному току протекать через терморецептор [4b(i)].

Когда применяется критическая температура 70 C, сопротивление термодатчика уменьшается на четыре порядка, а частичное напряжение мемристора постепенно увеличивается до напряжения SET с увеличением отклика рецептора [4b(ii)]. Как только VSET включает мемристор, он переходит в LRS от HRS с сопротивлением 9 кОм [4b(iii)].

На этом этапе формируется максимальный рецепторный ответ. LRS памяти будет сохраняться в течение длительного времени, даже если тепловой стимул полностью отключен. Чтобы перепрограммировать мемристор, отрицательное напряжение VRESET может перевести его с LRS в HRS [4b(iv)]. Для этого можно подавать отрицательное напряжение от неиспользуемых электродов ().

Искусственный ноцицептор


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

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

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

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

Ноцицептор демонстрирует два различных поведения в анормальных условиях: аллодиния и гипералгезия.

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

Для наблюдений за поведением искусственного терморецептора в нормальных условиях, который работает как ноцицептор во время вредоносных стимулов, устройство было переключено на LRS, а показания считывались при VREAD80 мВ.

Поскольку срабатывание биологического ноцицептора в значительной степени зависит от интенсивности стимула, на искусственный ноцицептор воздействовали серией температурных стимулов с различной интенсивностью в диапазоне от 66 до 82 C (5a).


Изображение 5

График 5b демонстрирует ответный сигнал относительно интенсивности приложенного теплового стимула. Следует отметить, что ноцицептор не включается, пока температурный импульс не достигнет 68 C, что является температурой перехода используемого VO2. Таким образом, когда VO2 попадает в LRS из-за температурно-индуцированного перехода, более высокий ток начинает течь по всей цепи. Подобное повеление напоминает биологические системы, в которых ноцицептор генерирует запускающий мозг потенциал действия, когда сила стимула достигает значений выше критического.

Дальнейшее увеличение интенсивности стимула выше порогового значения приводит к большему току, что также согласуется с реакцией биологического аналога: чем выше интенсивность стимула, тем выше интенсивность ответной реакции. На графике 5c показаны стимулы нагрева и охлаждения и соответствующий ответный сигнал при 68 C.

График 5d показывает ослабление ответного сигнала с течением времени после того, как вредоносный стимул был отключен, т.е. процесс релаксации, который определяется VO2, так как нет влияния температуры на мемристор на основе STO.

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

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

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

Ноцицептор нагревали до 90 C со скоростью 20 градусов в минуту и охлаждали до 60 C, что ниже порогового значения (68 C) в нормальных условиях.

Далее следовал повторный нагрев с 60 до 90 C, необходимый для определения наличия/отсутствия генерации пониженного порога и усиленного ответа, которые являются основными свойствами аллодинии и гипералгезии.

Последовательность 60 90 60 90 применялась к VO2 части рецептора и ко всему рецептору, содержащему VO2 и стек металл-изолятор-металл ().


Изображение 6

На графиках отчетливо видно, что ответный сигнал намного более линейный в VO2 () по сравнению с сигналом всего ноцицептора (6b). Это вполне ожидаемо, так как при высокой интенсивности стимула VO2 находится в почти металлическом состоянии с относительно низким сопротивлением 5 кОм после перехода. Более того, приложенное напряжение смещения VREAD (80 мВ) электрически настраивает VO2, чтобы сделать его еще более металлическим, что приводит к линейному отклику.

В то же время, когда подобное смещение VREAD появляется на всем ноцицепторе, максимальное падение напряжения происходит на мемристоре, который находится в состоянии LRS (9 кОм). На этом этапе напряжения на VO2 недостаточно, чтобы показать линейный отклик. Следовательно, данное поведение является нелинейным.

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

На 6d показан отклик по отношению к двум циклам нагрева последовательности с последовательностью 60 90 60 90 C. Тут видно, что отклик для второго цикла нагрева усиливается, а порог снижается.

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


Демонстрация принципа работы электронного эквивалента тельца Пачини.

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

Эпилог


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

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

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

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

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

Благодарю за внимание, оставайтесь любопытствующими и отличных всем выходных, ребята! :)

Немного рекламы


Спасибо, что остаётесь с нами. Вам нравятся наши статьи? Хотите видеть больше интересных материалов? Поддержите нас, оформив заказ или порекомендовав знакомым, облачные VPS для разработчиков от $4.99, уникальный аналог entry-level серверов, который был придуман нами для Вас: Вся правда о VPS (KVM) E5-2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps от $19 или как правильно делить сервер? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40GB DDR4).

Dell R730xd в 2 раза дешевле в дата-центре Equinix Tier IV в Амстердаме? Только у нас 2 х Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 ТВ от $199 в Нидерландах! Dell R420 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB от $99! Читайте о том Как построить инфраструктуру корп. класса c применением серверов Dell R730xd Е5-2650 v4 стоимостью 9000 евро за копейки?
Подробнее..

Испытывают ли боль беспозвоночные?

19.04.2021 14:06:47 | Автор: admin

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

Представление боли в картине Пабло Пикассо. Герника. 1937Представление боли в картине Пабло Пикассо. Герника. 1937

Не для кого не секрет, что позвоночные практически во всей своей массе могут испытывать боль. Исключениями могут быть всякие там рыбы и примитивные хордовые, но даже и для них существуют доказательства, что всё-таки и они имеют какой-то там слабый аффективный компонент боли [4].

Рисунок показывает сложные траектории плавания взрослых рыбок данио, которым сделали больно. Так у рыбок данио, подвергнутых анестезии, но не испытывающих предполагаемую боль поведение не менялось, а у рыбок данио, получивших повреждение для плавника, или укол уксусной кислоты наблюдалось короткое аффективное состояние боли, которое устранялось анестезией и измерялось значениями в диапазоне от нормального (1,15) до низкого (0,83), демонстрируя снижение сложности реакции на стрессовое или болезненное лечение, с произвольными точками, указывающими на воздействие стресса в виде легкой, умеренной и сильной боли. Рисунок показывает сложные траектории плавания взрослых рыбок данио, которым сделали больно. Так у рыбок данио, подвергнутых анестезии, но не испытывающих предполагаемую боль поведение не менялось, а у рыбок данио, получивших повреждение для плавника, или укол уксусной кислоты наблюдалось короткое аффективное состояние боли, которое устранялось анестезией и измерялось значениями в диапазоне от нормального (1,15) до низкого (0,83), демонстрируя снижение сложности реакции на стрессовое или болезненное лечение, с произвольными точками, указывающими на воздействие стресса в виде легкой, умеренной и сильной боли.

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

Участки коры головного мозга человека ответственныеза восприятие негативного аффективного и тоническогосостояния боли. Участки коры головного мозга человека ответственныеза восприятие негативного аффективного и тоническогосостояния боли.

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


Так называемые дискретные циклы боли в центральном мозге производят два различных компонента переживания боли:

  • различительный компонент, охватывающий локализацию, качество и интенсивность боли.

  • аффективный компонент, охватывающий негативное эмоциональное состояние.

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

  • сложные поведенческие реакции (хромота, зализывание ран, укрывание больного места) которые могут модулироваться обезболиванием,

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

  • ассоциативное изучение различных ситуаций, которые так или иначе сигнализируют о вредных ощущениях.

Сложная нервная система некоторых ракообразныхпоказывающая наличие ноцицепоторов Сложная нервная система некоторых ракообразныхпоказывающая наличие ноцицепоторов

Такие поведенческие исследования, демонстрирующие наличие этих способностей доказывающие аффективный компонент боли, наиболее широко были продемонстрированы на ракообразных. Так, например, раки отшельники предпочитали страдать от тока в раковине, нежели быть съеденными хищниками вне раковины, а если им давали новую раковину, где током били меньше, то они выбирали новую раковину. Креветки, которым наносили увечья, ухаживали за повреждённым органом с помощью конечностей или ротового аппарата, а крабы, которых травили ядами пытались избегать ущерба, как существа, которые чувствуют боль. Т.е. они предоставляли те участки тела к уничтожению, которые были более защищены от внешнего воздействия, или они покидали то место где их варварски угнетали [1].

Влияние дофамина на пчёл делает их счастливыми. Нервные центры отвечающие за разные эмоции сходны [2]Влияние дофамина на пчёл делает их счастливыми. Нервные центры отвечающие за разные эмоции сходны [2]

Доказательства сложных эмоциональных состояний подобных боли были обнаружены и у некоторых насекомых [2]. Всё это показывает нам, что эмоциональная обработка сенсорных переживаний у беспозвоночных может быть, как сложной, так и широко распространенной. Однако на подобного рода доказательства всё равно найдётся очередной Илон Маск с козырем в рукаве в виде распространенного аргумента против возможности аффективного состояния боли у беспозвоночных.

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

Рак-отшельник вне панциря пытается защитить свой мягкий и беззащитный животик от хищников. Данная шутливая фотография с википедии не является контраргументом Илона Маска. Контраргумент ниже. Рак-отшельник вне панциря пытается защитить свой мягкий и беззащитный животик от хищников. Данная шутливая фотография с википедии не является контраргументом Илона Маска. Контраргумент ниже.

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

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

Определение ПП в документах ЮНЕСКООпределение ПП в документах ЮНЕСКО

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

А спонсором требуемых доказательств является исследование от 2020 года опубликованное в журнале ISCIENCE, на котором и базируется весь мой текст [3]. Суть данного исследования заключается в том, что к объектам исследования, тобишь осьминогам применялась методика оценки аффективных аспектов боли, применяемая до этого практически только к позвоночным, в частности к млекопитающим.

(A) Осьминог в стартовой камере.(B) Схема устройства с рисунком на стенках камеры представлена только для ясности. В ходе экспериментальных испытаний визуальные подсказки покрывали все четыре стены.(C) График эксперимента, показывающий последовательности избегания камер. В этом примере осьминог продемонстрировал первоначальное предпочтение в комнате с точками [сеccия 1] после введения уксусной кислоты осьминог ретировался в камеру с полосками [cессия 2] и возвращался обратно в предпочтительную камеру после анестезии [cессия 3].(A) Осьминог в стартовой камере.(B) Схема устройства с рисунком на стенках камеры представлена только для ясности. В ходе экспериментальных испытаний визуальные подсказки покрывали все четыре стены.(C) График эксперимента, показывающий последовательности избегания камер. В этом примере осьминог продемонстрировал первоначальное предпочтение в комнате с точками [сеccия 1] после введения уксусной кислоты осьминог ретировался в камеру с полосками [cессия 2] и возвращался обратно в предпочтительную камеру после анестезии [cессия 3].

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

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

Рисунок показывает соотношение во времени, при котором разные осьминожки ухаживали за больным местом без анестезии и с анестезией. В исследовании принимали четыре взрослые особи. О том, почему мы биологи используем малое количество животных в опытах я рассказывал здесь [http://personeltest.ru/aways/habr.com/ru/post/543428/]Рисунок показывает соотношение во времени, при котором разные осьминожки ухаживали за больным местом без анестезии и с анестезией. В исследовании принимали четыре взрослые особи. О том, почему мы биологи используем малое количество животных в опытах я рассказывал здесь [http://personeltest.ru/aways/habr.com/ru/post/543428/]

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

Схематичное изображение того как выглядит боль.Схематичное изображение того как выглядит боль.

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

(A) Примеры спонтанной (продолжающейся) и вызванной активности в соединительной ткани плеча после уколов уксусом и болюсом. Боль прекращалась после анестезии.(B) Непрерывное самопроизвольное возбуждение в соединительной ткани плеча усиливается после инъекции болюса и блокируется инъекцией лидокаина(C) Сводные данные, показывающие реакцию на прикосновение к руке в четырех местах (обозначены заштрихованными синими кружками на контуре тела осьминога).(A) Примеры спонтанной (продолжающейся) и вызванной активности в соединительной ткани плеча после уколов уксусом и болюсом. Боль прекращалась после анестезии.(B) Непрерывное самопроизвольное возбуждение в соединительной ткани плеча усиливается после инъекции болюса и блокируется инъекцией лидокаина(C) Сводные данные, показывающие реакцию на прикосновение к руке в четырех местах (обозначены заштрихованными синими кружками на контуре тела осьминога).

Осьминогам удалили маленькую часть соединительной ткани разорвав её по центру межплечевой спайки, оголив центральную нервную систему, для того чтобы было удобно получать эти самые электрофизиологические записи. После чего в руку особи вводилась инъекция болюса, которая приводила к постоянной и очень сильной активности нервной системы более 30 минут. Эта активность гасилась постепенным введением лидокаина, тем самым успокаивая нервную систему особи. Электрофизиологические данные убедительно подтвердили существование длительного негативного аффективного состояния у осьминогов, являющимся первым свидетельством боли в этой неврологически сложной кладе беспозвоночных. Более того данные из всех трёх экспериментов над осьминогами абсолютно доказали, что осьминоги испытывают состояние постоянной (тонической) боли, что ранее считалось возможным только у млекопитающих. Поэтому по-моему мнению принцип предосторожности с такими животными категорически необходим.

Данное исследование в полном объёме представляет собой первый пример вероятной продолжающейся боли у любого животного, не являющегося млекопитающим, что собственно заставляет с одной стороны задуматься, например, на сколько сильно, страдает живой рак, кипящий в котле, а с другой стороны радоваться, что реинкарнация существует только в буддизме. P.s. А вы варите раков живыми?

Данная статья написана мной и опубликована в нашем научно-популярном сообществе Фанерозой.

Источники:
Подробнее..

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-е лежит здесь.


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


Подробнее..

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 всё, на следующей неделе расскажу про кейсы с вложенными графами навигации.

Подробнее..

Navigation Component-дзюцу, vol. 2 вложенные графы навигации

16.09.2020 12:14:05 | Автор: admin


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


Это вторая из трёх статей про реализацию кейсов навигации при помощи Navigation Component-а.


Первая статья про BottomNavigationView.


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



В этом кейсе мы говорим про следующую часть общей схемы навигации:



Представим такую ситуацию: у нас есть 4 экрана A, B, C и D. Пусть с экранов A и B вы можете перейти на экран C, с экрана C в экран D, а после D вернуться на тот экран, который начал флоу C->D.


А можно нагляднее?

В тестовом приложении, которое я приготовил для разбора Navigation Component-а, есть две вкладки BottomNavigationView (на схеме это Search и Responses но пусть они будут экранами A и B):



С обеих этих вкладок мы можем перейти на некоторый вложенный флоу, который состоит из двух экранов (C и D):



Если мы перейдём на экран C с вкладки Search (экрана A), то после экрана D мы должны вернуться на вкладку Search:



А если мы стартуем экран C со вкладки Responses, то после завершения внутреннего флоу C->D мы должны вернуться на вкладку Responses:



Этот кейс описывает старт последовательности экранов из разных мест приложения, а после её завершения возврат на тот экран, который начал эту последовательность.


Как это реализовать? Для начала вы объявляете вашу вложенную последовательность экранов в отдельном XML-файле навигации, чтобы можно было вкладывать её в другие графы:


Объявление графа вложенной навигации
<!-- company_flow__nav_graph.xml --><navigation    android:id="@+id/company_flow__nav_graph"    app:startDestination="@id/CompanyFragment">    <fragment        android:id="@+id/CompanyFragment"        android:name="ui.company.CompanyFragment">        <action            android:id="@+id/action__CompanyFragment__to__CompanyDetailsFragment"            app:destination="@id/CompanyDetailsFragment" />    </fragment>    <fragment        android:id="@+id/CompanyDetailsFragment"        android:name="ui.company.CompanyDetailsFragment"/></navigation>

Затем следует вложить созданный граф навигации в уже существующий граф и использовать идентификатор вложенного графа для описания action-ов:


Добавление графа навигации в другой граф
<navigation    android:id="@+id/menu__search"    app:startDestination="@id/SearchContainerFragment">    <fragment        android:id="@+id/SearchContainerFragment"        android:name="ui.tabs.search.SearchContainerFragment">        <action            android:id="@+id/action__SearchContainerFragment__to__CompanyFlow"            app:destination="@id/company_flow__nav_graph" />    </fragment>    <include app:graph="@navigation/company_flow__nav_graph" /></navigation>

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


Проблема в том, что Navigation Component не позволяет нормально описывать навигацию НАЗАД, только навигацию ВПЕРЁД. Но при этом даёт возможность описывать удаление экранов из back stack-а при помощи атрибутов popBackUp и popBackUpInclusive в XML, а также при помощи функции popBackStack в NavController-е.


Пока размышлял над этим, заметил интересную вещь: я подключился дебаггером перед переходом с экрана Splash на экран с нижней навигацией и обнаружил, что поле mBackStack внутри NavController-а Splash-экрана содержит два объекта NavBackStackEntry.


А можно на картинке?


Честно говоря, я не ожидал увидеть там два объекта, поскольку в back stack-е фрагментов точно был только один SplashFragment. Откуда взялась вторая сущность? Оказалось, что первый объект представляет собой NavGraph, который запустился в моей корневой Activity, а второй объект мой SplashFragment, который представлен классом FragmentNavigator.Destination.


И тут у меня появилась идея а что если вызвать на NavController-е функцию popBackStack и передать туда идентификатор графа? Коль скоро граф находится в back stack-е NavController-а, это должно удалить все экраны, которые были добавлены в рамках этого графа.


И эта идея сработала.


Возврат из flow при помощи popBackStack
class CompanyDetailsFragment : Fragment(R.layout.fragment_company_details) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        finish_flow_button.setOnClickListener {            findNavController().popBackStack(R.id.company_flow__nav_graph, true)        }    }}

Минус такого подхода к определению обратной навигации очевиден: эта навигация не отобразится в визуальном редакторе. Конечно, можно определить action в XML-е вот таким образом:


Определение action-а для закрытия графа навигации
<fragment  android:id="@+id/CompanyDetailsFragment"  android:name="ui.company.CompanyDetailsFragment"  android:label="@string/fragment_company_details__title"  tools:layout="@layout/fragment_company_details">  <action      android:id="@+id/action__finishCompanyFlow"      app:popUpTo="@id/company_flow__nav_graph"      app:popUpToInclusive="true" /></fragment>

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


findNavController().navigate(R.id.action__finishCompanyFlow)

Но есть в этом что-то семантически неправильное: странно использовать слово navigate для закрытия экранов и обратной навигации.


Возврат результата из вложенного флоу


Что ж, мы получили некоторое подобие обратной навигации. Но возникает ещё один вопрос: есть ли способ вернуть из вложенного флоу какой-нибудь результат?


Да, есть. В Navigation Component 2.3 Google представил нам специальное key-value хранилище для проброса результатов с других экранов SavedStateHandle. К этому хранилищу можно получить доступ через свойства NavControllerpreviousBackStackEntry и currentBackStackEntry. Но в своих примерах Google почему-то считает, что ваш вложенный флоу всегда состоит только из одного экрана.


Типичный пример работы с SavedStateHandle
// Flow screenfindNavController().previousBackStackEntry    ?.savedStateHandle    ?.set("some_key", "value")// Screen that waits resultval result = findNavController().currentBackStackEntry    ?.savedStateHandle    ?.remove<String>("some_key")

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


Посмотреть на фикс
fragment_company_details__button.setOnClickListener {    // Here we are inside nested navigation flow    findNavController().popBackStack(R.id.company_flow__nav_graph, true)    // At this line, "findNavController().currentBackStackEntry" means    // screen that STARTED current nested flow.    // So we can send the result!    findNavController().currentBackStackEntry      ?.savedStateHandle      ?.set(COMPANY_FLOW_RESULT_FLAG, true)}

Суть в следующем: до вызова findNavController().popBackStack вы находитесь ВНУТРИ вашего флоу экранов, а вот сразу после вызова popBackStack уже на экране, который НАЧАЛ ваш флоу! И это означает, что вы можете использовать для доступа к SavedStateHandle свойство currentBackStackEntry. Этот entry будет означать ваш стартовый экран, которому нужен результат из флоу.


В свою очередь, на на экране, который начал вложенный флоу, вы тоже используете currentBackStackEntry для доступа к SavedStateHandle. И, следовательно, читаете правильные данные:


Читаем данные из SavedStateHandle
// Read result from nested navigation flowval companyFlowResult = findNavController().currentBackStackEntry    ?.savedStateHandle    ?.remove<Boolean>(CompanyDetailsFragment.COMPANY_FLOW_RESULT_FLAG)text__company_flow_result.text = "${companyFlowResult}"

Выводы по работе с вложенным флоу


  • Для обратной навигации из вложенного флоу, состоящего из нескольких экранов, можно использовать функцию NavController.popBackStack, передав туда идентификатор графа навигации вашего флоу.
  • Для проброса какого-либо результата из вложенного флоу можно использовать SavedStateHandle.


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


Пусть у нас два графа навигации граф A и граф B. Я буду называть граф B вложенным в граф A, если мы вкладываем его через include. И, наоборот, я буду называть граф A внешним по отношению к графу B, если граф А включает в себя граф B.


Ещё немного картинок

Граф B вложенный в граф A:



Граф А внешний по отношению к графу B:



А теперь давайте разберём кейс навигации из вложенного графа во внешний граф.



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


Что? В смысле, это тот самый первый кейс, который ты уже разобрал? Разве вы не заметили, что у этой последовательности НЕТ нижней навигации?


Приблизить картинку

Смотрите, вот экран с нижней навигацией:



А вот последовательность экранов без неё:



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


Неправильный подход к такой навигации

Пусть мы вставили граф auth flow-навигации в наш граф вкладки нижней навигации и добавили action для перехода в него:


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/menu__profile"    app:startDestination="@id/ProfileContainerFragment">    <fragment        android:id="@+id/ProfileContainerFragment"        android:name="ui.tabs.profile.ProfileContainerFragment">        <action            android:id="@+id/action__ProfileContainerFragment__to__AuthFlow"            app:destination="@id/auth__nav_graph" />    </fragment>    <include app:graph="@navigation/auth__nav_graph" /></navigation>

В этом случае первый экран auth-флоу появится в контейнере с нижней навигацией, а мы этого не хотели:



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


В каком случае мы получим желаемый результат? Если каким-то образом осуществим навигацию из контейнера с BottomNavigationView (не из самой вкладки, а из контейнера, который является Host-ом для всех этих вкладок), то Auth-граф откроется без нижней навигации.


А на картинке можно?

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



Давайте введём action для навигации между MainFragment-ом и флоу авторизации:


Описание навигации
<! app_nav_graph.xml ><fragment  android:id="@+id/SplashFragment"  android:name="com.aaglobal.jnc_playground.ui.splash.SplashFragment"/><fragment  android:id="@+id/MainFragment"  android:name="com.aaglobal.jnc_playground.ui.main.MainFragment">  <action      android:id="@+id/action__MainFragment__to__AuthFlow"      app:destination="@id/auth__nav_graph" /></fragment><include app:graph="@navigation/auth__nav_graph" />

Но проблема в том, что если мы попытаемся использовать этот action прямо из вкладки нижней навигации, вот так:


fragment_profile_container__button__open_auth_flow.setOnClickListener {    findNavController().navigate(R.id.action__MainFragment__to__AuthFlow)}

то приложение упадёт с IllegalArgumentException, потому что NavController текущей вкладки ничего не знает о навигации вне своего Host-а навигации.


Ищем правильный NavController


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


В Navigation Component есть специальная утилитная функция для поиска NavController-а, который привязан к нужному вам контейнеру, Navigation.findNavController:


Открываем флоу авторизации правильно
fragment_profile_container__button__open_auth_flow.setOnClickListener {  Navigation.findNavController(    requireActivity(),    R.id.activity_root__fragment__nav_host  ).navigate(R.id.action__MainFragment__to__AuthFlow)}


Проблемы с навигацией по кнопке Back


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


Покажи гифку


Исключение, которое мы получаем:


java.lang.IllegalArgumentException: No view found for id 0x7f08009a (com.aaglobal.jnc_playground:id/fragment_main__nav_host_container) for fragment NavHostFragment{5150965} (e58fc3a2-b046-4c80-9def-9ca40957502d) id=0x7f08009a bottomNavigation#0}

Эту проблему можно решить, переопределив поведение кнопки Back. В одной из новых версий AndroidX появился удобный OnBackPressedCallback. Раз мы используем неправильный NavController по умолчанию, значит, мы можем подменить его на правильный:


Переопределяем back-навигацию для первого экрана auth-графа
class StartAuthFragment : Fragment(R.layout.fragment_start_auth) {    private var callback: OnBackPressedCallback? = null    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        callback = object : OnBackPressedCallback(true) {            override fun handleOnBackPressed() {                Navigation.findNavController(                    requireActivity(),                    R.id.activity_root__fragment__nav_host                ).popBackStack()            }        }.also {            requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, it)        }    }}

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


И это работает! Но есть одно но: чтобы это продолжало работать на протяжении всего auth-флоу, нам надо добавить точно такой же OnBackPressedCallback в каждый экран этого флоу =(


И, конечно же, придётся поправить закрытие всего auth-флоу там мы тоже должны добавить получение правильного NavController-а:


Как это выглядит?
class FinishAuthFragment : Fragment(R.layout.fragment_finish_auth) {  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {      super.onViewCreated(view, savedInstanceState)      fragment_finish_auth__button.setOnClickListener {          Navigation.findNavController(              requireActivity(),              R.id.activity_root__fragment__nav_host          ).popBackStack(R.id.auth__nav_graph, true)          findNavController().currentBackStackEntry            ?.savedStateHandle            ?.set(AUTH_FLOW_RESULT_KEY, true)      }  }}

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


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


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


Покажи на картинке


У этого кейса есть пара особенностей, которые отличают его от первого рассмотренного случая.


  • Когда пользователь нажмёт на кнопку Back на первом экране флоу авторизации, мы хотим не вернуться назад (потому что зачем нам второй раз показывать Splash), а закрыть приложение.
  • После завершения флоу авторизации мы не просто закрываем открытый нами граф, но и двигаемся вперёд.

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


Покажи код

Определяем флажок для StartAuthFragment:


<fragment  android:id="@+id/StartAuthFragment"  android:name="com.aaglobal.jnc_playground.ui.auth.StartAuthFragment"  android:label="Start auth"  tools:layout="@layout/fragment_start_auth">  <argument      android:name="isFromSplashScreen"      android:defaultValue="false"      app:argType="boolean"      app:nullable="false" />  <action      android:id="@+id/action__StartAuthFragment__to__FinishAuthFragment"      app:destination="@id/FinishAuthFragment" /></fragment>

А теперь используем этот флажок в OnBackPressedCallback:


class StartAuthFragment : Fragment(R.layout.fragment_start_auth) {    private val args: StartAuthFragmentArgs by navArgs()    private var callback: OnBackPressedCallback? = null    private fun getOnBackPressedCallback(): OnBackPressedCallback {      return object : OnBackPressedCallback(true) {          override fun handleOnBackPressed() {              if (args.isFromSplashScreen) {                  requireActivity().finish()              } else {                  Navigation.findNavController(                    requireActivity(),                    R.id.activity_root__fragment__nav_host                  ).popBackStack()              }          }      }    }}

Поскольку у нас Single Activity, requireActivity().finish() будет достаточно, чтобы закрыть наше приложение.


Со вторым пунктом чуть интереснее. Я вижу два способа реализовать такую пост-навигацию.


  • Первый способ: Navigation Component позволяет в runtime-е менять граф навигации, мы могли бы где-нибудь сохранить @id будущего destination-а и добавить немного логики при завершении авторизации.
  • Второй способ закрывать флоу авторизации как и раньше, а логику движения вперёд дописать в экран, который стартовал экраны авторизации, то есть в Splash.

Первый способ мне не нравится тем, что если появятся дополнительные destination-ы, которые надо открывать после экранов авторизации, появится и много лишней логики внутри флоу авторизации. Да и модифицировать граф навигации в runtime-е то ещё удовольствие.


Второй способ тоже не ахти потребуется сохранить предыдущий экран в back stack-е, чтобы, вернувшись на него и прочитав результат после авторизации, мы могли двигаться дальше. Но это всё равно приемлемый вариант: вложенный флоу будет отвечать только за свою собственную логику, а экран, который начинает подобную условную навигацию (выбор между main и auth на Splash-е, например), и так знает, как двигаться вперёд.


И реализовать это просто мы знаем, как закрыть auth-флоу, знаем, как прокинуть из него результат на экран, который стартовал экраны авторизации. Останется только поймать результат на SplashFragment-е.


Покажи код

Пробрасываем результат из auth-флоу:


// FinishAuthFragment.ktfragment_finish_auth__button.setOnClickListener {    // Save hasAuthData flag in prefs    GlobalDI.getAuthRepository().putHasAuthDataFlag(true)    // Navigate back from auth flow    Navigation.findNavController(        requireActivity(),        R.id.activity_root__fragment__nav_host    ).popBackStack(R.id.auth__nav_graph, true)    // Send signal about finishing flow    findNavController().currentBackStackEntry      ?.savedStateHandle      ?.set(AUTH_FLOW_RESULT_KEY, true)}

И ловим его на стороне SplashFragment-а:


// SplashFragment.ktoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {    super.onViewCreated(view, savedInstanceState)    val authResult = findNavController().currentBackStackEntry        ?.savedStateHandle        ?.remove<Boolean>(FinishAuthFragment.AUTH_FLOW_RESULT_KEY) == true    if (authResult) {        navigateToMainScreen()        return    }}

Выводы по кейсам вложенной навигации


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

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

Подробнее..

Категории

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

  • Имя: Макс
    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