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'm trying to pass the String parameter to the widgets but unfortunately it's not working and i don't know how is that happening because i have worked on the same parameter in another widget and it has worked :'(.
Here is my code:
MainPage.dart
//---------------------------------------------------------
//Assigned String from here and it works with the widgets except: MainHydrationProgressPage
//---------------------------------------------------------
late final String? tanksID = ModalRoute.of(context)?.settings.arguments as String?;
late final _pages = <Widget>[
MainTankHydrationPoolPage(tankID: tanksID,),
MainHydrationProgressPage(tanksID: tanksID,),
SummaryPage(tanksID: tanksID),
];
MainHydrationProgressPage.dart
import 'package:flutter/material.dart';
import 'package:smart_tank1/main_tank_detail_ui/hydration_progress/progress_view.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_locales/flutter_locales.dart';
class MainHydrationProgressPage extends StatefulWidget {
//-----------------
//Here is the String that i want to work on
//-----------------
final String? tanksID;
MainHydrationProgressPage({Key? key, required this.tanksID});
static const routeName = 'progress-screen';
#override
_MainHydrationProgressPageState createState() => _MainHydrationProgressPageState();
}
class _MainHydrationProgressPageState extends State<MainHydrationProgressPage> {
bool onAndOff = false;
late final dbRef = FirebaseDatabase.instance;
//final fStore = FirebaseFirestore.instance;
#override
void initState(){
dbRef;
//fStore;
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.only(bottom: 136, top: 32.0),
child: Column(
children: [
SizedBox(width: double.infinity),
LocaleText(
"waterPercentage",
style: Theme.of(context).textTheme.headline4,
),
Expanded(
//-------------------------------------------------------------------------------------------
//Trying to assign tanksID here but it's not working it keeps showing the error line under it
//-------------------------------------------------------------------------------------------
child: ProgressView(ID: tanksID),
),
SizedBox(height: 10,),
Padding(
padding: const EdgeInsets.only(left: 20.0, right: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleText('pump', style: TextStyle(fontSize: 20),),
buildSwitch(),
],
),
),
],
),
),
);
}
Widget buildSwitch() => Transform.scale(
scale: 1.5,
child: Switch.adaptive(
activeColor: Colors.blueAccent,
activeTrackColor: Colors.blue.withOpacity(0.4),
inactiveThumbColor: Colors.white,
inactiveTrackColor: Colors.grey,
splashRadius: 50,
value: onAndOff,
onChanged: (value){
final user = FirebaseAuth.instance.currentUser;
setState(() {
onAndOff = value;
if (value) {
//--------------
//1. Firestore
//--------------
// fStore.collection('esp').doc(user.uid).update({
// 'pump': 'on',
// });
//-----------------
//2. Realtime database
//-----------------
dbRef.ref().child('users'). child(user!.uid).child('pump').set('on').asStream();
}else {
//--------------
//1. Firestore
//--------------
// fStore.collection('esp').doc(user.uid).update({
// 'pump': 'off',
// });
//-----------------
//2. Realtime database
//-----------------
dbRef.ref().child('users').child(user!.uid).child('pump').set('off').asStream();
}
},
);
}
),
);
}
Here is what it tells me: Undefined name 'tanksID'.
Try correcting the name to one that is defined, or defining the name.
Anyone faced this problem before? and how can it be solved!
I did flutter clean and flutter get packages then restarted vscode but it didn't work! :'(
please help
It worked with you before because maybe you were using a stateless widget or the variable defined in the state itself, but in this case you have a stateful widget and the variable is defined in the stateful widget not in the state itself, so you have to call it like: widget.tanksID
i am trying to retrieve data using rest api from woocommerce website using flutter
this is the api for retrieve json data
Future<List<Product>> getProducts(String tagId) async {
List<Product> data = new List<Product>();
try {
String url = Config.url +
Config.productsURL +
"?consumer_key=${Config.key}&consumer_secret=${Config.secret}&tag=$tagId";
var response = await Dio().get(url,
options: new Options(
headers: {HttpHeaders.contentTypeHeader: "application/json"}));
if (response.statusCode == 200) {
data = (response.data as List).map((i) => Product.fromJson(i),).toList();
}
} on DioError catch (e) {
print(e.response);
}
return data;
}
this is the widget to handle the data to the mobile app
class WidgetHomeProducts extends StatefulWidget {
WidgetHomeProducts({Key key, this.labelName, this.tagId}) : super(key : key);
String labelName;
String tagId;
#override
_WidgetHomeProductsState createState() => _WidgetHomeProductsState();
}
class _WidgetHomeProductsState extends State<WidgetHomeProducts> {
APIServices apiServices;
#override
void initState() {
apiServices = new APIServices();
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
color: const Color(0xffF4F7FA),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 16, top: 4),
child: Text(
this.widget.labelName,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
Padding(
padding: EdgeInsets.only(left: 16, top: 4),
child: FlatButton(
onPressed: () {},
child: Text(
'View All',
style: TextStyle(color: Colors.blueAccent),
),
),
),
],
),
_productList(),
],
),
);
}
Widget _productList(){
return new FutureBuilder(
future: apiServices.getProducts(this.widget.tagId),
builder: (BuildContext context, AsyncSnapshot<List<Product>> model){
if(model.hasData){
return _buildList(model.data);
}if(model.hasError){
print("error");
}
return Center(child: CircularProgressIndicator(),);
});
}
i got The method error message that says
'getProducts' was called on null.
Receiver: null
Tried calling: getProducts("971")
can anyone help me to fix this?
I'm currently developing a Fingerspelling learning app. In the fingerspelling_screen.dart file, user will be able to choose which category that he/she would like to learn first. When the user chooses either one of the button, the app will query Firebase Firestore to get a list of signs object which contains the name of the sign, the category to which the sign belongs to and the video URL of each sign before navigating user to sign_video.dart which is a video player. From there, user can press next to switch to the next sign in the category. When the user presses on the check sign button, the user will be navigating to the check sign_checker.dart to check if they are performing a sign correctly.
I've been passing the 'category' variable from one screen to another using constructor and I do not think it is very effective. Is there any way I could solve this?
I wish to initilize vidlist variable (a variable that stores a list of Sign objects) in the initState because the video player controller needs to be initialized first. I've tried using StreamProvider, but for some reason, I couldn't initialize the vidlist variable in initState. The vidlist variable would always be null.
Thank you.
fingerspelling.dart
import 'package:slem_proto/screens/sign_video.dart';
import 'package:slem_proto/shared/constants.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:undraw/undraw.dart';
class FingerspellingScreen extends StatelessWidget {
static String routeName = 'fingerspelling_screen';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.only(left: 20, right: 20, top: 50),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Fingerspelling',
style: kHeadingTextStyle,
),
SizedBox(height: 30),
FingerspellingTab(
title: 'Alphabets',
illustration: UnDrawIllustration.learning,
onTap: () {
print('Alphabet tab tapped');
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SignVideoScreen(
category: 'alphabets',
),
),
);
},
),
SizedBox(
height: 15,
),
FingerspellingTab(
title: 'Numbers',
illustration: UnDrawIllustration.calculator,
onTap: () {
print('Number tab tapped');
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SignVideoScreen(
category: 'numbers',
),
),
);
},
),
],
),
),
);
}
}
class FingerspellingTab extends StatelessWidget {
final String title;
final UnDrawIllustration illustration;
final Function onTap;
const FingerspellingTab(
{#required this.title,
#required this.illustration,
#required this.onTap});
#override
Widget build(BuildContext context) {
return InkWell(
child: Container(
width: double.infinity,
height: 250,
decoration: BoxDecoration(
color: Color.fromRGBO(58, 139, 238, 0.2),
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.only(left: 20, top: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: kHeadingTextStyle.copyWith(
color: Color.fromRGBO(80, 80, 80, 0.8),
fontSize: 25,
),
),
SizedBox(
height: 15,
),
Container(
height: 150,
child: UnDraw(
color: Color(0xFF6C63FF),
illustration: illustration,
placeholder: Text(
"Illustration is loading..."), //optional, default is the CircularProgressIndicator().
),
),
],
),
),
),
onTap: onTap,
);
}
}
database.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:slem_proto/models/sign.dart';
class DatabaseService {
// collection reference
final CollectionReference signCollection =
FirebaseFirestore.instance.collection('signs');
// Sign list from snapshot
List<Sign> _signListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map((doc) {
return Sign(
category: doc.data()['category'] ?? '',
sign_name: doc.data()['sign_name'] ?? '',
sign_url: doc.data()['sign_url'] ?? '',
);
}).toList();
}
// get signs stream
Stream<List<Sign>> get signs {
return signCollection.snapshots().map(_signListFromSnapshot);
}
// get signs stream
Stream<List<Sign>> getSignFromCategory({String category}) {
return signCollection
.where('category', isEqualTo: category)
.snapshots()
.map(_signListFromSnapshot);
}
}
sign_checker.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
class SignChecker extends StatefulWidget {
final String category;
SignChecker({this.category});
#override
_SignCheckerState createState() => _SignCheckerState(category: this.category);
}
class _SignCheckerState extends State<SignChecker> {
final String category;
_SignCheckerState({this.category});
File _image;
bool predictionStarted = false;
bool predictionComplete = false;
var predictionResult = 'Please wait...';
Future getImage() async {
setState(() {
predictionStarted = false;
predictionComplete = false;
});
// Get image from camera
// var image = await ImagePicker.pickImage(source: ImageSource.camera);
// Get image from gallery
var image = await ImagePicker.pickImage(source: ImageSource.gallery);
setState(() {
_image = image;
predictionStarted = true;
});
// Base64 Encode the image
List<int> imageBytes = image.readAsBytesSync();
String base64Image = base64.encode(imageBytes);
// Print the base64 encoded string in console
print(base64Image);
// Send the encoded image with POST request
Map<String, String> headers = {"Accept": "application/json"};
Map body = {"image": base64Image};
// var response = await http.post('http://XX.XXX.XXX.X/automl.php',
// body: body, headers: headers);
var response = await http.post('http://XX.XXX.XXX.X/automl_alphabet.php',
body: body, headers: headers);
// Print the status code returned by server
print('Status code');
print(response.statusCode);
// Get prediction Result
setState(() {
predictionResult = response.body;
predictionComplete = true;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sign Checker'),
),
body: SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(
height: 20,
),
Text(
'Push the camera button',
textAlign: TextAlign.center,
),
RaisedButton(
onPressed: getImage,
child: Text('Camera'),
),
(_image != null)
? Image.file(
_image,
scale: 50,
)
: Text('No Image Picked'),
predictionBody()
],
),
),
);
}
Widget predictionBody() {
var predictionText = (predictionComplete) ? 'Result' : 'Prediction started';
if (predictionStarted) {
return Column(
children: <Widget>[
Divider(),
Text(predictionText),
Text(predictionResult)
],
);
} else {
return Container();
}
}
}
sign.dart
class Sign {
final String category;
final String sign_name;
final String sign_url;
Sign({this.category, this.sign_name, this.sign_url});
}
Firstly, using constructors to pass variables may not be inefficient, since Flutter only pass on references. Say,
var a = List(...huge list...);
var b = a;
Then the second line is not costly.
Secondly, if you ask about state management, you may try Mobx, Bloc, Redux, etc. There are many ways to do so.
I am building my first big app in Flutter, and the first one where I need State Management, so I turned to Provider which is the recommended package to use for State Management. However I am having some issues where I declare my Providers in the main.dart file and down the tree I want to make changes and interact with one of the Providers but no matter what solution I try, I keep getting the same error: "Tried to listen to a value exposed with provider, from outside of the widget tree.". I get this error even though according the flutter inspector, the widget from where I am trying to make changes to the provider is inside of the widget tree (the "HomeScreen" screen is from where I am updating the provider).
Below I also share my code:
main.dart:
import 'package:flutter/material.dart';
import 'package:tic_tac_2/screens/welcome_screen.dart';
import 'package:provider/provider.dart';
import 'package:tic_tac_2/models/restaurants_data.dart';
import 'package:tic_tac_2/models/promotions_data.dart';
import 'package:tic_tac_2/models/user.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<User>(create: (context) => User(),),
ChangeNotifierProvider<RestaurantsData>(create: (context) => RestaurantsData(),),
ChangeNotifierProvider<PromotionsData>(create: (context) => PromotionsData(),),
],
child: MaterialApp(
title: 'Tic Tac',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: WelcomeScreen(),
),
);
}
}
welcome_screen.dart:
import 'package:flutter/material.dart';
import 'package:animated_text_kit/animated_text_kit.dart';
import 'package:tic_tac_2/components/rounded_button.dart';
import 'login_screen.dart';
import 'register_screen.dart';
class WelcomeScreen extends StatelessWidget {
static const String id = 'welcome_screen';
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xff000080),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
children: <Widget>[
Hero(
tag: 'logo',
child: Container(
child: Image.asset('images/pin.png'),
height: 60.0,
),
),
TypewriterAnimatedTextKit(
text: ['Tic Tac'],
textStyle: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 45.0,
color: Colors.white
),
),
],
),
SizedBox(
height: 48.0,
),
RoundedButton(
title: 'Entrar',
colour: Colors.lightBlueAccent,
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginScreen()));
//Navigator.pushNamed(context, LoginScreen.id);
},
),
RoundedButton(
title: 'Registro',
colour: Colors.blueAccent,
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => RegistrationScreen()));
//Navigator.pushNamed(context, RegistrationScreen.id);
},
),
],
),
),
);
}
}
login_screen.dart:
import 'package:flutter/material.dart';
import 'package:tic_tac_2/components/rounded_button.dart';
import 'package:tic_tac_2/constants.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:modal_progress_hud/modal_progress_hud.dart';
import 'home_screen.dart';
import 'package:tic_tac_2/models/user.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:rflutter_alert/rflutter_alert.dart';
import 'package:email_validator/email_validator.dart';
final _firestore = Firestore.instance;
class LoginScreen extends StatefulWidget {
static const String id = 'login_screen';
#override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
bool showSpinner = false;
final _auth = FirebaseAuth.instance;
String email;
String password;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: ModalProgressHUD(
inAsyncCall: showSpinner,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Flexible(
child: Hero(
tag: 'logo',
child: Container(
height: 200.0,
child: Image.asset('images/pin.png'),
),
),
),
SizedBox(
height: 48.0,
),
TextFormField(
validator: (val) => !EmailValidator.validate(val, true)
? 'Correo inválido'
: null,
keyboardType: TextInputType.emailAddress,
textAlign: TextAlign.center,
onChanged: (value) {
email = value;
},
decoration: kTextFieldDecoration.copyWith(
hintText: 'Escribe tu correo'),
),
SizedBox(
height: 8.0,
),
TextFormField(
validator: (val) =>
val.length < 6 ? 'La contraseña es muy corta' : null,
obscureText: true,
textAlign: TextAlign.center,
onChanged: (value) {
password = value;
},
decoration: kTextFieldDecoration.copyWith(
hintText: 'Escribe tu contraseña'),
),
SizedBox(
height: 24.0,
),
RoundedButton(
title: 'Entrar',
colour: Colors.lightBlueAccent,
onPressed: () async {
if (_formKey.currentState.validate()) {
setState(() {
showSpinner = true;
});
try {
final user = await _auth.signInWithEmailAndPassword(
email: email, password: password);
if (user != null) {
return _firestore
.collection('user')
.document(user.user.uid)
.get()
.then((DocumentSnapshot ds) {
User localUser = User(
uid: user.user.uid,
email: email,
role: ds.data['role']);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomeScreen(
user: user.user,
newUser: localUser,
)));
});
}
setState(() {
showSpinner = false;
});
} catch (e) {
setState(() {
showSpinner = false;
});
Alert(
context: context,
title: "Error en el registro",
desc: e)
.show();
print(e);
}
}
},
),
],
),
),
),
),
);
}
}
home_screen.dart:
import 'package:tic_tac_2/models/user.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:async';
import 'package:tic_tac_2/models/restaurants_data.dart';
import 'package:provider/provider.dart';
import 'package:tic_tac_2/models/promotions_data.dart';
import 'package:tic_tac_2/widgets/RestaurantList.dart';
import 'package:geolocator/geolocator.dart';
Geoflutterfire geo = Geoflutterfire();
FirebaseUser loggedInUser;
User localUser;
class HomeScreen extends StatefulWidget {
final FirebaseUser user;
final User newUser;
const HomeScreen({Key key, this.user, this.newUser}) : super(key: key);
static const String id = 'home_screen';
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _firestore = Firestore.instance;
GoogleMapController mapController;
var pos;
Stream<dynamic> query;
StreamSubscription subscription;
#override
void dispose() {
// TODO: implement dispose
super.dispose();
subscription.cancel();
}
#override
void initState() {
// TODO: implement initState
super.initState();
if (localUser == null) {
localUser = widget.newUser;
loggedInUser = widget.user;
}
}
#override
Widget build(BuildContext context) {
void _getCurrentLocation(BuildContext context) async {
try {
Position position = await Geolocator()
.getCurrentPosition(desiredAccuracy: LocationAccuracy.low);
print('lat');
print(position.latitude);
print('lng');
print(position.longitude);
final QuerySnapshot restaurants = await _firestore.collection('restaurants').getDocuments();
for(var restaurant in restaurants.documents) {
print(restaurant);
Provider.of<RestaurantsData>(context).addRestaurant(
name: restaurant.data['name'],
owner: restaurant.data['owner'],
location: restaurant.data['location'],
uid: restaurant.data['uid'],
);
}
} catch (e) {
print(e);
}
}
WidgetsBinding.instance.addPostFrameCallback((_) => _getCurrentLocation(context));
print(Provider.of<RestaurantsData>(context).restaurants);
return Scaffold(
backgroundColor: Color(0xff000080),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: EdgeInsets.only(
top: 60.0,
bottom: 30.0,
left: 30.0,
right: 30.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
CircleAvatar(
child: Icon(
Icons.list,
size: 30.0,
color: Color(0xff000080),
),
backgroundColor: Colors.white,
radius: 30.0,
),
SizedBox(
height: 10.0,
),
Text(
'Tic Tac',
style: TextStyle(
fontSize: 50.0,
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
Text(
'Restaurantes',
style: TextStyle(color: Colors.white, fontSize: 18.0),
)
],
),
),
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
),
),
child:
Provider.of<RestaurantsData>(context).restaurants.length > 0
? RestaurantList()
: Container(),
),
),
],
),
);
}
}
The thing causing the problem in the home_screen file, as far as I can tell, is the "getCurrentLocation(BuildContext context){}" function, and how and when I call it.
I have tried turning everything into statelessWidgets, calling the getLocation funtion without the "WidgetsBinding.instance.addPostFrameCallback(() => _getCurrentLocation(context));" line. I have tried not passing the context to the function, among other solutions that I have tried.
I really appreciate your help and I would like to thank you in advance. If you have any doubts regarding the code I will be more than happy to answer all of them.
Please understand the solution either on your own or via my explanation below. Don't just use my answer without understanding it. Although this is a simple flag you can just specify/flip, understanding it is the core of why Provider is even used.
New Solution
In your _getCurrentLocation method, which is hypothetically updated to the latest Provider pub version. Change:
Provider.of<RestaurantsData>(context).addRestaurant();
context.watch<RestaurantsData>().addRestaurant();
TO
Provider.of<RestaurantsData>(context, listen: false).addRestaurant();
context.read<RestaurantsData>().addRestaurant();
Drawing parallel to the old solution related to the old verison, read plays the same role as listen: false. Either is used to fix the OP's exception that's caused by watch playing the same role as listen: true. Important explanation on this can be found here and here. Thanks to user Vinoth Vino for alerting this new change via his comment.
Old Solution
In your _getCurrentLocation method, change
Provider.of<RestaurantsData>(context).addRestaurant()
to
Provider.of<RestaurantsData>(context, listen: false).addRestaurant()
Explanation
As the error illustrates
Tried to listen to a value exposed with provider, from outside of the widget tree.
You're getting notification update from your Provider instance from outside the widget tree. i.e. your Provider instance is calling Provider method NotifyListeners() which sends updates to all listeners. And this particular invocation in your question is listening to those updates, which is: Provider.of<RestaurantsData>(context)
This is happening because addPostFrameCallback is causing its parameter callback to be called outside your widget tree. This latter callback is encapsulating _getCurrentLocation local function. In turn this function has the Provider instance invocation. This sequence of events led the provider invocation to listen to updates outside the widget tree.
It's erroneous to listen to notification updates outside your widget tree e.g. user-action callbacks or initState.
To fix this issue, you need to assign listen flag to its non-default value false in code scopes outside your widget tree. e.g. initState or user-interaction callbacks or any code scope not directly under the widget's build method.
Provider Usage
This is how I use provider:
When watching/listening to Provider's values, Consumer in general and Selector for being picky/selective about when to cause a widget rebuild for performance reasons when you have a lot of Provider listen updates for different reasons and you just want to rebuild your widget tree for one particular reason. These methods for listening to changes are more versatile: makes it more clear which block of widgets are being rebuilt and also makes it's possible to access Provider without BuildContext e.g. from StatelessWidget or some helper method of a StatefulWidget that does not have a reference to BuildContext.
When reading/accessing Provider's values without caring about notifications/updates/changes to them. Then use Provider.of<T>(context, listen: false)
When using/calling Provider's services/methods and not values, use Provider.of<T>(context, listen: false).myMethod() e.g. Provider.of<RestaurantsData>(context, listen: false).addRestaurant() since most of the time you don't need to listen to Provider updates in this case.
Related References
To further understand listen flag behavior and the reasoning behind your exception, check out the GitHub docs here and source code docs. If you're REALLY interested, check this GitHub discussion.
To understand listen flag default value, check these author's issue comments here and here.