Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I’m a beginner when it comes to coding. I just started using flutter and want to make a morning devotion app for church.
I’m currently using the routes method to join pages for month and day but I just realized I have to do the same for every single day
As in :
January > Day one
Day two, three and so on.
Is there a more efficient way to do this?
these are the routes I have so far. The loading screen, home, devotion, months, January etc
'''dart
void main()=>runApp(MaterialApp(
initialRoute: "/",
routes: {
"/": (context)=> loading(),
"/home":(context)=>home(),
"/Devotion":(context)=>Dpg(),
"/Months":(context)=>months(),
"/jan":(context)=>jan(),
"/feb":(context)=>feb(),
},
));
'''
Right now i want to work for the month of january so there will have to be pages for Day one to 31 and then February all the way to december. Im not so sure i have to continue with
"/JAN Day one":(context)=>jd1(),
"/JAN Day two":(context)=>jd2(),
then go to
"/FEB Day one":(context)=>fb1(),
etc.
is there a better way to do this?
Welcome to Stack Overflow! If I understand your question correctly, you are currently defining your routes using the routes parameter similar to this:
MaterialApp(
routes: {
'/01-01': (context) {
return Scaffold(
appBar: AppBar(
title: const Text('01 JAN'),
),
);
},
'/02-01': (context) {
return Scaffold(
appBar: AppBar(
title: const Text('02 JAN),
),
);
},
},
)
Instead of defining each route manually, you could investigate using the new Navigator 2.0 changes to easily read parameters from the URL similar to this:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
final _routeInformationParser = AppRouteInformationParser();
final _routerStateData = RouterStateData();
late AppRouterDelegate _routerDelegate;
#override
Widget build(BuildContext context) {
return RouterState(
notifier: _routerStateData,
child: MaterialApp.router(
routeInformationParser: _routeInformationParser,
routerDelegate: _routerDelegate,
),
);
}
#override
void dispose() {
_routerDelegate.dispose();
_routerStateData.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_routerDelegate = AppRouterDelegate(
routerStateData: _routerStateData,
);
}
}
/// https://api.flutter.dev/flutter/widgets/InheritedNotifier-class.html
class RouterState extends InheritedNotifier<RouterStateData> {
const RouterState({
Key? key,
RouterStateData? notifier,
required Widget child,
}) : super(
key: key,
notifier: notifier,
child: child,
);
static RouterStateData? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<RouterState>()?.notifier;
}
}
/// https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html
class RouterStateData extends ChangeNotifier {
/// The devotion day.
int? _devotionDay;
/// The devotion month.
int? _devotionMonth;
/// Gets the devotion day.
int? get devotionDay => _devotionDay;
/// Gets the devotion month.
int? get devotionMonth => _devotionMonth;
/// Updates the state of the router to a devotion day and a devotion month.
void setDevotionDayMonth(int? devotionDay, int? devotionMonth) {
_devotionDay = devotionDay;
_devotionMonth = devotionMonth;
notifyListeners();
}
}
/// A base route path that all route paths can extend from.
abstract class RoutePath {
const RoutePath();
}
/// The route path of the home.
class HomeRoutePath extends RoutePath {
const HomeRoutePath();
}
/// The route path of a devotion.
class DevotionRoutePath extends RoutePath {
/// The day of the devotion.
final int day;
/// The month of the devotion.
final int month;
const DevotionRoutePath({
required this.day,
required this.month,
});
}
/// https://api.flutter.dev/flutter/widgets/RouteInformationParser-class.html
class AppRouteInformationParser extends RouteInformationParser<RoutePath> {
#override
Future<RoutePath> parseRouteInformation(
RouteInformation routeInformation,
) async {
/// Gets the uri of the route, for example "/devotions/01-01".
final uri = Uri.parse(routeInformation.location!);
/// Switches on the number of path segments of the uri.
switch (uri.pathSegments.length) {
/// Cases on uris that have 2 path segments, for example "/devotions/1-1".
case 2:
/// Switches on the value of the first path segment of the uri.
switch (uri.pathSegments[0]) {
/// Cases on uris that start with devotions, for example "/devotions/1-1".
case 'devotions':
/// Gets the day and month dynamically from the uri.
final dayMonth = uri.pathSegments[1].split('-');
/// Returns the devotion route path with the day and month from the uri.
return SynchronousFuture(
DevotionRoutePath(
day: int.parse(dayMonth[0]),
month: int.parse(dayMonth[1]),
),
);
}
break;
}
/// Returns the default home route path if no other route paths match the uri.
return SynchronousFuture(HomeRoutePath());
}
#override
RouteInformation? restoreRouteInformation(
RoutePath configuration,
) {
/// If the current route path is home, then sets the uri to /.
if (configuration is HomeRoutePath) {
return RouteInformation(
location: '/',
);
/// If the current route path is devotion, then sets the uri to /devotions/day-month, for example "/devotions/1-1".
} else if (configuration is DevotionRoutePath) {
return RouteInformation(
location: '/devotions/${configuration.day}-${configuration.month}',
);
}
return null;
}
}
/// https://api.flutter.dev/flutter/widgets/RouterDelegate-class.html
class AppRouterDelegate extends RouterDelegate<RoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RoutePath> {
#override
final navigatorKey = GlobalKey<NavigatorState>();
final RouterStateData routerStateData;
AppRouterDelegate({
required this.routerStateData,
}) {
routerStateData.addListener(notifyListeners);
}
#override
RoutePath? get currentConfiguration {
final day = routerStateData.devotionDay;
final month = routerStateData.devotionMonth;
/// If both the day and the month are not null, then returns the route path for devotion; otherwise, returns the route path for home.
return day != null && month != null
? DevotionRoutePath(day: day, month: month)
: HomeRoutePath();
}
#override
Widget build(BuildContext context) {
final day = routerStateData.devotionDay;
final month = routerStateData.devotionMonth;
return Navigator(
key: navigatorKey,
pages: [
/// Pushes the home page onto the navigator stack.
const MaterialPage<void>(
child: HomePage(),
key: ValueKey('home_page'),
),
/// If both the day and the month are not null, then pushes the devotion page onto the navigator stack.
if (day != null && month != null)
MaterialPage<void>(
child: DevotionPage(
day: day,
month: month,
),
key: ValueKey('devotion_page'),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
/// If the devotion page is being popped, then clears the devotion day and devotion month from the router state.
routerStateData.setDevotionDayMonth(null, null);
return true;
},
);
}
#override
void dispose() {
routerStateData.removeListener(notifyListeners);
super.dispose();
}
#override
Future<void> setNewRoutePath(RoutePath configuration) async {
/// If the route path is home, then clears the devotion day and devotion month from the router state.
if (configuration is HomeRoutePath) {
routerStateData.setDevotionDayMonth(
null,
null,
);
/// If the route path is devotion, then sets the devotion day and devotion month in the router state.
} else if (configuration is DevotionRoutePath) {
routerStateData.setDevotionDayMonth(
configuration.day,
configuration.month,
);
}
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _dayController = TextEditingController();
final _monthController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: ListView(
children: [
TextFormField(
controller: _dayController,
decoration: InputDecoration(
labelText: 'Day',
hintText: '01',
),
),
TextFormField(
controller: _monthController,
decoration: InputDecoration(
labelText: 'Month',
hintText: '01',
),
),
ElevatedButton(
onPressed: () {
/// Updates the router state with the entered devotion day and devotion month. This calls the `notifyListeners()` internally, which notifies the `AppRouterDelegate` that the route needs updating.
RouterState.of(context)?.setDevotionDayMonth(
int.parse(_dayController.text),
int.parse(_monthController.text),
);
},
child: Text('GO TO DEVOTION'),
),
],
),
);
}
#override
void dispose() {
_dayController.dispose();
_monthController.dispose();
super.dispose();
}
}
class DevotionPage extends StatelessWidget {
final int day;
final int month;
const DevotionPage({
Key? key,
required this.day,
required this.month,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Devotion'),
),
body: Center(
child: Text('$day-$month'),
),
);
}
}
Please note this is by no means the only solution and others can be found here and here.
If you have any questions, then please do let me know!
Related
How can a bloc show percentage progress bar
//For example, there is a regular bloc
#override
Stream<JobState> mapEventToState(JobEvent event) async* {
if (event is HardJobEvent) {
yield* _mapHardJobToState();
}
}
Stream<UpdateState> _mapHardJobToState() async* {
try {
//It is necessary to display a progress bar for this method.
await doSomeHardJob();
} catch (e) {
print(e);
}
}
doSomeHardJob() async* {
for( var i = 1 ; i < 1000; i++ ) {
//This yield does not work. Doesn't display any errors
//State not transfer
yield HardJob(nowCounter: i);
}
}
I use cubit instead of bloc. but the technique should be similar.
I have a broadcast stream controller in the payload generating function. In the event dispatcher (this should be your bloc) I listen to it and emit loading states with a double value. the bloc builder in the widgets can react to it. check put my little implementation:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Model
class Data {
final DateTime date;
Data(this.date);
}
/// Repo
class DataRepository {
/// here comes the trick:
final StreamController<double> progress = StreamController.broadcast(); // make it broadcast to allow multiple subcribtions
Future<List<Data>> generateData() async {
/// here comes your time taking work
progress.sink.add(0.0); // set progress to 0
List<Data> payload =
[]; // this will be the data you want to transport in the loadED event
await Future.forEach(List.generate(10, (i) => i), (int i) async {
/// here comes the progess; dont send it too late otherwise
/// the loadED state will be followed by a loadING state and
/// you will see a never ending spinner
progress.sink.add(i / 10);
/// this would be like eg a http call
payload.add(Data(DateTime.now()));
await Future.delayed(
const Duration(seconds: 1)); // simulate the time consuming action
});
return payload;
}
}
/// State
#immutable
abstract class DataState {}
class DataInitial extends DataState {
DataInitial();
}
class DataLoading extends DataState {
/// this state will emit the actual progress value to the spinner
final double progress;
DataLoading(this.progress);
/// boilerplate code to tell state events apart from each other even though they are of the same type
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DataLoading && other.progress == progress;
}
#override
int get hashCode => progress.hashCode;
}
class DataLoaded extends DataState {
/// this state will transport the payload data
final List<Data> data;
DataLoaded(this.data);
/// same as aboth
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DataLoaded && other.data == data;
}
#override
int get hashCode => data.hashCode;
}
/// Cubit
class DataCubit extends Cubit<DataState> {
/// Cubit works like a simple Bloc
final DataRepository dataRepository;
/// the repo will do the actual work
DataCubit({required this.dataRepository}) : super(DataInitial());
Future<void> generateData() async {
/// this will bring the progress value to the loading spinner widget.
/// each time a new progress is made a new DataLoadING state will be emitted
dataRepository.progress.stream
.listen((progress) => emit(DataLoading(progress)));
/// this await is sincere; it will take aaages; really
final payload = await dataRepository.generateData();
/// finally the payload will be sent to the widgets
emit(DataLoaded(payload));
}
}
late DataRepository dataRepository;
void main() {
/// init the repo that will do the heavy lifting like eg a db or http request
dataRepository = DataRepository();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
/// makes the cubit (like a baby bloc) available to all child widgets of the app
return BlocProvider<DataCubit>(
create: (context) => DataCubit(dataRepository: dataRepository),
child: const MaterialApp(
title: 'Flutter Progress Demo',
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Progress Example'),
),
body: Center(
child: BlocBuilder<DataCubit, DataState>(
builder: (context, state) {
if (state is DataLoading) {
return CircularProgressIndicator.adaptive(
value: state.progress,
);
} else if (state is DataLoaded) {
List<Data> longAnticipatedData = state.data;
return ListView.builder(
itemCount: longAnticipatedData.length,
itemBuilder: (context, i) => ListTile(
title:
Text(longAnticipatedData[i].date.toIso8601String()),
));
} else {
/// initial state
return const Center(
child: Text('press the FAB'),
);
}
},
),
),
floatingActionButton: FloatingActionButton(
/// generate data; will take a good while
onPressed: () => context.read<DataCubit>().generateData(),
child: const Icon(Icons.start),
),
);
}
}
Intro
We have a ListView named OrganizationList which contains a ListTile widget for every item. We use a Text widget to display the name of the organization inside the ListTile.
We would like to test if the organization name is displayed correctly and in the correct order.
Our current solution
We have the following assertions:
var organizationA = find
.descendant(
of: find.byType(OrganizationList),
matching: find.byKey(
Key('0'),
),
)
.evaluate()
.first
.widget as Text;
expect(textOrganization.data, 'organization-a');
var organizationB = find
.descendant(
of: find.byType(OrganizationList),
matching: find.byKey(
Key('1'),
),
)
.evaluate()
.first
.widget as Text;
expect(textOrganization.data, 'organization-b');
This feels like a very cumbersome way of testing if the right label is shown for the list items. But I fail to find a more elegant way.
Question
What is a more elegant way in Flutter / Dart to assert both the content of a list item and the order of all items?
With my solution you can check if a ListItemWidget at specific position and text is visible(or not):
void checkPosition(int index, SomeObj obj, {bool isOnPosition = true}) {
final listItemFinder = find.byType(ListItemWidget);
final listItem = listItemFinder.evaluate().isEmpty
? null
: listItemFinder.at(index);
if (listItem == null) {
if (isOnPosition) {
fail('List not found');
}
return;
}
final positionText = find.text(obj.text);
expect(find.descendant(of: listItem, matching: positionText),
isOnPosition ? findsOneWidget : findsNothing);
}
The easiest way to get elements from List and verify is widgetList, which gives a list of elements of whatever data type we have provided.
Sample test:
Problem: Verify the second item from List
const List<String> listString = [
"Ac",
"Fuel Sensor",
"Power",
"Panic",
"Camera",
"Relay",
"Duty Button",
"Other"
];
Test:
testWidgets('Listview item verify', (WidgetTester tester) async {
await tester.pumpWidget(ListViewApp());
/// Verify OrganizationIte sequence to get and verify title.
OrganizationItem item = tester
.widgetList<OrganizationItem>(find.byType(OrganizationItem))
.elementAt(2);
String data = item.title;
expect("Power", data);
});
App Code:
class ListViewApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Checked Listview',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: MyHomePage(title: 'Flutter Checked Listview'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String> _list = [];
#override
void initState() {
setState(() {
for (int i = 0; i < listString.length; i++) {
_list.add('${listString[i]}');
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Listview Sample'),
),
body: ListView.builder(
itemBuilder: (_, int index) {
return OrganizationItem(
title: _list[index],
);
},
itemCount: _list.length,
),
);
}
}
class OrganizationItem extends StatelessWidget {
final String title;
const OrganizationItem({Key? key, required this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(this.title),
);
}
}
Try a for loop:
// This is for saving a reference to the ListTile from the previous iteration
ListTile? previousListTile;
for (int j = 0; j < aListOfModels.length; j++) {
final organizationA = find.descendant(
of: find.byType(OrganizationList),
matching: find.byKey(
Key('0'),
),
);
// This is important to show the invisible late elements
await widgetTester.scrollUntilVisible(organizationA, tryToFindTheSuitableDelta);
// I suppose that organizationA finds a ListTile widget
ListTile currentListTile = widgetTester.widget<ListTile>(organizationA);
/*
Make your expectations about the content as you like
....
...
*/
// To expect about the order, do the following:
final listTileWidgetList = widgetTester
.widgetList<ListTile>(find.byType(ListTile))
.toList();
if (j > 0) {
// Expect that the currentListTile is the first next ListTile of the previousListTile
expect(listTileWidgetList.indexOf(currentListTile),
equals(listTileWidgetList.indexOf(previousListTile!) + 1));
}
// To allow you to make your expectation about the order in the next iteration
previousListTile = currentListTile;
}
I'm starting to learn Flutter/Dart by building a simple Todo app using Provider, and I've run into a state management issue. To be clear, the code I've written works, but it seems... wrong. I can't find any examples that resemble my case enough for me to understand what the correct way to approach the issue is.
This is what the app looks like
It's a grocery list divided by sections ("Frozen", "Fruits and Veggies"). Every section has multiple items, and displays a "x of y completed" progress indicator. Every item "completes" when it is pressed.
TheGroceryItemModel looks like this:
class GroceryItemModel extends ChangeNotifier {
final String name;
bool _completed = false;
GroceryItemModel(this.name);
bool get completed => _completed;
void complete() {
_completed = true;
notifyListeners();
}
}
And I use it in the GroceryItem widget like so:
class GroceryItem extends StatelessWidget {
final GroceryItemModel model;
GroceryItem(this.model);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: model,
child: Consumer<GroceryItemModel>(builder: (context, groceryItem, child) {
return ListTile(
title: Text(groceryItem.name),
leading: groceryItem.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked)
onTap: () => groceryItem.complete();
})
);
}
}
The next step I want is to include multiple items in a section, which tracks completeness based on how many items are completed.
The GroceryListSectionModel looks like this:
class GroceryListSectionModel extends ChangeNotifier {
final String name;
List<GroceryItemModel> items;
GroceryListSectionModel(this.name, [items]) {
this.items = items == null ? [] : items;
// THIS RIGHT HERE IS WHERE IT GETS WEIRD
items.forEach((item) {
item.addListener(notifyListeners);
});
// END WEIRD
}
int itemCount() => items.length;
int completedItemCount() => items.where((item) => item.completed).length;
}
And I use it in the GroceryListSection widget like so:
class GroceryListSection extends StatelessWidget {
final GroceryListSectionModel model;
final ValueChanged<bool> onChanged;
GroceryListSection(this.model, this.onChanged);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: model,
child: Consumer<GroceryListSectionModel>(
builder: (context, groceryListSection, child) {
return Container(
child: ExpansionTile(
title: Text(model.name),
subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
children: groceryListSection.items.map((groceryItemModel) =>
GroceryItem(groceryItemModel)).toList()
)
);
}
)
);
}
}
The Problems:
It seems weird to have a ChangeNotifierProvider and a Consumer in both Widgets. None of the examples I've seen do that.
It's definitely wrong to have the GroceryListSectionModel listening to changes on all the GroceryItemModels for changes to propagate back up the tree. I don't see how that can scale right.
Any suggestions? Thanks!
this ist not a nested Provider, but i think in your example it is the better way..
only one ChangeNotifierProvider per section ("Frozen", "Fruits and Veggies") is defined
the complete() function from a ItemModel is in the GroceryListSectionModel() and with the parameter from the current List Index
class GroceryListSection extends StatelessWidget {
final GroceryListSectionModel model;
// final ValueChanged<bool> onChanged;
GroceryListSection(this.model);
#override
Widget build(BuildContext context) {
return new ChangeNotifierProvider<GroceryListSectionModel>(
create: (context) => GroceryListSectionModel(model.name, model.items),
child: new Consumer<GroceryListSectionModel>(
builder: (context, groceryListSection, child) {
return Container(
child: ExpansionTile(
title: Text(model.name),
subtitle: Text("${groceryListSection.completedItemCount()} of ${groceryListSection.itemCount()} completed"),
children: groceryListSection.items.asMap().map((i, groceryItemModel) => MapEntry(i, GroceryItem(groceryItemModel, i))).values.toList()
)
);
}
)
);
}
}
class GroceryItem extends StatelessWidget {
final GroceryItemModel model;
final int index;
GroceryItem(this.model, this.index);
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(model.name),
leading: model.completed ? Icon(Icons.check_circle, color: Colors.green) : Icon(Icons.radio_button_unchecked),
onTap: () => Provider.of<GroceryListSectionModel>(context, listen: false).complete(index),
);
}
}
class GroceryListSectionModel extends ChangeNotifier {
String name;
List<GroceryItemModel> items;
GroceryListSectionModel(this.name, [items]) {
this.items = items == null ? [] : items;
}
int itemCount() => items.length;
int completedItemCount() => items.where((item) => item.completed).length;
// complete Void with index from List items
void complete(int index) {
this.items[index].completed = true;
notifyListeners();
}
}
// normal Model without ChangeNotifier
class GroceryItemModel {
final String name;
bool completed = false;
GroceryItemModel({this.name, completed}) {
this.completed = completed == null ? false : completed;
}
}
After hours of searching about the topic and due to lack of documentation on Flutter Web I am asking this question.
I was trying to create a web app using flutter and had an requirement where URL such as below
website.com/user/someUserCode
would be called and an page will be launched where the data (someUserCode) will be passed to the page
but haven't got any solutions yet to resolve it.
so just rounding it all up,
How to pass and fetch the data using (get / post) methods to flutter web app?
EDIT 1
What all I know / have tried yet
I am using below code to read if some parameters are being to some class file
final Map<String, String> params = Uri.parse(html.window.location.href).queryParameters;
String data = params["userData"];
all this actually solves the Fetch part of my question (maybe)
but the part where that data will be passed to the page via URL is still missing.
EDIT 2
Since I haven't got any replies and was not able to find anything i raised an ticket on Flutter GitHub page here
anyone else looking for the same issue can track it there (if it gets resolve)
May you could do it in a easy way:
import 'dart:html';
import 'package:flutter/material.dart';
import 'home_page.dart';
void getParams() {
var uri = Uri.dataFromString(window.location.href);
Map<String, String> params = uri.queryParameters;
var origin = params['origin'];
var destiny = params['destiny'];
print(origin);
print(destiny);
}
void main() {
getParams();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Your app',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
And then call it from browser:
http://localhost:52695/?origin=pointA&destiny=pointB
Output:
pointA
pointB
I tried the above method from #Mariano Zorrilla but it still opened the pages in order:
/
/user
/user/yFbOfUAwx1OCC93INK8O7VqgBXq2
I have found Fluro, and works efficiently and cleanly you only need to add one routing file and do all the routing in one file rather than editing every page you want to route to, here's how you would implement it:
main.dart
void main() {
FluroRouter.setupRouter();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Website Title',
onGenerateRoute: FluroRouter.router.generator
);
}
}
fluro_router.dart
class FluroRouter {
static Router router = Router();
//Define your routers here
static void setupRouter() {
router.define('/', handler: _homeHandler);
router.define('/login', handler: _loginHandler);
router.define('/online-enquiry/:userId', handler: _userHandler);
}
//Add your handlers here
static Handler _homeHandler = Handler(handlerFunc: (context, Map<String, dynamic> params) => Home());
static Handler _loginHandler = Handler(handlerFunc: (context, Map<String, dynamic> params) => Login());
static Handler _userHandler = Handler(handlerFunc: (context, Map<String, dynamic> params) => UserProfile(userID: params['userId'].first));
}
Source
You can get everything (paths, parameters, etc) from onGenerateRoute. Your Home will be / and everything from there can be grabbed and used to redirect users.
My approach to solve this is the following. Your base App() should be like:
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Website Title',
onGenerateRoute: (settings) => NavigatorRoute.route(settings.name),
);
}
}
and the class NavigatorRoute will be:
class NavigatorRoute extends StatefulWidget {
final String path;
static Route<dynamic> route(String path) {
return SimpleRoute(
name: '', // this one is always empty as you didn't route yet
title: 'Website Title',
builder: (_) => NavigatorRoute(path: path),
animated: false
);
}
const NavigatorRoute({Key key, this.path}) : super(key: key);
#override
_NavigatorRouteState createState() => _NavigatorRouteState();
}
class _NavigatorRouteState extends State<NavigatorRoute> {
#override
void initState() {
super.initState();
Future.microtask(() {
if (widget.path == '/') {
Navigator.of(context).pushAndRemoveUntil(HomeScreen.route(false), (_) => false);
return;
} else if (widget.path == '/user') {
Navigator.of(context).pushAndRemoveUntil(UserScreen.route(false), (_) => false);
return;
} else if (widget.path.contains('/user/')) {
Navigator.of(context).pushAndRemoveUntil(UserScreen.routeCode(widget.path.split('/')[2]), (_) => false);
return;
} else if (widget.path == '/about') {
Navigator.of(context).pushAndRemoveUntil(AboutScreen.route(), (_) => false);
return;
} else {
Navigator.of(context).pushAndRemoveUntil(HomeScreen.route(), (_) => false);
return;
}
});
}
#override
Widget build(BuildContext context) {
return SizedBox();
}
}
The code for the SimpleRoute is:
class SimpleRoute extends PageRoute {
SimpleRoute({#required String name, #required this.title, #required this.builder, #required this.animated})
: super(settings: RouteSettings(name: name));
final String title;
final WidgetBuilder builder;
final bool animated;
#override
Color get barrierColor => null;
#override
String get barrierLabel => null;
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(milliseconds: 200);
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return animated
? FadeTransition(
opacity: animation,
child: Title(
title: this.title,
color: Theme.of(context).primaryColor,
child: builder(context),
),
)
: Title(
title: this.title,
color: Theme.of(context).primaryColor,
child: builder(context),
);
}
}
So, finally... if you want to easily open one of your screens, you can do:
class HomeScreen extends StatefulWidget {
static Route<dynamic> route(bool animated) {
return SimpleRoute(name: '/', title: 'Home', builder: (_) => HomeScreen(), animated: animated);
}
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
...
}
The routeCode could be:
static Route<dynamic> routeCode(String id) {
return SimpleRoute(name: '/user/$id', title: 'User', builder: (_) => UserScreen(id: id), animated: false);
}
The main benefit of doing this is avoiding the stack of pages generated by accessing the last screen.
For example, if you're using directly the onGenerateRoute for "www.mywebsite.com/user/userId/edit" then Flutter will open:
Home Screen
User Screen
UserId Screen
Edit Screen
but with this approach, only "Edit Screen" will be open.
I'm using an inherited Widget to access a Bloc with some long running task (e.g. search).
I want to trigger the search on page 1 and continue to the next page when this is finished. Therefore I'm listening on a stream and wait for the result to happen and then navigate to the result page.
Now, due to using an inherited widget to access the Bloc I can't access the bloc with context.inheritFromWidgetOfExactType() during initState() and the exception as I read it, recommends doing this in didChangeDependencies().
Doing so this results in some weird behavior as the more often I go back and forth, the more often the stream I access fires which would lead to the second page beeing pushed multiple times. And this increases with each back and forth interaction. I don't understand why the stream why this is happening. Any insights here are welcome. As a workaround I keep a local variable _onSecondPage holding the state to avoid pushing several times to the second Page.
I found now How to call a method from InheritedWidget only once? which helps in my case and I could access the inherited widget through context.ancestorInheritedElementForWidgetOfExactType() and just listen to the stream and navigate to the second page directly from initState().
Then the stream behaves as I would expect, but the question is, does this have any other side effects, so I should rather get it working through listening on the stream in didChangeDependencides() ?
Code examples
My FirstPage widget listening in the didChangeDependencies() on the stream. Working, but I think I miss something. The more often i navigate from first to 2nd page, the second page would be pushed multiple times on the navigation stack if not keeping a local _onSecondPage variable.
#override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint("counter: $_counter -Did change dependencies called");
// This works the first time, after that going back and forth to the second screen is opened several times
BlocProvider.of(context).bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
void _handleRouting(bool isFinished) async {
if (isFinished && !_onSecondPage) {
_onSecondPage = true;
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => NAVIGATE TO OTHER PAGE");
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
_onSecondPage = false;
} else {
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => not finished, nothing to do now");
}
}
#override
void dispose() {
debugPrint("counter: $_counter - disposing my homepage State");
subscription?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: BlocProvider.of(context).bloc.counter.stream,
initialData: 0,
builder: (context, snapshot) {
_counter = snapshot.data;
return Text(
"${snapshot.data}",
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
A simple Bloc faking some long running work
///Long Work Bloc
class LongWorkBloc {
final BehaviorSubject<bool> startLongWork = BehaviorSubject<bool>();
final BehaviorSubject<bool> finished = BehaviorSubject<bool>();
int _counter = 0;
final BehaviorSubject<int> counter = BehaviorSubject<int>();
LongWorkBloc() {
startLongWork.stream.listen((bool start) {
if (start) {
debugPrint("Start long running work");
Future.delayed(Duration(seconds: 1), () => {}).then((Map<dynamic, dynamic> reslut) {
_counter++;
counter.sink.add(_counter);
finished.sink.add(true);
finished.sink.add(false);
});
}
});
}
dispose() {
startLongWork?.close();
finished?.close();
counter?.close();
}
}
Better working code
If I however remove the code to access the inherited widget from didChangeDependencies() and listen to the stream in the initState() it seems to be working properly.
Here I get hold of the inherited widget holding the stream through context.ancestorInheritedElementForWidgetOfExactType()
Is this ok to do so? Or what would be a flutter best practice in this case?
#override
void initState() {
super.initState();
//this works, but I don't know if this is good practice or has any side effects?
BlocProvider p = context.ancestorInheritedElementForWidgetOfExactType(BlocProvider)?.widget;
if (p != null) {
p.bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
}
Personally, I have not found any reason not to listen to BLoC state streams in initState. As long as you remember to cancel your subscription on dispose
If your BlocProvider is making proper use of InheritedWidget you should not have a problem getting your value inside of initState.
like So
void initState() {
super.initState();
_counterBloc = BlocProvider.of(context);
_subscription = _counterBloc.stateStream.listen((state) {
if (state.total > 20) {
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return TestPush();
}));
}
});
}
Here is an example of a nice BlocProvider that should work in any case
import 'package:flutter/widgets.dart';
import 'bloc_base.dart';
class BlocProvider<T extends BlocBase> extends StatefulWidget {
final T bloc;
final Widget child;
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}) : super(key: key);
#override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider =
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<BlocBase>> {
#override
Widget build(BuildContext context) {
return _BlocProviderInherited<T>(
bloc: widget.bloc,
child: widget.child,
);
}
#override
void dispose() {
widget.bloc?.dispose();
super.dispose();
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
final T bloc;
_BlocProviderInherited({
Key key,
#required Widget child,
#required this.bloc,
}) : super(key: key, child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
... and finally the BLoC
import 'dart:async';
import 'bloc_base.dart';
abstract class CounterEventBase {
final int amount;
CounterEventBase({this.amount = 1});
}
class CounterIncrementEvent extends CounterEventBase {
CounterIncrementEvent({amount = 1}) : super(amount: amount);
}
class CounterDecrementEvent extends CounterEventBase {
CounterDecrementEvent({amount = 1}) : super(amount: amount);
}
class CounterState {
final int total;
CounterState(this.total);
}
class CounterBloc extends BlocBase {
CounterState _state = CounterState(0);
// Input Streams/Sinks
final _eventInController = StreamController<CounterEventBase>();
Sink<CounterEventBase> get events => _eventInController;
Stream<CounterEventBase> get _eventStream => _eventInController.stream;
// Output Streams/Sinks
final _stateOutController = StreamController<CounterState>.broadcast();
Sink<CounterState> get _states => _stateOutController;
Stream<CounterState> get stateStream => _stateOutController.stream;
// Subscriptions
final List<StreamSubscription> _subscriptions = [];
CounterBloc() {
_subscriptions.add(_eventStream.listen(_handleEvent));
}
_handleEvent(CounterEventBase event) async {
if (event is CounterIncrementEvent) {
_state = (CounterState(_state.total + event.amount));
} else if (event is CounterDecrementEvent) {
_state = (CounterState(_state.total - event.amount));
}
_states.add(_state);
}
#override
void dispose() {
_eventInController.close();
_stateOutController.close();
_subscriptions.forEach((StreamSubscription sub) => sub.cancel());
}
}