I've changed from Statefulwidget using initState to fetch the data and Futurebuilder to load it to Futureprovider. But it seems like Futureprovider is execute build method twice, while my previous approach executed it once. Is this behaviour normal?
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg()
);
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
if (reportList == null) {
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Det finns inga rapporter."));
}
print(reportList.length);
return Container();
}
}
Im relativly new to flutter but I think its because StatelessWidget is #immutable, which means whenever something changes it needs to rebuild itself.
At first build there is async calling made and ReportWidg() is rendered.
Then this line final reportList = Provider.of<List<ReportModel>>(context); get new fetched data as result of async function therefore immutable widget needs to rebuild itself because it cannot be "changed".
In object-oriented and functional programming, an immutable object
(unchangeable object) is an object whose state cannot be modified
after it is created. ... This is in contrast to a mutable object
(changeable object), which can be modified after it is created
or am I wrong ?
I suspect your FutureProvider should be hoisted to a single instantiation, like placed into a global variable outside any build() methods. This will of course cache the result, so you can set it up for rebuild by having the value depend on other Providers being watch()ed or via FutureProvider.family.
You can copy paste run full code below
Yes. it's normal
First time Execute Build reportList is null and show CircularProgressIndicator()
Second time Execute Build reportList has data and show data
If you set listen: false , final reportList = Provider.of<List<ReportModel>>(context, listen: false);
You get only one Execute Build and the screen will always show CircularProgressIndicator()
In working demo simulate 5 seconds network delay so you can see CircularProgressIndicator() then show ListView
You can reference https://codetober.com/flutter-provider-examples/
code snippet
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
working demo
full code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ReportModel {
String title;
ReportModel({this.title});
}
class ReportsProvider with ChangeNotifier {
Future<List<ReportModel>> loadReportData(int no) async {
await Future.delayed(Duration(seconds: 5), () {});
return Future.value([
ReportModel(title: "1"),
ReportModel(title: "2"),
ReportModel(title: "3")
]);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ReportsPage(),
);
}
}
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg());
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
itemBuilder: (context, index) {
return Card(
elevation: 6.0,
child: Padding(
padding: const EdgeInsets.only(
top: 6.0, bottom: 6.0, left: 8.0, right: 8.0),
child: Text(reportList[index].title.toString()),
));
}),
);
}
}
In your case you should use Consumer, i.e.
FutureProvider<List<ReportModel>(
create: (_) => ...,
child: Consumer<List<ReportModel>(
builder: (_, reportList, __) {
return reportList == null ?
CircularProgressIndicator() :
ReportWidg(reportList);
}
),
),
But in this case you must to refactor your ReportWidg.
Related
import 'package:flutter/material.dart';
import 'package:notes/db/database_provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: "/",
routes: {
"/":(context)=>HomeScreen()
},
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
getNotes()async{
final notes = await DatabaseProvider.db.getNotes();
return notes;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Notes"),
),
body: FutureBuilder(
future: getNotes(),
builder: (context,noteData){
switch(noteData.connectionState){
case ConnectionState.waiting:
{
return Center(child: CircularProgressIndicator(),);
}
case ConnectionState.done:
{
if(noteData.data !=null){
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
itemCount:noteData.data!.length,
itemBuilder: (context,index){
String title=noteData.data[index]['title'];
},
),
);
}
else {
return Center(
child: Text("You don't have any notes yet,create one?"),
);
}
}
}
},
),
);
}
}
So, I am making a notes app using SQF lite taking guidance from DocorCode on youtube, i wrote the same code but its showing some errors related to null checks and i am unable to bypass them not matter what.
no matter what I do add null checks or anything it always shows error , is there anyway to switch off the null check , Please tell me how to fix the problem.
The issue arise on title=noteData.data[index]['title'] because it is possible to get null on data. As you've checked null on top level, you can use ! here or better approach is providing default value based on cases.
String title = noteData.data![index]['title'];
String title = noteData.data?[index]['title']?? "default text";
As for the FutureBuilder including data type provide more flexibility. Replace int with your model and you can make it nullable or just pass empty list from getNotes.
Future<List<Note>> getNotes() async {...}
Useing FutureBuilder inside StatefulWidget will recall the api on on setState(state change). Better use initState
Provide a default return from switch case, including .hasError is a good choice.
body: FutureBuilder<List<int>>(
future: getNotes(),
builder: (context, noteData) {
switch (noteData.connectionState) {
case ConnectionState.waiting:
{
return Center(
child: CircularProgressIndicator(),
);
}
case ConnectionState.done:
{
if (noteData.hasData) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
itemCount: noteData.data?.length,
itemBuilder: (context, index) {
// String title = noteData.data![index]['title'];
String title = noteData.data![index].toString();
return Text(title);
},
),
);
} else {
return const Center(
child: Text("You don't have any notes yet,create one?"),
);
}
}
default:
return CircularProgressIndicator();
}
},
),
Explore more about
FutureBuilder
when we should use it.
understanding null safety
I am trying to set the home page of the Flutter app asynchronously, but that is not working because the build method cannot have async properties.
class _MyAppState extends State<MyApp> {
// Widget homeWidget;
// #override
// void initState() async {
// super.initState();
// homeWidget = (await AuthUser.getCurrentUser() != null)
// ? NavBarPage()
// : OnBoardingWidget();
// }
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WizTkt',
theme: Theme.of(context).copyWith(
appBarTheme: Theme.of(context)
.appBarTheme
.copyWith(brightness: Brightness.dark),
primaryColor: Colors.blue),
home: (await AuthUser.getCurrentUser() != null)
? NavBarPage()
: OnBoardingWidget(),
);
}
}
As you can see in the code, I also tried to use initState to set the homepage widget but I cannot make initState an asynchronous function. I feel like there is a better way to choose your homepage in Flutter. What am I missing?
Do note that AuthUser.getCurrentUser() has to be an async function because I use the SharedPreferences library to obtain the login token stored in memory.
You can use FutureBuilder which allows you to build an Widget in a future time.
Here an example:
class OnBoardingWidget extends StatefulWidget {
const OnBoardingWidget({Key key}) : super(key: key);
#override
State<OnBoardingWidget> createState() => _OnBoardingWidgetState();
}
class _OnBoardingWidgetState extends State<OnBoardingWidget> {
final Future<String> _waiter = Future<String>.delayed(
const Duration(seconds: 2), () => 'Data Loaded',
);
#override
Widget build(BuildContext context) {
return Container(
child: FutureBuilder<String>(
future: _waiter,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
Widget wdgt;
if (snapshot.hasData) {
wdgt = Text('Result: ${snapshot.data}');
} else if (snapshot.hasError) {
wdgt = Text('Ops ops ops');
} else {
wdgt = Text('Not ready yet');
}
return Center(child: wdgt);
},
),
);
}
}
class CustomImageRectangle extends StatefulWidget {
final String url;
const CustomImageRectangle({Key key, this.url}): super(key: key);
#override
_CustomImageRectangleState createState() => _CustomImageRectangleState();
}
class _CustomImageRectangleState extends State<CustomImageRectangle> {
#override
void initState() {
super.initState();
print(widget.url != null?'not null':'null');
}
Above class, I called 3 times in my home widget, but only one-time run initState. why is that?
only 1 print in the console.
logo != ''
? FutureBuilder<String>(
future: storage.getVendorLogo(logo),
builder: (context, snapshot) {
if (snapshot.hasData) {
return CustomImageRectangle(url: snapshot.data);
} else {
return CustomImageRectangle();
}
})
: CustomImageRectangle(),
CustomImageRectangle isn't being created 3 times at once. It is being conditionally rendered once. Hence only once print is observed.
In the code snippet :
logo != ''
? FutureBuilder<String>(
future: storage.getVendorLogo(logo),
builder: (context, snapshot) {
if (snapshot.hasData) {
return CustomImageRectangle(url: snapshot.data);
} else {
return CustomImageRectangle();
}
})
: CustomImageRectangle(),
The FutureBuilder will only be returned if logo!='' is true.
In the builder of FutureBuilder, either if (snapshot.hasData) will be true or false.
Depending on that only one of the CustomImageRectangle(url: snapshot.data) or CustomImageRectangle() will be returned.
Otherwise if logo!='' is false the CustomImageRectangle() in the ternary operator will be returned.
Here CustomImageRectangle is created only once in all cases. Hence only once the initState is called.
Consider the below example where it is rendered 3 times at once:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
children: [
CustomImageRectangle(),
CustomImageRectangle(),
CustomImageRectangle(),
],
),
),
),
);
}
}
Console Output:
null
null
null
I'm a new in Flutter.
I have a problem with a calling future method in constructor. I create method, that return a classes with widgets depends of selected item. The problem is that I need to call this method several times, the first time to build the body, the second time to update the body on tap. But I see error "type 'Future' is not a subtype of type 'Widget'" If I add the type of void instead Future, it will be executed once to create a body.
Code snippets:
class DataPageState extends State<DataPage> {
....
_tables() async {
if (selectedValue == "a") {
return DataA();
}
if (selectedValue == "b") {
return DataB();
}
if (selectedValue == "c") {
return DataC();
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(...
body: new Stack(children: <Widget>[
_tables(), //errors this //I need to call method this
... new Stack(children: <Widget>[
AnimatedContainer(...),
InkWell(onTap: () => setState(
() {
_tables(); //and this
},
),)])...}
You _tables() function should return some Widget. If you want to build Widget using some async call you can use FutureBuilder.
_tables() can not be async. you have to return Widget instead of Future<widget>.
Here is the demo of of how to add widget on click.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Home(),
);
}
}
class Home extends StatefulWidget {
Home({Key key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
Widget _add = Container();
test() {
_add = Text("datcdsvcdsvsvdjvkjdsvsa");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Container(
child: Stack(
children: <Widget>[
RaisedButton(
color: Colors.amber,
child: Text("Press"),
onPressed: () {
setState(() {
test();
});
},
),
_add,
],
),
),
);
}
}
You probably should just edit the function _tables to make it synchronous.
like this:
Widget _tables() {
if (selectedValue == "a") {
return DataA();
}
if (selectedValue == "b") {
return DataB();
}
if (selectedValue == "c") {
return DataC();
}
}
Nowever, If you have some reason to make _tables asyncronous, then do this:
Tables is a type Future. You need a `futureBuilder` for this.
Stack(children: <Widget>[
FutureBuilder<Widget>(
future: _tables(),
builder: (BuildContext _, snapshot) {
if(snapshot.hasError) { // Error
return const MyErrorWidget(); // You will have to create this widget
} else if(!(snapshot.hasData)) { // Loading
return CircularProgressIndicator();
}/ Loaded without any errors
return snapshot.data; // The widget that was returned;
},
),
// the rest of the widgets in the Stack
]);
Now this won't solve the problem. You will have to add a return type to _tables().
so do this
Future<Widget> _tables() async {
I'm trying to find the best way to show errors from a Change Notifier Model with Provider through a Snackbar.
Is there any built-in way or any advice you could help me with?
I found this way that is working but I don't know if it's correct.
Suppose I have a simple Page where I want to display a list of objects and a Model where I retrieve those objects from api. In case of error I notify an error String and I would like to display that error with a SnackBar.
page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Page extends StatefulWidget {
Page({Key key}) : super(key: key);
#override
_PageState createState() => _PageState();
}
class _PageState extends State< Page > {
#override
void initState(){
super.initState();
Provider.of<Model>(context, listen: false).load();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
Provider.of< Model >(context, listen: false).addListener(_listenForErrors);
}
#override
Widget build(BuildContext context){
super.build(context);
return Scaffold(
appBar: AppBar(),
body: Consumer<Model>(
builder: (context, model, child){
if(model.elements != null){
...list
}
else return LoadingWidget();
}
)
)
);
}
void _listenForErrors(){
final error = Provider.of<Model>(context, listen: false).error;
if (error != null) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
backgroundColor: Colors.red[600],
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.error),
Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(error) )),
],
),
),
);
}
}
#override
void dispose() {
Provider.of<PushNotificationModel>(context, listen: false).removeListener(_listenForErrors);
super.dispose();
}
}
page_model.dart
import 'package:flutter/foundation.dart';
class BrickModel extends ChangeNotifier {
List<String> _elements;
List<String> get elements => _elements;
String _error;
String get error => _error;
Future<void> load() async {
try{
final elements = await someApiCall();
_elements = [..._elements, ...elements];
}
catch(e) {
_error = e.toString();
}
finally {
notifyListeners();
}
}
}
Thank you
Edit 2022
I ported (and reworked) this package also for river pod if anyone is interested
https://pub.dev/packages/riverpod_messages/versions/1.0.0
EDIT 2020-06-05
I developed a slightly better approach to afford this kink of situations.
It can be found at This repo on github so you can see the implementation there, or use this package putting in your pubspec.yaml
provider_utilities:
git:
url: https://github.com/quantosapplications/flutter_provider_utilities.git
So when you need to present messages to the view you can:
extend your ChangeNotifier with MessageNotifierMixin that gives your ChangeNotifier two properties, error and info, and two methods, notifyError() and notifyInfo().
Wrap your Scaffold with a MessageListener that will present a Snackbar when it gets called notifyError() or NotifyInfo()
I'll give you an example:
ChangeNotifier
import 'package:flutter/material.dart';
import 'package:provider_utilities/provider_utilities.dart';
class MyNotifier extends ChangeNotifier with MessageNotifierMixin {
List<String> _properties = [];
List<String> get properties => _properties;
Future<void> load() async {
try {
/// Do some network calls or something else
await Future.delayed(Duration(seconds: 1), (){
_properties = ["Item 1", "Item 2", "Item 3"];
notifyInfo('Successfully called load() method');
});
}
catch(e) {
notifyError('Error calling load() method');
}
}
}
View
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_utilities/provider_utilities.dart';
import 'notifier.dart';
class View extends StatefulWidget {
View({Key key}) : super(key: key);
#override
_ViewState createState() => _ViewState();
}
class _ViewState extends State<View> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: MessageListener<MyNotifier>(
child: Selector<MyNotifier, List<String>>(
selector: (ctx, model) => model.properties,
builder: (ctx, properties, child) => ListView.builder(
itemCount: properties.length,
itemBuilder: (ctx, index) => ListTile(
title: Text(properties[index])
),
),
)
)
);
}
}
OLD ANSWER
thank you.
Maybe I found a simpler way to handle this, using the powerful property "child" of Consumer.
With a custom stateless widget (I called it ErrorListener but it can be changed :))
class ErrorListener<T extends ErrorNotifierMixin> extends StatelessWidget {
final Widget child;
const ErrorListener({Key key, #required this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<T>(
builder: (context, model, child){
//here we listen for errors
if (model.error != null) {
WidgetsBinding.instance.addPostFrameCallback((_){
_handleError(context, model); });
}
// here we return child!
return child;
},
child: child
);
}
// this method will be called anytime an error occurs
// it shows a snackbar but it could do anything you want
void _handleError(BuildContext context, T model) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
backgroundColor: Colors.red[600],
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.error),
Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(model.error) )),
],
),
),
);
// this will clear the error on model because it has been handled
model.clearError();
}
}
This widget must be put under a scaffold if you want to use a snackbar.
I use a mixin here to be sure that model has a error property and a clarError() method.
mixin ErrorNotifierMixin on ChangeNotifier {
String _error;
String get error => _error;
void notifyError(dynamic error) {
_error = error.toString();
notifyListeners();
}
void clearError() {
_error = null;
}
}
So for example we can use this way
class _PageState extends State<Page> {
// ...
#override
Widget build(BuildContext context) =>
ChangeNotifierProvider(
builder: (context) => MyModel(),
child: Scaffold(
body: ErrorListener<MyModel>(
child: MyBody()
)
)
);
}
You can create a custom StatelessWidget to launch the snackbar when the view model changes. For example:
class SnackBarLauncher extends StatelessWidget {
final String error;
const SnackBarLauncher(
{Key key, #required this.error})
: super(key: key);
#override
Widget build(BuildContext context) {
if (error != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _displaySnackBar(context, error: error));
}
// Placeholder container widget
return Container();
}
void _displaySnackBar(BuildContext context, {#required String error}) {
final snackBar = SnackBar(content: Text(error));
Scaffold.of(context).hideCurrentSnackBar();
Scaffold.of(context).showSnackBar(snackBar);
}
}
We can only display the snackbar once all widgets are built, that's why we have the WidgetsBinding.instance.addPostFrameCallback() call above.
Now we can add SnackBarLauncher to our screen:
class SomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Title',
),
),
body: Stack(
children: [
// Other widgets here...
Consumer<EmailLoginScreenModel>(
builder: (context, model, child) =>
SnackBarLauncher(error: model.error),
),
],
),
);
}
}