0 added
0 removed
Original
2026-01-01
Modified
2026-03-10
1
<p>В данном цикле статей я хотел бы показать, как может происходить создание приложений с использованием Flutter. Я использую данную технологию в работе, а также своих собственных проектах на постоянной основе. У меня есть несколько Open Source решений (популярных и не очень), которые будут применены и в данном приложении (не ради галочки, а в качестве решения возникающих проблем). В процессе работы над этим приложением я затрону почти все аспекты разработки с Flutter, за исключением явного взаимодействия с нативной частью (когда нативный код придется писать самому), но если у вас будет желание увидеть и это -- то прошу в комментарии. Ну и самое главное -- верхнеуровневая идея приложения у меня в голове уже есть, и код на эту статью и следующую уже написан, но если у вас будут возникать идеи, которые можно было бы реализовать в данном приложении в рамках закона коридора первоначальной идеи -- прошу высказывать их в комментариях.</p>
1
<p>В данном цикле статей я хотел бы показать, как может происходить создание приложений с использованием Flutter. Я использую данную технологию в работе, а также своих собственных проектах на постоянной основе. У меня есть несколько Open Source решений (популярных и не очень), которые будут применены и в данном приложении (не ради галочки, а в качестве решения возникающих проблем). В процессе работы над этим приложением я затрону почти все аспекты разработки с Flutter, за исключением явного взаимодействия с нативной частью (когда нативный код придется писать самому), но если у вас будет желание увидеть и это -- то прошу в комментарии. Ну и самое главное -- верхнеуровневая идея приложения у меня в голове уже есть, и код на эту статью и следующую уже написан, но если у вас будут возникать идеи, которые можно было бы реализовать в данном приложении в рамках закона коридора первоначальной идеи -- прошу высказывать их в комментариях.</p>
2
<h2>Идея приложения</h2>
2
<h2>Идея приложения</h2>
3
<p>Изначально я не знал, о чем будет это приложение, когда у меня появилась идея написать эти статьи. Все что я хотел -- показать весь процесс от начала и до конца, а также, чего греха таить? -- показать использование своих Open Source решений на таком, полу-реальном проекте, чтобы иметь возможность ссылаться на него, а не только на примеры в этих пакетах. Собственно, идея пришла совсем недавно -- приложение будет отображать котировки в виде списка, а также график каждой из котировок, если перейти в конкретный тикер.</p>
3
<p>Изначально я не знал, о чем будет это приложение, когда у меня появилась идея написать эти статьи. Все что я хотел -- показать весь процесс от начала и до конца, а также, чего греха таить? -- показать использование своих Open Source решений на таком, полу-реальном проекте, чтобы иметь возможность ссылаться на него, а не только на примеры в этих пакетах. Собственно, идея пришла совсем недавно -- приложение будет отображать котировки в виде списка, а также график каждой из котировок, если перейти в конкретный тикер.</p>
4
<p>К сожалению, в ходе более чем полуторамесячного, прокрастинационного и крайне непростого срока я решил отказаться от первоначальной идеи реализовывать именно биржевые котировки, так как проанализировав (и даже уже использовав в приложении) множество ресурсов пришел к выводу, что ни один из них не предоставляет все необходимую информацию в виде, пригодном для данного проекта без необходимости его переусложнения и с достаточными ограничениями в бесплатной версии. Поэтому выбор пал на криптовалютные API, благо, с ними все намного лучше.</p>
4
<p>К сожалению, в ходе более чем полуторамесячного, прокрастинационного и крайне непростого срока я решил отказаться от первоначальной идеи реализовывать именно биржевые котировки, так как проанализировав (и даже уже использовав в приложении) множество ресурсов пришел к выводу, что ни один из них не предоставляет все необходимую информацию в виде, пригодном для данного проекта без необходимости его переусложнения и с достаточными ограничениями в бесплатной версии. Поэтому выбор пал на криптовалютные API, благо, с ними все намного лучше.</p>
5
<p>Для интерфейса я нашел такой макет:</p>
5
<p>Для интерфейса я нашел такой макет:</p>
6
<p>В качестве основы я возьму из этого макета цвета, компоновку экрана и стиль графиков. Экранов, как я пока думаю, будет два.</p>
6
<p>В качестве основы я возьму из этого макета цвета, компоновку экрана и стиль графиков. Экранов, как я пока думаю, будет два.</p>
7
<p>Первый -- список позиций, которые есть на бирже, а также поиск по ним. Основой будет выступать этот фрагмент дизайна:</p>
7
<p>Первый -- список позиций, которые есть на бирже, а также поиск по ним. Основой будет выступать этот фрагмент дизайна:</p>
8
<p>Второй экран -- переход на страницу самой позиции. Он будет самым интересным -- я планирую реализовать график котировок позиции, отображение текущей цены, и небольшой игровой элемент -- две кнопки Up / Down (как в бинарных опционах, только без реальных денег, обмана и для пользы и интереса). Получится такое мини-игровое приложение, где можно будет не только смотреть котировки, но и "играть" -- введу счетчик побед и что-нибудь с этим связанное (детально этот аспект я пока не прорабатывал -- пишите идеи).</p>
8
<p>Второй экран -- переход на страницу самой позиции. Он будет самым интересным -- я планирую реализовать график котировок позиции, отображение текущей цены, и небольшой игровой элемент -- две кнопки Up / Down (как в бинарных опционах, только без реальных денег, обмана и для пользы и интереса). Получится такое мини-игровое приложение, где можно будет не только смотреть котировки, но и "играть" -- введу счетчик побед и что-нибудь с этим связанное (детально этот аспект я пока не прорабатывал -- пишите идеи).</p>
9
<h2>Реализация</h2>
9
<h2>Реализация</h2>
10
<p>Ну вот, идею описал, пора переходить к делу. Исходя из всего вышеописанного я могу описать структуру проекта примерно следующим образом:</p>
10
<p>Ну вот, идею описал, пора переходить к делу. Исходя из всего вышеописанного я могу описать структуру проекта примерно следующим образом:</p>
11
/root /service /routing /di /... /domain /main /dto /model /logic /ui /position /dto /model /logic /ui<p>Начнем мы с реализации сервисного слоя:</p>
11
/root /service /routing /di /... /domain /main /dto /model /logic /ui /position /dto /model /logic /ui<p>Начнем мы с реализации сервисного слоя:</p>
12
<h3>DI</h3>
12
<h3>DI</h3>
13
<p>Тут на помощь разработчику может прийти большое количество различных пакетов, решающих эту задачу -- с кодогенерацией и без, с большим количеством бойлерплейта и нет, но мне кажется, что это тривиальная задача, и решить её самостоятельно очень просто. Так и сделаем! Вся логика умещается в двух файлах -- сам контейнер, и логика добавления зависимостей в него:</p>
13
<p>Тут на помощь разработчику может прийти большое количество различных пакетов, решающих эту задачу -- с кодогенерацией и без, с большим количеством бойлерплейта и нет, но мне кажется, что это тривиальная задача, и решить её самостоятельно очень просто. Так и сделаем! Вся логика умещается в двух файлах -- сам контейнер, и логика добавления зависимостей в него:</p>
14
import 'package:flutter/cupertino.dart'; class Di { static final Map<String, dynamic> _dependencies = <String, dynamic>{}; static final Map<String, ValueGetter<dynamic>> _builders = <String, ValueGetter<dynamic>>{}; static String _generateDiCode<T>([String name = '']) { return '$T$name'; } static void reg<T>(ValueGetter<T> builder, {String name = '', bool asBuilder = false}) { final String code = _generateDiCode<T>(name); if (asBuilder) { _builders[code] = builder; } else { _dependencies[code] = builder(); } } static T get<T>({String name = ''}) { final String code = _generateDiCode<T>(name); late T value; if (!_dependencies.containsKey(code) && !_builders.containsKey(code)) { throw Exception('Dependency for type $T with code $code not registered'); } else if (_dependencies.containsKey(code)) { value = _dependencies[code]; } else { value = _builders[code]!(); } return value; } }<p>Как и в других решениях мы можем внедрять идентичные сущности, добавляя к ним свои префиксы, и создавать синглтоны, так и постоянно новые инстансы классов.</p>
14
import 'package:flutter/cupertino.dart'; class Di { static final Map<String, dynamic> _dependencies = <String, dynamic>{}; static final Map<String, ValueGetter<dynamic>> _builders = <String, ValueGetter<dynamic>>{}; static String _generateDiCode<T>([String name = '']) { return '$T$name'; } static void reg<T>(ValueGetter<T> builder, {String name = '', bool asBuilder = false}) { final String code = _generateDiCode<T>(name); if (asBuilder) { _builders[code] = builder; } else { _dependencies[code] = builder(); } } static T get<T>({String name = ''}) { final String code = _generateDiCode<T>(name); late T value; if (!_dependencies.containsKey(code) && !_builders.containsKey(code)) { throw Exception('Dependency for type $T with code $code not registered'); } else if (_dependencies.containsKey(code)) { value = _dependencies[code]; } else { value = _builders[code]!(); } return value; } }<p>Как и в других решениях мы можем внедрять идентичные сущности, добавляя к ним свои префиксы, и создавать синглтоны, так и постоянно новые инстансы классов.</p>
15
<p>Второй файл: добавление зависимостей в сам контейнер, чтобы ему было что создавать и возвращать:</p>
15
<p>Второй файл: добавление зависимостей в сам контейнер, чтобы ему было что создавать и возвращать:</p>
16
import 'package:flutter/cupertino.dart'; import 'package:high_low/service/di/di.dart'; import 'package:high_low/service/routing/default_router_information_parser.dart'; import 'package:high_low/service/routing/page_builder.dart'; import 'package:high_low/service/routing/root_router_delegate.dart'; void initDependencies() { Di.reg<BackButtonDispatcher>(() => RootBackButtonDispatcher()); Di.reg<RouteInformationParser<Object>>(() => DefaultRouterInformationParser()); Di.reg<RouterDelegate<Object>>(() => RootRouterDelegate()); Di.reg(() => PageBuilder()); }<p>В будущем, чтобы добавить новые зависимости будет достаточно регистрировать в этой функции их фабрики и все будет работать как надо.</p>
16
import 'package:flutter/cupertino.dart'; import 'package:high_low/service/di/di.dart'; import 'package:high_low/service/routing/default_router_information_parser.dart'; import 'package:high_low/service/routing/page_builder.dart'; import 'package:high_low/service/routing/root_router_delegate.dart'; void initDependencies() { Di.reg<BackButtonDispatcher>(() => RootBackButtonDispatcher()); Di.reg<RouteInformationParser<Object>>(() => DefaultRouterInformationParser()); Di.reg<RouterDelegate<Object>>(() => RootRouterDelegate()); Di.reg(() => PageBuilder()); }<p>В будущем, чтобы добавить новые зависимости будет достаточно регистрировать в этой функции их фабрики и все будет работать как надо.</p>
17
<h3>Routing</h3>
17
<h3>Routing</h3>
18
<p>Второй аспект, один из самых сложных в любом приложении. Я буду использовать подход Navigator 2.0.</p>
18
<p>Второй аспект, один из самых сложных в любом приложении. Я буду использовать подход Navigator 2.0.</p>
19
<p>На самом деле все не сильно сложно, и согласно этой схеме:</p>
19
<p>На самом деле все не сильно сложно, и согласно этой схеме:</p>
20
<p>нам нужно реализовать следующие классы:</p>
20
<p>нам нужно реализовать следующие классы:</p>
21
<ul><li>RouteInformationProvider</li>
21
<ul><li>RouteInformationProvider</li>
22
<li>RouteInformationParser</li>
22
<li>RouteInformationParser</li>
23
<li>RouterDelegate</li>
23
<li>RouterDelegate</li>
24
<li>Router</li>
24
<li>Router</li>
25
</ul><p>Их внедрение в контейнер DI я уже проспойлерил, давайте посмотрим, что там внутри.</p>
25
</ul><p>Их внедрение в контейнер DI я уже проспойлерил, давайте посмотрим, что там внутри.</p>
26
<h3>RouteInformationProvider</h3>
26
<h3>RouteInformationProvider</h3>
27
<p>Представляет собой провайдер дополнительной информации, которая будет добавлена к урлу, по которому осуществляется переход, и передана дальше в<strong>RouteInformationParser</strong>. В целом, это не обязательный фрагмент логики навигации в нашем случае, поэтому пока его реализация остается под вопросом.</p>
27
<p>Представляет собой провайдер дополнительной информации, которая будет добавлена к урлу, по которому осуществляется переход, и передана дальше в<strong>RouteInformationParser</strong>. В целом, это не обязательный фрагмент логики навигации в нашем случае, поэтому пока его реализация остается под вопросом.</p>
28
<h3>RouteInformationParser</h3>
28
<h3>RouteInformationParser</h3>
29
<p>Должен парсить урл, вытаскивать из него нужные параметры, и передавать их дальше -- в<strong>RouterDelegate</strong>. Вот код нашей реализации (на текущий момент):</p>
29
<p>Должен парсить урл, вытаскивать из него нужные параметры, и передавать их дальше -- в<strong>RouterDelegate</strong>. Вот код нашей реализации (на текущий момент):</p>
30
import 'package:flutter/cupertino.dart'; import 'package:high_low/service/routing/route_configuration.dart'; import 'package:high_low/service/routing/routes.dart'; class DefaultRouterInformationParser extends RouteInformationParser<RouteConfiguration> { @override Future<RouteConfiguration> parseRouteInformation(RouteInformation routeInformation) { return Future.sync(() => Routes.getRouteConfiguration(routeInformation.location ?? Routes.root())); } }<p>Также нам интересен класс RouteConfiguration, вот он:</p>
30
import 'package:flutter/cupertino.dart'; import 'package:high_low/service/routing/route_configuration.dart'; import 'package:high_low/service/routing/routes.dart'; class DefaultRouterInformationParser extends RouteInformationParser<RouteConfiguration> { @override Future<RouteConfiguration> parseRouteInformation(RouteInformation routeInformation) { return Future.sync(() => Routes.getRouteConfiguration(routeInformation.location ?? Routes.root())); } }<p>Также нам интересен класс RouteConfiguration, вот он:</p>
31
import 'package:flutter/cupertino.dart'; import 'package:high_low/service/logs/logs.dart'; import 'package:high_low/service/routing/routes.dart'; import 'package:high_low/service/types/types.dart'; import 'package:json_annotation/json_annotation.dart'; part 'route_configuration.g.dart'; @immutable @JsonSerializable() class RouteConfiguration { const RouteConfiguration({ required this.initialPath, required this.routeName, required this.routeParams, }); const RouteConfiguration.empty({ required this.initialPath, required this.routeName, }) : routeParams = const RouteParams(params: <String, String>{}, query: <String, String>{}); factory RouteConfiguration.unknown() => RouteConfiguration.empty(initialPath: Routes.unknown(), routeName: Routes.unknown()); factory RouteConfiguration.fromJson(Json json) => _$RouteConfigurationFromJson(json); final String initialPath; final String routeName; final RouteParams routeParams; Json toJson() => _$RouteConfigurationToJson(this); @override String toString() => prettyJson(toJson()); } @immutable @JsonSerializable() class RouteParams { const RouteParams({ required this.params, required this.query, }); factory RouteParams.fromJson(Json json) => _$RouteParamsFromJson(json); final Json params; final Json query; Json toJson() => _$RouteParamsToJson(this); }<p>Тут вы можете заметить появление еще одного пакета -- json_annotation, он нужен для генерации конструкторов и методов классов для сериализации в JSON и десериализации из JSON. Его необходимо устанавливать совместно еще с парочкой:</p>
31
import 'package:flutter/cupertino.dart'; import 'package:high_low/service/logs/logs.dart'; import 'package:high_low/service/routing/routes.dart'; import 'package:high_low/service/types/types.dart'; import 'package:json_annotation/json_annotation.dart'; part 'route_configuration.g.dart'; @immutable @JsonSerializable() class RouteConfiguration { const RouteConfiguration({ required this.initialPath, required this.routeName, required this.routeParams, }); const RouteConfiguration.empty({ required this.initialPath, required this.routeName, }) : routeParams = const RouteParams(params: <String, String>{}, query: <String, String>{}); factory RouteConfiguration.unknown() => RouteConfiguration.empty(initialPath: Routes.unknown(), routeName: Routes.unknown()); factory RouteConfiguration.fromJson(Json json) => _$RouteConfigurationFromJson(json); final String initialPath; final String routeName; final RouteParams routeParams; Json toJson() => _$RouteConfigurationToJson(this); @override String toString() => prettyJson(toJson()); } @immutable @JsonSerializable() class RouteParams { const RouteParams({ required this.params, required this.query, }); factory RouteParams.fromJson(Json json) => _$RouteParamsFromJson(json); final Json params; final Json query; Json toJson() => _$RouteParamsToJson(this); }<p>Тут вы можете заметить появление еще одного пакета -- json_annotation, он нужен для генерации конструкторов и методов классов для сериализации в JSON и десериализации из JSON. Его необходимо устанавливать совместно еще с парочкой:</p>
32
dependencies: json_annotation: ^4.3.0 #... dev_dependencies: build_runner: ^2.1.4 json_serializable: ^6.0.1 #...<p>Если же говорить о функциональности самого класса -- в него преобразуется любой входящий урл, и из него мы будем брать интересующие нас параметры для дальнейшей логики<strong>RouterDelegate</strong>. Например для такого входящего deep link flutter run --route="/item/AAPL?interval=day" мы получим следующий RouteConfiguration:</p>
32
dependencies: json_annotation: ^4.3.0 #... dev_dependencies: build_runner: ^2.1.4 json_serializable: ^6.0.1 #...<p>Если же говорить о функциональности самого класса -- в него преобразуется любой входящий урл, и из него мы будем брать интересующие нас параметры для дальнейшей логики<strong>RouterDelegate</strong>. Например для такого входящего deep link flutter run --route="/item/AAPL?interval=day" мы получим следующий RouteConfiguration:</p>
33
{ "initialPath": "/item/AAPL?interval=day", "routeName": "/item/:itemCode", "routeParams": { "params": { "itemCode": "AAPL" }, "query": { "interval": "day" } } }<p>Происходит это преобразование урла в конфигурацию в методе Routes.getRouteConfiguration(...):</p>
33
{ "initialPath": "/item/AAPL?interval=day", "routeName": "/item/:itemCode", "routeParams": { "params": { "itemCode": "AAPL" }, "query": { "interval": "day" } } }<p>Происходит это преобразование урла в конфигурацию в методе Routes.getRouteConfiguration(...):</p>
34
import 'package:high_low/service/routing/route_configuration.dart'; typedef RouteParamName = String; typedef RouteParamValue = String; const String itemCode = 'itemCode'; abstract class Routes { static String root() => '/'; static String item(String itemCode) => '/item/$itemCode'; static String unknown() => '/404'; static List<String> names = [ Routes.root(), Routes.item(':$itemCode'), Routes.unknown(), ]; static RouteConfiguration getRouteConfiguration(String route) { if (route == Routes.root()) { return RouteConfiguration.empty(initialPath: route, routeName: Routes.root()); } final Uri routeUri = Uri.parse(route); final List<String> routeSubPaths = routeUri.pathSegments; if (routeSubPaths.isEmpty) { return RouteConfiguration.empty(initialPath: route, routeName: Routes.unknown()); } for (final String routeName in names) { final List<String> routeNameSubPaths = routeName.split('/').where((String segment) => segment.isNotEmpty).toList(); if (routeNameSubPaths.length != routeSubPaths.length) { continue; } bool isTargetName = true; final Map<RouteParamName, RouteParamValue> params = {}; for (int i = 0; i < routeSubPaths.length; i++) { final String routeSubPath = routeSubPaths[i]; final String routeNameSubPath = routeNameSubPaths[i]; final bool isDynamicSubPath = routeNameSubPath.contains(':'); if (routeSubPath != routeNameSubPath && !isDynamicSubPath) { isTargetName = false; break; } else if (isDynamicSubPath) { params[routeNameSubPath.replaceFirst(':', '')] = routeSubPath; } } if (isTargetName) { return RouteConfiguration(initialPath: route, routeName: routeName, routeParams: RouteParams(params: params, query: routeUri.queryParameters)); } } return RouteConfiguration.empty(initialPath: route, routeName: Routes.unknown()); } }<p>Эту логику можно расширить. Например -- сейчас этот код не обработает query-параметры массивы, вроде<em>/item/AAPL?interval=month,day</em>, а на другом способе указания параметров массивов:<em>/item/AAPL?interval=month&interval=day</em>-- Flutter вообще не запускается со следующей ошибкой:</p>
34
import 'package:high_low/service/routing/route_configuration.dart'; typedef RouteParamName = String; typedef RouteParamValue = String; const String itemCode = 'itemCode'; abstract class Routes { static String root() => '/'; static String item(String itemCode) => '/item/$itemCode'; static String unknown() => '/404'; static List<String> names = [ Routes.root(), Routes.item(':$itemCode'), Routes.unknown(), ]; static RouteConfiguration getRouteConfiguration(String route) { if (route == Routes.root()) { return RouteConfiguration.empty(initialPath: route, routeName: Routes.root()); } final Uri routeUri = Uri.parse(route); final List<String> routeSubPaths = routeUri.pathSegments; if (routeSubPaths.isEmpty) { return RouteConfiguration.empty(initialPath: route, routeName: Routes.unknown()); } for (final String routeName in names) { final List<String> routeNameSubPaths = routeName.split('/').where((String segment) => segment.isNotEmpty).toList(); if (routeNameSubPaths.length != routeSubPaths.length) { continue; } bool isTargetName = true; final Map<RouteParamName, RouteParamValue> params = {}; for (int i = 0; i < routeSubPaths.length; i++) { final String routeSubPath = routeSubPaths[i]; final String routeNameSubPath = routeNameSubPaths[i]; final bool isDynamicSubPath = routeNameSubPath.contains(':'); if (routeSubPath != routeNameSubPath && !isDynamicSubPath) { isTargetName = false; break; } else if (isDynamicSubPath) { params[routeNameSubPath.replaceFirst(':', '')] = routeSubPath; } } if (isTargetName) { return RouteConfiguration(initialPath: route, routeName: routeName, routeParams: RouteParams(params: params, query: routeUri.queryParameters)); } } return RouteConfiguration.empty(initialPath: route, routeName: Routes.unknown()); } }<p>Эту логику можно расширить. Например -- сейчас этот код не обработает query-параметры массивы, вроде<em>/item/AAPL?interval=month,day</em>, а на другом способе указания параметров массивов:<em>/item/AAPL?interval=month&interval=day</em>-- Flutter вообще не запускается со следующей ошибкой:</p>
35
ProcessException: Process exited abnormally: Starting: Intent { act=android.intent.action.RUN flg=0x20000000 (has extras) } /system/bin/sh: --ez: inaccessible or not found Error: Activity not started, unable to resolve Intent { act=android.intent.action.RUN flg=0x30000000 (has extras) } Command: C:\\Users\\Mikle\\AppData\\Local\\Android\\sdk\\platform-tools\\adb.exe -s emulator-5554 shell am start -a android.intent.action.RUN -f 0x20000000 --ez enable-background-compilation true --ez enable-dart-profiling true --es route /item/AAPL?interval=month&interval=day --ez enable-checked-mode true --ez verify-entry-points true --ez start-paused true com.alphamikle.high_low/com.alphamikle.high_low.MainActivity<p>В общем -- брать за основу этот код можно смело, но под специфичные урлы своего проекта еще нужно будет дорабатывать.</p>
35
ProcessException: Process exited abnormally: Starting: Intent { act=android.intent.action.RUN flg=0x20000000 (has extras) } /system/bin/sh: --ez: inaccessible or not found Error: Activity not started, unable to resolve Intent { act=android.intent.action.RUN flg=0x30000000 (has extras) } Command: C:\\Users\\Mikle\\AppData\\Local\\Android\\sdk\\platform-tools\\adb.exe -s emulator-5554 shell am start -a android.intent.action.RUN -f 0x20000000 --ez enable-background-compilation true --ez enable-dart-profiling true --es route /item/AAPL?interval=month&interval=day --ez enable-checked-mode true --ez verify-entry-points true --ez start-paused true com.alphamikle.high_low/com.alphamikle.high_low.MainActivity<p>В общем -- брать за основу этот код можно смело, но под специфичные урлы своего проекта еще нужно будет дорабатывать.</p>
36
<h3>RouterDelegate</h3>
36
<h3>RouterDelegate</h3>
37
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:high_low/domain/main/ui/main_view.dart'; import 'package:high_low/service/di/di.dart'; import 'package:high_low/service/logs/logs.dart'; import 'package:high_low/service/routing/page_builder.dart'; import 'package:high_low/service/routing/route_configuration.dart'; import 'package:high_low/service/routing/routes.dart'; class RootRouterDelegate extends RouterDelegate<RouteConfiguration> with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteConfiguration> { RootRouterDelegate() : navigatorKey = GlobalKey(); @override final GlobalKey<NavigatorState> navigatorKey; PageBuilder get pageBuilder => Di.get(); final List<Page> pages = []; @override RouteConfiguration currentConfiguration = RouteConfiguration.empty(initialPath: Routes.root(), routeName: Routes.root()); bool onPopRoute(Route<dynamic> route, dynamic data) { if (route.didPop(data) == false) { return false; } pages.removeLast(); notifyListeners(); return true; } Future<void> mapRouteConfigurationToRouterState(RouteConfiguration configuration) async { final String name = configuration.routeName; pages.clear(); if (name == Routes.unknown()) { // openUnknownView(); Logs.warn('TODO: Open Unknown View'); } } @override Future<void> setNewRoutePath(RouteConfiguration configuration) async { Logs.debug('setNewRoutePath: $configuration'); currentConfiguration = configuration; await mapRouteConfigurationToRouterState(configuration); notifyListeners(); } @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, pages: [ pageBuilder.buildUnAnimatedPage(const MainView(), name: Routes.root()), ...pages, ], onPopPage: onPopRoute, ); } }<p>Это только основа делегата, но из интересного тут -- метод mapRouteConfigurationToRouterState, который вызывается из метода setNewRoutePath -- который, в свою очередь, и обрабатывает конфигурации роутинга, поступающие сюда из<strong>RouteInformationParser</strong>. В будущем мы будем писать здесь методы навигации.</p>
37
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:high_low/domain/main/ui/main_view.dart'; import 'package:high_low/service/di/di.dart'; import 'package:high_low/service/logs/logs.dart'; import 'package:high_low/service/routing/page_builder.dart'; import 'package:high_low/service/routing/route_configuration.dart'; import 'package:high_low/service/routing/routes.dart'; class RootRouterDelegate extends RouterDelegate<RouteConfiguration> with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteConfiguration> { RootRouterDelegate() : navigatorKey = GlobalKey(); @override final GlobalKey<NavigatorState> navigatorKey; PageBuilder get pageBuilder => Di.get(); final List<Page> pages = []; @override RouteConfiguration currentConfiguration = RouteConfiguration.empty(initialPath: Routes.root(), routeName: Routes.root()); bool onPopRoute(Route<dynamic> route, dynamic data) { if (route.didPop(data) == false) { return false; } pages.removeLast(); notifyListeners(); return true; } Future<void> mapRouteConfigurationToRouterState(RouteConfiguration configuration) async { final String name = configuration.routeName; pages.clear(); if (name == Routes.unknown()) { // openUnknownView(); Logs.warn('TODO: Open Unknown View'); } } @override Future<void> setNewRoutePath(RouteConfiguration configuration) async { Logs.debug('setNewRoutePath: $configuration'); currentConfiguration = configuration; await mapRouteConfigurationToRouterState(configuration); notifyListeners(); } @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, pages: [ pageBuilder.buildUnAnimatedPage(const MainView(), name: Routes.root()), ...pages, ], onPopPage: onPopRoute, ); } }<p>Это только основа делегата, но из интересного тут -- метод mapRouteConfigurationToRouterState, который вызывается из метода setNewRoutePath -- который, в свою очередь, и обрабатывает конфигурации роутинга, поступающие сюда из<strong>RouteInformationParser</strong>. В будущем мы будем писать здесь методы навигации.</p>
38
<h3>Logging</h3>
38
<h3>Logging</h3>
39
<p>Последний пункт -- логирование. Тут все совсем просто -- я сделал небольшую обертку поверх библиотеки logging, которая, как по мне -- дает одни из лучших возможностей по логированию. Теперь мы можем передавать любые аргументы в методы логирования.</p>
39
<p>Последний пункт -- логирование. Тут все совсем просто -- я сделал небольшую обертку поверх библиотеки logging, которая, как по мне -- дает одни из лучших возможностей по логированию. Теперь мы можем передавать любые аргументы в методы логирования.</p>
40
import 'dart:convert'; import 'package:logger/logger.dart' as logger; String _getJoinedArguments(dynamic p1, [dynamic p2, dynamic p3]) { String result = p1.toString(); result += p2 == null ? '' : ' ${p2.toString()}'; result += p3 == null ? '' : ' ${p3.toString()}'; return result; } String prettyJson(Map<String, dynamic> json) { const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' '); return jsonEncoder.convert(json); } final _logger = logger.Logger( printer: logger.PrefixPrinter( logger.PrettyPrinter( colors: true, printEmojis: false, methodCount: 0, errorMethodCount: 3, stackTraceBeginIndex: 0, ), ), ); abstract class Logs { static void debug(dynamic p1, [dynamic p2, dynamic p3]) { _logger.d(_getJoinedArguments(p1, p2, p3)); } static void info(dynamic p1, [dynamic p2, dynamic p3]) { _logger.i(_getJoinedArguments(p1, p2, p3)); } static void warn(dynamic p1, [dynamic p2, dynamic p3]) { _logger.w(_getJoinedArguments(p1, p2, p3)); } static void error(dynamic p1, [dynamic p2, dynamic p3]) { _logger.e(_getJoinedArguments(p1, p2, p3)); } static void fatal(dynamic p1, [dynamic p2, dynamic p3]) { _logger.wtf(_getJoinedArguments(p1, p2, p3)); } static void trace(dynamic p1, [dynamic p2, dynamic p3]) { _logger.v(_getJoinedArguments(p1, p2, p3)); } static void pad(dynamic p1, [dynamic p2, dynamic p3]) { print(_getJoinedArguments(p1, p2, p3)); } }<h3>Другое</h3>
40
import 'dart:convert'; import 'package:logger/logger.dart' as logger; String _getJoinedArguments(dynamic p1, [dynamic p2, dynamic p3]) { String result = p1.toString(); result += p2 == null ? '' : ' ${p2.toString()}'; result += p3 == null ? '' : ' ${p3.toString()}'; return result; } String prettyJson(Map<String, dynamic> json) { const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' '); return jsonEncoder.convert(json); } final _logger = logger.Logger( printer: logger.PrefixPrinter( logger.PrettyPrinter( colors: true, printEmojis: false, methodCount: 0, errorMethodCount: 3, stackTraceBeginIndex: 0, ), ), ); abstract class Logs { static void debug(dynamic p1, [dynamic p2, dynamic p3]) { _logger.d(_getJoinedArguments(p1, p2, p3)); } static void info(dynamic p1, [dynamic p2, dynamic p3]) { _logger.i(_getJoinedArguments(p1, p2, p3)); } static void warn(dynamic p1, [dynamic p2, dynamic p3]) { _logger.w(_getJoinedArguments(p1, p2, p3)); } static void error(dynamic p1, [dynamic p2, dynamic p3]) { _logger.e(_getJoinedArguments(p1, p2, p3)); } static void fatal(dynamic p1, [dynamic p2, dynamic p3]) { _logger.wtf(_getJoinedArguments(p1, p2, p3)); } static void trace(dynamic p1, [dynamic p2, dynamic p3]) { _logger.v(_getJoinedArguments(p1, p2, p3)); } static void pad(dynamic p1, [dynamic p2, dynamic p3]) { print(_getJoinedArguments(p1, p2, p3)); } }<h3>Другое</h3>
41
<p>Еще вы могли заметить тип Json -- это алиас, располагаемый в файле types.dart. В этот файл мы будем писать и другие алиасы, которые будут использоваться в приложении:</p>
41
<p>Еще вы могли заметить тип Json -- это алиас, располагаемый в файле types.dart. В этот файл мы будем писать и другие алиасы, которые будут использоваться в приложении:</p>
42
typedef Json = Map<String, dynamic>;<p>Для использования алиасов не только для функций необходимо повысить минимальную версию Dart в<em>pubspec.yaml до >= 2.14.0</em>.</p>
42
typedef Json = Map<String, dynamic>;<p>Для использования алиасов не только для функций необходимо повысить минимальную версию Dart в<em>pubspec.yaml до >= 2.14.0</em>.</p>
43
<h3>Заключение</h3>
43
<h3>Заключение</h3>
44
<p>На текущий момент реализована самая базовая логика, которая нужна для дальнейшей разработки бизнес-функционала. Исходный код текущей части можно посмотреть<a>здесь</a>.</p>
44
<p>На текущий момент реализована самая базовая логика, которая нужна для дальнейшей разработки бизнес-функционала. Исходный код текущей части можно посмотреть<a>здесь</a>.</p>
45
<p>Продолжение<a>здесь</a>.</p>
45
<p>Продолжение<a>здесь</a>.</p>
46
<p>Больше материалов смотрите<a>в моем блоге</a>на Хабре.</p>
46
<p>Больше материалов смотрите<a>в моем блоге</a>на Хабре.</p>
47
47