Navigation (continued)
In this section, we are going to add another feature to demonstrate more about
navigation. We are going to use Navigator to show pop-up modals.
Add files:
-
lib/src/model/item.dart
-
lib/src/unit_widget/app/data/filter_items.dart
-
lib/src/unit_widget/app/model/filter_type.dart
-
lib/src/widget/criteria_card.dart
-
lib/src/widget/filter_selection_page.dart
Copy file contents:
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 {}
class AppFilterChoiceSelection extends AppPaper {
final FilterType type;
AppFilterChoiceSelection(this.type);
}
class AppCriteriaApplying extends AppPaper {}
class AppCriteriaSelection extends AppPaper {
final Item category;
final Item currency;
final Item timeFrame;
AppCriteriaSelection({
required this.category,
required this.currency,
required this.timeFrame,
});
}
class AppFilterValueSelection extends AppPaper {
final FilterType type;
final String key;
AppFilterValueSelection({required this.type, required this.key});
}
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)
?.on<AppCriteriaSelection>(onCriteriaSelection)
?.on<AppFilterChoiceSelection>(onFilterChoiceSelection)
?.on<AppCriteriaApplying>(onCriteriaApplying)
?.on<AppFilterValueSelection>(onFilterValueSelection);
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();
}
Future<void> onCriteriaApplying(
AppCriteriaApplying p,
AppState s,
SourceVerifier ifFrom,
) async {
final category = s.criteriaCardController.getCategory!.key;
final currency = s.criteriaCardController.getCurrency!.key;
final timeFrame = s.criteriaCardController.getTimeFrame!.key;
s.criteria = CriteriaUnit(
selectedCategory: category,
selectedCurrency: currency,
selectedTimeFrame: timeFrame,
);
s.navigator.currentState?.pop();
await s.coinsListing.process(CoinsListingLoading());
s.networkDao.process(
NetworkDaoDataFetching(
NetworkDaoCoinsListParam(
page: 1,
category: category,
currency: currency,
timeFrame: timeFrame,
),
),
);
}
void onCriteriaSelection(
AppCriteriaSelection p,
AppState s,
SourceVerifier ifFrom,
) {
s.showCriteriaDialog(
selectedCategory: p.category,
selectedCurrency: p.currency,
selectedTimeFrame: p.timeFrame,
);
}
void onFilterChoiceSelection(
AppFilterChoiceSelection p,
AppState s,
SourceVerifier ifFrom,
) {
s.showFiltersSelectionPage(type: p.type);
}
void onFilterValueSelection(
AppFilterValueSelection p,
AppState s,
SourceVerifier ifFrom,
) {
s.navigator.currentState?.pop();
switch (p.type) {
case FilterType.category:
s.criteriaCardController.setCategory = s.categories[p.key]!;
break;
case FilterType.currency:
s.criteriaCardController.setCurrency = s.currencies[p.key]!;
break;
case FilterType.timeFrame:
s.criteriaCardController.setTimeFrame = s.timeFrames[p.key]!;
break;
}
}
}
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 '../../model/item.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/criteria_card.dart';
import '../../widget/filter_selection_page.dart';
import '../../widget/keep_alive_page.dart';
import '../coins_listing/coins_listing.dart';
part 'app_paper.dart';
part 'app_script.dart';
part 'data/filter_items.dart';
part 'model/filter_type.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 criteriaCardController = FilterCardController();
final navigator = GlobalKey<NavigatorState>();
final navigatorStack = CurrentNavigationObserver();
late final categories = {for (var e in categoryItems) e.key: e};
late final currencies = {for (var e in currencyItems) e.key: e};
late final timeFrames = {for (var e in timeFrameItems) e.key: e};
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 showCriteriaDialog({
required Item selectedCategory,
required Item selectedCurrency,
required Item selectedTimeFrame,
}) {
navigator.currentState?.push(DialogRoute(
settings: RouteSettings(name: 'filter-dialog'),
context: navigator.currentContext!,
builder: (context) => Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: FilterCard(
key: criteriaCardController,
categoryLabel: l10n.f_CATEGORY,
currencyLabel: l10n.f_CURRENCY,
timeFrameLabel: l10n.f_TIMEFRAME,
category: selectedCategory,
currency: selectedCurrency,
timeFrame: selectedTimeFrame,
onCategoryTapped: onCategoryChoiceSelected,
onCurrencyTapped: onCurrencyChoiceSelected,
onTimeFrameTapped: onTimeFrameChoiceSelected,
onDismissTapped: onNeedToPop,
onApplyTapped: onCriteriaApplied,
),
),
),
traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop,
));
}
void showFiltersSelectionPage({required FilterType type}) {
final String label;
final ListModel<Item> items;
switch (type) {
case FilterType.category:
label = l10n.f_CATEGORY;
items = ListModel(
categories.values.toList(),
keyGen: (_) => 'category',
);
break;
case FilterType.currency:
label = l10n.f_CURRENCY;
items = ListModel(
currencies.values.toList(),
keyGen: (_) => 'currency',
);
break;
case FilterType.timeFrame:
label = l10n.f_TIMEFRAME;
items = ListModel(
timeFrames.values.toList(),
keyGen: (_) => 'time-frame',
);
break;
}
navigator.currentState?.push(MaterialPageRoute(
settings: RouteSettings(name: 'filters-selection'),
builder: (context) => FilterSelectionPage(
title: label,
filters: items,
onFilterTapped: (key) => onFilterValueSelected(type, key),
),
));
}
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());
void onCategoryChoiceSelected() => report(
AppFilterChoiceSelection(FilterType.category),
);
void onCurrencyChoiceSelected() => report(
AppFilterChoiceSelection(FilterType.currency),
);
void onTimeFrameChoiceSelected() => report(
AppFilterChoiceSelection(FilterType.timeFrame),
);
void onCriteriaApplied() => report(AppCriteriaApplying());
void onFilterValueSelected(FilterType type, String value) => report(
AppFilterValueSelection(type: type, key: value),
);
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);
})?.on<CoinsListingCriteriaSelection>((p) {
return AppCriteriaSelection(
category: categories[criteria.selectedCategory]!,
currency: currencies[criteria.selectedCurrency]!,
timeFrame: timeFrames[criteria.selectedTimeFrame]!,
);
}),
);
}
/// 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);
}
class CoinsListingLoading extends CoinsListingPaper {}
class CoinsListingCriteriaSelection extends CoinsListingPaper {}
part of 'coins_listing.dart';
class CoinsListingScript extends Script<CoinsListingPaper, CoinsListingState> {
void map() => on<CoinsListingCurrentCoins>(onCurrentCoins)
?.on<CoinsListingErrorText>(onErrorText)
?.on<CoinsListingLoading>(onLoading);
void onCurrentCoins(
CoinsListingCurrentCoins p,
CoinsListingState s,
SourceVerifier ifFrom,
) {
s.coins = p.coins?.toList();
s.isFilterButtonEnabled = true;
s.render();
}
void onErrorText(
CoinsListingErrorText p,
CoinsListingState s,
SourceVerifier ifFrom,
) {
s.coins = null;
s.errorText = p.text;
s.render();
}
void onLoading(
CoinsListingLoading p,
CoinsListingState s,
SourceVerifier ifFrom,
) {
s.coins = null;
s.errorText = null;
s.render();
}
}
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 bool isFilterButtonEnabled;
late final ValueNotifier<bool> isProfileEnabled;
void initState() {
super.initState();
coins = widget.coins?.toList();
errorText = widget.errorText;
isFilterButtonEnabled = coins != null;
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:
isFilterButtonEnabled ? onFilterButtonTapped : null,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: isFilterButtonEnabled
? ColorDecor().x8BC540
: Colors.transparent,
width: 2,
),
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(0),
),
child: Icon(
Icons.filter_alt,
color: isFilterButtonEnabled
? ColorDecor().x8BC540
: 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));
void onFilterButtonTapped() => report(CoinsListingCriteriaSelection());
}
Run:
Breakdown
Similar to what we have done with showing the loading indicator, the dialog modal
should also be pushed into the screen by the root Navigator. Again, if you dig
into showAboutDialog, showDialog, and even showBottomSheet, they also
implement pushing dialogs and bottom sheets by Navigator.of(context).
CoinsListing
- When the coins list is available to display, the filter button should be
active. This is achieved in
onCurrentCoinsinCoinsListingScript:
void onCurrentCoins(
CoinsListingCurrentCoins p,
CoinsListingState s,
SourceVerifier ifFrom,
) {
s.coins = p.coins?.toList();
s.isFilterButtonEnabled = true;
s.render();
}
- When the filter button is tapped,
CoinsListingreportsCoinsListingCriteriaSelectionto its parent -App.
App
-
When
AppreceivesCoinsListingCriteriaSelection, it will transfer the info into and processAppCriteriaSelection. By processingAppCriteriaSelection,Appshows the criteria pop-up. -
When users tap the "Apply" button after selecting desired criteria, the callback will be triggered and
AppprocessesAppCriteriaApplying, which will: (1) Pop the Filter popup; (2) Show the loading indicator; and (3) Command theNetworkDaoto processNetworkDaoDataFetching. The rest should be similar to what happened when it commands the same paper toNetworkDaoin the section Data Fetching
FilterSelectionPage
- The flows with
FilterSelectionPageshould be familiar to you at this moment. You can try debugging and comprehend the idea behind it.