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
Related
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
}
}
I am making a list of stateless widget as shown below and passing the id as the parameter to the widgets.
Code for cartPage:-
class Cart extends StatefulWidget {
#override
_CartState createState() => _CartState();
}
class _CartState extends State<Cart> {
bool loading=true;
List<CartTile> cartTiles=[];
#override
void initState() {
super.initState();
if(currentUser!=null)
getData();
}
getData()async
{
QuerySnapshot snapshot=await cartReference.doc(currentUser.id).collection('cartItems').limit(5).get();
snapshot.docs.forEach((doc) {
cartTiles.add(CartTile(id: doc.data()['id'],index: cartTiles.length,));
});
setState(() {
loading=false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: loading?Center(child:CircularProgressIndicator():SingleChildScrollView(
child: Column(
children: cartTiles,
),
),
);
}
}
Code for CartTile:-
class CartTile extends StatelessWidget {
final String id;
CartTile({this.id,});
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: productReference.doc(id).snapshots(),
builder: (context,snapshot)
{
//here am using the snapshot to build the cartTile.
},
);
}
}
So, my question is whenever I will call setState in my homepage then will the stateless widget be rebuilt and increase my document reads. Because i read somewhere that when we pass the same arguments or parameters to a stateless widget then due to its cache mechanism it doesn't re build. If it will increase my reads then is there any other way to solve this problem?
I have a Widget which uses bloc builder to map the different state of widget.
class BodyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<NewsBloc, NewsState>(builder: (context, state) {
return state.map(
.....
);
});
}
....
}
The BodyWidget is created in a Widget with BlocProvider.
class MainPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
getIt<NewsBloc>()..add(const NewsEvent.fetchNewsData()),
child: BodyWidget(),
);
}
....
}
And the NewsBloc is defined as
#injectable
class NewsBloc extends Bloc<NewsEvent, NewsState> {
final GetNews getNews;
NewsBloc({
#required this.getNews,
}) : super(const _Initial());
#override
Stream<NewsState> mapEventToState(
NewsEvent event,
) async* { ... }
}
I am using get_it and injectable for Dependency Injection.
Now I am trying to write a simple widget test for BodyWidget and I am not so sure how to inject all these dependency in test.
class MockBuildContext extends Mock implements BuildContext {}
class MockNewsBloc extends Mock implements NewsBloc {}
void main() {
ForYouNewsTab _widget;
MockBuildContext _context;
NewsBloc _newsBloc;
Widget makeTestableWidgets({Widget child}) {
return MaterialApp(
home: Scaffold(
// body: BlocProvider(
// create: (_context) => getIt<NewsBloc>(),
// child: child,
// ),
body: child,
),
);
}
setUp(() {
_context = MockBuildContext();
_widget = ForYouNewsTab();
});
test('ForYouNewsTab is sub class of StatelessWidget', () {
expect(_widget, isA<StatelessWidget>());
});
testWidgets('should return sized box for initial state',
(WidgetTester tester) async {
await tester.pumpWidget(makeTestableWidgets(child: _widget));
});
}
I did search in stackoverflow, but could not found a solution that works form me.
I solved my issue by following very basic steps. Not so sure if its the right way. Anyway if anyone ever comes to the same problem, it might help them.
class MainPage extends StatelessWidget {
//added line
final NewsBloc newsBloc;
const MainPage({
Key key,
#required this. newsBloc,
})
#override
Widget build(BuildContext context) {
return BlocProvider(
// changed line
// create: (context) => getIt<NewsBloc>()..add(const NewsEvent.fetchNewsData()),
create: (context) => newsBloc..add(const NewsEvent.fetchNewsData()),
child: BodyWidget(),
);
}
....
}
Now in my test case I can create MockNewsBloc and inject it easily to the MainPage when it is under testing.
Minimal reproducible code:
void main() => runApp(FooApp());
class FooApp extends StatefulWidget {
#override
_FooAppState createState() => _FooAppState();
}
class _FooAppState extends State<FooApp> {
bool _showPage2 = false;
void _onPressed(bool value) => setState(() => _showPage2 = value);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Navigator(
onPopPage: (route, result) => route.didPop(result),
pages: [
MaterialPage(child: Page1(onPressed: _onPressed)),
if (_showPage2) MaterialPage(child: Page2()),
],
),
);
}
}
class Page1 extends StatelessWidget {
final ValueChanged<bool> onPressed;
const Page1({Key key, this.onPressed}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Page1')),
body: ElevatedButton(
onPressed: () => onPressed(true),
child: Text('Page2'),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
void dispose() {
super.dispose();
print('dispose');
}
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Page2')));
}
didPop:
When this function returns true, the navigator removes this route from the history but does not yet call dispose. Instead, it is the route's responsibility to call NavigatorState.finalizeRoute, which will in turn call dispose on the route. This sequence lets the route perform an exit animation (or some other visual effect) after being popped but prior to being disposed.
But in my example, you can see without having to call NavigatorState.finalizeRoute in Page2, dispose method does get called contradiciting Docs.
It's done internally when using MaterialPage/_PageBasedMaterialPageRoute. You can poke around in the code starting in the Navigator class, which appears to lead up to this OverlayRoute class. If you do want to trace through yourself, it wasn't a walk in the park for me and you'll have to pay close attention to how each class is related.
This class has the finishedWhenPopped getter, which is true by default. And if you look at the didPop override implementation right below the getter definition, didPop will internally call finalizeRoute when finishedWhenPopped is true.
Implementation from OverlayRoute class
#protected
bool get finishedWhenPopped => true;
#override
bool didPop(T? result) {
final bool returnValue = super.didPop(result);
assert(returnValue);
if (finishedWhenPopped)
navigator!.finalizeRoute(this);
return returnValue;
}
This is true only for at least MaterialPage/_PageBasedMaterialPageRoute. Other implementations don't necessarily do this.
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).