Getx argumentsbeing cleared after using showDialog() in Flutter - flutter

The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.

One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}

Related

How to modify a variable using async method before it use in widget build?

I have a variable named userName,which depends on databse query,so async is a must.
My older code can be concluded liks this
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<void> initUser() async{
UserTable().getUserInfo(curUserEmail).then((value)=>null);
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
initUser().then((value){
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () =>router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
//The static global variable is used in Body in other files
body: Body()
),
),
);
});
}
}
It warns that miss return,I dont knwo how to amend my code.
Thanks!!
You can achive this by using the FutureBuilder widget. Please refer the code below.
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<Map> initUser() async {
final data =
await UserTable().getUserInfo(curUserEmail);
return data;
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: initUser(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () => router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
body: Body(),
),
),
);
} else {
// Returns empty container untill the data is loaded
Container();
}
},
);
}
}

Consumer Provider doesn't seem to notify listeners?

The minimal reproducible code below aims to have a loading icon when a button is pressed(to simulate loading when asynchronous computation happen).
For some reason, the Consumer Provider doesn't rebuild the widget when during the callback.
My view:
class HomeView extends StatefulWidget {
const HomeView();
#override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ChangeNotifierProvider(
create: (_) => HomeViewModel(99),
child: Consumer<HomeViewModel>(
builder: (_, myModel, __) => Center(
child: ButtonsAtBottom(
addEventAction: () => myModel.increment(context),
busy: myModel.busy
),
),
),
),
);
}
}
My model where I simulate to do business logic:
class HomeViewModel extends LoadableModel {
late int integer;
HomeViewModel(this.integer);
void increment(BuildContext context) {
super.setBusy(true);
Future.delayed(Duration(seconds: 3), () => print(integer++));
super.setBusy(false);
//Passed in context just to try to simulate my real app
//print(context);
}
}
class LoadableModel extends ChangeNotifier {
bool _busy = false;
bool get busy => _busy;
void setBusy(bool value) {
_busy = value;
notifyListeners();
}
}
PROBLEM: When the callback executes, and setBusy() methods within it are executed, they should notify the listeners and update the busy field passed in it. Subsequently, either a text or loader should be displayed.
BUT, busy field is never updated and remains as false.
class ButtonsAtBottom extends StatelessWidget {
final bool busy;
final void Function() addEventAction;
const ButtonsAtBottom({required this.busy, required this.addEventAction});
#override
Widget build(BuildContext context) {
print("busy value: $busy");
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.clear_rounded),
),
ElevatedButton.icon(
onPressed: addEventAction,
icon: Icon(Icons.save_alt_rounded),
label: busy
? CircularProgressIndicator.adaptive(
backgroundColor: Colors.white,
)
: Text("Save")),
],
);
}
}
did you try to await the future?

Undefined named 'context' while navigating

import 'package:flutter/material.dart';
import 'fruits_listing_card.dart';
import 'fruits_page.dart';
Map<String, Widget> fruits = {
"banana": FruitsListingCards(
fruitBGColor: 0xFFF8A8B5,
fruitImagePath: 'images/fruits/banana.png',
fruitName: 'Banana',
fruitPrice: 'Rs. 105',
fruitShortDescription: 'Ripe & Tasty',
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => FruitsPage()),);
},
),
}
// Second File
import 'package:flutter/material.dart';
class FruitsPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Container(),
),
);
}
}
Both the code are in different files.
FruitsListingCards is a widget that has a Gesture detector functionality. onTap is a parameter that takes function.
I am using FruitsListingCards in the main file and whenever a user taps on it, should go to the FruitsPage screen. But the error is not letting me to do so. Any solution with proper explanantion will help me a lot.
EDIT:
For proper understanding of code, check my repo:
https://github.com/RaghavTheGreat1/fruits_delivery/tree/master/lib
You have to provide context some how, so that it can connect the last screen and next screen.
You can wrap inside a function for that.
Following minimal code will help you more to understand.
class DeleteWidget extends StatefulWidget {
#override
_DeleteWidgetState createState() => _DeleteWidgetState();
}
class FruitsPage extends StatelessWidget {
final Function call;
FruitsPage({this.call});
#override
Widget build(BuildContext context) {
return Container(
child: RaisedButton(
child: Text("press"),
onPressed: call,
),
);
}
}
class NewPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Text("FruiysPage"),
),
),
);
}
}
callme(context) {
Map<String, Widget> fruits = {
"banana": FruitsPage(
call: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NewPage()),
);
},
)
};
return fruits;
}
Well, I figured out the problem on omy own and it was easier than what other people had suggested me even if they haven't looked into my code committed in my GitHub repo.
The quick solution or fix was to add Navigator.push(context, MaterialPageRoute(builder: (context) => fruitsPage)); inside the anonymous function of GestureDetector in FruitsListingCards and then returning fruitsPage(which is a dynamic variable that will take Class object as parameter).

How to get context in the any function of StatelessWidget?

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

How to share the bloc between contexts

I'm trying to access the bloc instance created near the root of my application after navigating to a new context with showDialog(). However, if I try getting the bloc like I usually do, by getting it from the context like _thisBlocInstance = BlocProvider.of<ThisBlocType>(context), I get an error that indicates there is no bloc provided in this context.
I assume this is because the showDialog() builder method assigns a new context to the widgets in the dialog that don't know about the Bloc I am trying to find, which was instantiated as soon as the user logs in:
#override
Widget build(BuildContext context) {
_authBloc = BlocProvider.of<AuthBloc>(context);
_accountBloc = AccountBloc(authBloc: _authBloc);
return BlocProvider(
bloc: _accountBloc,
....
There is a button in the corner that displays a dialog:
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (newContext) => EventDialog(),
).then(
(val) => print(val)
),
child: Icon(Icons.add),
),
),
);
}
And in the EventDialog, I try to find the bloc with the context again:
#override
void build(BuildContext context) {
_accountBloc = BlocProvider.of<AccountBloc>(context);
_userMenuItems = _accountBloc.usersInAccount
.map((user) => DropdownMenuItem(
child: Text(user.userName),
value: user.userId,
))
.toList();
}
And this fails, with an error 'the getter bloc was called on null', or, there is no bloc of that type attached to this context.
Is there some way to access the bloc just from the context after using showDialog(), or otherwise navigating to a new context?
This is the bloc provider class:
import 'package:flutter/material.dart';
//This class is a generic bloc provider from https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/
//it allows easy access to the blocs by ancestor widgets and handles calling their dispose method
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}): super(key: key);
final T bloc;
final Widget child;
#override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
#override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context){
return widget.child;
}
}
abstract class BlocBase {
void dispose();
}
The best way I found to access the original bloc in a new context is by passing a reference to it to a new bloc that manages the logic of the new context. In order to keep the code modular, each bloc shouldn't control more than one page worth of logic, or one thing (e.g. log-in state of the user). So, when I create a new screen/context with showDialog(), I should also have a new bloc that deals with the logic in that screen. If I need a reference to the original bloc, I can pass it to the constructor of the new bloc via the dialog widget's constructor, so any information in the original bloc can still be accessed by the new bloc/context:
child: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (newContext) => NewEventDialog(
accountBloc: BlocProvider.of<AccountBloc>(context),
),
).then((event) => eventsBloc.addEvent(event)),
...
class NewEventDialog extends StatelessWidget {
final AccountBloc accountBloc;
NewEventBloc _newEventBloc;
NewEventDialog({this.accountBloc}) : assert(accountBloc != null);
#override
Widget build(BuildContext context) {
_newEventBloc = NewEventBloc(accountBloc: accountBloc);
return BlocProvider(
bloc: _newEventBloc,
...
The last answer is okay but it can be simplified, that is just transfering Bloc to its child widget.
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(18.0),
child: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (newContext) => EventDialog((
accountBloc: BlocProvider.of<AccountBloc>(context),
),
).then(
(val) => print(val)
),
child: Icon(Icons.add),
),
),
);
}
class NewEventDialog extends StatelessWidget {
final AccountBloc accountBloc;
NewEventDialog({this.accountBloc}) : assert(accountBloc != null);
#override
void build(BuildContext context) {
_accountBloc = accountBloc;
_userMenuItems = _accountBloc.usersInAccount
.map((user) => DropdownMenuItem(
child: Text(user.userName),
value: user.userId,
))
.toList();
}
So far I find this problem occurs when going to widget via page routing. We can transfer the Bloc widget to widget to avoid this problem.