Share
Навигация с архитектурными компонентами от Google: взгляд прагматика
Александр Блинов, HeadHunter – MOSDROID 10 #NEON
Одной из проблем, с которыми сталкивается разработчик немного подразросшегося приложения — навигация между экранами. Когда сценарии становятся нелинейными, уже тяжело обойтись стандартными startActivity и changeFragment. Эту проблему каждый решал по-своему: делал какое-то свое решение для навигации, использовал стороннее (к примеру, Cicerone) или же оставлял все как есть и городил кучу флагов и if else. Это очень огорчало инженеров Google, и вот уже на Google I/O 2018 появилось решение Navigation, входящее в состав набирающего хайпа JetPack-а.
Что мы вообще хотим от фреймворка для навигации? Как устроено решение Google изнутри? Закрыт ли вопрос навигации навсегда? В своем докладе я постараюсь ответить на эти и другие насущные вопросы.
MOSDROID
Transcript
61 slides
-
Обо мне Руководитель Android практики Соавтор Moxy Соведущий подкастов Админ
чата по архитектуре Android Выступаю на конференциях Александр Блинов [email protected] -
Затравка Затравка 3 % 8 % 12 % 69 %
8 % Что это? Слышал, не пробовал Смотрел в эдиторе Использую в своём петпроджекте Использую в живом проект Кто работал с новой навигацией от Google? Убийца Чичероне? -
1 Требования к presentation слою Содержание 2 Устройство 3 Принцип
работы 4 5 Подведение итогов Слайды Пример работы -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 8 Приоритет 1 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 15 16 -
Принципы навигации Фиксированная точка старта Навигация работает в виде стека
LIFO Кнопки Back и Up работают одинаково* Диплинки в приложение порождают стек экранов, аналогичный тому, если бы мы дошли до экрана самостоятельно Постулаты 1 2 = 1 2 3 -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 8 Приоритет 1 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 15 16 14 -
Navigation Editor <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" app:startDestination="@id/homeFragment">
<fragment android:id="@+id/homeFragment" android:name="com.github.xanderblinov.nav.HomeFragment" android:label="fragment_home" tools:layout="@layout/fragment_home"> <action android:id="@+id/action_homeFragment_to_introFragment" app:destination="@id/introFragment" app:launchSingleTop="true" /> </fragment> <fragment android:id="@+id/introFragment" android:name="com.github.xanderblinov.nav.IntroFragment" android:label="fragment_intro" tools:layout="@layout/fragment_intro"> <deepLink android:id="@+id/deepLink" app:uri="intro" /> <argument android:name="amount" android:defaultValue="1" app:argType="integer" /> </fragment> </navigation> Text mode -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 8 Приоритет 1 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 16 6 4 15 14 15 12 15 16 -
Ключевые сущности Экран, который открывается при навигации. • Activity •
Fragment NavDestination Содержит набор Action -
Ключевые сущности Коллекция узлов NavDestination NavGraph • Destination достаются по
ID • Имеется стартовый экран • Можно добавить целый граф • Создается механизмом аналогичным view • NavInflater из Resources • Динамически из кода -
Ключевые сущности Контейнер для навигации при помощи NavController NavHostFragment :
NavHost • Управляет сохранением и восстановлением состояния NavController • Устанавливает NavController во View public interface NavHost { @NonNull NavController getNavController(); } -
Ключевые сущности Мозги навигации NavController • Осуществляет переходы по стеку
вперед(navigate) и назад ( popBackStack / navigateUp() ) состояния NavController • Предоставляет возможность наблюдать за навигацией • Умеет сохранять состояние в Bundle и восстанавливать его обратно -
Ключевые сущности Руки навигации Navigator • Реализует переходы по стеку
вперед(navigate) и назад ( popBackStack / navigateUp() ) состояния NavController • Предоставляет возможность наблюдать за навигацией • Умеет сохранять состояние в Bundle и восстанавливать его обратно -
Уровень Destination • Destination ID • NavOptions Destination #1 Action
#2 Action #1 Устройство -
NavOptions Устройство /** * NavOptions stores special options for navigate
actions */ public class NavOptions { static final int LAUNCH_SINGLE_TOP = 0x1; static final int LAUNCH_DOCUMENT = 0x2; static final int LAUNCH_CLEAR_TASK = 0x4; private int mLaunchMode; @IdRes private int mPopUpTo; private boolean mPopUpToInclusive; @AnimRes @AnimatorRes private int mEnterAnim; @AnimRes @AnimatorRes private int mExitAnim; @AnimRes @AnimatorRes private int mPopEnterAnim; @AnimRes @AnimatorRes private int mPopExitAnim; /** some code**/ } -
Уровень Fragment / Activity Container Fragment/Activity NavHostFragment Navigator NavGraph Destination
#1 Destination #2 Destination #3 NavController Устройство Сохранение состояний, инстанциирование Транзакции фрагментов Логика переходов Хранение карты переходов -
Механика работы Инстанциирование Container Fragment/Activity NavHostFragment NavGraph Destination #1 Destination
#2 Destination #3 NavController Inflate Fragment 1 Create 3 Inflate 6 Create 2 Set navigator 4 Set graph id / Restore state 5 Navigator -
Задание графа из кода Инстанциирование public class NavController { /*
… */ public void setGraph(@NavigationRes int graphResId) { mGraph = getNavInflater().inflate(graphResId); mGraphId = graphResId; onGraphCreated(); } public void setGraph(@NonNull NavGraph graph) { mGraph = graph; mGraphId = 0; onGraphCreated(); } /* … */ } Победа? -
Задание графа из кода Корректировка val navController = findNavController(requireActivity(), R.id.nav_host_fragment_home)
with(navController) { // Создаем Destination val destination = FragmentNavigator.Destination(navigatorProvider) destination.id = NOTIFICATION_DETAILS_ID destination.setFragmentClass(NotificationDetailsFragment::class.java) // Добавляем Destination в граф graph.addDestination(destination) // Добавляем Action на открытие добавленного Destination val notificationDestination = graph.findNode(R.id.notificationFragment) notificationDestination?.putAction(OPEN_NOTIFICATION_DETAILS_ACTION_ID, NOTIFICATION_DETAILS_ID) } -
val navController = findNavController(requireActivity(), R.id.nav_host_fragment_home) with(navController) { // Создаем Destination
val destination = FragmentNavigator.Destination(navigatorProvider) destination.id = NOTIFICATION_DETAILS_ID destination.setFragmentClass(NotificationDetailsFragment::class.java) // Добавляем Destination в граф graph.addDestination(destination) // Добавляем Action на открытие добавленного Destination val notificationDestination = graph.findNode(R.id.notificationFragment) notificationDestination?.putAction(OPEN_NOTIFICATION_DETAILS_ACTION_ID, NOTIFICATION_DETAILS_ID) } Задание графа из кода Корректировка -
/** some code */ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction;
@Navigator.Name("fragment") public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> { /** some code */ @NonNull public Destination setFragmentClass(@NonNull Class<? extends Fragment> clazz) { mFragmentClass = clazz; return this; } /** some code */ } Jetifier -
navigation-common navigation-common-ktx navigation-fragment navigation-runtime navigation-runtime-ktx navigation-testing navigation-testing-ktx navigation-ui navigation-safe-args-generator navigation-fragment-ktx
navigation-ui navigation-ui-ktx navigation-common navigation-common-ktx Dependency Graph -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 7 8 Приоритет 1 6 6 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 15 16 6 4 14 15 16 15 12 -
Механика работы Навигация Container Fragment/Activity NavHostFragment NavGraph Destination #1 Destination
#2 Destination #3 NavController navigate(args) 1 Find Destination 2 navigate(args) 3 Navigator Transaction 5 navigate(args) 4 -
public class NavController { public NavController(@NonNull Context context) { mContext
= context; while (context instanceof ContextWrapper) { if (context instanceof Activity) { mActivity = (Activity) context; break; } context = ((ContextWrapper) context).getBaseContext(); } mNavigatorProvider.addNavigator(new NavGraphNavigator(mContext)); mNavigatorProvider.addNavigator(new ActivityNavigator(mContext)); /** some code*/ } /** some code*/ } Navigator Использование public class NavHostFragment extends Fragment implements NavHost { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Context context = requireContext(); mNavController = new NavController(context); mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator()); /** some code*/ } /** some code*/ } -
Механика работы Навигация Container Fragment/Activity NavHostFragment NavGraph Destination #1 Destination
#2 Destination #3 NavController navigate(args) 1 Find Destination 2 navigate(args) 3 NavigatonProvider Navigator #3 Navigator #2 Navigator Transaction 5 navigate(args) 4 -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 8 Приоритет 1 6 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 15 16 6 4 14 15 16 15 12 10 -
Передача аргументов Подключение buildscript { ext.kotlin_version = ‘1.2.61' repositories {
google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.3.0-alpha06' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha05' } } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'androidx.navigation.safeargs' -
Передача аргументов Имплементация <fragment android:id="@+id/introFragment" android:name="com.github.xanderblinov.nav.IntroFragment" android:label="fragment_intro" tools:layout="@layout/fragment_intro"> <argument android:name="amount"
android:defaultValue="1" app:argType="integer" /> </fragment> val introFragmentArgs: IntroFragmentArgs = IntroFragmentArgs .Builder() .setAmount(3) .build() val introFragmentDirection: NavDirections = HomeFragmentDirections .actionHomeFragmentToIntroFragment() .setAmount(3) // Навигация при помощи Args navController.navigate(R.id.introFragment, introFragmentArgs.toBundle()) // Навигация при помощи Direction navController.navigate(introFragmentDirection) -
Передача аргументов Имплементация public static class Builder { private int
amount = 1; public Builder(IntroFragmentArgs original) { this.amount = original.amount; } public Builder() { } @NonNull public IntroFragmentArgs build() { IntroFragmentArgs result = new IntroFragmentArgs(); result.amount = this.amount; return result; } @NonNull public Builder setAmount(int amount) { this.amount = amount; return this; } public int getAmount() { return amount; } } } -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 8 Приоритет 1 6 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 16 6 5 15 15 14 6 4 14 15 16 15 12 10 -
Передача аргументов Shared element /** * Used with custom Transitions
to map a View from a removed or hidden * Fragment to a View from a shown or added Fragment. * <var>sharedElement</var> must have a unique transitionName in the View hierarchy. * * @param sharedElement A View in a disappearing Fragment to match with a View in an * appearing Fragment. * @param name The transitionName for a View in an appearing Fragment to match to the shared * element. * @see Fragment#setSharedElementReturnTransition(Object) * @see Fragment#setSharedElementEnterTransition(Object) */ @NonNull public abstract FragmentTransaction addSharedElement(@NonNull View sharedElement, @NonNull String name); -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 8 Приоритет 1 6 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 15 16 6 5 7 6 4 14 15 15 15 16 15 12 10 -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 8 Приоритет 1 6 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 16 6 5 7 6 4 6 2 15 15 14 15 16 15 12 10 6 3 -
Жизненный цикл public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> { @Override public
void navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions) { if (mFragmentManager.isStateSaved()) { Log.i(TAG, "Ignoring navigate() call: FragmentManager has already" + " saved its state"); return; } final Fragment frag = destination.createFragment(args); final FragmentTransaction ft = mFragmentManager.beginTransaction(); /* some code*/ } /* some code*/ } -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 8 Приоритет 1 6 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 16 6 5 7 6 4 6 2 6 3 15 15 14 15 16 15 12 10 1 -
Сравнение с Cicerone public interface Navigator { /** * Performs
transition described by the navigation command * * @param commands the navigation command array to apply * per single transaction */ void applyCommands(Command[] commands); } /** * Navigation command describes screens transition. * that can be processed by {@link ru.terrakok.cicerone.Navigator}. */ public interface Command {} -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 Приоритет 1 6 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 16 6 5 7 6 4 6 2 6 3 15 15 8 8 14 15 16 15 12 10 13 1 -
Deeplinks private fun initDeepLink(navController: NavController) { val deepLinkBuilder = navController.createDeepLink()
deepLinkBuilder.setDestination(R.id.introFragment) // для аргументов deepLinkBuilder.setArguments(Bundle()) // если задаем без NavController c NavDeepLinkBuilder(@NonNull Context context) deepLinkBuilder.setGraph(R.navigation.nav_graph) val createPendingIntent = deepLinkBuilder.createPendingIntent() Notifier.createNotificationChannel(baseContext) Notifier.notify(createPendingIntent, Notifier.HOME_DESTINATION_NOTIFICATION_ID, baseContext) } Способ задания -
Deeplinks public boolean onHandleDeepLink(@Nullable Intent intent) { /* some code*/
if (!mBackStack.isEmpty()) { navigate(mGraph.getStartDestination(), bundle, new NavOptions.Builder() .setPopUpTo(mGraph.getId(), true) .setEnterAnim(0).setExitAnim(0).build()); } int index = 0; while (index < deepLink.length) { int destinationId = deepLink[index++]; NavDestination node = findDestination(destinationId); if (node == null) { throw new IllegalStateException("unknown destination during deep link: " + NavDestination.getDisplayName(mContext, destinationId)); } node.navigate(bundle, new NavOptions.Builder().setEnterAnim(0).setExitAnim(0).build()); } /* some code*/ } Способ работы -
Deeplinks Выводы 1 Не открывает вложенную навигацию [улучшение] 2 Если
на экране по пути deepLink есть вложенный граф, то падает [баг] 3 Нет хуков для условной навигации [нереализуемо] -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 Приоритет 1 6 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 16 6 5 7 6 4 6 2 6 3 15 15 8 8 14 15 16 15 12 9 10 13 1 -
Тестирование навигации public class NavController { /**some code*/ private final
SimpleNavigatorProvider mNavigatorProvider = new SimpleNavigatorProvider() { @Nullable @Override public Navigator<? extends NavDestination> addNavigator(@NonNull String name, @NonNull Navigator<? extends NavDestination> navigator) { /**some code*/ } }; /**some code*/ } -
Требования к навигации 1 Отвязка от жизненного цикла Возможность осуществлять
вложенную навигацию Автооткрытие предыдущего экран при команде «Назад» Возможность добавить анимацию смены экранов Передача аргументов Возможность перестраивать навигацию в рантайме Поддержка Shared element Открытие и закрытие цепочки экранов 2 3 4 5 6 7 Приоритет 1 6 9 Удобный механизм работы с deeplink Возможность имплементации на Activity, Fragment, View Тестирование навигации Гибкость при изменениях Возможность навигации из бизнес-логики Возможность подменять экраны, в навигационном стеке Проверка аргументов во время компиляции Визуальное представление для проектирования 10 Приоритет 2 11 12 13 14 16 6 5 7 6 4 6 2 6 3 15 15 8 8 14 11 15 16 15 12 9 10 13 1 -
Google I/O '18 May 8, 2018 Mosdroid. Neon August 25,
2018 August 10, 2018 July 19, 2018 July 12, 2018 June 7, 2018 New Library: Navigation Navigation provides a framework for building in-app navigation. This initial release is 1.0.0-alpha01. 1.0.0-alpha02 • FragmentNavigator now uses setReorderingAllowed(true) • NavDeepLink fixes • Lifecycle fixes • Support Library 27.1.1 • Nested graph and fragment fixes • Overlapping Fragments launchSingleTop issue fixes. • NavigationUI and MenuItem fixes • Deprecated clearTask , launchDocument 1.0.0-alpha03 • A NavigationUI supports Toolbar and CollapsingToolbarLayout • Safe Args types and nullability fixes • Safe Args plugin in modules and other plugin fixes • Lifecycle behavior changes and fixes • Nested graphs fixes • Logic and memory leak fixes • NavHostFragment will always set the current Fragment as the primary navigation fragment, ensuring that child fragment managers are popped before the outer NavController is popped • Fixed conflicts with ConstraintLayout and Ko • Safe Args error message clickable • Safe Args types and nullability fixes • back after deep linking issue fix • Backstack behavior fixes • Safe Arg fixes • Add missing nullability annotations. 1.0.0-alpha04 1.0.0-alpha05 1.0.0-alpha01 Timeline изменений -
Можно ли использовать? Для чего подходит 1 Как часть навигации
(на одном из экранов) [Помним про Jetifier] 2 Проекты с минимальным числом программистов 3 Для проектов - лендингов 4 Пет-проекты -
Выводы Библиотека в разработке и не готова для продакшена Для
полной навигации нужны надстройки Решение для “Быстрого старта” Копайте исходники — это захватывающе -
О чем мы говорили? Выводы Библиотека в разработке и не
готова для продакшена Для полной навигации нужны надстройки Решение для “Быстрого старта” Копайте исходники — это захватывающе 3 % 8 % 12 % 69 % 8 % Что это? Слышал, не пробовал Смотрел в эдиторе Использую в своём петпроджекте Использую в живом проект Кто работал с новой навигацией от Google?