I am learning Flutter. I wrote small app to getting key from API and print it on screen. The problem is that my getApiKey() method is looping.
Why? And How I can prevent it?
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider<TenderApiData>(
builder: (_) => TenderApiData(), child: HomePage()),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(), body: MyContainer());
}
}
class MyContainer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[MyTestWidget()],
);
}
}
class TenderApiData with ChangeNotifier {
String access_token;
String url = "https://";
getApiKey() async
{
var response = await http.post(url, headers: {"Accept": "application/json"});
// await Future.delayed(Duration(seconds: 25));
if (response.statusCode == 200)
{
access_token = json.decode(response.body)['access_token'];
notifyListeners();
}
}
}
class MyTestWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
Provider.of<TenderApiData>(context).getApiKey();
var result = Provider.of<TenderApiData>(context).access_token;
return Row(
children: <Widget>[
Flexible(child: Text("Data: $result"))
],
);
}
}
The reason this happens is because you are notifying listeners in your getApiKey function and then calling getApiKey in your build method. The build method is called when you notify your listeners, see why this loops?
Anyways, to prevent it, you simply convert your StatelessWidget to a StatefulWidget and only call getApiKey in State.didChangeDependencies (not in initState because you need access to the BuildContext):
class MyTestWidget extends StatefulWidget {
#override
_MyTestWidgetState createState() => _MyTestWidgetState();
}
class _MyTestWidgetState extends State<MyTestWidget> {
bool apiKeyLoaded;
#override
void initState() {
apiKeyLoaded = false;
super.initState();
}
#override
void didChangeDependencies() {
if (!apiKeyLoaded) {
Provider.of<TenderApiData>(context).getApiKey();
apiKeyLoaded = true;
}
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
var result = Provider
.of<TenderApiData>(context)
.access_token;
return Row(
children: <Widget>[
Flexible(child: Text("Data: $result"))
],
);
}
}
Related
I want to use a variable, which i want to fill with firebase data.
In the Firebase section it fills it up with the correct data and print it correctly to log, but after that in the Scaffold its looks like it using the data in the starting declaration.
But why? It's one variable with two data? So at the end of the code in the Text('$masik') i want to use the firebase data, not the starting string.
What am i doing wrong?
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String masik = '';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase',
home: AddData(),
);
}
}
class AddData extends StatelessWidget {
#override
Widget build(BuildContext context) {
String doksi = 'valami más';
var ezlett;
String masik = 'minden';
String sajt = '';
final Stream<QuerySnapshot> collectionStream = FirebaseFirestore.instance.collection('zenek').snapshots();
FirebaseFirestore.instance
.collection('zenek')
.doc(doksi)
.get()
.then((DocumentSnapshot documentSnapshot) {
if (documentSnapshot.exists) {
String ezegyszoveg = documentSnapshot.data().toString();
print('Document exists on the database $ezegyszoveg');
ezlett = ezegyszoveg.substring(9, ezegyszoveg.indexOf(','));
print(ezlett);
masik = ezegyszoveg.substring(ezegyszoveg.indexOf('text: ')+6, ezegyszoveg.indexOf('}'));
print(masik);
}
});
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.green,
title: const Text("próba"),
),
body:Row(
children: [
Text('$masik'),
],
)
);
}
}
Something like this example will work to update the UI when there is new data has been updated
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String masik = '';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase',
home: AddData(),
);
}
}
class AddData extends StatefulWidget {
#override
State<AddData> createState() => _AddDataState();
}
class _AddDataState extends State<AddData> {
String doksi = 'valami más';
var ezlett;
String masik = 'minden';
String sajt = '';
#override
void initState() {
super.initState();
updateTheUi();
}
void updateTheUi() {
final Stream<QuerySnapshot> collectionStream =
FirebaseFirestore.instance.collection('zenek').snapshots();
FirebaseFirestore.instance
.collection('zenek')
.doc(doksi)
.get()
.then((DocumentSnapshot documentSnapshot) {
if (documentSnapshot.exists) {
String ezegyszoveg = documentSnapshot.data().toString();
ezlett = ezegyszoveg.substring(9, ezegyszoveg.indexOf(','));
masik = ezegyszoveg.substring(
ezegyszoveg.indexOf('text: ') + 6, ezegyszoveg.indexOf('}'));
setState(() {});
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.green,
title: const Text("próba"),
),
body: Row(
children: [
Text('$masik'),
],
));
}
}
hey u r doing a silly mistake u have to use setState when u update your variable :
setState(){
masik = ezegyszoveg.substring(ezegyszoveg.indexOf('text: ')+6,ezegyszoveg.indexOf('}'));
} // this line put in setState in your code
I have the following code, to get initial data for the screen and this SchedulerBinding seems to be a hack, but if I remove it, request data is lost.
I think it happens due to the fact widgets(streamBuilders etc.) are not yet built.
Any ideas how can I fix this?
Full screen code: https://gist.github.com/Turbozanik/7bdfc69b36fea3dd38b94d8c4fcdcc84
Full bloc code: https://gist.github.com/Turbozanik/266d3517a297b1d08e7a3d7ff6ff245f
SchedulerBining is not a hack,according to docs addPostFrame call callback only once and if you remove it your stream will never get the data
but you can call your stream loading in iniState
void initState(){
super.initState();
_mblock.loadSpotMock();
}
You can load your data asynchronously in the initState method, meanwhile you can show a loader or message. Once your data has loaded, call setState to redraw the widget.
Here is an example of this:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
createState() => new MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
String _data;
Future<String> loadData() async {
// Simulate a delay loading the data
await Future<void>.delayed(const Duration(seconds: 3));
// Return the data
return "This is your data!";
}
#override
initState() {
super.initState();
// Call loadData asynchronously
loadData().then((s) {
// Data has loaded, rebuild the widget
setState(() {
_data = s;
});
});
}
#override
Widget build(BuildContext context) {
if (null == _data) {
return Text("Loading...");
}
return Text(_data);
}
}
You can test it in https://dartpad.dartlang.org
It works like this:
initState will call loadData asynchronously, then the build method will draw the widget.
when loadData returns, the call to setState will redraw the widget.
Using StreamBuilder
The following example uses a StreamBuilder to show the data, once it's loaded:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
createState() => new MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
// Create a stream and execute it
final Stream<String> _myStream = (() async* {
// Simulate a delay loading the data
await Future<void>.delayed(const Duration(seconds: 3));
// Return the data
yield "This is your data!";
})();
#override
Widget build(BuildContext context) {
return StreamBuilder<String>(
stream: _myStream,
builder: (BuildContext context, s) {
String result;
if (s.hasError) {
result = "Error";
}
else {
if (s.connectionState == ConnectionState.done) {
result = s.data;
}
else {
result = "Loading...";
}
}
return Text(result);
}
);
}
}
Hope this helps :)
When I run main.dart on my real phone code runs without any mistakes, I can see the string value exactly on my Location page but when I adjust my android emulator phone's location Turkey/İstanbul it stuckes on spinner (Loading page), Spinner keeps turning forever so Location page never open, by the way "I/eatherforecast(19501): Waiting for a blocking GC ProfileSaver" error written on console. I really wonder the reason of this tedious issue. Have a nice day..
import 'package:flutter/material.dart';
import 'package:weatherforecast2/loadingpage.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark().copyWith(),
home: LoadingPage(),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:weatherforecast2/locationpage.dart';
import 'locationfinder.dart';
import 'package:geolocator/geolocator.dart';
import 'network.dart';
class LoadingPage extends StatefulWidget {
static String id = "loadingpage";
#override
_LoadingPageState createState() => _LoadingPageState();
}
class _LoadingPageState extends State<LoadingPage> {
Map<String,dynamic> currentLocationWeatherData;
void initState(){
super.initState();
getDecodedCurrentLocationWeatherData();
}
void getDecodedCurrentLocationWeatherData()async{
Position position=await LocationFinder().getCurrentLocation();
currentLocationWeatherData = await NetworkHelper().getCurrentLocationWeather(position.latitude,
position.longitude);
Navigator.push(context, MaterialPageRoute(builder: (context) {
return LocationPage(
currentLocationWeatherData: currentLocationWeatherData,
);
}));
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child:SpinKitFadingCube(
color: Colors.white,
size: 50,
),
),
);
}
}
import 'dart:convert';
import 'package:http/http.dart';
const String appid="57aad03f4e48ca815bb1184e74624f46";
const String openWeatherMapURL="https://api.openweathermap.org/data/2.5/weather";
class NetworkHelper{
Future<dynamic>getCurrentLocationWeather(lat,lon)async{
Response response=await get("https://api.openweathermap.org/data/2.5/weather?
lat=$lat&lon=$lon&appid=$appid");
if (response.statusCode == 200){
return jsonDecode(response.body);
}
}
}
import 'package:flutter/material.dart';
class LocationPage extends StatefulWidget {
LocationPage({#required this.currentLocationWeatherData});
final Map<String,dynamic> currentLocationWeatherData;
static String id = "locationpage";
#override
_LocationPageState createState() => _LocationPageState();
}
class _LocationPageState extends State<LocationPage> {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Center(
child: Text(
widget.currentLocationWeatherData["weather"][0]["description"],
),
),
);
}
}
import 'package:geolocator/geolocator.dart';
class LocationFinder{
Future<Position> getCurrentLocation()async{
Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.low);
return position;
}
}
My Setup
Login Route - asynchronously checks if a user is logged
Checkin Route - Consists of a main stateful widget and several stateless children widgets that depend on the information of the logged in user
My Idea
After the user login is identified on the login route, use Navigator to pass along the user object to the checkin route
children widgets can receive the user object through their constructor
My Problem
the user object is not 'arriving' in the stateful widget of the checkin route
Files
Here are the relevant code snippets to check (I added them from more complex files and hope I got all imports correct):
Main.dart
import 'package:flutter/material.dart';
import 'loginView.dart';
import 'checkinView.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => LoginView(),
'/checkin': (BuildContext context) => CheckinView(),
}
);
}
}
LoginView.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class LoginView extends StatefulWidget {
#override
LoginViewState createState() => LoginViewState();
}
class LoginViewState extends State<LoginView> {
#override
void initState() {
super.initState();
checkLoginAndRedirect();
}
void checkLoginAndRedirect() async {
FirebaseAuth.instance.currentUser().then((FirebaseUser currUser){
if(currUser != null){
print("[LoginView - user] $currUser");
Navigator.pushNamed(context, '/checkin', arguments: {currUser});
}
});
}
}
CheckinView.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'checkinList.dart';
class CheckinView extends StatefulWidget {
final FirebaseUser user;
CheckinView({this.user});
#override
CheckinViewState createState() {
print("[CheckinView - user] $user");
return CheckinViewState();
}
}
class CheckinViewState extends State<CheckinView> {
FirebaseUser _currentUser;
#override
void initState() {
super.initState();
setState((){
_currentUser = widget.user;
});
}
#override
Widget build(BuildContext context) {
print("[CheckinViewState - widget.user] " + widget.user.toString());
return Scaffold(
body: new Column(
children: <Widget>[
CheckinList(user: _currentUser)
]
)
);
}
}
CheckinList.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class CheckinList extends StatelessWidget{
FirebaseUser user = null;
CheckinList({this.user = null});
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(30.0),
child: Text(user['name'])
);
}
}
Solution
The arguments need extraction in the build method of the WidgetState (because only there we have the BuildContext)
Navigator is automagically handling the arguments correct, even with Stateful Widgets - THANKS FLUTTER TEAM!!!!
My adapted Files
Main.dart
import 'package:flutter/material.dart';
import 'loginView.dart';
import 'checkinView.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => LoginView(),
'/checkin': (BuildContext context) => CheckinView(),
}
);
}
}
LoginView.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class LoginView extends StatefulWidget {
#override
LoginViewState createState() => LoginViewState();
}
class LoginViewState extends State<LoginView> {
#override
void initState() {
super.initState();
checkLoginAndRedirect();
}
void checkLoginAndRedirect() async {
FirebaseAuth.instance.currentUser().then((FirebaseUser currUser){
if(currUser != null){
print("[LoginView - user] $currUser");
//// pass a map as 'arguments' to be ready to pass along more variables in the future
Navigator.pushNamed(context, '/checkin', arguments: {'user': currUser});
}
});
}
}
CheckinView.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'checkinList.dart';
class CheckinView extends StatefulWidget {
//// leave the stateful class as usual
#override
CheckinViewState createState() => CheckinViewState();
}
class CheckinViewState extends State<CheckinView> {
//// don't bother in the initState method as the BuildContext is missing here
#override
Widget build(BuildContext context) {
//// access the passed arguments
Map<String, dynamic> args = ModalRoute.of(context).settings.arguments;
print("[CheckinViewState - widget.user] " + widget.user.toString());
return Scaffold(
body: new Column(
children: <Widget>[
//// access the passed variables through 'args'
CheckinList(user: args.user)
]
)
);
}
}
CheckinList.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class CheckinList extends StatelessWidget{
FirebaseUser user = null;
//// assign the user in your constructor
CheckinList(FirebaseUser user){
this.user = user;
};
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(30.0),
child: Text(user['name'])
);
}
}
Read more in the cookbook about it:
https://flutter.dev/docs/cookbook/navigation/passing-data#create-a-detail-screen-to-extract-the-arguments
https://flutter.dev/docs/cookbook/navigation/navigate-with-arguments
You can try it. I hope it can help you
class LoginViewState extends State<LoginView> {
#override
void initState() {
super.initState();
checkLoginAndRedirect();
}
void checkLoginAndRedirect() async {
FirebaseAuth.instance.currentUser().then((FirebaseUser currUser){
if(currUser != null){
print("[LoginView - user] $currUser");
Navigator.pushNamed(context, '/checkin', arguments: currUser); /// <--- fixed here
}
});
}
}
class CheckinView extends StatefulWidget {
RouteSettings settings = ModalRoute.of(context).settings /// <--- fixed here
FirebaseUser user = settings.arguments; /// <--- fixed here
#override
CheckinViewState createState() {
print("[CheckinView - user] $user");
return CheckinViewState();
}
}
You need to access passed data using ModalRoute inside build method, below code will give you can idea how to do it,
class LevelViewChildScreen extends StatelessWidget {
UserModel user;
#override
Widget build(BuildContext context) {
user = ModalRoute.of(context).settings.arguments;
return LevelViewChild(user);
}
}
class LevelViewChild extends StatefulWidget {
UserModel user;
LevelViewChild(this.user);
I want to complete get request to server to get data for my app at it start. I read several topics that describe how to run method after building widgets. But all of them are describe situations when provider is not using. And I am not sure that it's good idea to do this request inside widget.
I tried several approaches but did not get success.
Here is my code:
void main() async {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider<TenderApiData>(
builder: (_) => TenderApiData(), child: HomePage()),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(), body: MainContainer());
}
}
class TenderApiData with ChangeNotifier {
String access_token;
List<Map<String, String>> id_names;
String access_token_url = "https://...";
getApiKey() async { // I need to call this method at app start up
var response = await http
.post(access_token_url, headers: {"Accept": "application/json"});
if (response.statusCode == 200) {
access_token = json.decode(response.body)['access_token'];
notifyListeners();
}
}
}
class MyTestWidget extends StatefulWidget {
MyTestWidgetState createState() => MyTestWidgetState();
}
class MyTestWidgetState extends State<MyTestWidget> {
bool isKeyGetted = false;
// before I used this when I extracted data on click event.
// I am not sure that it's needed now
#override
void didChangeDependencies() {
if (!isKeyGetted) {
Provider.of<TenderApiData>(context).getApiKey();
isKeyGetted = !isKeyGetted;
}
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
if (!isKeyGetted) {
Provider.of<TenderApiData>(context).getApiKey();
isKeyGetted = !isKeyGetted;
}
var result = Provider.of<TenderApiData>(context).access_token;
var test = Provider.of<TenderApiData>(context).id_names;
return Column(
children: <Widget>[
RaisedButton(
onPressed: Provider.of<TenderApiData>(context).getRegionsList,
child: Text("get regions"),
),
],
);
}
}
class MainContainer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Table(
children: [
TableRow(children: [
Row(
children: <Widget>[
Container(child: MyTestWidget()),
Container(child: Text("Regions"),),
Expanded(child: SelectRegions(), )
],
)
]),
TableRow(children: [
Row(
children: <Widget>[
Text("Label"),
Text("Value"),
],
)
]),
],
);
}
}
You can store TenderApiData as member of MyApp, make a startup call in MyApp constructor and pass existing instance to descendants.
Here is how it will look:
class MyApp extends StatelessWidget {
final TenderApiData _tenderApiData = TenderApiData();
MyApp() {
_tenderApiData.getApiKey();
};
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider<TenderApiData>(
builder: (_) => _tenderApiData, child: HomePage()),
);
}
}
Other classes will stay unchanged.
Another option would be to pass TenderApiData as constructor parameter into MyApp, making it more testable.
void main() {
final TenderApiData tenderApiData = TenderApiData();
tenderApiData.getApiKey(); // call it here or in MyApp constructor - now it can be mocked and tested
runApp(MyApp(tenderApiData));
}
class MyApp extends StatelessWidget {
final TenderApiData _tenderApiData;
MyApp(this._tenderApiData);
// ...
You can add a constructor on your TenderApiData do trigger custom logic:
class TenderApiData with ChangeNotifier {
TenderApiData() {
// TODO: call `getApiKey`
}
}
You can use FutureProvider value.
Separate method api to service (my_service.dart):
class MyService {
Future<String> getApiKey() async {
// I need to call this method at app start up
var response = await http
.post(access_token_url, headers: {"Accept": "application/json"});
if (response.statusCode == 200) {
return json.decode(response.body)['access_token'];
}
}
}
And than call from MyApp
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureProvider<String>.value(
value: MyService().getApiKey(),
child: HomePage(),
);
}
}
In HomePage:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
String token = Provider.of<String>(context);
return Scaffold();
}
}
You could also just use a boolean flag to run the method only once.
Provider
import 'screen.abstract.dart';
class MyProvider with ChangeNotifier {
_hasInitialized = false;
// This will only be run once, when called
fetchApiOnce() {
if (_hasInitialized) {
return;
}
_hasInitialized = true;
/* DO STUFF */
}
}