I have a ScreenA with 2 buttons. Button 1 pushes ScreenB(snapshot) and is received as ScreenB(this.snapshot). Button 2 pushes ScreenB(null). Depending on wether snapshot is null or not in ScreenB, different methods are triggered in ScreenB initState.
Now, If I first press Button 2, i.e. ScreenB(null), snapshot is indeed null in ScreenB. But If I press Button1, i.e. ScreenB(snapshot), then pop ScreenB, then press Button2, I would expect snapshot to be null in ScreenB, but it's not.
Am I missing something here? Appreciate some enlightment.
Test Code added as requested:
ScreenA:
import 'screen_b.dart';
import "package:flutter/material.dart";
import 'mock_data.dart';
List < MockData > snapshot = List < MockData > ();
class ScreenA extends StatelessWidget {
#override
Widget build(BuildContext context) {
snapshot.add(MockData("1", "title", "description"));
return Scaffold(
appBar: AppBar(
title: Text("ScreenA"),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: ListView(
children: < Widget > [
Center(child: FlatButton(
child: Text("Goto ScreenB with null", style: TextStyle(fontSize: 20.0, color: Colors.blue)),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ScreenB(null)));
},
), ),
SizedBox(height: 20.0), Center(child: FlatButton(
child: Text("Goto ScreenB with data", style: TextStyle(fontSize: 20.0, color: Colors.blue)),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ScreenB(snapshot[0])));
},
), ),
],
),
), );
}
}
ScreenB:
import "package:flutter/material.dart";
import 'mock_data.dart';
bool editing = false;
class ScreenB extends StatefulWidget {
ScreenB(this._snapshot);
final MockData _snapshot;
#override
_ScreenBState createState() => _ScreenBState();
}
class _ScreenBState extends State < ScreenB > {
#override
void initState() {
super.initState();
if (widget._snapshot != null) {
editing = true;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ScreenB"),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(30.0),
child: Center(child: Text("Is Editing: $editing ",
style: TextStyle(fontSize: 20.0, color: Colors.red)),
),
), );
}
}
MockData Class:
class MockData {
String id;
String title;
String description;
MockData(this.id, this.title, this.description);
}
To test:
1. run ScreenA and tap button "Goto ScreenB with null". Text on ScreenB shows "Is Editing: false" as expected.
Hit back arrow on appBar to pop ScreenB, then tap "Goto ScreenB with data". Text on ScreenB shows "Is Editing: true" as expected.
Repeat 1 and now Text on ScreenB shows "Is Editing: true" as NOT expected.
Is the snaphot that you mentioned here of type AsyncSnapshot , if yes then the data snapshot is never null, the data field of snapshot is what is null
snapshot.data == null
Let me know if this helps.
Ended up doing:
#override
void dispose() {
editing = false;
super.dispose();
}
in ScreenB. Still puzzled though why the variable keeps the last value, even after the screen has been popped.
Related
Currently, I have two sample files Parent.dart and Child.dart.
In Parent.dart file this is what the code is like:
Parent.dart file:
children:
[
isDisabled
? Icon(Icons.public, color: Colors.grey)
: Icon(Icons.public, color:Colors.white),
InkWell(
onTap:()=> Navigator.push(context,MaterialPageRoute(builder: (context)=> Child(
isDisabled: isDisabled, function: ()=> function())),
]
function()
{
setState(()=> isDisabled = !isDisabled);
}
and in Child.dart the code is something like this:
children:
[
widget.isDisabled
? Icon(Icons.public, color: Colors.grey)
: Icon(Icons.public, color:Colors.white),
InkWell(
onTap:()=> widget.function(),
]
I have some data being fetched from a server that is used to populate a list of cards inside listview.builder.
What I'm trying to do is inherent variables from the parent and use their value to update the child. Currently, if I run this parent does change, but the child doesn't until you navigate back from parent to child.
For a better context: Imagine a list of cards. Each has an add-to-list button. Now if you click on the card it goes to another screen "child.dart" where it gives you more details about the item on the card you clicked. Now if you click the add-to-list button on the child screen it should also update the parent.
I tried different ways of achieving this "UI synchrony" for a better user experience. But I didn't find a proper way to implement it.
Things I tried: Provider (but it updates all the items on the list instead of each instance.),
a "hacky" method of editing the data in the list on the client side and updating the widget based on that. (This technique does work, but ewwwww)
I'm not really sure to understand your question.
If your question is how trigger a function in parent from child screen, here is your answer.
I made a working example. I think you were really close.
Another option for state management is riverpod 2.0
Or you can pass value in Navigator.pop and trigger the function in parent.
Parent model
class Parent {
String title;
bool isDisabled = false;
Parent({required this.title});
}
Main.dart
import 'package:flutter/material.dart';
import 'parent.dart';
import 'ParentCard.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
MyApp({super.key});
List<Parent> parentList = [Parent(title: 'Item 1'), Parent(title: 'Item 2')];
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: ListView.builder(
itemCount: parentList.length,
itemBuilder: (BuildContext context, int position) {
return ParentCard(title: parentList[position].title);
},
),
),
);
}
}
ParentCard.dart
import 'package:flutter/material.dart';
import 'child.dart';
class ParentCard extends StatefulWidget {
String title;
ParentCard({super.key, required this.title});
#override
State<ParentCard> createState() => _ParentCardState();
}
class _ParentCardState extends State<ParentCard> {
bool isDisabled = false;
#override
Widget build(BuildContext context) {
return Row(
children: [
Text(widget.title),
isDisabled
? Icon(Icons.public, color: Colors.green)
: Icon(Icons.public, color: Colors.black),
IconButton(
icon: Icon(Icons.plus_one),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ChildCard(isDisabled: isDisabled, handler: handler)),
),
)
],
);
}
handler() {
setState(() => isDisabled = !isDisabled);
}
}
** child.dart**
import 'package:flutter/material.dart';
class ChildCard extends StatefulWidget {
VoidCallback handler;
bool isDisabled;
ChildCard({super.key, required this.isDisabled, required this.handler});
#override
State<ChildCard> createState() => _ChildCardState();
}
class _ChildCardState extends State<ChildCard> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
const Text('child !'),
widget.isDisabled
? const Icon(Icons.public, color: Colors.green)
: const Icon(Icons.public, color: Colors.black),
ElevatedButton(
onPressed: () {
setState(() {
widget.isDisabled = !widget.isDisabled;
});
widget.handler();
},
child: const Text('click to trigger'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('pop it'),
)
]),
);
}
}
I want to create a dropdown menu on flutter where the handler button that opens the dropdown uses just an icon and the menu list opened by it uses an icon and a text.
I almost manage to create it, as you can check on the following screenshots:
Closed
Opened
I'm struggling with the opened width, so my question is how to give the opened menu enough width and keep the handler button on its current width.
Notice that I want the dropdown to be at the end of the Row, so consider this black box to be an area of something else, nothing important.
I'm adding the relevant code below and the complete code on the following links.
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "Question Dropdown",
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(
optionStream: BehaviorSubject<Option>(),
),
);
}
}
class HomePage extends StatelessWidget {
final BehaviorSubject<Option> optionStream;
const HomePage({
Key? key,
required this.optionStream,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Question Dropdown"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Expanded(
child: Container(
height: 48,
color: Colors.black,
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: StreamBuilder<Option>(
initialData: Option.A,
stream: optionStream,
builder: (context, snapshot) {
final option = snapshot.data ?? Option.A;
return _dropDownMenu(context, option);
},
),
),
],
),
],
),
),
);
}
Widget _dropDownMenu(
BuildContext context,
Option option,
) {
const items = Option.values;
return DropdownButtonHideUnderline(
child: DropdownButton<Option>(
value: option,
selectedItemBuilder: (context) =>
items.map((e) => _dropdownHandler(context, e)).toList(),
items: items.map((e) => _dropdownItem(context, e)).toList(),
onChanged: (e) => optionStream.add(e ?? Option.A),
),
);
}
OptionsItemHelper _dropDownItemData(
BuildContext context,
Option option,
) {
Widget icon;
String text;
switch (option) {
case Option.A:
icon = const Icon(Icons.ac_unit);
text = "An option";
break;
case Option.B:
icon = const Icon(Icons.baby_changing_station);
text = "Best option";
break;
case Option.C:
icon = const Icon(Icons.cake_sharp);
text = "Closest option";
break;
case Option.D:
icon = const Icon(Icons.dashboard);
text = "Dumb option";
break;
}
return OptionsItemHelper(text, icon);
}
Widget _dropdownHandler(
BuildContext context,
Option option,
) {
final helper = _dropDownItemData(context, option);
return helper.icon;
}
DropdownMenuItem<Option> _dropdownItem(
BuildContext context,
Option option,
) {
final helper = _dropDownItemData(context, option);
return DropdownMenuItem<Option>(
value: option,
child: Row(
children: [
helper.icon,
const SizedBox(width: 16),
Text(helper.text),
],
),
);
}
}
enum Option {
A,
B,
C,
D,
}
class OptionsItemHelper {
final String text;
final Widget icon;
OptionsItemHelper(
this.text,
this.icon,
);
}
Complete code on Github
Complete code on Gitlab
I did find a workaround using GestureDetector and showMenu, I'm sharing here and pushing to the repo as "workaround" commit in case you need the same as I need now, I'm keeping the question without answer in case someone finds a better way using the dropdown.
The new dropDownMenu function
Widget _dropDownMenu(
BuildContext context,
Option option,
) {
const items = Option.values;
return GestureDetector(
onTapDown: (details) async {
final offset = details.globalPosition;
final newOption = await showMenu(
context: context,
position: RelativeRect.fromLTRB(offset.dx, offset.dy, 0, 0),
items: items.map((e) => _dropdownItem(context, e, option)).toList(),
);
if (newOption != null) {
optionStream.add(newOption);
}
},
child: _dropdownHandler(context, option),
);
}
and the new dropdownItem function.
PopupMenuEntry<Option> _dropdownItem(
BuildContext context,
Option option,
Option selected,
) {
final helper = _dropDownItemData(context, option);
return CheckedPopupMenuItem<Option>(
value: option,
checked: option == selected,
child: Row(
children: [
Expanded(child: Container()),
Text(helper.text),
const SizedBox(width: 16),
helper.icon,
],
),
);
}
How it looks like
Closed
Opened
Bigger Screen
I want to set the updated text value of button throughout the app, when i click on button its text changes to current time, but when I navigate to other screen, and then come back to the screen where I created a button, it is not showing the updated text.
here is my button widget
String getTime;
//from here i get the current time
void _getTime() {
final String formattedDateTime =
DateFormat('kk:mm:ss a').format(DateTime.now()).toString();
setState(() {
getTime = formattedDateTime;
print("time");
print(getTime);
});
}
String timeInText = "Time in";
Widget _timein() {
//enable- initial case
bool firstCaseFlag = true;
if (getTimeInStatus == false && timeInButtonPressed == true) {
print("i1");
return FlatButton(
color: timeInButtonPressed ? Colors.blue[500] : Colors.blue[200],
textColor: Colors.white,
padding: EdgeInsets.all(15.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(buttonRoundRadius)),
child: Row(children: <Widget>[
Icon(
Icons.timer,
),
Expanded(
child: Text(
timeInText,
textAlign: TextAlign.center,
style: TextStyle(fontSize: textFontSize),
),
),
]),
onPressed: () {
_getTime();
setState(() {
if (firstCaseFlag == true) {
timeInText = getTime; //here i set the button text to current time
timeIn = timeInText;
firstCaseFlag = false;
} else {
}
});
calltimeInApi();
});
Conditions:
There are certain conditions where button will change there state, like i have 2 button namely timein and timeout, initially timein button will be enable to click and timeout will be disable, so if user click on timein button its text change to current time and timeout button will be enable (this is all happening), and if user moved to other screen and come to home screen (where i created timein and timeout buttons) then timein button text should display that time when user click on it.
Problem:
My problem is when I moved to other screen and come to home screen timein button is enabled and not showing the time when i click on it.
please help how i can fix it.
I prefer using statemanagement StateProvider. here is an example just using global variable.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:stack_overflow/exports.dart';
String buttonText = "Click to set";
///for riverpod
///final buttonState = StateProvider((ref) => "Click to set");
class BaseWidget extends StatefulWidget {
const BaseWidget({Key? key}) : super(key: key);
#override
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
void _getTime() {
final String formattedDateTime =
DateFormat('kk:mm:ss a').format(DateTime.now()).toString();
setState(() {
buttonText = formattedDateTime;
print("time");
print(buttonText);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
ElevatedButton(
onPressed: () {
_getTime();
},
child: Text(buttonText),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => NextWidget(),
));
},
child: Text("next"),
),
],
),
);
}
}
class NextWidget extends StatelessWidget {
const NextWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("Back"),
),
);
}
}
Use state management like Provider to keep the values and then access anywhere.
Package link: https://pub.dev/packages/provider
Helpful reference: https://flutter.dev/docs/development/data-and-backend/state-mgmt/intro
I have built a home screen with is rendering cards using ListView.builder. These cards have a confirm button which fetches the confirmation status from firestore. When I tap on the confirm button, a bottom sheet appears asking whether I am sure. Once I tap Yes on the bottom sheet, I want the card on homepage to be rebuilt and change the button from confirm to confirm.
I used the setState to change the value at the onPressed event and it is successfully changing it but, the confirm button is not changing to confirmed.
Any leads on how to solve this issue would be really appreciated.
Homepage cards layout
class HomepageCards extends StatefulWidget {
final FirebaseUser user;
final Map cardDetails;
HomepageCards({#required this.user, this.cardDetails});
#override
_HomepageCardsState createState() => _HomepageCardsState();
}
class _HomepageCardsState extends State<HomepageCards> {
#override
Widget build(BuildContext context) {
// Confirmation status from firebase about the captain
bool isConfirmed = widget.cardDetails['c'];
final screenHeight = MediaQuery.of(context).size.height;
final screenWidth = MediaQuery.of(context).size.width;
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(screenHeight / 60, screenHeight / 90,
// UI Code here......
Container(
height: screenHeight / 80,
),
// Confirm Button checking condition and building UI accordingly
isConfirmed == true
? captainConfirmed(context, isConfirmed) // if confirmed then different button style widget
: confirmAndCancelButton(context, isConfirmed), //if not confirmed then show confirm and cancel button in the card
],
),
// Some UI
);
}
}
Once clicking on cancel, the bottom sheet:
Widget confirmCaptainBookingBottomSheet(
BuildContext context, bool isConfirmed) {
final screenHeight = MediaQuery.of(context).size.height;
final screenWidth = MediaQuery.of(context).size.width;
showModalBottomSheet(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Container(
// UI Code
Text(
'Do you want to confirm?',
style: TextStyle(
color: black.color,
fontSize: headSize.fontSize,
),
),
child: FlatButton(
child: Text(
'YES',
style: TextStyle(
color: cyan.color,
fontSize: headSize.fontSize),
),
onPressed: () {
print(isConfirmed);
setState(() {
// change the value of is confirmed which is used to build different buttons in the UI as shown in the above code
isConfirmed = true;
});
print(isConfirmed);
Navigator.pop(context);
}),
),
child: FlatButton(
child: Text(
'NO',
style: TextStyle(
color: cyan.color,
fontSize: headSize.fontSize),
),
onPressed: () {
Navigator.pop(context);
}),
),
});
}
You can create a function in _HomepageCardsState class which change state of isConfirmed and pass that function to widget where you want change the state. Then on onPressed of yes just give that function. it will change state of isConfirmed in _HomepageCardsState widget so you can see captainConfirmed widget.
I am leaving small demo which simulates how you can do that in your case.
I hope following code clear your idea.
class DeleteWidget extends StatefulWidget {
#override
_DeleteWidgetState createState() => _DeleteWidgetState();
}
class _DeleteWidgetState extends State<DeleteWidget> {
bool isConfirmed = false;
changeconfirmed() {
setState(() {
isConfirmed = !isConfirmed;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: isConfirmed
? Home1()
: Home2(
function: changeconfirmed,
),
)),
);
}
}
class Home1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Text("confirmed Widget"),
);
}
}
class Home2 extends StatelessWidget {
final Function function;
Home2({this.function});
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text("press"),
onPressed: function,
);
}
}
I have this value of type list that I'll be using on a ListViewBuilder.
I have a condition that checks if the list has a length of 0 or more so that I can show an Empty State widget if there are no available items in the list and to show the ListViewBuilder if there are items in the list.
Running the logic actually show's that it works but my problem is that since the list object's length starts at 0 it always draws my "Empty State" widget first but then quickly draws the actual ListViewBuilder when my Future function finishes incrementing on my list. Though it works, the experience for the user to quickly see the "Empty State" even if there actually items on list is quite jarring.
Hoping you guys can help me on a way I can only show either state my list is on without having to pass through from the initial state of zero.
Below is a representation of what I am talking about, you can see that isolated my three states: NULL (White), ==0 (Blue), >=1 (Green).
What I'm trying to achieve is to just go to Green without showing Blue.
EDIT: ADDED SAMPLE CODE BELOW
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MainProvider extends ChangeNotifier {
List<String> list = <String>[];
MainProvider() {
initList();
}
initList() async {
// CODE BELOW IS TO SIMULATE MAKING DATABASE CALLS
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(milliseconds: 800), () {
addToList('String');
});
}
}
addToList(String string) {
list.add(string);
notifyListeners();
}
}
void main() => runApp(AppIndex());
class AppIndex extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: AppProvider(),
);
}
}
class AppProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MainProvider>(
create: (context) => MainProvider(),
child: AppContent(),
);
}
}
class AppContent extends StatelessWidget {
#override
Widget build(BuildContext context) {
var mainProvider = Provider.of<MainProvider>(context);
return Scaffold(
appBar: AppBar(),
body: mainProvider.list == null
? Container(
color: Colors.red,
child: Center(
child: Center(
child: Text(
'NULL',
),
),
),
)
: mainProvider.list.length == 0
? Container(
color: Colors.blue,
child: Center(
child: Text(
mainProvider.list.length.toString(),
style: TextStyle(fontSize: 32.0),
),
),
)
: mainProvider.list.length >= 0
? Container(
color: Colors.green,
child: Center(
child: Text(
mainProvider.list.length.toString(),
style: TextStyle(fontSize: 32.0),
),
),
)
: Container(
color: Colors.red,
child: Center(
child: Text(
mainProvider.list.length.toString(),
style: TextStyle(fontSize: 32.0),
),
),
),
);
}
}
In my code, I am using my Future function to call out database items
to add to my list. In the sample code above, I intentionally added a
delay to simulate database call times.
Given your comment, I think something like this will do:
class _HomePageState extends State<HomePage> {
Future<List<Item>> _items;
#override
void initState() {
super.initState();
// here maybe you won't use the server side, but some local storage
_items = Web.fetchItems();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Item>>(
future: _items,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (_items.isEmpty) {
return _MyCallToActionButtonLayout();
}
return Scaffold(
appBar: AppBar(
title: Text('My title'),
),
body: buildListView(snapshot.data)),
);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
} else {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
},
);
}