HTML Diff
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&lt;String, dynamic&gt; _dependencies = &lt;String, dynamic&gt;{}; static final Map&lt;String, ValueGetter&lt;dynamic&gt;&gt; _builders = &lt;String, ValueGetter&lt;dynamic&gt;&gt;{}; static String _generateDiCode&lt;T&gt;([String name = '']) { return '$T$name'; } static void reg&lt;T&gt;(ValueGetter&lt;T&gt; builder, {String name = '', bool asBuilder = false}) { final String code = _generateDiCode&lt;T&gt;(name); if (asBuilder) { _builders[code] = builder; } else { _dependencies[code] = builder(); } } static T get&lt;T&gt;({String name = ''}) { final String code = _generateDiCode&lt;T&gt;(name); late T value; if (!_dependencies.containsKey(code) &amp;&amp; !_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&lt;String, dynamic&gt; _dependencies = &lt;String, dynamic&gt;{}; static final Map&lt;String, ValueGetter&lt;dynamic&gt;&gt; _builders = &lt;String, ValueGetter&lt;dynamic&gt;&gt;{}; static String _generateDiCode&lt;T&gt;([String name = '']) { return '$T$name'; } static void reg&lt;T&gt;(ValueGetter&lt;T&gt; builder, {String name = '', bool asBuilder = false}) { final String code = _generateDiCode&lt;T&gt;(name); if (asBuilder) { _builders[code] = builder; } else { _dependencies[code] = builder(); } } static T get&lt;T&gt;({String name = ''}) { final String code = _generateDiCode&lt;T&gt;(name); late T value; if (!_dependencies.containsKey(code) &amp;&amp; !_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&lt;BackButtonDispatcher&gt;(() =&gt; RootBackButtonDispatcher()); Di.reg&lt;RouteInformationParser&lt;Object&gt;&gt;(() =&gt; DefaultRouterInformationParser()); Di.reg&lt;RouterDelegate&lt;Object&gt;&gt;(() =&gt; RootRouterDelegate()); Di.reg(() =&gt; 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&lt;BackButtonDispatcher&gt;(() =&gt; RootBackButtonDispatcher()); Di.reg&lt;RouteInformationParser&lt;Object&gt;&gt;(() =&gt; DefaultRouterInformationParser()); Di.reg&lt;RouterDelegate&lt;Object&gt;&gt;(() =&gt; RootRouterDelegate()); Di.reg(() =&gt; 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&lt;RouteConfiguration&gt; { @override Future&lt;RouteConfiguration&gt; parseRouteInformation(RouteInformation routeInformation) { return Future.sync(() =&gt; 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&lt;RouteConfiguration&gt; { @override Future&lt;RouteConfiguration&gt; parseRouteInformation(RouteInformation routeInformation) { return Future.sync(() =&gt; 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: &lt;String, String&gt;{}, query: &lt;String, String&gt;{}); factory RouteConfiguration.unknown() =&gt; RouteConfiguration.empty(initialPath: Routes.unknown(), routeName: Routes.unknown()); factory RouteConfiguration.fromJson(Json json) =&gt; _$RouteConfigurationFromJson(json); final String initialPath; final String routeName; final RouteParams routeParams; Json toJson() =&gt; _$RouteConfigurationToJson(this); @override String toString() =&gt; prettyJson(toJson()); } @immutable @JsonSerializable() class RouteParams { const RouteParams({ required this.params, required this.query, }); factory RouteParams.fromJson(Json json) =&gt; _$RouteParamsFromJson(json); final Json params; final Json query; Json toJson() =&gt; _$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: &lt;String, String&gt;{}, query: &lt;String, String&gt;{}); factory RouteConfiguration.unknown() =&gt; RouteConfiguration.empty(initialPath: Routes.unknown(), routeName: Routes.unknown()); factory RouteConfiguration.fromJson(Json json) =&gt; _$RouteConfigurationFromJson(json); final String initialPath; final String routeName; final RouteParams routeParams; Json toJson() =&gt; _$RouteConfigurationToJson(this); @override String toString() =&gt; prettyJson(toJson()); } @immutable @JsonSerializable() class RouteParams { const RouteParams({ required this.params, required this.query, }); factory RouteParams.fromJson(Json json) =&gt; _$RouteParamsFromJson(json); final Json params; final Json query; Json toJson() =&gt; _$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() =&gt; '/'; static String item(String itemCode) =&gt; '/item/$itemCode'; static String unknown() =&gt; '/404'; static List&lt;String&gt; 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&lt;String&gt; routeSubPaths = routeUri.pathSegments; if (routeSubPaths.isEmpty) { return RouteConfiguration.empty(initialPath: route, routeName: Routes.unknown()); } for (final String routeName in names) { final List&lt;String&gt; routeNameSubPaths = routeName.split('/').where((String segment) =&gt; segment.isNotEmpty).toList(); if (routeNameSubPaths.length != routeSubPaths.length) { continue; } bool isTargetName = true; final Map&lt;RouteParamName, RouteParamValue&gt; params = {}; for (int i = 0; i &lt; routeSubPaths.length; i++) { final String routeSubPath = routeSubPaths[i]; final String routeNameSubPath = routeNameSubPaths[i]; final bool isDynamicSubPath = routeNameSubPath.contains(':'); if (routeSubPath != routeNameSubPath &amp;&amp; !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&amp;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() =&gt; '/'; static String item(String itemCode) =&gt; '/item/$itemCode'; static String unknown() =&gt; '/404'; static List&lt;String&gt; 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&lt;String&gt; routeSubPaths = routeUri.pathSegments; if (routeSubPaths.isEmpty) { return RouteConfiguration.empty(initialPath: route, routeName: Routes.unknown()); } for (final String routeName in names) { final List&lt;String&gt; routeNameSubPaths = routeName.split('/').where((String segment) =&gt; segment.isNotEmpty).toList(); if (routeNameSubPaths.length != routeSubPaths.length) { continue; } bool isTargetName = true; final Map&lt;RouteParamName, RouteParamValue&gt; params = {}; for (int i = 0; i &lt; routeSubPaths.length; i++) { final String routeSubPath = routeSubPaths[i]; final String routeNameSubPath = routeNameSubPaths[i]; final bool isDynamicSubPath = routeNameSubPath.contains(':'); if (routeSubPath != routeNameSubPath &amp;&amp; !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&amp;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&amp;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&amp;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&lt;RouteConfiguration&gt; with ChangeNotifier, PopNavigatorRouterDelegateMixin&lt;RouteConfiguration&gt; { RootRouterDelegate() : navigatorKey = GlobalKey(); @override final GlobalKey&lt;NavigatorState&gt; navigatorKey; PageBuilder get pageBuilder =&gt; Di.get(); final List&lt;Page&gt; pages = []; @override RouteConfiguration currentConfiguration = RouteConfiguration.empty(initialPath: Routes.root(), routeName: Routes.root()); bool onPopRoute(Route&lt;dynamic&gt; route, dynamic data) { if (route.didPop(data) == false) { return false; } pages.removeLast(); notifyListeners(); return true; } Future&lt;void&gt; mapRouteConfigurationToRouterState(RouteConfiguration configuration) async { final String name = configuration.routeName; pages.clear(); if (name == Routes.unknown()) { // openUnknownView(); Logs.warn('TODO: Open Unknown View'); } } @override Future&lt;void&gt; 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&lt;RouteConfiguration&gt; with ChangeNotifier, PopNavigatorRouterDelegateMixin&lt;RouteConfiguration&gt; { RootRouterDelegate() : navigatorKey = GlobalKey(); @override final GlobalKey&lt;NavigatorState&gt; navigatorKey; PageBuilder get pageBuilder =&gt; Di.get(); final List&lt;Page&gt; pages = []; @override RouteConfiguration currentConfiguration = RouteConfiguration.empty(initialPath: Routes.root(), routeName: Routes.root()); bool onPopRoute(Route&lt;dynamic&gt; route, dynamic data) { if (route.didPop(data) == false) { return false; } pages.removeLast(); notifyListeners(); return true; } Future&lt;void&gt; mapRouteConfigurationToRouterState(RouteConfiguration configuration) async { final String name = configuration.routeName; pages.clear(); if (name == Routes.unknown()) { // openUnknownView(); Logs.warn('TODO: Open Unknown View'); } } @override Future&lt;void&gt; 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&lt;String, dynamic&gt; 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&lt;String, dynamic&gt; 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&lt;String, dynamic&gt;;<p>Для использования алиасов не только для функций необходимо повысить минимальную версию Dart в<em>pubspec.yaml до &gt;= 2.14.0</em>.</p>
42 typedef Json = Map&lt;String, dynamic&gt;;<p>Для использования алиасов не только для функций необходимо повысить минимальную версию Dart в<em>pubspec.yaml до &gt;= 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