Navigation
Navigation is quite a big concern in Flutter development, we guess. Some third-party packages are made to handle navigation. We have worked with Router, Go Router, GetX.
We don't recommend Go Router and GetX for navigation.
GetX just provides a wrapper of Flutter's navigation APIs and claims that the
BuildContext can be ignored. In our view, BuildContext should not be ignored
as it determines the position of the widget that needs to access a desired
Navigator's instances.
Regarding Go Router, we had a bad experience with it. First, its
implementation is unnecessarily complicated. The configuration is very similar
to Navigator.onGenerateRoute, and with even more wrapping layer concepts:
GoRoute, ShellRoute, etc. Second, the idea of integrating the bottom
navigation into the navigation is irrelevant. The navigation provides the push
and pop mechanism. In contrast, there should be no screens or widgets overlaid on
others in bottom navigation. The bottom navigation should be treated as
synchronous states of PageView and BottomNavigator, just as demonstrated in
Bottom Navigation.
We do love Router. Many developers don't and we understand why. It looks complex, overwhelming, and maybe hard to comprehend the idea behind it, so if developers do not use it correctly, it can be a problem. But if Router is implemented properly, it is one of the best navigation handlers, in our opinion. In fact, it is the inspiration for us to create this pattern.
Despite that, we believe that navigation seems to become something "too big"
to worry about, compared to its scope. After working with navigation, we
realized that it is just like a Stack literally. Deep down in the structure
of Navigator, it is a Stack. Then we should treat it as functional as a
Stack, nothing more, nothing less. We suggest the Flutter's
Navigator is very sufficient to handle it. Of course, the implementation shall
be done in different ways.
First, let's code it down.
-
assets/data/binancecoin.json
-
assets/data/bitcoin.json
-
assets/data/cardano.json
-
assets/data/dogecoin.json
-
assets/data/ethereum.json
-
assets/data/solana.json
-
assets/data/staked-ether.json
-
assets/data/tether.json
-
assets/data/usd-coin.json
-
assets/data/xrp.json
-
lib/src/model/coin_details.dart
-
lib/src/unit/network_dao/models/coin_details.dart(Be cautious, this is different from the one above despite having the same name)
-
lib/src/widget/coin_details_page.dart
Copy the following contents to the corresponding files:
part of '../network_dao.dart';
abstract class NetworkDaoParam<T> extends Model {
const NetworkDaoParam();
}
class NetworkDaoCoinsListParam extends NetworkDaoParam<String> {
String get key => '$page&$category&$currency&$timeFrame';
final int page;
final String category;
final String currency;
final String timeFrame;
const NetworkDaoCoinsListParam({
required this.page,
required this.category,
required this.currency,
required this.timeFrame,
});
}
class NetworkDaoCoinDetailsParam extends NetworkDaoParam<String> {
String get key => coinId;
final String coinId;
const NetworkDaoCoinDetailsParam({required this.coinId});
}
part of 'network_dao.dart';
class NetworkDaoPaper extends Paper {}
class NetworkDaoDataFetching extends NetworkDaoPaper {
final NetworkDaoParam param;
NetworkDaoDataFetching(this.param);
}
class NetworkDaoCoinsListSelection extends NetworkDaoPaper {
final Result<JsonCoinsList, NetworkDaoExceptionCode> result;
NetworkDaoCoinsListSelection(this.result);
}
class NetworkDaoCoinDetailsSelection extends NetworkDaoPaper {
final Result<JsonCoinDetails, NetworkDaoExceptionCode> result;
NetworkDaoCoinDetailsSelection(this.result);
}
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:paper/paper.dart';
part 'model/coin_details.dart';
part 'model/coins_list.dart';
part 'model/exception.dart';
part 'model/param.dart';
part 'network_dao_paper.dart';
part 'network_dao_script.dart';
class NetworkDao extends Unit<NetworkDaoPaper> {
NetworkDao(super.agent, {required super.listener});
Script<NetworkDaoPaper, NetworkDaoState> createScript() => NetworkDaoScript();
UnitState<NetworkDaoPaper, Unit<NetworkDaoPaper>> createState() =>
NetworkDaoState();
}
class NetworkDaoState extends UnitState<NetworkDaoPaper, NetworkDao> {
late final reportMap = <Type, FutureOr<void> Function(NetworkDaoParam)>{
NetworkDaoCoinsListParam: fetchCoinsList,
NetworkDaoCoinDetailsParam: fetchCoinDetails,
};
Timer? coinPricesTimer;
Future<void> fetchCoinsList(NetworkDaoParam param) async {
if (param is! NetworkDaoCoinsListParam) return;
try {
await Future.delayed(const Duration(seconds: 2));
final response = await rootBundle
.loadString(
'assets/data/coins_list.json',
)
.timeout(
const Duration(seconds: 2),
onTimeout: () => throw NetworkDaoExceptionCode.timeout,
);
final json = jsonDecode(response);
if (json is! List<dynamic>) {
throw NetworkDaoExceptionCode.wrongFormat;
}
final coinsList = JsonCoinsList(json, key: param.key);
report(
NetworkDaoCoinsListSelection(
Result.success(coinsList),
),
);
} on NetworkDaoExceptionCode catch (e) {
report(
NetworkDaoCoinsListSelection(
Result.failure(e),
),
);
} catch (e) {
report(
NetworkDaoCoinsListSelection(
Result.failure(NetworkDaoExceptionCode.resourcesNotFound),
),
);
}
}
Future<void> fetchCoinDetails(NetworkDaoParam param) async {
if (param is! NetworkDaoCoinDetailsParam) return;
try {
await Future.delayed(const Duration(seconds: 2));
final response = await rootBundle
.loadString(
'assets/data/${param.coinId}.json',
)
.timeout(
const Duration(seconds: 2),
onTimeout: () => throw NetworkDaoExceptionCode.timeout,
);
final json = jsonDecode(response);
if (json is! Map<String, dynamic>) {
throw NetworkDaoExceptionCode.wrongFormat;
}
final coinDetails = JsonCoinDetails(json);
report(
NetworkDaoCoinDetailsSelection(
Result.success(coinDetails),
),
);
} on NetworkDaoExceptionCode catch (e) {
report(
NetworkDaoCoinDetailsSelection(
Result.failure(e),
),
);
} catch (e) {
report(
NetworkDaoCoinDetailsSelection(
Result.failure(NetworkDaoExceptionCode.resourcesNotFound),
),
);
}
}
}
part of 'app.dart';
abstract class AppPaper extends Paper {}
class AppInitialize extends AppPaper {}
class AppCurrentTab extends AppPaper {
final int index;
AppCurrentTab(this.index);
}
class AppCurrentMarket extends AppPaper {
final Result<JsonCoinsList, NetworkDaoExceptionCode> coinsResult;
AppCurrentMarket(this.coinsResult);
}
class AppCoinDetailsSelection extends AppPaper {
final String key;
AppCoinDetailsSelection(this.key);
}
class AppCurrentCoinDetails extends AppPaper {
final Result<JsonCoinDetails, NetworkDaoExceptionCode> detailsResult;
AppCurrentCoinDetails(this.detailsResult);
}
class AppTopWidgetPopping extends AppPaper {}
part of 'app.dart';
class AppScript extends Script<AppPaper, AppState> {
void map() => on<AppCurrentTab>(onCurrentTab)
?.on<AppInitialize>(onInitialize)
?.on<AppCurrentMarket>(onCurrentMarket)
?.on<AppCoinDetailsSelection>(onCoinDetailsSelection)
?.on<AppCurrentCoinDetails>(onCurrentCoinDetails)
?.on<AppTopWidgetPopping>(onTopWidgetPopping);
void onInitialize(
AppInitialize p,
AppState s,
SourceVerifier ifFrom,
) async {
final page = 1;
final category = s.criteria.selectedCategory;
final currency = s.criteria.selectedCurrency;
final timeFrame = s.criteria.selectedTimeFrame;
s.networkDao.process(
NetworkDaoDataFetching(
NetworkDaoCoinsListParam(
page: page,
category: category,
currency: currency,
timeFrame: timeFrame,
),
),
);
}
void onCurrentTab(
AppCurrentTab p,
AppState s,
SourceVerifier ifFrom,
) async {
ifFrom(s.bottomBarController)?.setIndex = p.index;
s.setPage(ifFrom(s.pageController), p.index);
}
void onCurrentMarket(
AppCurrentMarket p,
AppState s,
SourceVerifier ifFrom,
) {
final result = p.coinsResult;
if (result is Success<JsonCoinsList>) {
final coins = <CoinOverview>[];
for (var i = 0; i < result.data.length; i++) {
final current = result.data.item(i);
final id = current?.id();
final name = current?.name();
final symbol = current?.symbol();
final currentPrice = current?.currentPrice()?.toDouble();
final priceChange24hPercentage =
current?.priceChange24hPercentage()?.toDouble();
final currentCurrency = s.criteria.selectedCurrency;
final currencySign = CurrencySymbolMapper().symbol(currentCurrency);
if (id == null ||
name == null ||
symbol == null ||
currentPrice == null ||
priceChange24hPercentage == null ||
currencySign == null) {
continue;
}
coins.add(
CoinOverview(
key: id,
name: name,
symbol: symbol,
currentPrice: Figure(currentPrice),
changePercentage: Figure(priceChange24hPercentage),
currencySign: currencySign,
),
);
}
s.coinsListing.process(
CoinsListingCurrentCoins(
ListModel(coins, keyGen: (_) => result.data.key),
),
);
} else if (result is Failure<NetworkDaoExceptionCode>) {
final String text;
switch (result.exp) {
case NetworkDaoExceptionCode.timeout:
text = s.l10n.s_TimeoutErrorFetchingAlert;
break;
case NetworkDaoExceptionCode.wrongFormat:
case NetworkDaoExceptionCode.resourcesNotFound:
text = s.l10n.s_GeneralErrorFetchingAlert;
break;
}
s.coinsListing.process(CoinsListingErrorText(text));
}
}
void onCoinDetailsSelection(
AppCoinDetailsSelection p,
AppState s,
SourceVerifier ifFrom,
) async {
s.showLoading();
s.networkDao.process(
NetworkDaoDataFetching(NetworkDaoCoinDetailsParam(coinId: p.key)),
);
}
void onCurrentCoinDetails(
AppCurrentCoinDetails p,
AppState s,
SourceVerifier ifFrom,
) {
final result = p.detailsResult;
if (result is Success<JsonCoinDetails>) {
final data = result.data;
final id = data.id();
final name = data.name();
final symbol = data.symbol();
final marketCapRank = data.marketCapRank();
final marketCap = data.marketCap();
final totalVolume = data.totalVolume();
final high24h = data.high24h();
final low24h = data.low24h();
final totalSupply = data.totalSupply();
final allTimeHigh = data.allTimeHigh();
final athChangePercentage = data.athChangePercentage();
final allTimeLow = data.allTimeLow();
final atlChangePercentage = data.atlChangePercentage();
if (id == null ||
name == null ||
symbol == null ||
marketCapRank == null ||
marketCap == null ||
totalVolume == null ||
high24h == null ||
low24h == null ||
totalSupply == null ||
allTimeHigh == null ||
athChangePercentage == null ||
allTimeLow == null ||
atlChangePercentage == null) {
throw Exception();
}
final details = CoinDetails(
key: id,
name: name,
marketCapRank: '${numUSFormat.format(marketCapRank)} \$',
marketCap: '${numUSFormat.format(marketCap)} \$',
tradingVolume: '${numUSFormat.format(totalVolume)} \$',
low24h: '${numUSFormat.format(low24h)} \$',
high24h: '${numUSFormat.format(high24h)} \$',
totalSupply: numUSFormat.format(totalSupply),
allTimeHigh: '${numUSFormat.format(allTimeHigh)} \$',
sinceAllTimeHigh: '${numUSFormat.format(athChangePercentage)} %',
allTimeLow: '${numUSFormat.format(allTimeHigh)} \$',
sinceAllTimeLow: '${numUSFormat.format(atlChangePercentage)} %',
);
s.navigator.currentState?.pop();
s.showCoinDetailsPage(details: details);
} else if (result is Failure<NetworkDaoExceptionCode>) {
s.navigator.currentState?.pop();
s.showErrorDialog(s.l10n.s_CoinDetailsNotFoundAlert);
}
}
void onTopWidgetPopping(
AppTopWidgetPopping p,
AppState s,
SourceVerifier ifFrom,
) async {
s.navigator.currentState?.pop();
}
}
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:paper/paper.dart';
import '../../../l10n/app_localizations.dart';
import '../../data/i18n.dart';
import '../../model/coin_details.dart';
import '../../model/coin_overview.dart';
import '../../model/currency_symbol_mapper.dart';
import '../../model/decor.dart';
import '../../model/figure.dart';
import '../../unit/network_dao/network_dao.dart';
import '../../unit_passive/criteria_unit.dart';
import '../../widget/bottom_bar.dart';
import '../../widget/coin_details_page.dart';
import '../../widget/keep_alive_page.dart';
import '../coins_listing/coins_listing.dart';
part 'app_paper.dart';
part 'app_script.dart';
class App extends UnitWidget<AppPaper> {
// ignore: use_key_in_widget_constructors
App(
super.parent, {
required super.agent,
super.listener,
});
UnitWidgetState<AppPaper, App> createState() => AppState();
Script<AppPaper, AppState> createScript() => AppScript();
}
class AppState extends UnitWidgetState<AppPaper, App> {
final networkDao = UnitAgent<NetworkDaoPaper>();
final coinsListing = WidgetAgent<CoinsListingPaper>();
final bottomBarController = BottomBarController();
final pageController = PageController();
final navigator = GlobalKey<NavigatorState>();
final navigatorStack = CurrentNavigationObserver();
late CriteriaUnit criteria;
late EdgeInsets safeAreaPadding;
late AppLocalizations l10n;
Set<Agent>? register() => {
networkDao.log((a) => NetworkDao(a, listener: networkDaoListener)),
};
void initState() {
super.initState();
criteria = CriteriaUnit(
selectedCategory: 'all',
selectedCurrency: 'usd',
selectedTimeFrame: '24h',
);
}
void didChangeDependencies() {
super.didChangeDependencies();
safeAreaPadding = MediaQuery.viewPaddingOf(context);
}
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: navigator,
navigatorObservers: [navigatorStack],
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
Locale('en'), // English
Locale('es'), // Spanish
],
home: Builder(
builder: (context) {
l10n = AppLocalizations.of(context)!;
return Material(
child: Column(
children: [
Expanded(
child: PageView(
controller: pageController,
onPageChanged: onPageChange,
children: [
KeepAlivePage(
child: CoinsListing(
this,
agent: coinsListing,
listener: coinsListingListener,
l10n: l10n,
topPadding: safeAreaPadding.top,
),
),
KeepAlivePage(
child: Center(
child: Text('Second Page'),
),
),
],
),
),
BottomBar(
controller: bottomBarController,
onChanged: onBottomTabChange,
index: 0,
width: double.infinity,
height: 60,
bottomPadding: safeAreaPadding.bottom,
),
],
),
);
},
),
);
}
bool _isSetPageByController = false;
void setPage(PageController? controller, int index) {
_isSetPageByController = controller != null;
controller?.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
void showLoading() {
navigator.currentState?.push(DialogRoute(
settings: RouteSettings(name: 'loading'),
context: navigator.currentContext!,
builder: (context) => Center(
child: CircularProgressIndicator(
color: ColorDecor().x8BC540,
)),
barrierDismissible: false,
traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop,
));
}
void showCoinDetailsPage({required CoinDetails details}) {
navigator.currentState?.push(PageRouteBuilder(
settings: RouteSettings(name: 'coin-details?id=${details.key}'),
pageBuilder: (context, animation, secondaryAnimation) {
return CoinDetailsPage(
l10n: l10n,
topPadding: safeAreaPadding.top,
bottomPadding: safeAreaPadding.bottom,
details: details,
onBackButtonTapped: onNeedToPop,
);
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.ease;
var tween =
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(position: animation.drive(tween), child: child);
},
));
}
void showErrorDialog(String errorText) {
navigator.currentState?.push(DialogRoute(
settings: RouteSettings(name: 'error-$errorText'),
context: navigator.currentContext!,
builder: (context) => Center(
child: Material(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: ColorDecor().xFFFFFF,
borderRadius: BorderRadius.circular(10),
),
padding: EdgeInsets.all(16),
child: Text(
errorText,
style: TextDecor().s15w600(color: ColorDecor().xEF5350),
textAlign: TextAlign.center,
),
),
),
),
traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop,
));
}
void onPageChange(int index) {
if (_isSetPageByController) {
_isSetPageByController = false;
return;
}
report(AppCurrentTab(index), from: pageController);
}
void onBottomTabChange(int index) => report(
AppCurrentTab(index),
from: bottomBarController,
);
void onNeedToPop() => report(AppTopWidgetPopping());
PaperListener<NetworkDaoPaper, AppPaper> get networkDaoListener => reporter(
(r) => r
.on<NetworkDaoCoinsListSelection>(
(p) => AppCurrentMarket(p.result),
)
?.on<NetworkDaoCoinDetailsSelection>(
(p) => AppCurrentCoinDetails(p.result),
),
);
PaperListener<CoinsListingPaper, AppPaper> get coinsListingListener =>
reporter(
(r) => r.on<CoinsListingCoinSelection>((p) {
return AppCoinDetailsSelection(p.key);
}),
);
}
/// The observer is for keep track the state of the navigation stack,
/// which is not able to be accessed by current Flutter Navigator.
///
/// It is treated as a listener, and retrieve the state of stack; nothing else.
class CurrentNavigationObserver extends NavigatorObserver {
final List<String> stack = [];
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
stack.add(route.settings.name!);
}
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
if (oldRoute == null) {
if (newRoute != null) {
// Add the route to the top?
stack.add(newRoute.settings.name!);
}
return;
}
if (newRoute == null) {
stack.remove(oldRoute.settings.name!);
return;
}
if (stack.contains(oldRoute.settings.name)) {
stack[stack.indexOf(oldRoute.settings.name!)] = newRoute.settings.name!;
} else {
// Add the route to the top?
stack.add(newRoute.settings.name!);
}
}
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
stack.remove(route.settings.name);
}
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
stack.remove(route.settings.name);
}
}
part of 'coins_listing.dart';
abstract class CoinsListingPaper extends Paper {}
class CoinsListingCurrentCoins extends CoinsListingPaper {
final ListModel<CoinOverview>? coins;
CoinsListingCurrentCoins(this.coins);
}
class CoinsListingErrorText extends CoinsListingPaper {
final String text;
CoinsListingErrorText(this.text);
}
class CoinsListingCoinSelection extends CoinsListingPaper {
final String key;
CoinsListingCoinSelection(this.key);
}
import 'package:flutter/material.dart';
import 'package:paper/paper.dart';
import '../../../l10n/app_localizations.dart';
import '../../model/coin_overview.dart';
import '../../model/decor.dart';
import '../../widget/coin_summary_card.dart';
part 'coins_listing_paper.dart';
part 'coins_listing_script.dart';
class CoinsListing extends UnitWidget<CoinsListingPaper> {
// ignore: use_key_in_widget_constructors
CoinsListing(
super.parent, {
required super.agent,
super.listener,
required this.l10n,
this.topPadding,
this.height,
this.width,
this.coins,
this.errorText,
});
final AppLocalizations l10n;
final double? topPadding;
final double? height;
final double? width;
final ListModel<CoinOverview>? coins;
final String? errorText;
UnitWidgetState<CoinsListingPaper, CoinsListing> createState() =>
CoinsListingState();
Script<CoinsListingPaper, CoinsListingState> createScript() =>
CoinsListingScript();
}
class CoinsListingState
extends UnitWidgetState<CoinsListingPaper, CoinsListing> {
late List<CoinOverview>? coins;
late String? errorText;
late final ValueNotifier<bool> isProfileEnabled;
void initState() {
super.initState();
coins = widget.coins?.toList();
errorText = widget.errorText;
isProfileEnabled = ValueNotifier(false);
}
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: widget.width,
height: widget.height,
),
child: Column(
children: [
SizedBox(height: widget.topPadding),
SizedBox(
height: 60,
child: Stack(
children: [
Center(
child: Text(
widget.l10n.f_MagicCoins,
style: TextDecor().s25w700(color: ColorDecor().x000000),
),
),
Positioned(
right: 16,
top: 0,
bottom: 0,
child: ValueListenableBuilder<bool>(
valueListenable: isProfileEnabled,
builder: (context, value, child) {
return GestureDetector(
child: Icon(
Icons.account_circle,
size: 40,
color: value
? ColorDecor().x8BC540
: ColorDecor().x9E9E9E,
),
);
},
),
),
],
),
),
SizedBox(
height: 52,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: Container(
height: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
width: 2,
color: ColorDecor().x8BC540,
),
),
padding: EdgeInsets.all(5),
child: Row(
children: [
const Icon(
Icons.search_outlined,
color: Colors.black,
size: 19,
),
const SizedBox(width: 5),
Expanded(
child: GestureDetector(
onTap: () {},
child: Text(widget.l10n.s_ClickSearch),
),
),
],
),
),
),
const SizedBox(width: 5),
SizedBox(
height: double.infinity,
width: 40,
child: ElevatedButton(
onPressed: null,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.transparent, width: 2),
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(0),
),
child: Icon(
Icons.filter_alt,
color: ColorDecor().x9E9E9E,
),
),
),
],
),
),
),
if (coins == null && errorText == null)
Expanded(child: Center(child: CircularProgressIndicator())),
if (coins != null && coins!.isNotEmpty)
Expanded(
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: coins!.length,
itemBuilder: (context, index) {
final coin = coins![index];
return CoinSummaryCard(
name: coin.name,
ticker: coin.symbol,
price: coin.currencyFormattedCurrentPrice,
change: coin.changePercentage.trailingSpacingPercentage,
isChangeUp: coin.changePercentage.num >= 0,
onTap: () => onCardTapped(coin.key),
);
},
separatorBuilder: (context, index) {
return Divider(
thickness: 1,
height: 5,
color: ColorDecor().x9E9E9E,
);
},
),
),
if (coins != null && coins!.isEmpty)
Expanded(
child: Center(
child: Text(
widget.l10n.s_NoCoinsFoundAlert,
textAlign: TextAlign.center,
),
),
),
if (coins == null && errorText != null)
Expanded(
child: Center(
child: Text(errorText!, textAlign: TextAlign.center),
),
),
],
),
);
}
void onCardTapped(String key) => report(CoinsListingCoinSelection(key));
}
Run the app:
Breakdown
You may find it very strange about using Navigator inside App to show
the loading indicator and a dialog. Actually, it is exactly what we advise for how
to use the Navigator in the Paper pattern as well. As mentioned, treat Navigator
as merely a Stack. Pay attention to the UI and we can see the loading indicator
and the dialog literally overlay all the widgets below. Therefore, they
should be pushed into the UI tree by the Navigator - the root Stack of the
App. Moreover, if we dig into the source code of showDialog,
showAboutDialog, showBottomSheet, etc., we can see they are literally
pushing a route by the Navigator. It even applies to Tooltips and DropDown.
We strongly recommend wrapping the widgets that show tooltips with Stack or
Overlay, and DropDown if the DropDown widgets stay within the parent
widgets.
CoinsListing
- When a user taps an item in the listing,
CoinsListingwill reportCoinsListingCoinSelectionwith the corresponding key of the coin. BecauseCoinsListingdoes not need to process anything withCoinsListingCoinSelection, the paper will be transferred to the parent ofCoinsListing-App.
App
-
Appwill process the receivedCoinsListingCoinSelectionand process it with its paper -AppCurrentCoinDetails. -
AppprocessesAppCurrentCoinDetailsby showing the loading indicator to tell the user to wait for the coin details to be fetched. Then it commandsNetworkDaoto fetch the data. Again, it is very similar to how to do with fetching data of the coins list. -
After successfully fetching the coin details,
Appwill push theCoinDetailsPageto show the details.
NavigatorObserver
So what is CurrentNavigationObserver in app.dart for? The problem is
Navigator does not expose the state of its routes stack, which is kind of
against our concept. The parent has the right to access any attributes of its
children. Besides, as our experience, access to the routes stack is crucial to
achieve development features. The CurrentNavigationObserver is used by
App to keep track of the current routes stack.
In addition, please note that creating a NavigatorObserver for reacting to
changes of the routes stack is NOT recommended. For example, react to popping a
screen and do something afterwards. Navigator is not capable of changing its
routes stack by itself. In this case, App is the only object who commands
Navigator to push, pop, etc., so all actions following the routes stack should
be handled by AppScript.
We are also developing a different Navigator which allows access to its stack.