I am implementing navigation in my flutter app using onGenerateRoute in MaterialApp.
For one of the routes, I am getting this error when I press the backbutton Flutter provides in Appbar.
E/flutter (22996): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)]
Unhandled Exception: Failed assertion: boolean expression must not be
null
E/flutter (22996): #0 ModalRoute.willPop
(package:flutter/src/widgets/routes.dart) E/flutter (22996):
E/flutter (22996): #1 NavigatorState.maybePop
(package:flutter/src/widgets/navigator.dart:1964:57) E/flutter
(22996):
E/flutter (22996): #2 Navigator.maybePop
(package:flutter/src/widgets/navigator.dart:1291:34)
E/flutter (22996): #3 BackButton.build.
(package:flutter/src/material/back_button.dart:91:19)
Can you please help me understand what is wrong ?
My Route settings ->
Route<dynamic> routes(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return HomePage();
},
);
break;
case '/contactlist':
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return FriendsList();
},
);
break;
case '/ChatroomFormAdd':
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return const ChatroomInfo(
mode: 'Add');
},
);
break;
case '/ChatroomFormEdit':
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return const ChatroomInfo(
mode: 'Edit');
},
);
break;
case '/ChatroomFormView':
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return const ChatroomInfo(
mode: 'View');
},
);
break;
case '/ChatroomFormApprove':
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return const ChatroomInfo(
mode: 'Approve');
},
);
break;
case '/errorscreen':
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return ErrorScreen();
},
);
break;
default:
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return ChatDetail(chatroomID: chatroomID);
},
);
break;
}
}
Back button works for all the routes expect when I navigate back from the following route:
Navigator.of(context).pushNamed('/ChatroomFormView');
Did you use WillPopScope widget? if so you need to provide a Function that return Future<boolean> to onWillPop parameter.
WillPopScope(
onWillPop: () {
//need to return Future<bool> here
}
}
Related
I have a StatefulWidget. Then when I click a button, it shows an alert dialog. When I implement:
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Hello"),
);
}
}
Everything works fine. But when I transfered the things inside the builder to a different StatefulWidget, then this error occurs:
A build function returned null.
I/flutter ( 3647): The offending widget is: Builder
I/flutter ( 3647): Build functions must never return null. To return an empty space that causes the building widget to
I/flutter ( 3647): fill available room, return "new Container()". To return an empty space that takes as little room as
I/flutter ( 3647): possible, return "new Container(width: 0.0, height: 0.0)".
Here is the code:
Here is the calling StatefulWidget:
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
LastVacDialog(
currentDose: currDose,
currentDate: currDate,
currentIndex: i,
setValue: changeDoseValueAndDate,
);
},
);
},
Here is the new StatefulWidget:
class LastVacDialog extends StatefulWidget {
LastVacDialog({
this.currentDose,
this.currentDate,
this.setValue,
this.currentIndex,
});
final int currentDose;
final DateTime currentDate;
final void Function(int, DateTime, int) setValue;
final currentIndex;
#override
LastVacDialogState createState() => new LastVacDialogState();
}
class LastVacDialogState extends State<LastVacDialog> {
int _dose;
DateTime _today;
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Last Dose"),
);
}
}
Is there something wrong with my code? I just omitted some variables for simplicity.
Add the Word Return in Front of - LastVacDialog
builder: (BuildContext context) {
return LastVacDialog(
...
As Error is Stating Build function must never return null. So return your LastVacDialog Widget by adding return in front of it.
Let's say I have something like this:
return FutureBuilder(
future: _loadingDeals,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh,
...
)
}
)
In the _handleRefresh method, I want to programmatically trigger the re-run of the FutureBuilder.
Is there such a thing?
The use case:
When a user pulls down the refreshIndicator, then the _handleRefresh simply makes the FutureBuilder rerun itself.
Edit:
Full code snippet end to end, without the refreshing part. I've switched to using the StreamBuilder, how will the refreshIndicator part fit in all of it?
class DealList extends StatefulWidget {
#override
State<StatefulWidget> createState() => new _DealList();
}
class _DealList extends State<DealList> with AutomaticKeepAliveClientMixin {
// prevents refreshing of tab when switch to
// Why? https://stackoverflow.com/q/51224420/1757321
bool get wantKeepAlive => true;
final RestDatasource api = new RestDatasource();
String token;
StreamController _dealsController;
#override
void initState() {
super.initState();
_dealsController = new StreamController();
_loadingDeals();
}
_loadingDeals() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
this.token = prefs.getString('token');
final res =
this.api.checkInterests(this.token).then((interestResponse) async {
_dealsController.add(interestResponse);
return interestResponse;
});
return res;
}
_handleRefresh(data) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final token = prefs.getString('token');
await this.api.checkInterests(token).then((interestResponse) {
_dealsController.add(interestResponse);
});
return null;
}
#override
Widget build(BuildContext context) {
super.build(context); // <-- this is with the wantKeepAlive thing
return StreamBuilder(
stream: _dealsController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
...
}
if (snapshot.connectionState != ConnectionState.done) {
return Center(
child: CircularProgressIndicator(),
);
}
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return Text('No deals');
}
if (snapshot.hasData) {
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data['deals'].length,
itemBuilder: (context, index) {
final Map deal = snapshot.data['deals'][index];
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DealsDetailPage(
dealDetail: deal,
),
),
);
},
title: Text(deal['name']),
subtitle: Text(deal['expires']),
);
},
),
}
},
);
}
}
Why not using a StreamBuilder and a Stream instead of a FutureBuilder?
Something like that...
class _YourWidgetState extends State<YourWidget> {
StreamController<String> _refreshController;
...
initState() {
super...
_refreshController = new StreamController<String>();
_loadingDeals();
}
_loadingDeals() {
_refreshController.add("");
}
_handleRefresh(data) {
if (x) _refreshController.add("");
}
...
build(context) {
...
return StreamBuilder(
stream: _refreshController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return RefreshIndicator(
onRefresh: _handleRefresh(snapshot.data),
...
)
}
);
}
}
I created a Gist with the Flutter main example using the StreamBuilder, check it out
Using StreamBuilder is a solution, however, to trigger the FutureBuilder programmatically, just call setState, it'll rebuild the Widget.
return RefreshIndicator(
onRefresh: () {
setState(() {});
},
...
)
I prefer FutureBuilder over StreamBuilder since I am using Firestore for my project and you get billed by reads so my solution was this
_future??= getMyFuture();
shouldReload(){
setState(()=>_future = null)
}
FutureBuilder(
future: _future,
builder: (context, snapshot){
return Container();
},
)
and any user activity that needs you to get new data simply call shouldReload()
I am trying to generate a dynamic list of slivers from a GET request. But I am having trouble, it seems the response data is null. Here is my code:
import 'package:flutter/material.dart';
import 'boardSummary.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import 'package:flutter/foundation.dart';
class HomepageBody extends StatefulWidget{
#override
State<StatefulWidget> createState() {
return HomepageBodyState();
}
}
class HomepageBodyState extends State <HomepageBody> {
#override
Widget build(BuildContext context) {
return new Expanded(
child: new Container(
color: new Color(0xFF736AB7),
child: new FutureBuilder <List<Post>>(
future: fetchPost(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) print(snapshot.error);
else
return jobscroll(context, snapshot);
//: Center(child: CircularProgressIndicator());
}
),
),
);
}
}
Future<List<Post>>fetchPost() async {
final response = await http.get('https://jsonplaceholder.typicode.com/posts?userId=1');
return compute(parsePosts, response.body);
}
List<Post> parsePosts(String responseBody){
final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Post>((json)=>Post.fromJson(json)).toList();
}
class Post {
final String userId;
final String hashtag;
final String price;
final String description;
Post({this.userId, this.hashtag, this.price, this.description});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
userId: json['userId'],
hashtag: json['id'],
price: json['title'],
description: json['body'],
);
}
}
Widget jobscroll(BuildContext context, AsyncSnapshot snapshot) {
List data = snapshot.data;
return CustomScrollView(
scrollDirection: Axis.vertical,
shrinkWrap: false,
slivers: <Widget>[new SliverPadding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
sliver: new SliverList(
delegate: new SliverChildBuilderDelegate(
(context, index) => new BoardSummary(data[index]),
childCount: data.length,
),
),
),
],
);
}
BoardSummary is a stateless widget class that just takes creates a "card" using the properties on each "Post". It takes in a object of type "Post."
The console spit out a lot of errors but this was the last one that seemed meaningful it also appeared in my emulator:
I/flutter (18882): Another exception was thrown: NoSuchMethodError: The getter 'length' was called on null.
EDIT Here's also the first few lines from my slack trace:
E/flutter (13964): type 'int' is not a subtype of type 'String'
E/flutter (13964): #0 new Post.fromJson (file:///home/daniel/Desktop/testapp/lib/ui/homePageBody.dart:76:19)
E/flutter (13964): #1 parsePosts.<anonymous closure> (file:///home/daniel/Desktop/testapp/lib/ui/homePageBody.dart:60:40)
E/flutter (13964): #2 MappedListIterable.elementAt (dart:_internal/iterable.dart:414:29)
E/flutter (13964): #3 ListIterable.toList (dart:_internal/iterable.dart:219:19)
E/flutter (13964): #4 parsePosts (file:///home/daniel/Desktop/testapp/lib/ui/homePageBody.dart:60:56)
E/flutter (13964): #5 _IsolateConfiguration.apply (package:flutter/src/foundation/isolates.dart:88:16)
E/flutter (13964): #6 _spawn.<anonymous closure> (package:flutter/src/foundation/isolates.dart:96:30)
What should I do?
Edit casting this piece of code to string made it work, just have overflow to fix. If there is a better solution feel free to share!
return Post(
userId: json['userId'].toString(),
hashtag: json['id'].toString(),
price: json['title'].toString(),
description: json['body'].toString(),
);
FutureBuilder builds immediately even when the value is not yet available, because build() is sync and can't be delayed.
The FutureBuilder example shows how to check if the value is already available (default: ... and not snapshot.hasError):
new FutureBuilder<String>(
future: _calculation, // a Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none: return new Text('Press button to start');
case ConnectionState.waiting: return new Text('Awaiting result...');
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Text('Result: ${snapshot.data}');
}
},
)
I want to have an inherited widget at the root of my application, which will contain my data providers, which I would use throughout the app. So I have this inherited widget, but every time I try to load it I get this The getter 'data' was called on null and I can't figure out why.
So here's my main.dart:
void main() => runApp(new MatAppRoot());
class MatAppRoot extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'MyCoolApp',
routes: <String, WidgetBuilder>{
'Login': (BuildContext context) => new LoginPage(),
'Cool': (BuildContext context) => new CoolPage(),
},
home: new CoolApp(),
);
}
}
class CoolAppextends StatefulWidget {
final Widget child;
CoolApp({this.child});
#override
CoolAppState createState() => new CoolAppState();
static CoolAppState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(CoolInherit) as CoolInherit).data;
}
}
class CoolAppState extends State<CoolApp> {
String randomString = 'AYEEAS!!!';
#override
void initState() { super.initState();
Navigator.of(context).pushNamedAndRemoveUntil('Login', (Route<dynamic> route) => false);
}
#override
Widget build(BuildContext context) {
return new CoolInherit(
data: this,
child: new LoginPage(),
);
}
}
class CoolInherit extends InheritedWidget {
final CoolAppState data;
CoolInherit({
Key key,
this.data,
Widget child,
}): super(
key: key,
child: child
);
#override
bool updateShouldNotify(CoolInherit old) {
return true;
}
}
then my LoginPage basically redirects after the login like this:
if (logInSuccessful) {
Navigator.of(context).pushNamedAndRemoveUntil('Cool', (Route<dynamic> route) => false);
}
In my Cool page I try to load another page when clicking a button like this:
viewCoolDetails() {
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new CoolDetailsPage()),
);
}
but in my CoolDetailsPage it crashes when I do this:
#override
Widget build(BuildContext context) {
final inheritedWidget = CoolApp.of(context);
print(inheritedWidget.randomString); <-- ERROR: The getter 'data' was called on null
return new Text('Cool!');
}
Error Details:
I/flutter ( 6129): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 6129): The following NoSuchMethodError was thrown building CoolDetailsPage(dirty, state:
I/flutter ( 6129): _CoolDetailsPage#ba0bb):
I/flutter ( 6129): The getter 'data' was called on null.
I/flutter ( 6129): Receiver: null
I/flutter ( 6129): Tried calling: data
I/flutter ( 6129):
I/flutter ( 6129): When the exception was thrown, this was the stack:
I/flutter ( 6129): #0 Object.noSuchMethod (dart:core/runtime/libobject_patch.dart:46:5)
I/flutter ( 6129): #1 CoolApp.of (/lib/main.dart:56:83)
... etc etc
main.dart:56 is return (context.inheritFromWidgetOfExactType(CoolInherit) as CoolInherit).data; and so if my detective work is up to par, I suspect it is something to with navigations/context, which is preventing my final widget from accessing the inheritedWidget, but I'm not sure about that.
UPDATE:
the best I can tell, I need to insert my InheritedWidget at a higher level; before the navigator. so I inserted this into the MaterialApp:
builder: (context, child) {
return new CoolApp(child: child);
},
but that didn't seen to work...
E/flutter (32321): [ERROR:topaz/lib/tonic/logging/dart_error.cc(16)] Unhandled exception:
E/flutter (32321): Navigator operation requested with a context that does not include a Navigator.
E/flutter (32321): The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
E/flutter (32321): #0 Navigator.of.<anonymous closure> (package:flutter/src/widgets/navigator.dart:1180:9)
E/flutter (32321): #1 Navigator.of (package:flutter/src/widgets/navigator.dart:1187:6)
I had the same problem for a long time and I realized that if you wrap the MaterialApp with the Inherited Widget, your data is accessible through the entire app. But in your case, you need to pass data after the user login so to do that you need to create a new Navigator and wrap it with your Inherited Widget. You can see this project https://github.com/johnstef99/inherited-widget-demo
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'InheritedWidget Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyNav(),
);
}
}
Route generatePage(child) {
return MaterialPageRoute(builder: (context) => child);
}
class MyNav extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MyData(
data: 'omg',
child: Navigator(
onGenerateRoute: (settings) {
switch (settings.name) {
case 'page1':
return generatePage(PageOne());
case 'page2':
return generatePage(PageTwo());
case 'page3':
return generatePage(PageThree());
}
},
initialRoute: 'page1',
),
);
}
}
class MyData extends InheritedWidget {
MyData({Key key, this.child, this.data}) : super(key: key, child: child);
final Widget child;
final String data;
static MyData of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(MyData) as MyData);
}
#override
bool updateShouldNotify(MyData oldWidget) {
return true;
}
}
class PageOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Page 1'),
),
backgroundColor: Colors.red,
body: RaisedButton(
child: Text("Goto page 2, data=${MyData.of(context).data}"),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => PageTwo()));
},
),
);
}
}
class PageTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Page 2'),
),
backgroundColor: Colors.red,
body: RaisedButton(
child: Text("Goto page 3, data=${MyData.of(context).data}"),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => PageThree()));
},
),
);
}
}
class PageThree extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Page 3'),
),
backgroundColor: Colors.green,
body: RaisedButton(
child: Text("Goto page 4, data=${MyData.of(context).data}"),
onPressed: null,
),
);
}
}
That is because you're trying to access CoolApp which is in the route / from another route (dynamic).
But inside your dynamic route, there's no CoolApp. So CoolApp.of(context) returns null, and therefore accessing .data crashes.
You need to find a way to have a CoolApp instance inside your new route.
For more informations, take a look at Get access to the context of InheritedWidget
Yesterday I made a simple widget that fetches data to display some basic info, but I noticed that when I pop back to the list, the data is usually absent and I only get my error texts.
I figured this is due to these widgets originally being stateless, so I'm trying to convert them to stateful in order to reload the data when the page is loaded.
This is how I gather the data for my widget:
class BasicDogWidget extends StatefulWidget {
String URL;
BasicDogWidget(this.URL);
#override
createState() => new BasicDogWidgetState(URL);
}
class BasicDogWidgetState extends State<BasicDogWidget> {
String URL;
BasicDogWidgetState(this.URL);
var result;
var imageLink;
var dogName;
var dogType;
var dogColor;
var dogGender;
var dogAge;
#override
initState() {
fetchImageLink(URL).then((result) {
setState(imageLink = result);
});
fetchDogInfo(URL, 'datas-nev').then((result) {
setState(dogName = result);
});
fetchDogInfo(URL, 'datas-tipus').then((result) {
setState(dogType = result);
});
fetchDogInfo(URL, 'datas-szin').then((result) {
setState(dogColor = result);
});
fetchDogInfo(URL, 'datas-nem').then((result) {
setState(dogGender = result);
});
fetchDogInfo(URL, 'datas-kor').then((result) {
setState(dogAge = result);
});
super.initState();
}
#override
Widget build(BuildContext context) {
if (imageLink == null) {
return new Container();
}
if (dogName == null) {
return new Container();
}
if (dogType == null) {
return new Container();
}
if (dogColor == null) {
return new Container();
}
if (dogGender == null) {
return new Container();
}
if (dogAge == null) {
return new Container();
}
return buildBasicWidget(
imageLink, dogName, dogType, dogColor, dogGender, dogAge, URL);
}
}
However, it seems that the data collected by fetchDogInfo can't be passed in the setState method as it is a string.
E/flutter (12296): [ERROR:topaz/lib/tonic/logging/dart_error.cc(16)] Unhandled exception:
E/flutter (12296): type 'String' is not a subtype of type 'VoidCallback' of 'fn' where
E/flutter (12296): String is from dart:core
E/flutter (12296):
E/flutter (12296): #0 State.setState (package:flutter/src/widgets/framework.dart:1086)
E/flutter (12296): #1 BasicDogWidgetState.initState.<anonymous closure> (package:osszefogasaszanhuzokert/dog.dart:230)
Is there any way this issue can be bypassed?
You are executing async code
fetchImageLink(URL).then((result) {
which means fetchImageLink(URL) will eventually return a value and then then(...) is executed, but this call is async, which means it's added to the event queue for later execution and the code synchronically continues to execute until the end of initState and then build until this sync code is run to its completion, then the next "task" from the event queue is executed, which might be the then(...) part from your fetchImageLink() call if it already completed.
That shouldn't be a problem though.
You could just check if the value is available
#override
Widget build(BuildContext context) {
if(imageLink == null) {
return new Container(); // dummy widget until there is something real to render
}
... // your other build code
Flutter have FutureBuilder class. It provides a widget that does all the heavy lifting to support asynchronous Future objects. The nice thing about FutureBuilder is that is can be added directly to the widget tree, and based off the current status of the connection, the relevant UI can be displayed to indicate progress, a result or an error.
Example:
class JokePageState extends State<JokePage> {
Future<String> response;
initState() {
super.initState();
response = http.read(dadJokeApi, headers: httpHeaders);
}
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new FutureBuilder<String>(
future: response,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return const Icon(Icons.sync_problem);
case ConnectionState.waiting:
case ConnectionState.active:
return const CircularProgressIndicator();
case ConnectionState.done:
final decoded = json.decode(snapshot.data);
if (decoded['status'] == 200) {
return new Text(decoded['joke']);
} else {
return const Icon(Icons.error);
}
}
},
),
),
);
}
}