I have a page where I can choose a user theme color that is used as colors in my app. I have it set up so a modal bottom sheet pops up with the color options and then sets it using Provider so when the navigation is popped the new color can be seen on MyView.
Problem
When the user makes a change BUT hits the close button I essentially want to revert all changes made, so to try and tackle this I have a variable called loggedInUser which I initialise in my init State function and I keep out of the build method. So its set once and that's it. The plan is that if the user hits the close button I use Provider to set the details back to the data in loggedInUser (which shoulldn't have the updated color choices).
This does not happen and loggedInUser though not reinitialised has the new colors I chose.
Code
class MyView extends StatefulWidget {
static const String id = "my_view";
#override
State<MyView> createState() => _MyViewState();
}
class _MyViewState extends State<MyView> {
UserDto loggedInUser;
#override
void initState() {
super.initState();
loggedInUser = Provider.of<UserData>(context, listen: false).user;
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: kThemeColor,
body: Column(
children: [
SafeArea(
child: CloseButton(
onPressed: () {
var test = loggedInUser;
//when debugged, test has the new color, not the old one it was initialised to back in initState();
//i want the old values to persist
Navigator.pop(context);
},
color: Colors.white,
),
),
Expanded(
child: Container(
height: double.infinity,
width: double.infinity,
decoration: kCurvedContainerBoxDecoration,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => SingleChildScrollView(
child: Container(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).viewInsets.bottom),
child: AccountThemePickerView(),
),
),
);
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
UserHelper.getColorFromString(
Provider.of<UserData>(context).user.themeColor),
),
shape:
MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
)
],
),
),
),
)
],
),
);
}
}
class AccountThemePickerView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
color: Color(0xff757575),
child: Container(
decoration: kModalBottomSheetBoxDecoration,
padding: EdgeInsets.only(left: 15, bottom: 30, right: 15, top: 15),
child: GridView.count(
shrinkWrap: true,
crossAxisCount: 3,
crossAxisSpacing: 30,
mainAxisSpacing: 30,
children: [
AccountThemePickerColor(
colorName: "Coral Red", color: Color(0xffff6961)),
AccountThemePickerColor(
colorName: "Forest Green", color: Color(0xff129a7d)),
],
),
),
);
}
}
class AccountThemePickerColor extends StatelessWidget {
final Color color;
final String colorName;
AccountThemePickerColor({this.colorName, this.color});
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
Provider.of<UserData>(context, listen: false)
.updateUserThemeColor(colorName, color.toString());
Navigator.pop(context);
},
style: ButtonStyle(
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
backgroundColor: MaterialStateProperty.all<Color>(color),
),
);
}
}
UserData class
class UserData extends ChangeNotifier{
UserDto user;
void setUser(UserDto userDto){
user = userDto;
notifyListeners();
}
void updateUserThemeColor(String themeColorName, String themeColor){
//note I have a helper method which simply converts string to color, for your debug purposes you can just use an actual Color value
user.themeColor = themeColor;
user.themeColorName = themeColorName;
notifyListeners();
}
}
I believe it has something to do with copy constructors.
For example, this code:
class X{
int y;
int z;
X(this.y, this.z);
}
void main() {
X obj1 = X(2,3);
X obj2 = obj1;
X obj3 = obj2;
obj1.y = 10;
print(obj2.y);
print(obj3.y);
}
outputs
10
10
because variables are references to objects. And when you assign an object to another object, it points to the same location in memory instead of copying its elements.
Provider.of<UserData>(context, listen: false).user; would return the same object each time it is called. So, you change its value. And hence, the loggedInUser also changes.
Try to create a new object and store data in it.
Related
I keep getting this error when trying to post data and the solutions say to check if(mounted) before calling setState but I don't know where this setState is? The code is below; the function is in the first widget and then I call it in another widget:
// ignore_for_file: use_build_context_synchronously
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:mne/Forms/form_model.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../Network/api.dart';
class FormWidget extends StatefulWidget {
const FormWidget({Key? key}) : super(key: key);
#override
State<FormWidget> createState() => FormWidgetState();
}
class FormWidgetState extends State<FormWidget> {
// to show error message
_showScaffold(String message) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message, style: const TextStyle(color: Colors.black)),
duration: const Duration(seconds: 20),
action: SnackBarAction(
textColor: Colors.white,
label: 'Press to go back',
onPressed: () {
Navigator.of(context).pop();
},
),
backgroundColor: Colors.redAccent));
}
final List<FormModel> _formfields = [];
var loading = false;
var isEmpty = false;
List activityResponses = [];
late Map<String, List<dynamic>> data;
// to fetch form fields
fetchFormFields() async {
setState(() {
loading = true;
});
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences localStorage = await SharedPreferences.getInstance();
var saved = localStorage.getString('activitytask_id');
var res = await Network().getData('mobile/activity-fields/$saved');
if (res.statusCode == 200) {
final data = jsonDecode(res.body);
final tdata = data['data'];
if (tdata.length == 0) {
Navigator.of(context).pushReplacementNamed('error');
}
var formfieldsJson = tdata;
setState(() {
for (Map formfieldJson in formfieldsJson) {
_formfields.add(FormModel.fromJson(formfieldJson));
}
loading = false;
});
}
}
final TextEditingController _textController = TextEditingController();
final TextEditingController _numberController2 = TextEditingController();
#override
void initState() {
super.initState();
fetchFormFields();
}
submitResult() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences localStorage = await SharedPreferences.getInstance();
final String? responseData = localStorage.getString('responses');
final String responseList = json.decode(responseData!);
final List responseList2 = [responseList];
debugPrint(responseList.toString());
data = {"activity_responses": responseList2};
var res = await Network().authData(data, 'mobile/activity-result');
if (res.statusCode == 200) {
Navigator.of(context).pushReplacementNamed('activitytasks');
} else {
Navigator.of(context).pushReplacementNamed('error');
}
}
#override
void dispose() {
_textController.dispose();
_numberController2.dispose();
super.dispose();
}
// to display form fields
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: loading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: _formfields.length,
itemBuilder: (context, i) {
final nDataList = _formfields[i];
return Form(
child: Column(children: [
Column(children: [
if (nDataList.type == 'text')
Column(children: [
Container(
alignment: Alignment.centerLeft,
padding:
const EdgeInsets.only(bottom: 5, left: 6),
child: Text('Add a ${nDataList.name}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold)),
),
Container(
margin: const EdgeInsets.all(8),
child: TextFormField(
controller: _textController,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.all(15),
border: const OutlineInputBorder(),
filled: true,
fillColor: Colors.grey[200],
labelText: nDataList.name),
)),
Container(
alignment: Alignment.centerRight,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(
Colors.green)),
onPressed: () async {
var responseData = {
"activity_field_id": nDataList.id,
"value": _textController.text
};
activityResponses.add(responseData);
debugPrint(activityResponses.toString());
},
child: const Text('Save',
style: TextStyle(color: Colors.white)),
),
),
]),
if (nDataList.type == 'number')
Column(children: [
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(
bottom: 5, left: 6, top: 5),
child: Text('Add a ${nDataList.name}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold)),
),
Container(
margin: const EdgeInsets.all(8),
child: TextFormField(
controller: _numberController2,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.all(15),
border: const OutlineInputBorder(),
filled: true,
fillColor: Colors.grey[200],
labelText: nDataList.name),
)),
Container(
alignment: Alignment.centerRight,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(
Colors.green)),
onPressed: () async {
// valueResponses.add(_numberController2.text);
// idResponses.add(nDataList.id);
var numberData = {
"activity_field_id": nDataList.id,
"value": _numberController2.text
};
activityResponses.add(numberData);
debugPrint(activityResponses.toString());
SharedPreferences localStorage =
await SharedPreferences.getInstance();
final String encodedData =
(activityResponses).asMap().toString();
final String encodedData2 =
jsonEncode(encodedData);
localStorage.setString(
'responses', encodedData2);
},
child: const Text('Save',
style: TextStyle(color: Colors.white)),
),
),
]),
]),
]));
})));
}
}
The above code is the first widget where I have written submitResult() . The code below is for the page containing the button that will call submitResult() :
import 'package:flutter/material.dart';
import '../Design/custom_shape.dart';
import 'form_widget.dart';
class Workspace extends StatefulWidget {
const Workspace({Key? key}) : super(key: key);
#override
State<Workspace> createState() => WorkspaceState();
}
class WorkspaceState extends State<Workspace> {
bool isLoading = true;
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: const Color.fromARGB(255, 236, 246, 219),
body: Column(
children: [
Container(
alignment: Alignment.center,
child: Stack(clipBehavior: Clip.none, children: [
ClipPath(
clipper: CustomShape(),
child: Container(
padding: const EdgeInsets.only(bottom: 128),
height: 335,
child: Image.asset('assets/images/fields.png',
fit: BoxFit.fitWidth))),
Container(
height: 650,
width: double.infinity,
color: Colors.transparent,
),
Positioned(
top: 140,
right: 8,
height: 600,
child: Container(
alignment: Alignment.center,
width: 360,
margin: const EdgeInsets.all(5),
padding: const EdgeInsets.all(5),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(10))),
child: Column(
children: [
Container(
height: 450,
width: double.infinity,
child: const FormWidget()),
if (isLoading)
Container(
alignment: Alignment.bottomCenter,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(
Colors.green)),
onPressed: () async {
setState(() {
isLoading = false;
});
await FormWidgetState().submitResult();
if (!mounted) return;
setState(() {
isLoading = true;
});
// Navigator.of(context).pop();
},
child: const Text('Submit',
style: TextStyle(color: Colors.white)),
),
)
else
const Center(
child: CircularProgressIndicator(
backgroundColor:
Color.fromRGBO(0, 161, 39, 1)))
],
)),
),
])),
],
),
);
}
}
Any help is appreciated thank you
Edit: Adding this image after removing the
// ignore_for_file: use_build_context_synchronously
to show where the error is.
The above code is the first widget where I have written submitResult() . The code below is for the page containing the button that will call submitResult()
Something is wrong. You should not call a method of a State object by simply initialising that State object and calling its method inside a different widget altogether as you did somewhere in the second snippet:
await FormWidgetState().submitResult();
State objects are tied to widgets. Flutter continuously discards and recreates widgets but keeps their State object. For Flutter to properly manage a State object, the tied widget must be part of the widget tree.
The IDE shows the above error because the context of that Navigator is not part of any widget tree. There is no widget linked to the State object when submitResult is called.
From the arrangement in the second code snippet, you have a FormWidget inside a Column. In that same Colum, you have the if-block that has a button with the problematic await FormWidgetState().submitResult(); in its onPressed callback.
Column(
children: [
Container(
height: 450,
width: double.infinity,
child: const FormWidget()),
if (isLoading)
Container(
alignment: Alignment.bottomCenter,
child: ElevatedButton(
// ...
onPressed: () async {
// ...
await FormWidgetState().submitResult();
if (!mounted) return;
// ...
},
child: const Text('Submit',
style: TextStyle(color: Colors.white)),
),
)
else
const Center(
child: CircularProgressIndicator(
backgroundColor: Color.fromRGBO(0, 161, 39, 1)))
],
),
If my guess is right, you want to display the FormWidget and have the submit button to call submitResult() from that FormWidget. And that's why you did await FormWidgetState().submitResult(); in the onPressed callback.
Well, the await FormWidgetState().submitResult(); line is not tied to a widget, it is separate. It has no parent per se and it is not part of the widget tree. So, this explains why you get the above error. The FormWidget in the Container above the if-block is very different from the State widget you reinitialized in the problematic await FormWidgetState().submitResult(); line. When rendering the UI, Flutter had auto-initialized and linked a hidden FormWidgetState object for the above FormWidget.
In fact, if (!mounted) return; really has no effect the way we think it should. Because its own containing widget will always mounted inside that method. But then the preceding problematic line's mounted getter has nothing to do with this one's own. (Hope you understand).
Solution
The solution is to merge the upper and lower part of the Column into one widget. So that submitResult call would share the same context as the displayed widget (or rather will have a valid context).
You have 2 options:
Move this entire column to inside FormWidget
Move the definition submitResult out of FormWidget and into the second code snippet's file. Then call submitResult when the FormWidget's form is filled. You will have to find a way to save data externally (maybe using state management or shared preferences) across both widgets.
I expanded the details/steps of Option 1 as follows:
Let FormWidget take the isLoading bool parameter. The parent (second code snippet) will give it its value: FormWidget(isLoading).
Move the Column of the second code snippet to inside the build method of the first. FormWidgetState's build method should return that column. Your good programming skills will guide you on this step as it is a little technical. The upper part of the Column should be the ListView.builder. The Column should of course be inside the Scaffold's body.
The Column's lower part (the if-else block) should use widget.isLoading as bool condition. Remember that this isLoading came from the parent (step 1).
Noticed some other issue with the first code snippet.
Look at the definition of fetchFormFields() method that is called in initState(). You see, that method seems to have a possibility of an error too. You have an if block that does Navigation but does not add a return statement. So if navigation was successfully, the following setState call might be an error (as the FormWidget is no longer mounted).
The comments in the following code will explain. I added the return statement:
fetchFormFields() async {
setState(() {
loading = true;
});
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences localStorage = await SharedPreferences.getInstance();
var saved = localStorage.getString('activitytask_id');
var res = await Network().getData('mobile/activity-fields/$saved');
if (res.statusCode == 200) {
final data = jsonDecode(res.body);
final tdata = data['data'];
if (tdata.length == 0) {
Navigator.of(context).pushReplacementNamed('error');
// maybe a return statement is supposed to be found here.
// adding return statement here prevents the next setState from being called
return;
}
var formfieldsJson = tdata;
// because if tdata.length was actually zero, the Navigator would have
// navigated out and this widget will no longer be mounted.
//
// setState here would then be called on a "no-longer-existing" widget.
//
// But the added return statement in the if block above should prevent this.
setState(() {
for (Map formfieldJson in formfieldsJson) {
_formfields.add(FormModel.fromJson(formfieldJson));
}
loading = false;
});
}
}
I need help with flutter build management. My Goal is to rebuild my side navigation bar with the SmallSideMenu() or the opposite SideMenu().
Do you have an idea how I can trigger the Build process of my LargeView widget, so that it's rebuilt with the correspondent SideMenu?
The button is defined like this:
IconButton(
onPressed: () {
checkState();
},
icon: HeroIcon(
HeroIcons.arrowNarrowRight,
size: 16.0,
),
),
The value of the sideMenuOpen variable and function is set globally;
checkState() {
if (sideMenuOpen == true) {
sideMenuOpen = false;
} else {
sideMenuOpen = true;
}
}
the SideMenu is defined here.
class LargeView extends StatefulWidget {
const LargeView({
Key? key,
}) : super(key: key);
#override
State<LargeView> createState() => _LargeViewState();
}
class _LargeViewState extends State<LargeView> {
#override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: SizedBox(
width: 100,
child: sideMenuOpen ? SideMenu() : SmallSideMenu(),
),
),
Expanded(
flex: 10,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20.0),
color: greyColor,
child: Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0)),
child: localNavigator())),
),
)
],
);
}
}
make sideMenuOpen a state in stateful widget and use setState as
checkState() {
if (sideMenuOpen == true) {
setState((){
sideMenuOpen = false;
});
} else {
setState((){
sideMenuOpen = true;
});
}
}
if you want to keep state (open/close) of navigation bar globally for that use some state management like provider instead of using global function.
You can use ValueNotifier instead of single bool. And to update UI it can be used on ValueListenableBuilder.
final ValueNotifier<bool> sideMenuOpen = ValueNotifier(false);
And
child: SizedBox(
width: 100,
child: ValueListenableBuilder<bool>(
valueListenable: sideMenuOpen,
builder: (context, value, child) => value
? SideMenu(),
: SmallSideMenu(),
),
),
And change value like
sideMenuOpen.value = true;
This is my code for making the star turn yellow and grey consistently, but it didn't work. Why?
Widget placeButton(BuildContext context, String name, String image, String id) {
var color;
var isClicked = true;
return Column(children: [
Padding(
padding: const EdgeInsets.fromLTRB(130.0, 0.0, 130.00, 0.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SizedBox(
height: 300,
width: 400,
child: GestureDetector(
onTap: () {
Navigator.pushNamed(context, id);
},
child: Card(
child: ListTile(
title: Padding(
padding: const EdgeInsets.fromLTRB(20, 5, 20, 0),
child: Text(AppLocalizations.of(context)!.translate(name),
style: TextStyle(fontFamily: 'Manrope', fontSize: 18)),
),
subtitle: Image.network(
image,
height: 250,
),
),
),
),
),
),
),
Container(
height: 40,
width: 200,
padding: EdgeInsets.only(top: 0),
color: Colors.white,
child: ListTile(
leading: Text(
"Rating:",
style: TextStyle(fontFamily: 'Klasik'),
),
trailing: IconButton(
splashRadius: 20,
icon: Icon(Icons.star_border_outlined, color: color),
onPressed: () {
if (isClicked) {
color = Colors.grey;
isClicked = !isClicked;
} else {
color = Colors.yellow;
isClicked = !isClicked;
}
},
),
),
),
]);
}
P/S: Feel free to ask me about the code below. I tried setState but it didn't work. It returns error like this:
The function 'setState' isn't defined.
Try importing the library that defines 'setState', correcting the name to the name of an existing function, or defining a function named 'setState'.
setState is only available within the scope of a StatefulWidget. You might want change the StatelessWidget to a stateful one. Once that's done, you'll need to introduce the variables color and isClicked outside of the build method or any other method that's called from build (such as placeButton in this example). Then you can call setState and change those values.
Here's a simplified example
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// Define these outside of your build method
Color color = Colors.grey;
bool isClicked = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: placeButton(context, 'name', 'image', 'id')),
);
}
Widget placeButton(
BuildContext context,
String name,
String image,
String id,
) {
// rest of your function...
onPressed: () {
// Use setState to change the values
setState(() {
if (isClicked) {
color = Colors.grey;
isClicked = !isClicked;
} else {
color = Colors.yellow;
isClicked = !isClicked;
}
});
},
}
}
When using setState, Flutter will run the build function again and notice the values of color and isClicked have changed and use those new values to update the UI.
Here is how I am trying to read data from a single document in Firestore.
final CollectionReference myCollection =
FirebaseFirestore.instance.collection('collection');
//
Future getScore(String level) async {
var document = await myCollection.doc(uid).get();
for (int i = 0; i < document.data().length; i++) {
print(document.data().keys.elementAt(i));
print(document.data().values.elementAt(i));
}
}
I call this code on press of a button like this:
Expanded(
flex: 1,
child: Container(
alignment: Alignment.centerLeft,
child: ElevatedButton(
onPressed: () {
//////////////////////////////////////////////////////////////////////
// This is where I call the getScore function in database dart file //
//////////////////////////////////////////////////////////////////////
DatabaseService(uid: globals.uid).getScore(
'level11',
);
/////////////////////////////////////////////////////////////////////////////////////////////////
// I would like the circular indicator until data is fetched and before the new view is pushed //
/////////////////////////////////////////////////////////////////////////////////////////////////
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => GameHome()));
},
child: Text(
'Play',
style: Theme.of(context).textTheme.headline2,
),
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
padding: EdgeInsets.symmetric(
horizontal: 5.25 * SizeConfig.textMultiplier,
vertical: 1.125 * SizeConfig.textMultiplier),
side: BorderSide(width: 1, color: Colors.black26),
backgroundColor: Colors.black,
),
),
),
),
I was wondering how can I show the circular progress indicator until the data is fetched and the view switch code is executed.
Thanks for any pointers.
Make your Widget a StatefullWidget and set a boolean value isLoading to false.
Make sure that your DatabaseService is asynchronous and await it.
If the value of isLoading is true show a CircularProgressIndicator.
Here is a basic example:
import 'package:flutter/material.dart';
class YourWidget extends StatefulWidget {
#override
_YourWidgetState createState() => _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
bool isLoading = false;
Future getScore() async {
setState(() {
isLoading = true;
});
//////////////////////////////////////////////////////////////////////
// This is where I call the getScore function in database dart file //
//////////////////////////////////////////////////////////////////////
await DatabaseService(uid: globals.uid).getScore(
'level11',
);
/////////////////////////////////////////////////////////////////////////////////////////////////
// I would like the circular indicator until data is fetched and before the new view is pushed //
/////////////////////////////////////////////////////////////////////////////////////////////////
setState(() {
isLoading = false;
});
Navigator.push(
context, CupertinoPageRoute(builder: (context) => GameHome()));
}
#override
Widget build(BuildContext context) {
return Expanded(
flex: 1,
child: Container(
alignment: Alignment.centerLeft,
child: Column(
children: [
ElevatedButton(
onPressed: getScore,
child: Text(
'Play',
style: Theme.of(context).textTheme.headline2,
),
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
padding: EdgeInsets.symmetric(
horizontal: 5.25 * SizeConfig.textMultiplier,
vertical: 1.125 * SizeConfig.textMultiplier),
side: BorderSide(width: 1, color: Colors.black26),
backgroundColor: Colors.black,
),
),
Visibility(visible: isLoading, child: CircularProgressIndicator())
],
),
),
);
}
}
Make your Widget a StatefullWidget and set a boolean value isLoading to false.
Make sure that your DatabaseService is asynchronous and await it.
If the value of isLoading is true show a CircularProgressIndicator.
Edit: You don't need to setState(){isLoading=false} because once you push to a new screen the state will be updated. Thus avoids building the screen everytime
Here is a basic example:
import 'package:flutter/material.dart';
class YourWidget extends StatefulWidget {
#override
_YourWidgetState createState() => _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
bool isLoading = false;
Future getScore() async {
setState(() {
isLoading = true;
});
//////////////////////////////////////////////////////////////////////
// This is where I call the getScore function in database dart file //
//////////////////////////////////////////////////////////////////////
await DatabaseService(uid: globals.uid).getScore(
'level11',
);
/////////////////////////////////////////////////////////////////////////////////////////////////
// I would like the circular indicator until data is fetched and before the new view is pushed //
/////////////////////////////////////////////////////////////////////////////////////////////////
Navigator.push(
context, CupertinoPageRoute(builder: (context) => GameHome()));
}
#override
Widget build(BuildContext context) {
return Expanded(
flex: 1,
child: Container(
alignment: Alignment.centerLeft,
child: Column(
children: [
ElevatedButton(
onPressed: getScore,
child: Text(
'Play',
style: Theme.of(context).textTheme.headline2,
),
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
padding: EdgeInsets.symmetric(
horizontal: 5.25 * SizeConfig.textMultiplier,
vertical: 1.125 * SizeConfig.textMultiplier),
side: BorderSide(width: 1, color: Colors.black26),
backgroundColor: Colors.black,
),
),
Visibility(visible: isLoading, child: CircularProgressIndicator())
],
),
),
);
}
}
This question already has answers here:
how to disable tooltip dynamcically in flutter?
(4 answers)
Closed 2 years ago.
I can disable the tooltip statically.
But I want to disable tooltip dynamically when i click flatbutton.But Couldnt disable dynamically and i have no idea to do that.
If I give statically false. it works fine.
For example : If add child like TopToolbar(showTooltip : false),it works fine,
But If i give toolbar.showTooltip = false in Flatbutton onPressed method,it doesnt work.
I want to disble it in dynamically. please help me to do that.
This is my code:
import 'package:flutter/material.dart';
void main(){
runApp(MaterialApp(home: HelloWorld(),debugShowCheckedModeBanner: false,));
}
class HelloWorld extends StatefulWidget {
#override
_HelloWorldState createState() => _HelloWorldState();
}
class _HelloWorldState extends State<HelloWorld> {
bool check = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(children: <Widget>[
TopToolbar(),
FlatButton(
child: Text("Disable Tooltip"),
onPressed: () {
setState(() {
TopToolbar toolbar = new TopToolbar();
toolbar.showTooltip = false;
});
},
),
]),
),
));
}
}
class TopToolbar extends StatefulWidget {
bool showTooltip;
final Color backgroundColor;
final double height;
bool isVisible;
TopToolbar({
this.height = 55,
this.isVisible = true,
this.backgroundColor = const Color(0xFFEEEEEE),
Key key,this.showTooltip=true,
}) : super(key: key);
#override
_TopToolbarState createState() => _TopToolbarState();
}
class _TopToolbarState extends State<TopToolbar> {
#override
Widget build(BuildContext context) {
if (widget.isVisible) {
return Container(
foregroundDecoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey,
),
),
),
margin: EdgeInsets.only(bottom: 1),
color: widget.backgroundColor,
height: widget.height,
child: Stack(
children: <Widget>[
Positioned(
top: 7,
right: 60,
height: 40,
width: 40,
child: RawMaterialButton(
elevation: 0.0,
fillColor: widget.backgroundColor,
splashColor: Colors.grey[300],
child: IconButton(
icon: Icon(
Icons.bookmark,
color: Colors.grey[500],
size: 25,
),
onPressed: (){},
tooltip: widget.showTooltip ? "Bookmark" : null,
),
onPressed: (){},
),
),
],
),
);
} else {
return Container();
}
}
}
You have to store whether to show the tooltip in _HelloWorldState, not in the TopToolbar.
This would lead to doing something like this in _HelloWorldState:
class _HelloWorldState extends State<HelloWorld> {
bool showTip = true;
bool check = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(children: <Widget>[
TopToolbar(showTip),
FlatButton(
child: Text("Disable Tooltip"),
onPressed: () {
setState(() {
showTip = false;
});
},
),
]),
),
));
}
}
showTooltip should also be marked as final in TopToolbar class.
Your current implementation creates a new TopToolbar widget, it doesn't modify the existing widget. TopToolbar toolbar = new TopToolbar(); creates a completely different widget, just one that isn't ever mounted and shown. Therefore, toolbar.showTooltip = false; has no visible effect.
Alternatively to what I have shown you can access of the State of the TopToolbar using a GlobalKey, but I wouldn't recommend this for a beginner, it isn't necessary for your implementation at the moment, and GlobalKeys are relatively expensive.
This is too simple buddy,
make 1 global variable below main method
bool isTooltipActive = true;
Now change onPressed method like this
FlatButton(
child: Text("Disable Tooltip"),
onPressed: () {
setState(() {
if(isToolTipAvtive == false){
isToolTipAvtive = true;
}else{
isToolTipAvtive = false;
}
});
},
),
And change bookmark tooltip line like this
tooltip: isToolTipAvtive ? "Bookmark" : null,