My Widget of which i want to write widget test
InkWell(
key: const Key(kDraftTileKey),
onTap: () {
context.read<CreatorHubTapViewModel>().startPublishingFlowAgain(startPublishingFlowAgainPressed: startPublishingFlowAgainPressed);
},
child:...
);
Fucntion which called when user click on a tap
void startPublishingFlowAgainPressed() {
widget.viewModel.saveNFT(nft: widget.nft);
Navigator.of(context).pushNamed(RouteUtil.kRouteHome);
}
I create a Mock of this viewmodel using Mockito
class CreatorHubTapViewModel extends ChangeNotifier {
void onViewOnPylons({required VoidCallback onViewOnPylonsPressed}) {
onViewOnPylonsPressed.call();
}
void startPublishingFlowAgain({required VoidCallback startPublishingFlowAgainPressed}) {
startPublishingFlowAgainPressed.call();
}
}
Now in test when i try to make a stub on this function it is not calling thenAnswer also not calling Callback itself. Any way to achieve i just want to toggle my variable when this function calls in actual screen.
when(viewModel.startPublishingFlowAgain(startPublishingFlowAgainPressed: () {
clicked = true;
})).thenAnswer((realInvocation) {});
This is how you can test this function.
when(viewModel.startPublishingFlowAgain(startPublishingFlowAgainPressed: anyNamed("startPublishingFlowAgainPressed"))).thenAnswer((realInvocation) {
clicked = true;
});
Assume that I'm on page-A now. I navigate to page-B. When I pop the page-B and come back to page-A, currently nothing happens. How can I reload page-A and load the new API data from the init state of page-A? Any Ideas?
first main page
void refreshData() {
id++;
}
FutureOr onGoBack(dynamic value) {
refreshData();
setState(() {});
}
void navigateSecondPage() {
Route route = MaterialPageRoute(builder: (context) => SecondPage());
Navigator.push(context, route).then(onGoBack);
}
second page
RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go Back'),
),
more details check here
From the explanation that you have described, so when you are popping the page.
This below Code will be on the second page.
Navigator.of(context).pop(true);
so the true parameter can be any thing which ever data that you want to send.
And then when you are pushing from one page to another this will be the code.
this is on the first page.
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PageOne()),
);
so if you print the result you will get the bool value that you send from the second page.
And based on bool you can hit the api. if the bool true make an api call.
Let me know if this works.
There are one more solutions for this situtation.
İf you want to trigger initState again
You can use pushAndRemoveUntil method for navigation. ( if you use only push method this is not remove previous page on the stack)
You can use key
You can set any state manegement pattern.( not for only trigger initState again)
There are 2 ways:
Using await
await Navigator.push(context, MaterialPageRoute(builder: (context){
return PageB();
}));
///
/// REFRESH DATA (or) MAKE API CALL HERE
Passing fetchData constructor to pageB and call it on dispose of pageB
class PageA {
void _fetchData() {}
Future<void> goToPageB(BuildContext context) async {
await Navigator.push(context, MaterialPageRoute(builder: (context) {
return PageB(onFetchData: _fetchData);
}));
}
}
class PageB extends StatefulWidget {
const PageB({Key? key, this.onFetchData}) : super(key: key);
final VoidCallback? onFetchData;
#override
State<PageB> createState() => _PageBState();
}
class _PageBState extends State<PageB> {
#override
void dispose() {
widget.onFetchData?.call();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
I have a app with a List<Match> as state, which is kept in
class MatchesChangeNotifier extends ChangeNotifier {
List<Match> matches;
refresh() async {
matches = await MatchesFirestore.fetchMatches(); // goes to db
notifyListeners();
}
}
then I have the following widgets (simplified)
class AvailableMatches extends StatelessWidget {
#override
Widget build(BuildContext context) {
var matches = context.watch<MatchesChangeNotifier>().matches;
return Column(children: matches.map(e => MatchInfo(e)).toList());
}
}
class MatchInfo extends StatelessWidget {
Match match;
MatchInfo(this.match);
Widget build(BuildContext context) {
// use match for a bunch of stuff
return InkWell(
child: ...,
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) => MatchDetails(match)));
await context.read<MatchesChangeNotifier>().refresh();
}
}
In MatchDetails I have a bunch of buttons. One of them has this callback
onTap() async => await context.read<MatchesChangeNotifier>().refresh()
When I tap this button I see (debugging) that
AvailableMatches gets rebuilt (since it watches matches)
MatchInfo gets rebuilt (since it's down the tree w.r.t. AvailableMatches)
MatchDetails doesn't get re-built
Why is that? Is it because it is somehow called in the onTap function?
After I press the button I would like to see MatchDetails change when it's on top of the screen (i.e. the user is seeing it). Instead this doesn't happen
If I modify the way I push MatchDetails from MatchInfo with this ugly trick the thing works but I would like to avoid it
await Navigator.push(context,
MaterialPageRoute(builder: (context) =>
MatchDetails(context.watch<MatchesChangeNotifier>().matches.firstWhere((m) => m.id == match.id))));
I have noticed a new lint issue in my project.
Long story short:
I need to use BuildContext in my custom classes
flutter lint tool is not happy when this being used with aysnc method.
Example:
MyCustomClass{
final buildContext context;
const MyCustomClass({required this.context});
myAsyncMethod() async {
await someFuture();
# if (!mounted) return; << has no effect even if i pass state to constructor
Navigator.of(context).pop(); # << example
}
}
UPDATE: 17/September/2022
It appears that BuildContext will soon have a "mounted" property
So you can do:
if (context.mounted)
It basically allows StatelessWidgets to check "mounted" too.
Reference: Remi Rousselet Tweet
Update Flutter 3.7+ :
mounted property is now officially added to BuildContext, so you can check it from everywhere, whether it comes from a StatefulWidget State, or from a Stateless widget.
While storing context into external classes stays a bad practice, you can now check it safely after an async call like this :
class MyCustomClass {
const MyCustomClass();
Future<void> myAsyncMethod(BuildContext context) async {
Navigator.of(context).push(/*waiting dialog */);
await Future.delayed(const Duration(seconds: 2));
if (context.mounted) Navigator.of(context).pop();
}
}
// Into widget
#override
Widget build(BuildContext context) {
return IconButton(
onPressed: () => const MyCustomClass().myAsyncMethod(context),
icon: const Icon(Icons.bug_report),
);
}
// Into widget
Original answer
Don't stock context directly into custom classes, and don't use context after async if you're not sure your widget is mounted.
Do something like this:
class MyCustomClass {
const MyCustomClass();
Future<void> myAsyncMethod(BuildContext context, VoidCallback onSuccess) async {
await Future.delayed(const Duration(seconds: 2));
onSuccess.call();
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
#override
Widget build(BuildContext context) {
return IconButton(
onPressed: () => const MyCustomClass().myAsyncMethod(context, () {
if (!mounted) return;
Navigator.of(context).pop();
}),
icon: const Icon(Icons.bug_report),
);
}
}
Use context.mounted*
In StatefulWidget/StatelessWidget or in any class that has BuildContext:
void foo(BuildContext context) async {
await someFuture();
if (!context.mounted) return;
Navigator.pop(context); // No warnings now
}
* If you're in a StatefulWidget, you can also use just mounted instead of context.mounted
If your class can extend from StatefulWidget then adding
if (!mounted) return;
would work!
EDIT
I had this issue again and again and here's the trick - use or declare variables using context before using async methods like so:
MyCustomClass{
const MyCustomClass({ required this.context });
final buildContext context;
myAsyncMethod() async {
// Declare navigator instance (or other context using methods/classes)
// before async method is called to use it later in code
final navigator = Navigator.of(context);
await someFuture();
// Now use the navigator without the warning
navigator.pop();
}
}
EDIT END
As per Guildem's answer, he still uses
if (!mounted) return;
so what's the point of adding more spaghetti code with callbacks? What if this async method will have to pass some data to the methods you're also passing context? Then my friend, you will have even more spaghetti on the table and another extra issue.
The core concept is to not use context after async bloc is triggered ;)
If you want to use mounted check in a stateless widget its possible by making an extension on BuildContext
extension ContextExtensions on BuildContext {
bool get mounted {
try {
widget;
return true;
} catch (e) {
return false;
}
}
}
and then you can use it like this
if (context.mounted)
Inspiration taken from GitHub PR for this feature and it passes the same tests in the merged PR
you can use this approach
myAsyncMethod() async {
await someFuture().then((_){
if (!mounted) return;
Navigator.of(context).pop();
}
});
In Flutter 3.7.0 BuildContext has the property mounted. It can be used both in StatelessWidget and StatefulWidgets like this:
void bar(BuildContext context) async {
await yourFuture();
if (!context.mounted) return;
Navigator.pop(context);
}
Just simpliy creat a function to call the navigation
void onButtonTapped(BuildContext context) {
Navigator.of(context).pop();
}
To avoid this in StatelessWidget you can refer to this example
class ButtonWidget extends StatelessWidget {
final String title;
final Future<String>? onPressed;
final bool mounted;
const ButtonWidget({
super.key,
required this.title,
required this.mounted,
this.onPressed,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
const SizedBox(height: 20),
Expanded(
child: ElevatedButton(
onPressed: () async {
final errorMessage = await onPressed;
if (errorMessage != null) {
// This to fix: 'Do not use BuildContexts across async gaps'
if (!mounted) return;
snackBar(context, errorMessage);
}
},
child: Text(title),
))
],
);
}
}
I handle it with converting the function become not async and using then
Future<void> myAsyncMethod(BuildContext context) {
Navigator.of(context).push(/*waiting dialog */);
Future.delayed(const Duration(seconds: 2)).then(_) {
Navigator.of(context).pop();
});
}
just save your navigator or whatever needs a context to a variable at the beginning of the function
myAsyncMethod() async {
final navigator = Navigator.of(context); // 1
await someFuture();
navigator.pop(); // 2
}
DO NOT use BuildContext across asynchronous gaps.
Storing BuildContext for later usage can easily lead to difficult to diagnose crashes. Asynchronous gaps are implicitly storing BuildContext and are some of the easiest to overlook when writing code.
When a BuildContext is used from a StatefulWidget, the mounted property must be checked after an asynchronous gap.
So, I think, you can use like this:
GOOD:
class _MyWidgetState extends State<MyWidget> {
...
void onButtonTapped() async {
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return;
Navigator.of(context).pop();
}
}
BAD:
void onButtonTapped(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1));
Navigator.of(context).pop();
}
I have a Card() widget which contains a ListTile() widget.
One of the ListTile() widget's properties is enabled. I would like to dynamically set the value of this enabled property by using the outcome of a Future<bool> which uses async and await. Is this possible?
Here is the Card() widget with the ListTile() in it
Card myCard = Card(
child: ListTile(
title: Text('This is my list tile in a card'),
enabled: needsToBeEnabled(1),
),
);
Here is my Future
Future<bool> cardNeedsToBeEnabled(int index) async {
bool thisWidgetIsRequired = await getAsynchronousData(index);
if (thisWidgetIsRequired == true) {
return true;
} else {
return false;
}
}
Other attempts
I have tried to use a Future Builder. This works well when I'm building a widget, but in this case I'm trying to set the widget's property; not build a widget itself.
You cannot do that for two reason:
enable does not accept Future<bool> but bool
you need to update the state after result is received (you need a StatefullWidget)
There are 1 million way to do what you want to do and one of this is FutureBuilder but if you want to not rebuild all widget you can use this flow (your main widget need to be Statefull):
create a local variable that contains your bool value, something like bool _enabled
on initState() method override you can launch the call that get asynchronous data and using the then() extension method you can provide the new state to your widget when the call will be completed.
Something like:
#override
void initState() {
super.initState();
getAsynchronousData(index).then((result) {
if (result == true) {
_enabled = true;
} else {
_enabled = false;
}
setState(() {});
});
}
assign the boolean var to the ListTile widget