I have learned that it brings problems when initating a future- or streambuilder inside the build of a widget since it will lead to unwanted fetching of data.
To do this you pass the stream outside of the build-method. But this leads to another problem. In my case I have to get an id-argument from the last widget but that can only be reached inside the build. I have searched the internet for explanations on how to solve this but I cant find an easy and clean explanation on the best way to do this.
A good solution that initialize the future with the build outside of the build.
My material app:
MaterialApp(
routes: appRoutes,
);
My routes table:
var appRoutes = {
Screen1.routeName: (context) => Screen1(),
Screen2.routeName: (context) => Screen2(),
};
How I send the argument from screen1:
onTap: () {
Navigator.of(context).pushNamed(
Screen2.routeName,
arguments: id,
);
},
screen2: (which doesnt work this way)
class Screen2 extends StatefulWidget {
static const routeName = '/screen2';
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
#override
final String id = ModalRoute.of(context)!.settings.arguments as String; <====== Impossible
final _stream = FirestoreService().getData(id);
Widget build(BuildContext context) {
return FutureBuilder<Map>(
future: _stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
print('succes');
} else {
print('Fail');
}
});
}
}
Way1: late keyword (unstable)
Use late keyword for lazy initialiation of your argument, stream.
class Screen2 extends StatefulWidget {
static const routeName = '/screen2';
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
#override
late final String id = ModalRoute.of(context)!.settings.arguments as String; <====== Impossible
late final _stream = FirestoreService().getData(id);
Widget build(BuildContext context) {
...
}
}
Way2: onGerateRoute + constructor (good)
You can check this answer for using onGenerateRoute. It is more proper way.
Way3: Create extraction delegation screen (good)
In the docs, the best practice is creating a delegation screen for extract arguments.
It parse argument in build method and pass it to new screen.
// A Widget that extracts the necessary arguments from
// the ModalRoute.
class ExtractArgumentsScreen extends StatelessWidget {
const ExtractArgumentsScreen({super.key});
static const routeName = '/extractArguments';
#override
Widget build(BuildContext context) {
// Extract the arguments from the current ModalRoute
// settings and cast them as ScreenArguments.
final args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;
return Scaffold(
appBar: AppBar(
title: Text(args.title),
),
body: Center(
child: Text(args.message),
),
);
}
}
Related
I am making a list of stateless widget as shown below and passing the id as the parameter to the widgets.
Code for cartPage:-
class Cart extends StatefulWidget {
#override
_CartState createState() => _CartState();
}
class _CartState extends State<Cart> {
bool loading=true;
List<CartTile> cartTiles=[];
#override
void initState() {
super.initState();
if(currentUser!=null)
getData();
}
getData()async
{
QuerySnapshot snapshot=await cartReference.doc(currentUser.id).collection('cartItems').limit(5).get();
snapshot.docs.forEach((doc) {
cartTiles.add(CartTile(id: doc.data()['id'],index: cartTiles.length,));
});
setState(() {
loading=false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: loading?Center(child:CircularProgressIndicator():SingleChildScrollView(
child: Column(
children: cartTiles,
),
),
);
}
}
Code for CartTile:-
class CartTile extends StatelessWidget {
final String id;
CartTile({this.id,});
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: productReference.doc(id).snapshots(),
builder: (context,snapshot)
{
//here am using the snapshot to build the cartTile.
},
);
}
}
So, my question is whenever I will call setState in my homepage then will the stateless widget be rebuilt and increase my document reads. Because i read somewhere that when we pass the same arguments or parameters to a stateless widget then due to its cache mechanism it doesn't re build. If it will increase my reads then is there any other way to solve this problem?
I have a Widget which uses bloc builder to map the different state of widget.
class BodyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<NewsBloc, NewsState>(builder: (context, state) {
return state.map(
.....
);
});
}
....
}
The BodyWidget is created in a Widget with BlocProvider.
class MainPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
getIt<NewsBloc>()..add(const NewsEvent.fetchNewsData()),
child: BodyWidget(),
);
}
....
}
And the NewsBloc is defined as
#injectable
class NewsBloc extends Bloc<NewsEvent, NewsState> {
final GetNews getNews;
NewsBloc({
#required this.getNews,
}) : super(const _Initial());
#override
Stream<NewsState> mapEventToState(
NewsEvent event,
) async* { ... }
}
I am using get_it and injectable for Dependency Injection.
Now I am trying to write a simple widget test for BodyWidget and I am not so sure how to inject all these dependency in test.
class MockBuildContext extends Mock implements BuildContext {}
class MockNewsBloc extends Mock implements NewsBloc {}
void main() {
ForYouNewsTab _widget;
MockBuildContext _context;
NewsBloc _newsBloc;
Widget makeTestableWidgets({Widget child}) {
return MaterialApp(
home: Scaffold(
// body: BlocProvider(
// create: (_context) => getIt<NewsBloc>(),
// child: child,
// ),
body: child,
),
);
}
setUp(() {
_context = MockBuildContext();
_widget = ForYouNewsTab();
});
test('ForYouNewsTab is sub class of StatelessWidget', () {
expect(_widget, isA<StatelessWidget>());
});
testWidgets('should return sized box for initial state',
(WidgetTester tester) async {
await tester.pumpWidget(makeTestableWidgets(child: _widget));
});
}
I did search in stackoverflow, but could not found a solution that works form me.
I solved my issue by following very basic steps. Not so sure if its the right way. Anyway if anyone ever comes to the same problem, it might help them.
class MainPage extends StatelessWidget {
//added line
final NewsBloc newsBloc;
const MainPage({
Key key,
#required this. newsBloc,
})
#override
Widget build(BuildContext context) {
return BlocProvider(
// changed line
// create: (context) => getIt<NewsBloc>()..add(const NewsEvent.fetchNewsData()),
create: (context) => newsBloc..add(const NewsEvent.fetchNewsData()),
child: BodyWidget(),
);
}
....
}
Now in my test case I can create MockNewsBloc and inject it easily to the MainPage when it is under testing.
Minimal reproducible code:
void main() => runApp(FooApp());
class FooApp extends StatefulWidget {
#override
_FooAppState createState() => _FooAppState();
}
class _FooAppState extends State<FooApp> {
bool _showPage2 = false;
void _onPressed(bool value) => setState(() => _showPage2 = value);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Navigator(
onPopPage: (route, result) => route.didPop(result),
pages: [
MaterialPage(child: Page1(onPressed: _onPressed)),
if (_showPage2) MaterialPage(child: Page2()),
],
),
);
}
}
class Page1 extends StatelessWidget {
final ValueChanged<bool> onPressed;
const Page1({Key key, this.onPressed}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Page1')),
body: ElevatedButton(
onPressed: () => onPressed(true),
child: Text('Page2'),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
void dispose() {
super.dispose();
print('dispose');
}
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Page2')));
}
didPop:
When this function returns true, the navigator removes this route from the history but does not yet call dispose. Instead, it is the route's responsibility to call NavigatorState.finalizeRoute, which will in turn call dispose on the route. This sequence lets the route perform an exit animation (or some other visual effect) after being popped but prior to being disposed.
But in my example, you can see without having to call NavigatorState.finalizeRoute in Page2, dispose method does get called contradiciting Docs.
It's done internally when using MaterialPage/_PageBasedMaterialPageRoute. You can poke around in the code starting in the Navigator class, which appears to lead up to this OverlayRoute class. If you do want to trace through yourself, it wasn't a walk in the park for me and you'll have to pay close attention to how each class is related.
This class has the finishedWhenPopped getter, which is true by default. And if you look at the didPop override implementation right below the getter definition, didPop will internally call finalizeRoute when finishedWhenPopped is true.
Implementation from OverlayRoute class
#protected
bool get finishedWhenPopped => true;
#override
bool didPop(T? result) {
final bool returnValue = super.didPop(result);
assert(returnValue);
if (finishedWhenPopped)
navigator!.finalizeRoute(this);
return returnValue;
}
This is true only for at least MaterialPage/_PageBasedMaterialPageRoute. Other implementations don't necessarily do this.
How to separate setState in Abc class?
I have lots of this kind of methods in one class. Now, my class is too long. Is there any way to separate this class to subclass or another way?
class _AbcState extends State<Abc> {
String name = 'name';
double number = 15.5;
int value = 10;
//REMOVE FROM ABC class
void changeName() {
setState(() {
name = 'new Name';
});
}
void changeNumber() {
setState(() {
number = 10.0;
});
}
void changeValue() {
setState(() {
value = 5;
});
}
//
#override
Widget build(BuildContext context) {
return Scaffold();
}
}
There are 2 ways to do this:
First:
You can separate the individual input fields into their stateful widgets if necessary and let them handle their individual states internally, only giving you the changed data in respective onChange callbacks.
This is the simplest and quickest solution.
A better approach is to:
Second:
Use a library/dependency/package for the state management in your application:
This is an example of the Provider flutter package with ChangeNotifier:
You can check the official docs here for detailed explanations and examples:
Provider
ChangeNotifier
Simple example:
Create a class extending the ChangeNotifier:
Whenever the some data changes in the ChangeNotifer you can call the notifyListeners() method.
class UserProvider extends ChangeNotifier {
String username;
String password;
void updateUsername(String username) {
this.username = username;
notifyListeners();
}
void updatePassword(String password) {
this.password = password;
notifyListeners();
}
}
Wrap your widget tree in a ChangeNotifierProvider widget:
This widget will let you access the instance of the ChangeNotifier class anywhere in down the widget tree like InheritedWidget.
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<UserProvider>(
lazy: false,
create: (context) => UserProvider(),
child: Home(),
);
}
}
Access the instance of ChangeNotifier from anywhere down the widget tree with and call any methods or data variables:
/// Get an instance of the UserProvider in the ancestors of the current widget tree like this.
UserProvider userProvider = Provider.of<UserProvider>(context);
/// Call any method inside the UserProvider class like this
userProvider.updateUsername("John Doe");
/// access any data variables inside the UserProvider class like this.
User userInfo = userProvider.username;
Changing UI based on data/state changes in ChangeNotifier:
You can use the Consumer and Selector widgets in the provider package, which provide an efficient ways to redraw the UI based on certain parameters of the ChangeNotifier class, when the notifyListeners() method is called from the ChangeNotifier class.
Consumer:
This widget will simply rebuild the entire widget tree wrapped inside it, whenever ANY data changes inside the ChangeNotifier with notifyListeners().
class Home extends StatelessWidget {
const Home({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<UserProvider>(
builder: (context, userProvider, _) {
return Scaffold(
appBar: AppBar(
title: const Text(userProvider.username),
),
body: Center(
child: Text(
"Your password is: ${userProvider.password}!",
),
),
);
},
);
}
}
Selector:
This widget will allow for more finer control over your widget rebuild. It will rebuild only part of the widget based on specific data variable changes inside the ChangeNotifier.
The below example will only rebuild the Text widget if the password data changes inside our UserProvider ChangeNotifier class.
class Home extends StatelessWidget {
const Home({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<UserProvider>(
builder: (context, userProvider, _) {
return Scaffold(
appBar: AppBar(
title: const Text(userProvider.username),
),
body: Center(
child: Selector<UserProvider, String>(
selector: (context, userProvider) => userProvider.password,
builder: (context, state, child) {
return Text(
"Your password is: ${userProvider.password}!",
);
},
),
),
);
},
);
}
}
We want to show an AlertDialog after some asynchronous processing such as network processes.
When calling 'showAlertDialog ()' from an external class, I want to call it without context. Is there a good way?
class SplashPage extends StatelessWidget implements SplashView {
BuildContext _context;
#override
Widget build(BuildContext context) {
this._context = context;
...
}
I've considered the above method, but I'm worried about side issues.
Help
My current code
class SplashPage extends StatelessWidget implements SplashView {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MyStoreColors.eats_white1_ffffff,
body: Center(
child: new SvgPicture.asset('assets/ic_splash.svg'),
),
);
}
#override
void showAlertDialog() {
showDialog<void>(
context: /*How to get context?*/,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Not in stock'),
content: const Text('This item is no longer available'),
actions: <Widget>[
FlatButton(
child: Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
#override
void moveToHomeContainer() {
}
#override
void moveToLoginContainer() {
}
}
To show an AlertDialog you need the context, but in StatelessWidget you do not have access to it directly as in StatefulWidget.
Few options are [1]:
passing it as GlobalKey [2]
passing build context as parameter to any other function inside StatelessWidget
use a service to inject the dialog without context [3]
Cheers.
You should trigger rebuild when the async event complete, either convert your widget to StatefulWidget and call setState() or use a state management solution like Bloc.
For example using StatefulWidget your code will look like this:
class SplashPage extends StatefulWidget {
#override
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> implements SplashView {
bool _asynOpDone = false;
/// Call this when the async operation is done.
void _onAsynOpDone() => setState(() => _asyncOpDone = true);
#override
Widget build(BuildContext context) {
if (_asyncOpDone) showAlertDialog(context);
return Scaffold(
...,
///
);
}
#override
void showAlertDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: ...,
);
}
}
You can apply Builder pattern concept to simplify this.
There is a little example here.
button_builder.dart