Need help chasing down: Exception while building using Provider in Flutter - flutter

I'm trying to learn Flutter and become more acquainted with passing data around. So i have this very simple app here that is a sort of complicated version of this: Provider version flutter starter demo
Like I said I'm trying to get acquainted, and I'm a relatively green dev. I'm creating this demo to learn StateManagement as well as Persistence.
My goal with this post, is to get help to fix this issue and also know what I'm doing wrong.
So far I have tried moving a few things around and some typical searches but can't seem to figure out specifically what I'm doing wrong here compared with others who are getting the same error.
The app works fine, exactly as expected, no crash and as far as my green grass eyes can tell my code is structured exactly like the Flutter example (with respect to the Provider StateManagement). However I'm getting this error in the console:
======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for Keeper:
setState() or markNeedsBuild() called during build.
This _InheritedProviderScope<Keeper> widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: _InheritedProviderScope<Keeper>
value: Instance of 'Keeper'
listening to value
The widget which was currently being built when the offending call was made was: Consumer<Keeper>
dirty
dependencies: [_InheritedProviderScope<Keeper>]
Page 1
class ScreenOne extends StatelessWidget {
static const String id = 'screen_one';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Page One',
),
),
backgroundColor: Colors.grey.shade200,
body: Container(
child: SafeArea(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Center(
child: Column(
children: [
CustomTextBoxes(title: 'Screen One'),
Consumer<Keeper>(
builder: (_, keeper, child) => Text(
'${keeper.pageOneValue}',
style: TextStyle(color: Colors.grey, fontSize: 20.0),
),
),
CustomTextBoxes(title: 'Screen Two'),
Consumer<Keeper>(
builder: (_, keeper, child) => Text(
'${keeper.pageTwoValue}',
style: TextStyle(color: Colors.grey, fontSize: 20.0),
),
),
CustomTextBoxes(title: 'Total'),
Consumer<Keeper>(
builder: (_, keeper, child) => Text(
'${keeper.addCounters()}',
style: TextStyle(color: Colors.grey, fontSize: 20.0),
),
),
SizedBox(
height: 20.0,
),
CustomButton(
text: 'Screen 2',
function: () {
Navigator.pushNamed(context, ScreenTwo.id);
},
),
],
),
),
),
),
),
floatingActionButton: CustomFloatingButton(
function: () {
var counter = context.read<Keeper>();
counter.incrementCounterOne();
},
),
);
}
}
"Keeper"
class Keeper with ChangeNotifier {
int pageOneValue = 0;
int pageTwoValue = 0;
int totalValue = 0;
void incrementCounterOne() {
pageOneValue += 1;
notifyListeners();
}
void incrementCounterTwo() {
pageTwoValue += 1;
notifyListeners();
}
int addCounters() {
totalValue = pageOneValue + pageTwoValue;
notifyListeners();
return totalValue;
}
}
Main
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Keeper(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.blue),
initialRoute: ScreenOne.id,
routes: {
ScreenOne.id: (context) => ScreenOne(),
ScreenTwo.id: (context) => ScreenTwo()
},
);
}
}

Your problem is in calling addCounters() inside your build method. addCounters() calls notifyListeners() that triggers a setState().
This cannot be perform within your build function. It can only be performed later at the request of a User action. For example, inside the onPressed of a button as you do for the incrementCounterOne().
Instead of computing and storing the total value of your two counters, you could use a getter:
Keeper:
class Keeper with ChangeNotifier {
int pageOneValue = 0;
int pageTwoValue = 0;
int get totalValue => pageOneValue + pageTwoValue;
void incrementCounterOne() {
pageOneValue += 1;
notifyListeners();
}
void incrementCounterTwo() {
pageTwoValue += 1;
notifyListeners();
}
}
ScreenOne:
Consumer<Keeper>(
builder: (_, keeper, child) => Text(
'${keeper.totalValue}', // addCounters()}',
style: TextStyle(color: Colors.grey, fontSize: 20.0),
),
),

Related

Stack with global z-index?

I have a DataTable in which some cells have links. Ideally, I would like to fetch a preview about the link's content whenever hovering over the link, which I was able to achieve using the Stack widget. However, since the stacked preview is inside the DataCell, it seems like I'm not able to raise its "z-index" to be on top of the rest of the table.
Is this not possible with Flutter, or is there a way around it?
The only way I imagine this working, without something to update a global z-index, would be for the cell to update a global state and then have the thumbnail preview appear on a Stack above the DataTable level. But I wish there was a less clunkier way to do it...
3 widgets I've tried but to no avail — they might work, I don't know —:
Tooltip
Overlay
FloatingActionButton
My whole app is here, and the precise commit is 0303732. The relevant code is this ClickableLink widget:
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:url_launcher/url_launcher.dart';
import '../schema/links.dart';
#immutable
class ClickableLink extends StatefulWidget {
const ClickableLink({
Key? key,
required this.link,
this.linkText,
this.color = Colors.blue,
}) : super(key: key);
final Link link;
final String? linkText;
final Color color;
#override
State<ClickableLink> createState() => _ClickableLinkState();
}
class _ClickableLinkState extends State<ClickableLink> {
Widget hoverWidget = const SizedBox.shrink();
void _fetchPreview(PointerEvent pointerEvent) {
setState(() {
if (widget.link.host == 'online-go.com' && widget.link.prePath == 'game') {
hoverWidget = Positioned(
top: 25,
child: Image.network('https://online-go.com/api/v1/games/${widget.link.id}/png'),
);
}
});
}
void _onExit(PointerEvent pointerEvent) {
setState(() {
hoverWidget = const SizedBox.shrink();
});
}
#override
Widget build(BuildContext context) {
return MouseRegion(
onHover: _fetchPreview,
onExit: _onExit,
child: Stack(
clipBehavior: Clip.none,
children: [
SelectableText.rich(
TextSpan(
text: widget.linkText ?? widget.link.id,
style: TextStyle(color: widget.color),
recognizer: TapGestureRecognizer()
..onTap = () async => launch(widget.link.completeLink),
),
),
hoverWidget,
],
),
);
}
}
The problem here is due to the fact that your Stack widget, defined inside ClickableLink, will be at a "lower" point (inside your app widget tree) than every other GameResultCell.
So even the higher z-index will still be behind the other GameResultCells.
To fix this I would reccomend changing your structure and define an higher point in your structure to show the preview.
Another way could be using a library to nest your preview inside a tooltip. Take a look at this one for example:
just_the_tooltip: ^0.0.11+2. With this package, you could even use a StatelessWidget.
The result here is more similar to what I suppose you were expecting.
class ClickableLink extends StatelessWidget {
#override
Widget build(BuildContext context) {
return JustTheTooltip(
content: Image.network(
'https://online-go.com/api/v1/games/${widget.link.id}/png',
),
child: SelectableText.rich(
TextSpan(
text: widget.linkText ?? widget.link.id,
style: TextStyle(
color: widget.color ??
(DogempTheme.currentThemeIsLight(context)
? const Color(0xff1158c7)
: Colors.orange.withOpacity(0.85)),
),
recognizer: TapGestureRecognizer()
..onTap = () async => launch(widget.link.completeLink),
),
),
);
}
}
Lastly you could use a Dialog, but the resulting behaviour is a bit different.
Take a look at this code if you want to try:
class _ClickableLinkState extends State<ClickableLink> {
Widget hoverWidget = const SizedBox.shrink();
void _fetchPreview(PointerEvent pointerEvent) {
showDialog(
context: context,
builder: (context) {
return Dialog(
backgroundColor: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
'https://online-go.com/api/v1/games/${widget.link.id}/png'),
const SizedBox(
height: 16.0,
),
TextButton(
onPressed: () async => launch(widget.link.completeLink),
child: const Text('Go to complete link'))
],
),
);
},
);
}
#override
Widget build(BuildContext context) {
return MouseRegion(
onHover: _fetchPreview,
child: Stack(
clipBehavior: Clip.none,
children: [
SelectableText.rich(
TextSpan(
text: widget.linkText ?? widget.link.id,
style: TextStyle(
color: widget.color ??
(DogempTheme.currentThemeIsLight(context)
? const Color(0xff1158c7)
: Colors.orange.withOpacity(0.85)),
),
recognizer: TapGestureRecognizer()
..onTap = () async => launch(widget.link.completeLink),
),
),
],
),
);
}
}

Undefined name 'task' isn't letting my app run

I am having a problem running my app. I am trying to create a todolist app but I am new to coding so I am battling. I am not sure where the problem is. I a using Android Studio I have two files and I have put all the code in here except the import and run code. I have tried fixing all the errors but they just seem to cause more. I cannot run my app. (I have removed brackets in places as there is too much code when posting this question) Please can you help me out.
main.dart:
import 'package:flutter/material.dart';
import 'Task_Card.dart';
void main() {
runApp(MaterialApp(
home: TaskList(),
));
}
class TaskList extends StatefulWidget {
#override
_TaskListState createState() => _TaskListState();
}
class _TaskListState extends State<TaskList> {
final task = Task('Grocery');
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[800],
appBar: AppBar(
title: Text('ToDoList'),
centerTitle: true,
backgroundColor: Colors.grey[900],
),
body: SingleChildScrollView(
child: Column(),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.black,
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => TaskCard(
task: task,
done: () {}
,)
),
);},
child: Icon(
Icons.add
),
),
);
}
}
class Task {
late String task;
Task(this.task);
}
Task_Card.dart:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class Task {
String task;
Task(this.task);
}
class TaskCard extends StatelessWidget {
final Task task;
final Function() done;
TaskCard({required this.task, required this.done});
#override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
task.task,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18.0,
color: Colors.grey[900],
),
),
SizedBox(height: 6.0,),
TextButton.icon(
onPressed: done,
label: Text(
'Delete Quote',
),
style: TextButton.styleFrom(
primary: Colors.black,
),
icon: Icon(
Icons.delete,
color: Colors.black,
),
),
],
),
),
);
}
}
Here is the error:
I hope you have a task model in your project. If it's not there create one (Just adding an example model, you need to create one based on your own requirement):
class Task {
String task;
Task(this.task);
}
In your TaskListState create an instance of this model:
class _TaskListState extends State<TaskList> {
final task = Task('Grocery');
// Remaining code
}
Note: The above code will always show Grocery, in your actual app you may need to dynamically create the task object with actual value.
please try this:
...
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) =>
TaskCard(task: widget.task, done: () {})
...
instead of
...
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) =>
TaskCard(task: task, done: () {})
...
Try this
Undefined 'task' usually means that the item referenced has no definition. Did you remember to create a separate Task class (in a data file) and import that file into the file throwing the error?
You'll also need to initialize the task data somewhere before using it.
I do not see where you pass the initialized task into your argument defined in your widget. You might want to override initState in the state widget to initialize the Task so that it is not null or pass a new task to the widget when you call it on your Navigator.
Finally in the State Content class you will want to refer to the task as widget.task because the final variable is only accessible by reference through widget.task.
To be clear:
You are currently doing this:
MaterialPageRoute(builder: (context) => TaskCard(
task: task,
done: () {}
,)
The problem is that you never initialized the task object. Even this might work (its hard to say since I don't see. the Task.dart file at this point).
MaterialPageRoute(builder: (context) => TaskCard(
task: Task(),
done: () {}
,)
Even better before you return Scaffold you could create a new task object and evaluate the data from inside the done completion handler.
Task mytask = Task();
...
return Scaffold
and then send that initialized task into the widget while evaluating any properties on the done completion handler.
MaterialPageRoute(builder: (context) => TaskCard(
task: mytask,
done: () {
print(mytask.prop1);
}
,)

Flutter - getx controller not updated when data changed

I am developing an app that has a bottomnavitaionbar with five pages. I use getx. In first page, i am listing data. My problem is that, when i changed data(first page in bottomnavigationbar) manually from database and thn i pass over pages, came back to first page i could not see changes.
Controller;
class ExploreController extends GetxController {
var isLoading = true.obs;
var articleList = List<ExploreModel>().obs;
#override
void onInit() {
fetchArticles();
super.onInit();
}
void fetchArticles() async {
try {
isLoading(true);
var articles = await ApiService.fetchArticles();
if (articles != null) {
//articleList.clear();
articleList.assignAll(articles);
}
} finally {
isLoading(false);
}
update();
}
}
and my UI;
body: SafeArea(
child: Column(
children: <Widget>[
Header(),
Expanded(
child: GetX<ExploreController>(builder: (exploreController) {
if (exploreController.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),
);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {
thanks to #Baker for the right answer. However, if you have a list and in viewModel and want to update that list, just use the list.refresh() when the list updated
RxList<Models> myList = <Models>[].obs;
when add or insert data act like this:
myList.add(newItem);
myList.refresh();
GetX doesn't know / can't see when database data has changed / been updated.
You need to tell GetX to rebuild when appropriate.
If you use GetX observables with GetX or Obx widgets, then you just assign a new value to your observable field. Rebuilds will happen when the obs value changes.
If you use GetX with GetBuilder<MyController>, then you need to call update() method inside MyController, to rebuild GetBuilder<MyController> widgets.
The solution below uses a GetX Controller (i.e. TabX) to:
hold application state:
list of all tabs (tabPages)
which Tab is active (selectedIndex)
expose a method to change the active/visible tab (onItemTapped())
OnItemTapped()
This method is inside TabX, the GetXController.
When called, it will:
set which tab is visible
save the viewed tab to the database (FakeDB)
rebuild any GetBuilder widgets using update()
void onItemTapped(int index) {
selectedIndex = index;
db.insertViewedPage(index); // simulate database update while tabs change
update(); // ← rebuilds any GetBuilder<TabX> widget
}
Complete Example
Copy/paste this entire code into a dart page in your app to see a working BottomNavigationBar page.
This tabbed / BottomNavigationBar example is taken from
https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html
but edited to use GetX.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyTabHomePage(),
);
}
}
class FakeDB {
List<int> viewedPages = [0];
void insertViewedPage(int page) {
viewedPages.add(page);
}
}
/// BottomNavigationBar page converted to GetX. Original StatefulWidget version:
/// https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html
class TabX extends GetxController {
TabX({this.db});
final FakeDB db;
int selectedIndex = 0;
static const TextStyle optionStyle =
TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
List<Widget> tabPages;
#override
void onInit() {
super.onInit();
tabPages = <Widget>[
ListViewTab(db),
Text(
'Index 1: Business',
style: optionStyle,
),
Text(
'Index 2: School',
style: optionStyle,
),
];
}
/// INTERESTING PART HERE ↓ ************************************
void onItemTapped(int index) {
selectedIndex = index;
db.insertViewedPage(index); // simulate database update while tabs change
update(); // ← rebuilds any GetBuilder<TabX> widget
// ↑ update() is like setState() to anything inside a GetBuilder using *this*
// controller, i.e. GetBuilder<TabX>
// Other GetX controllers are not affected. e.g. GetBuilder<BlahX>, not affected
// by this update()
// Use async/await above if data writes are slow & must complete before updating widget.
// This example does not.
}
}
/// REBUILT when Tab Page changes, rebuilt by GetBuilder in MyTabHomePage
class ListViewTab extends StatelessWidget {
final FakeDB db;
ListViewTab(this.db);
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: db.viewedPages.length,
itemBuilder: (context, index) =>
ListTile(
title: Text('Page Viewed: ${db.viewedPages[index]}'),
),
);
}
}
class MyTabHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
Get.put(TabX(db: FakeDB()));
return Scaffold(
appBar: AppBar(
title: const Text('BottomNavigationBar Sample'),
),
body: Center(
/// ↓ Tab Page currently visible - rebuilt by GetBuilder when
/// ↓ TabX.onItemTapped() called
child: GetBuilder<TabX>(
builder: (tx) => tx.tabPages.elementAt(tx.selectedIndex)
),
),
/// ↓ BottomNavBar's highlighted/active item, rebuilt by GetBuilder when
/// ↓ TabX.onItemTapped() called
bottomNavigationBar: GetBuilder<TabX>(
builder: (tx) => BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Business',
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'School',
),
],
currentIndex: tx.selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: tx.onItemTapped,
),
),
);
}
}
You don't need GetBuilder here, as its not meant for observable variables. Nor do you need to call update() in the fetchArticles function as that's only for use with GetBuilder and non observable variables.
So you had 2 widgets meant to update UI (GetBuilder and Obx) both following the same controller and all you need is just the OBX. So Rahuls answer works, or you can leave the Obx in place, get rid of of the GetBuilder and declare and initialize a controller in the beginning of your build method.
final exploreController = Get.put(ExploreController());
Then use that initialized controller in your OBX widget as the child of your Expanded.
Obx(() => exploreController.isLoading.value
? Center(
child:
SpinKitChasingDots(color: Colors.deepPurple[600], size: 40),
)
: ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {},
),
)
GetX< ExploreController >(builder: (controller) {
if (controller.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: controller.articleList.length,
separatorBuilder: (BuildContext context, int index) {});
});
If you change the value in the database 'manually', you need a STREAM to listen to the change on the database.
You can't do:
var articles = await ApiService.fetchArticles();
You need to do something like this:
var articles = await ApiService.listenToArticlesSnapshot();
The way you explained is like if you need the data to refresh after navigating to another page and clicking on a button, then navigating to first page (GetBuilder) OR automatically adds data from the within the first page (Obx). But your case is simple, just retrieve the articles SNAPSHOT, then in the controller onInit, subscribe to the snapshot with the bindStream method, and eventually use the function ever() to react to any change in the observable articleList.
Something like this:
create
final exploreController = Get.put(ExploreController());
Add
init: ExploreController();
body: SafeArea(
child: Column(
children: <Widget>[
Header(),
Expanded(
child: GetX<ExploreController>(builder: (exploreController) {
*** here ***
init: ExploreController();
if (exploreController.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),
);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {
using GetxBuilder approch on ui side and where you want update simple called built in function update();
The simplest way I could.
In the controller create an obs (var indexClick = 1.obs;)
On each Tile test the selected==index...;
On the click of each item change the indexClick sequentially
return Obx(() {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
ListTile(
leading: const Icon(Icons.dns),
title: const Text('Menu1'),
selected: controller.indexClick.value==1?true:false,
onTap: () {
controller.indexClick.value=1;
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.search),
title: const Text('Menu2'),
selected: controller.indexClick.value==2?true:false,
onTap: () {
controller.indexClick.value=2;
Navigator.pop(context);
},
),

Flutter - call Navigator inside switch which it is inside builder

I want to navigate to QrScan screen once the icons get pressed, instead, I got an error!!
setState() or markNeedsBuild() called during build
I want to navigate to that screen and get data from QR Codes, after that I want this data to be shown on another screen!
It says:
This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets.
A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building.
This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built.
Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
Overlay- [LabeledGlobalKey#a5a46]
The widget which was currently being built when the offending call wasmade was: builder
class MainTabsScreen extends StatefulWidget {
#override
_MainTabsScreenState createState() => _MainTabsScreenState();
}
class _MainTabsScreenState extends State<MainTabsScreen> {
int page = 3;
void _openScanner() {
Navigator.push(context, MaterialPageRoute(builder: (context) => QrScan()));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Builder(
builder: (context) {
switch (page) {
case 0:
return ExploreScreen();
case 1:
return OffersScreen();
case 2:
_openScanner();
break;
case 3:
return AltersScreen();
case 4:
return ChatsScreen();
default:
return ExploreScreen();
}
},
),
),
bottomNavigationBar: ConvexAppBar(
top: -20.0,
backgroundColor: Colors.white,
activeColor: Color(0xBB0BCC83),
color: Color(0xBB0BCC83),
height: 53.0,
elevation: 0.0,
initialActiveIndex: 3,
items: [
TabItem(
icon: Icons.home,
title: 'Home',
),
TabItem(
icon: Icons.list,
title: 'Offers',
),
TabItem(
icon: Icons.qr_code,
title: 'Scan',
),
TabItem(
icon: Icons.add_alert,
title: 'Notification',
),
TabItem(
icon: Icons.chat,
title: 'Chats',
),
],
onTap: (id) {
setState(() => page = id);
},
),
);
}
}
As discussed in comments, a solution was to call the navigator.push when id == 2 within the onTap function.

Is this a good solution for showing a SnackBar in case of an error with Flutter and MobX?

Yesterday I spent over ten hours trying to learn a bit of MobX and applying a simple SnackBar if there is an error coming from the API. My question is if the solution I found can be considered good and appropriate or there is a better one to be implemented.
class _LoginPageState extends State<LoginPage> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
final _controller = Modular.get<LoginController>();
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text(widget.title),
),
body: Observer(
builder: (context) {
if (_controller.token?.error != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(_controller.token?.error),
duration: Duration(seconds: 2),
));
});
}
return Center(
child: PrimaryButton(
onPressed: () => _controller.authenticate(),
text: 'Enviar',
icon: Icons.send,
),
);
},
),
);
}
}
In case you're curious about it, I'm using flutter_modular, hence the Modular.get<>()
I like this approach, that is as long as you make sure your snackbar does NOT cover the content of the page, as you know errors from API's could be complex and well documented, therefore you may come across a situation where the snackbar would cover your content.
I usually would use showDialog instead, as errors should not usually accur. when they do I would push a popup displaying and explaining the situation using the error details.
This is my customized version of popups:
class ButtonsAndAction extends FlatButton{
///Providing a func is "optional", just pass null if you want the defualt action to pop the navigator.
ButtonsAndAction(BuildContext context, String text, Function func ) : super(child: new Text(text, textDirection: Helper.textDirection(),style: TextStyle(color: ConstantValues.mainBackgroundColor)
,), onPressed: func == null ? () {Navigator.of(context).pop();} : func);
}
class Helper{
static TextDirection textDirection() => AppConfig.rtl ? TextDirection.rtl : TextDirection.ltr;
/// Used to push alerts of texts with a set of actions(buttons and actions) if wanted
static Future pushDialog(BuildContext context, String title, String body, {List<ButtonsAndAction> actions, bool dismissable = true}) {
return showDialog(
context: context,
builder: (BuildContext context) {
return new WillPopScope(
onWillPop: () async => dismissable,
child:
new AlertDialog(
shape: new RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(ConstantValues.roundRadius)),
side: BorderSide(color: ConstantValues.mainBackgroundColor, width: ConstantValues.roundBorderWidthForPopup)),
title: new Container(child: new Text(title, textDirection: textDirection(), style: TextStyle(color: ConstantValues.mainBackgroundColor),), width: double.infinity,),
content: new Container(child: SingleChildScrollView(child:
new Text(body, textDirection: textDirection(), style: TextStyle(color: ConstantValues.mainBackgroundColor))),
width: double.infinity),
actions: actions
));
},
);
}
}
Good luck!