Related
I'm trying to create a pageview that I can load a widget into that is defined in another file. This works fine, except when I try to add a callback, I get the following error:
FlutterError (setState() or markNeedsBuild() called during build.
This error is triggered when the email entered is considered to be valid (that is, when the code in the email_entry.dart calls the callback function that was passed from the account_onboarding.dart file.) I haven't been able to determine why this is happening, and no tutorials on this subject seem to exist. I am still pretty new to Dart/Flutter, so I'm hoping someone can point out what's happening (and a fix) here.
Here is my code:
-Parent widget, account_onboarding.dart
import 'package:flutter/material.dart';
import 'package:page_view_indicators/page_view_indicators.dart';
import 'package:animated_title_screen/screens/email_entry.dart';
class AccountOnboarding extends StatefulWidget {
const AccountOnboarding({Key? key}) : super(key: key);
#override
State<AccountOnboarding> createState() => _AccountOnboardingState();
}
class _AccountOnboardingState extends State<AccountOnboarding> {
final _pController = PageController(initialPage: 0);
final _currentPageNotifier = ValueNotifier<int>(0);
final List<Widget> _pages = [];
bool validEmail = false; //Callback should set this variable
#override
void initState() {
super.initState();
_pages.add( //Add the EmailEntry widget to the list
EmailEntry(emailIsValid: (p0) {
setState(() {
validEmail = p0;
});
},),
);
_pages.add(
Container(
color: Colors.blue,
child: Text("Pg2"),
),
);
_pages.add(
Container(
color: Colors.green,
child: Text("Pg3"),
),
);
}
#override
void dispose() {
_pController.dispose();
_currentPageNotifier.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Create Account",
style: Theme.of(context).textTheme.headline5,
),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
body: Stack(
fit: StackFit.expand,
children: [
Column(
children: [
Row(
children: [
Text(
"Step ${_currentPageNotifier.value + 1} of ${_pages.length}",
),
CirclePageIndicator(
dotColor: const Color(0xFF323232),
selectedDotColor: const Color(0xFFE4231F),
size: 10,
selectedSize: 10,
currentPageNotifier: _currentPageNotifier,
itemCount: _pages.length,
),
],
),
PageView(
controller: _pController,
onPageChanged: (index) {
setState(() {
_currentPageNotifier.value = index;
});
},
children: [
for (Widget p in _pages) p, //Display all pages in _pages[]
],
),
ElevatedButton(
child: const Text("Continue"),
onPressed: () => print("Pressed 2"),
style: ElevatedButton.styleFrom(
primary: validEmail ? const Color(0xFFE1251B) : Colors.black,
textStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
],
),
],
),
);
}
}
Here is the email_entry.dart code:
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class EmailEntry extends StatefulWidget {
final Function(bool) emailIsValid; //Function required in constructor
const EmailEntry({Key? key, required this.emailIsValid}) : super(key: key);
#override
State<EmailEntry> createState() => _EmailEntryState();
}
class _EmailEntryState extends State<EmailEntry> {
final _emailController = TextEditingController();
FocusNode _emailFocus = FocusNode();
#override
void initState() {
super.initState();
_emailController.addListener(() => setState(() {}));
_emailFocus.addListener(() {
print("Focus email");
});
}
#override
void dispose() {
_emailController.dispose();
super.dispose();
}
bool validateEmail(String email) {
bool valid = RegExp(
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+#[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?)*$")
.hasMatch(email);
if (valid) {
widget.emailIsValid(true); //Call the callback function
}
return valid;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Column(
children: [
Text(
"YOUR EMAIL",
style: Theme.of(context).textTheme.headline2,
),
Text(
"Please use an email address that you would like to make your login.",
style: Theme.of(context).textTheme.bodyText2,
textAlign: TextAlign.center,
),
Container(
child: Text(
"Email Address",
),
),
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
focusNode: _emailFocus,
suffixIcon: getTextFieldSuffix(emailController, _emailFocus), //OFFENDING CODE
),
],
),
],
),
);
}
//THIS FUNCTION CAUSED THE ISSUE. It is code I got from a youtube //tutorial. Probably should have guessed.
Widget getTextFieldSuffix(TextEditingController controller, FocusNode node) {
if (controller.text.isNotEmpty && node.hasFocus) {
return IconButton(
color: Colors.grey.withAlpha(150),
onPressed: () => controller.clear(),
icon: const Icon(Icons.close));
} else if (controller.text.isNotEmpty && !node.hasFocus) {
return const Icon(
Icons.check,
color: Colors.green,
);
}
return Container(
width: 0,
);
}
}
in initState,you need to call addPostFrameCallback.
like this...
#override
void initState() {
super.initState();
///Add This Line
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
///All of your code
});
}
I found out that there is some code in my production version that calls a redraw every time the user enters a letter into the textfield for the email address. This was causing a problem because the screen was already being redrawn, and I was calling setState to redraw it again. I will edit the code shown above to include the offending code.
I'm creating a mobile application where users will select the video from the list and play.
So I have built two widgets in a flutter, one for displaying a video list from a certain URL and the other one for playing a video when the user clicks the video from the list.
I'm stacking on the way of dynamic picking URL from the list widget, I have tried to setState in the BuildContext method but does not seem to work.
I have tried to initialize the video player inside a build method but does not work instead I get "(HTTPLog)-Static: isSBSettingEnabled false" and continue looping without ending
I'm looking for anyways of getting these variables I sent through the "Navigator pushNamed" method from the video list page before the player initialized. or anything else which can help
I saw this answer 'https://stackoverflow.com/questions/56262655/flutter-get-passed-arguments-from-navigator-in-widgets-states-initstate' but I could not understand since I'm new to a flutter, i decided to ask again my be I can get a good answer
Thanks in advance!
I use the video_player flutter package and below are my code:
**video_players**
class PlayerWidget extends StatefulWidget {
const PlayerWidget({Key? key}) : super(key: key);
#override
_PlayerWidgetState createState() => _PlayerWidgetState();
}
class _PlayerWidgetState extends State<PlayerWidget> {
late VideoPlayerController _controller;
dynamic videoData={};
String vidUrl='https://videos.pond5.com/sunset-woman-running-ocean-reflection-footage-027692108_main_xxl.mp4';
Duration? _duration;
Duration? _position;
#override
void initState() {
super.initState();
//change screen orientation method
setScreenOrientations();
}
//orientations monitor
Future setScreenOrientations() async{
//change the screen orientation
await SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight,DeviceOrientation.landscapeLeft,]);
//remove status bar when on landscape mode
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
// The following line will enable the Android and iOS wakelock.
await Wakelock.enable();
}
Future removeOrientation() async{
await SystemChrome.setPreferredOrientations(DeviceOrientation.values);
//setback status bar
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom,SystemUiOverlay.top]);
//let screen sleep if there is no activities
Wakelock.disable();
}
//video progress bar
Widget buildIndicator() => Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
VideoProgressIndicator(
_controller,
allowScrubbing: true,
colors: const VideoProgressColors(
playedColor: Color(0xfff49043),
bufferedColor: Colors.grey,
backgroundColor: Colors.white30,
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(child: Text('${_printDuration(_position)} / ${_printDuration(_duration)}',style: const TextStyle(color: Colors.white),),),
Expanded(child: Align(
alignment: Alignment.center,
child: GestureDetector(
onTap: (){},
child: RichText(text: TextSpan(children: [const WidgetSpan(child: Icon(Icons.speed_outlined,color: Colors.white, size: 16),),TextSpan(text: " Speed (${_controller.value.playbackSpeed})",style: const TextStyle(fontSize: 14)),],),),
),
),),
Expanded(child: Align(
alignment: Alignment.center,
child: GestureDetector(
onTap: (){Navigator.pushReplacement(context, MaterialPageRoute(builder: (context)=> const Home()));},
child: RichText(text: const TextSpan(children: [WidgetSpan(child: Icon(Icons.video_library,color: Colors.white, size: 14),),TextSpan(text: " View Video",style: TextStyle(fontSize: 14)),],),),
),
),),
],
),
)
],
),
);
//convert time to the human readable format
String _printDuration(duration) {
if(duration==null){ return '00:00'; }
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
if(_duration?.inHours == 00){return "$twoDigitMinutes:$twoDigitSeconds";}
else{return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";}
}
#override
void dispose() {super.dispose();_controller.dispose();removeOrientation();}
#override
Widget build(BuildContext context) {
//get all value sent from the video widget route and hold them
videoData = ModalRoute.of(context)!.settings.arguments;
setState((){ vidUrl = videoData['videoUrl'];});
_controller = VideoPlayerController.network("${videoData['videoUrl']}")
..addListener(() {
Timer.run(() {setState((){ _position = _controller.value.position;}); });
setState(() {_duration = _controller.value.duration;});
})
..initialize()
.then((_) {});
//play pause state listener to toggle the button
Widget playPauseBtn() => _controller.value.isPlaying ? Container(alignment: Alignment.center,color: Colors.black26, child: const Icon(Icons.pause, color: Colors.white, size: 80),)
: Container(alignment: Alignment.center,color: Colors.black26, child: const Icon(Icons.play_arrow, color: Colors.white, size: 80),);
//overlay on the video player
videoOverlay(){
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {setState(() {_controller.value.isPlaying ? _controller.pause() : _controller.play();});},
child: Stack(children: <Widget>[playPauseBtn(),Positioned(bottom: 0,left: 0,right: 0,child: buildIndicator(),),],
),
);
}
//video player stack
Widget videoStack() => Stack(fit: StackFit.expand,children:[ AspectRatio(aspectRatio: _controller.value.aspectRatio,child: VideoPlayer(_controller),),]);
//complete player
Widget completePlayer() => Stack(children: <Widget>[videoStack(),Positioned.fill(child: videoOverlay()),],);
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: Text(videoData['videoName']),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
onPressed: () async {await SystemChrome.setPreferredOrientations(DeviceOrientation.values);Navigator.pop(context);},
icon: const Icon(Icons.arrow_back_ios_outlined),
),
),
backgroundColor: const Color(0xff121212),
body: _controller.value.isInitialized ? Container(alignment: Alignment.topCenter, child: completePlayer()) : const Center(child: CircularProgressIndicator(),),
);
}
}
Use Navigator.push instead.
Follow these steps:
1. Add videoData to the parameters of your widget and use widget.videoData to read in initState
class PlayerWidget extends StatefulWidget {
final Map<String, dynamic> videoData;
const PlayerWidget({Key? key, required this.videoData}) : super(key: key);
#override
_PlayerWidgetState createState() => _PlayerWidgetState();
}
class _PlayerWidgetState extends State<PlayerWidget> {
late VideoPlayerController _controller;
dynamic videoData={};
String vidUrl='https://videos.pond5.com/sunset-woman-running-ocean-reflection-footage-027692108_main_xxl.mp4';
Duration? _duration;
Duration? _position;
#override
void initState() {
super.initState();
//change screen orientation method
videoData = widget.videoData;
setState((){});
setScreenOrientations();
}
2. Use Navigator.push to navigate.
Navigator.push(context, MaterialPageRoute(builder: (_) => PlayerWidget(videoData: {'videoName': videoName,'videoUrl':videoUrl,'posterUrl':posterUrl}));
I am trying to create a favorite button for my app. Which work is to change and save color, while the user presses it, So I decided to use hive db for it. When the icon button is tapped; the color get changed, which indicates to the user that it's been marked as their favorite. The problem is when I tap it again(if the user wants to unmark it ) though the color get changed ,when i move to other page or hot start/reload the page, the color changed back to it former self automatically(To the color when it was first pressed).I want the color reactive through the button and be saved. How can I solve this issue?(I am kinda confused at the key part. Maybe that's where the problem occurred)
class p1 extends StatefulWidget {
#override
_p1State createState() => _p1State();
}
class _p1State extends State<p1> {
Box box;
bool _isFavorite = false;
_p1State();
#override
void initstate(){
super.initState();
// Get reference to an already opened box
box = Hive.box(FAVORITES_BOX);
final data = box.get(_isFavorite).containskey("1" != null ? Colors.white:Colors.red );
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body:Stack(
children:<Widget>[
Image(
image:AssetImage("Image/Chowsun1.jpg"),
fit:BoxFit.cover,
width: double.infinity,
height: double.infinity,
),
Align(alignment: Alignment.center,
child: Text(' "\n The entire world , \n is not worth \n A single Tear.\n'
' " \n -Imam Hazrat Ali (R) '
,style: TextStyle(fontSize: 35.0,
color: Colors.white,
fontFamily: "Explora",
fontWeight: FontWeight.w900 ) )
),
Stack ( children: [Positioned(
top:90,
right: 20,
child:const Text(' 1 ',
style: TextStyle(
fontSize: 25.0,
color: Colors.white,
fontFamily: "Comforter"
),
),
)], ),
Align(
alignment: Alignment.bottomCenter,
child: (
IconButton(
icon: Icon(
Icons.favorite,
color:_isFavorite ? Colors.white: Colors.red
),
onPressed: () {
setState(() {
_isFavorite= !_isFavorite;
});
if(box.containsKey(1)){
box.delete(1);
}else
box.put(1, _isFavorite);
}
)
)
)])
),
);
}
}
I see you're using a bool _isFavorite to record a like, and then you check for the value of is favorite again to know if to give a like or remove the like, well under the hood your hive is working but basically, the code you're using to update the color is not coming from the hive so when you reupdate your state, e.g Hot Reload everything is reset to the initial leaving your favorite button unchanged.
You basically just need to re-model your logic for it to work properly.
import 'package:hive_flutter/hive_flutter.dart';
void main() async {
await Hive.initFlutter();
await Hive.openBox("favorites");
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Box? box = Hive.box("favorites");
bool _isFavorite = false;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Stack(
children: <Widget>[
Align(
alignment: Alignment.center,
child: Text(
' "\n The entire world , \n is not worth \n A single Tear.\n'
' " \n -Imam Hazrat Ali (R) ',
style: TextStyle(
fontSize: 35.0,
color: Colors.white,
fontFamily: "Explora",
fontWeight: FontWeight.w900),
),
),
Stack(
children: [
Positioned(
top: 90,
right: 20,
child: const Text(
' 1 ',
style: TextStyle(
fontSize: 25.0,
color: Colors.white,
fontFamily: "Comforter"),
),
)
],
),
Align(
alignment: Alignment.bottomCenter,
child: (IconButton(
icon: Icon(Icons.favorite,
color: box!.isEmpty ? Colors.white : Colors.red),
onPressed: () {
setState(() {
_isFavorite = !_isFavorite;
});
if (box!.isEmpty)
box!.put("isFavorite", _isFavorite);
else
box!.delete("isFavorite");
},
)),
)
],
),
),
);
}
}
You can directly initialize hive while it is already open
Box box = Hive.box(FAVORITES_BOX);
bool _isFavorite = false;
#override
void initState() {
super.initState();
_isFavorite = box.get(0) ?? false;
}
And changing value
onPressed: () {
setState(() {
_isFavorite = !_isFavorite;
});
box.put(0, _isFavorite);
},
I'm trying to make a weather app with Flutter. But for some reason, the build() method runs before the initState() method finishes. The thing is, all the state variables are initialized using the setState() method inside the initState(), whose variables are to be used in build() method. I guess the problem is that Flutter is trying to access those state variables before the setState() method, which keeps throwing me the error: A non-null String must be provided to a Text widget. I know these codes are too long to read. But I'd appreciate it if you could help me with this.
import 'package:flutter/material.dart';
import "package:climat/screens/LoadingScreen.dart";
import "package:climat/screens/MainScreen.dart";
import "package:climat/screens/SearchScreen.dart";
void main() {
runApp(
MaterialApp(
theme: ThemeData(
fontFamily: "Open Sans",
),
title: "Climat",
initialRoute: "/",
onGenerateRoute: (RouteSettings routeSettings) {
dynamic routes = <String, WidgetBuilder>{
"/": (context) => LoadingScreen(),
"/index": (context) => MainScreen(),
"/search": (context) => SearchScreen(),
};
WidgetBuilder builder = routes[routeSettings.name];
return MaterialPageRoute(builder: (context) => builder(context));
},
),
);
}
import "package:flutter/material.dart";
import "package:flutter_spinkit/flutter_spinkit.dart";
import "package:climat/services/GeolocatorHelper.dart";
import "package:climat/services/NetworkHelper.dart";
import "package:climat/utilities/constants.dart";
class LoadingScreen extends StatefulWidget {
#override
_LoadingScreenState createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> {
Future<void> getWeatherData() async {
Map<String, double> coordinates = await GeolocatorHelper().getGeoData();
final NetworkHelper networkHelper = NetworkHelper(
uri:
"$endPoint&lat=${coordinates["latitude"]}&lon=${coordinates["longitude"]}");
final dynamic data = await networkHelper.getData();
return await Navigator.pushNamed(context, "/index", arguments: data);
}
#override
void initState() {
this.getWeatherData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Climat"),
),
body: SafeArea(
child: Center(
child: SpinKitRing(
color: Colors.redAccent,
),
),
),
);
}
}
import "package:flutter/material.dart";
import "package:climat/services/WeatherHelper.dart";
import "package:climat/services/NetworkHelper.dart";
import "package:climat/utilities/constants.dart";
class MainScreen extends StatefulWidget {
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
Color backgroundColor;
String cityName;
int temperature;
String status;
String image;
int minTemperature;
int maxTemperature;
int humidity;
Future<void> updateUI({String userInput = null}) async {
dynamic weatherData;
if (userInput == null) {
weatherData = ModalRoute.of(context).settings.arguments;
} else {
final NetworkHelper networkHelper =
NetworkHelper(uri: "$endPoint&q=$userInput");
weatherData = await networkHelper.getData();
}
final int weatherCode = weatherData["weather"][0]["id"];
final WeatherHelper weatherHelper = WeatherHelper(
weatherCode: weatherCode,
);
setState(() {
this.backgroundColor = weatherHelper.getBackgroundColor();
this.cityName = weatherData["name"];
this.temperature = weatherData["main"]["temp"].toInt();
this.status = weatherHelper.getWeatherStatus();
this.minTemperature = weatherData["main"]["temp_min"].toInt();
this.maxTemperature = weatherData["main"]["temp_max"].toInt();
this.humidity = weatherData["main"]["humidity"];
this.image = weatherHelper.getWeatherImage();
});
}
#override
void initState() {
this.updateUI();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: this.backgroundColor,
appBar: AppBar(
title: Text("Climat"),
),
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
this.cityName,
style: TextStyle(
fontSize: 24.0,
color: Colors.white,
),
),
SizedBox(
height: 30.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
"./assets/images/${this.image}",
scale: 4.0,
),
SizedBox(
width: 30.0,
),
Text(
"${this.temperature}°C",
style: TextStyle(
fontSize: 96.0,
color: Colors.white,
),
),
],
),
SizedBox(
height: 30.0,
),
Text(
this.status.toUpperCase(),
style: TextStyle(
fontSize: 24.0,
color: Colors.white,
),
),
SizedBox(
height: 10.0,
),
Text(
"MIN / MAX : ${this.minTemperature.toString()} / ${this.maxTemperature.toString()}",
style: TextStyle(
fontSize: 24.0,
color: Colors.white,
),
),
SizedBox(
height: 10.0,
),
Text(
"HUMIDITY : ${this.humidity}%",
style: TextStyle(
fontSize: 24.0,
color: Colors.white,
),
),
SizedBox(
height: 30.0,
),
ElevatedButton(
onPressed: () async {
dynamic userInput =
await Navigator.pushNamed(context, "/search");
this.updateUI(
userInput: userInput.toString(),
);
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
),
child: Text(
"Search by city name",
style: TextStyle(
fontSize: 20.0,
),
),
),
],
),
),
),
);
}
}
if you want to use Future function in initState and want it run once and complete before build, please consider to use WidgetsBinding.instance.addPostFrameCallback, like
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await this.getWeatherData();
setState(() { });
});
}
and
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await this.updateUI();
setState(() { });
});
}
setState should be used for rebuild the widget
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.