How to call method at App start if I am using provider? - flutter

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 */
}
}

Related

How to rebuild specified widgets only in flutter

When calling onClick(), how can I prevent only WidgetOne is rebuild, while WidgetTwo is not?
class SomeClass extends StatefulWidget {
#override
State<SomeClass> createState() => _SomeClass();
}
class _SomeClass extends State<SomeClass>{
int _i = 0;
#override
Widget build(BuildContext context) {
return child: Column(
children: [
WidgetOne(i: _i),
WidgetTwo()
],
)
}
void onClick() {
setState(() {
i++;
});
}
I add the following text, because otherwise stackoverflow is written "It looks like your post is mostly code; please add some more details." :(
A simple and fast solution is to put WidgetOne in StatefulBuilder like this:
class SomeClass extends StatefulWidget {
#override
State<SomeClass> createState() => _SomeClass();
}
class _SomeClass extends State<SomeClass> {
int _i = 0;
Function? innerSetState;
#override
Widget build(BuildContext context) {
return Column(
children: [
StatefulBuilder(
builder: (context, innerSetState) {
this.innerSetState = innerSetState;
return WidgetOne(i: _i);
},
),
WidgetTwo()
],
);
}
void onClick() {
innerSetState?.call(() {
_i++;
});
}
}
Another solution is to use provider state manager along with Consumer widget.

What is the correct way to execute initial network request in Flutter?

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 :)

How to display an alert message if there is no data flutter

I am doing a query about the data from a mysql database and they are displayed in a list.
My problem is if there is no data in the database. I would like to display an alert message stating that or display a text to warn that there is no available data.
This is the code page that I use to display the data:
void main() {
runApp(Your()
);
}
class Your extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<Your> {
MyPreferences _myPreferences = MyPreferences();
String apiURL;
var id;
List<Flowerdata> _flowersDataForTheListView = List<Flowerdata>();
Future<List<Flowerdata>> fetchNotes() async {
id=_myPreferences.id;
final String url = 'https://=============/All.php?id=' + id.toString();
var response = await http.get(url);
var flowers = List<Flowerdata>();
if (response.statusCode == 200) {
var flowersJsonData = json.decode(response.body);
for (var flower in flowersJsonData) {
flowers.add(Flowerdata.fromJson(flower));
}
}
else{
}
return flowers;
}
#override
void initState() {
fetchNotes();
super.initState();
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return _listItem(index);
},
);
}
_listItem(index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text( _flowersDataForTheListView[index].Name,style: TextStyle(fontSize: 17)),
SizedBox(height: 5.0,),
],
),
),
],
),
],
);
}
}
PHP contact page:
<?php
include 'connt.php';
$id=$_GET['id'];
$sql = "SELECT * FROM user WHERE id=? ORDER BY id DESC";
$stmt = $con->prepare($sql);
$stmt->bind_param("s",$id);
$stmt->execute();
$result = $stmt->get_result();
//$result = $con->query($sql);
if ($result->num_rows >0) {
while($row[] = $result->fetch_assoc()) {
$item = $row;
$json = json_encode($item, JSON_NUMERIC_CHECK);
}
} else {
echo "No";
}
echo $json;
$con->close();
?>
How can I do this if someone knows please help me.
You don't seem to be using MaterialApp in your flutter code, but if you use it, Scaffold widget has a method called showSnackbar where you can display a message that pops up at the bottom of the page.
Here is a sample code for showSnackbar:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: MyList(),
);
}
}
class MyList extends StatefulWidget {
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
#override
Widget build(BuildContext context) {
return Container();
}
#override
void initState() {
fetchNotes();
super.initState();
}
Future<void> fetchNotes() async {
// Here, you actually fetch the data from API
await Future.delayed(Duration(seconds: 3));
final flowers = [];
if (flowers.isEmpty) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Data is empty'),
),
);
}
}
}

Can't find the stream provider when using named route navigation

I have a Stream Provider (connected to firebase) that is not working. I am guessing that the problem lies in the fact that I am using a named navigator [Navigator.pushNamed(context, '/route',)]. I guess this makes the 'route' widget to not be the son of the widget that calls it. Let me show it better below.
My app structure is as follows:
My main widget which handles routing and receives the Stream with user authentication (there is no problem here):
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: AuthService().user,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Wrapper(),
routes: {
'/home': (context) => Wrapper(),
'/edit_profile': (context) => UserProfile() //This is where I am having trouble.
}
),
);
}
}
The Wrapper that validates if the user is authenticated and acts accordingly:
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
// return either the Home or Authenticate widget
if (user == null){
return Authenticate();
} else {
return HomeWrapper();
}
}
}
The HomeWrapper which receives the second stream and redirects to the widget I am having trouble with:
class HomeWrapper extends StatefulWidget {
#override
_HomeWrapperState createState() => _HomeWrapperState();
}
class _HomeWrapperState extends State<HomeWrapper> {
String currentBodyName = 'home';
Widget currentBodyWidget = Home();
#override
Widget build(BuildContext context) {
Widget _drawerOptions = Row(
children: [
FlatButton(child: someChild, onPressed: () {Navigator.pushNamed(context, '/edit_profile',);},), //This is the actual call to the navigator.
],
);
return StreamProvider<Map>.value( //This is the problematic Stream!
value: DatabaseService().userDetail,
child: Scaffold(
//Body
body: currentBodyWidget,
//I am simplifying this to show the most important parts
bottomNavigationBar: myBottomNavigationBar(
buttons: <Widget>[
FlatButton(
icon: someIcon,
onPressed: () => _onItemTapped('home'),
),
FlatButton(
icon: otherIcon,
onPressed: () => _onItemTapped('second_screen'),
),
],)
//Drawer
drawer: Drawer(child: _drawerOptions,), //This one has the call to the problematic edit_profile route.
);
}
void _onItemTapped(String newBodyName) {
if (newBodyName != currentBodyName){
setState(() {
currentBodyName = newBodyName;
switch(newBodyName) {
case 'home': {
currentBodyWidget = Home();
}
break;
case 'second_screen': {
currentBodyWidget = SecondScreen();
}
break;
default: {
currentBodyWidget = Home();
}
break;
}
});
}
}
}
Finally the edit_profile route calls the UserProfile Widget which looks like this:
class UserProfile extends StatefulWidget {
#override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
#override
Widget build(BuildContext context) {
//This is where the error occurs!!
final userDocument = Provider.of<Map>(context) ?? [];
print(userDocument);
return Scaffold(body: Container());
}
}
This is the error that it throws:
The following ProviderNotFoundError was thrown building UserProfile(dirty, state: _UserProfileState#09125):
Error: Could not find the correct Provider<Map<dynamic, dynamic>> above this UserProfile Widget
Thank you very much!!
Turns out my approach was wrong.
Instead of wrapping the HomeWrapper with the StreamProvider, hoping that it would pass the data to the next route (UserProfile ), what I did was to wrap the UserProfile widget with a StreamProvider, as follows:
(Note: I changed the Map StreamProvider for a UserData StreamProvider.)
class UserProfile extends StatefulWidget {
#override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
UserData userData = snapshot.data;
return Scaffold(
body: Container(
//My Widget here
);
} else
return Loading();
});
}
}
This series was very helpful: https://www.youtube.com/playlist?list=PL4cUxeGkcC9j--TKIdkb3ISfRbJeJYQwC

How can I prevent my Build Method from looping?

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"))
],
);
}
}