Flutter Widget testing with HttpClient - flutter

I am trying to write a widget test for a screen, not the main app. It's my first time writing a widget test and I couldn't find a proper solution for the issue.
I don't know how to write a proper test for this one. I tried to write a simple widget test and it end up giving me an error as below
"Warning: At least one test in this suite creates an HttpClient. When
running a test suite that uses TestWidgetsFlutterBinding, all HTTP
requests will return status code 400, and no network request will
actually be made. Any test expecting a real network connection and
status code will fail.
To test code that needs an HttpClient, provide your own HttpClient
implementation to the code under test, so that your test can
consistently provide a testable response to the code under test."
I have just started learning it please help me.
NOTE: my test was just writing a basic test for finding Text widgets.
class BookingDetails extends StatefulWidget {
final booking;
BookingDetails(this.booking);
#override
_BookingDetailsState createState() => _BookingDetailsState();
}
class _BookingDetailsState extends State<BookingDetails>
with AutomaticKeepAliveClientMixin {
Row _buildTeacherInfo(Map<String, dynamic> teacherData) {
return teacherData != null
? Row(
children: <Widget>[
CircleAvatar(
radius: 53,
backgroundColor: MyColors.primary,
child: CircleAvatar(
radius: 50.0,
backgroundImage: teacherData['user']['img_url'] == null ||
teacherData['user']['img_url'] == ''
? AssetImage('assets/images/placeholder_avatar.png')
: NetworkImage(teacherData['user']['img_url']),
backgroundColor: Colors.transparent,
),
),
SizedBox(width: 20.0),
Column(
children: <Widget>[
Container(
child: Column(
children: <Widget>[
Text(
'${teacherData['user']['first_name']} ',
style: AppStyles.textHeader1Style,
),
Text(
'${teacherData['user']['last_name']}',
style: AppStyles.textHeader1Style,
),
],
),
),
ElevatedButton(
onPressed: () {
//View Profile method
},
style: ElevatedButton.styleFrom(
primary: MyColors.primary,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(25))),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.next_plan_outlined),
SizedBox(width: 10.0),
Text('VIEW PROFILE'),
],
),
),
],
),
],
)
: Row(
children: <Widget>[
CircleAvatar(
radius: 48,
backgroundColor: MyColors.primary,
child: CircleAvatar(
radius: 45.0,
backgroundImage:
AssetImage('assets/images/placeholder_avatar.png'),
backgroundColor: Colors.transparent,
),
),
SizedBox(width: 20.0),
Expanded(
child: Text(
'Teacher allocation in progress',
style: AppStyles.textHeader1Style,
),
)
],
);
}
Widget _buildBookingDetails(
Map<String, dynamic> booking,
List<dynamic> campusData, // one campus' data is an array for some reason.
Map<String, dynamic> instData,
) {
return Expanded(
child: Scrollbar(
child: ListView(
children: [
ListTile(
leading: Icon(Icons.location_on),
title: Text(
'${campusData[0]['address_line1']},'
' ${campusData[0]['suburb']}, '
'${campusData[0]['state']} ${campusData[0]['postcode']} ',
style: AppStyles.textHeader3Style,
),
),
}
#override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: Future.wait([_teacherData, _campusData, _classData, _instData]),
builder: (context, snapshot) => snapshot.connectionState ==
ConnectionState.waiting
? MyLoadingScreen(message: 'Loading booking data, please wait...')
: snapshot.hasData
? SafeArea(
child: Container(
margin: const EdgeInsets.only(top: 30.0),
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildTeacherInfo(snapshot.data[0]),
Divider(color: MyColors.dividerColor),
SizedBox(height: 10),
const SizedBox(height: 10),
Divider(
color: MyColors.primary,
thickness: 1,
),
const SizedBox(height: 10),
_buildBookingDetails(
widget.booking,
snapshot.data[1],
snapshot.data[3],
),
SizedBox(height: 10),
Divider(
color: MyColors.primary,
thickness: 1,
),
SizedBox(height: 10),
Center(
child: widget.booking['cancelled_by_inst'] == true
? Text(
'Canceled',
style: AppStyles.textHeader3StyleBold,
)
: widget.booking['teacher_id'] == null
? Center(
child: Text(
'Teacher Allocation in Progress',
style: AppStyles.textHeader3StyleBold,
),
)
: null,
),
}

I have reduced your code to the following minimal version, to be able to execute it:
snippet.dart:
import 'package:flutter/material.dart';
import 'dart:convert';
import 'api.dart';
class BookingDetails extends StatefulWidget {
final Map<String, String> booking;
BookingDetails(this.booking);
#override
_BookingDetailsState createState() => _BookingDetailsState();
}
class _BookingDetailsState extends State<BookingDetails> {
late Future _campusData;
Future<dynamic> _fetchCampusData() async {
var campusID = widget.booking['campus_id'];
if (campusID != null) {
var response = await api.getCampusByID(campusID);
return json.decode(response.body);
}
}
#override
void initState() {
_campusData = _fetchCampusData();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _campusData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return const Text('Displaying data');
} else if (snapshot.hasError) {
return const Text('An error occurred.');
} else {
return const Text('Loading...');
}
}
);
}
}
api.dart:
import 'package:http/http.dart' as http;
final _ApiClient api = _ApiClient();
class _ApiClient {
Future<http.Response> getCampusByID(String id) async {
var url = Uri.parse('https://run.mocky.io/v3/49c23ebc-c107-4dae-b1c6-5d325b8f8b58');
var response = await http.get(url);
if (response.statusCode >= 400) {
throw "An error occurred";
}
return response;
}
}
Here is a widget test which reproduces the error which you described:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:widget_test/snippet.dart';
void main() {
testWidgets('Should test widget with http call', (WidgetTester tester) async {
var booking = <String, String>{
'campus_id': '2f4fccd2-e199-4989-bad3-d8c48e66a15e'
};
await tester.pumpWidget(TestApp(BookingDetails(booking)));
expect(find.text('Loading...'), findsOneWidget);
await tester.pump();
expect(find.text('Displaying data'), findsOneWidget);
});
}
class TestApp extends StatelessWidget {
final Widget child;
TestApp(this.child);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: child,
);
}
}
Here is the error message, for a reference:
Test failed. See exception logs above.
The test description was: Should test widget with http call
Warning: At least one test in this suite creates an HttpClient. When
running a test suite that uses TestWidgetsFlutterBinding, all HTTP
requests will return status code 400, and no network request will
actually be made. Any test expecting a real network connection and
status code will fail.
To test code that needs an HttpClient, provide your own HttpClient
implementation to the code under test, so that your test can
consistently provide a testable response to the code under test.
Solution
The error tells you what the problem is: you must not execute HTTP calls in the widget tests. So you need to mock that HTTP call out, so that the mock is called instead of the real HTTP call. There are many options with which you can do that, e.g. using the mockito package.
Here a possible solution using the nock package which simulates an HTTP response at the HTTP level.
pubspec.yaml:
dev_dependencies:
nock: ^1.1.2
Widget test:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nock/nock.dart';
import 'package:widget_test/snippet.dart';
void main() {
setUpAll(nock.init);
setUp(() {
nock.cleanAll();
});
testWidgets('Should test widget with http call', (WidgetTester tester) async {
nock('https://run.mocky.io')
.get('/v3/49c23ebc-c107-4dae-b1c6-5d325b8f8b58')
.reply(200, json.encode('{"id": "49c23ebc-c107-4dae-b1c6-5d325b8f8b58", "name": "Example campus" }'));
var booking = <String, String>{
'campus_id': '2f4fccd2-e199-4989-bad3-d8c48e66a15e'
};
await tester.pumpWidget(TestApp(BookingDetails(booking)));
expect(find.text('Loading...'), findsOneWidget);
await tester.pump();
expect(find.text('Displaying data'), findsOneWidget);
});
}
class TestApp extends StatelessWidget {
final Widget child;
TestApp(this.child);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: child,
);
}
}

Related

List View in Flutter

I've been having trouble understanding how to correctly list of all the items from the API call, I have now made it to display the first from the list of the API, by adding the index [0] after the API response. I realize I can do that with the itemCount which I currently had it set at 1 since I can't seem to comprehend how to use the .length option that lists all of the item from the API. Here is the code:
API CALL:
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:practice/models/jobs.dart';
Future<JobData> fetchJobs() async {
final response = await http.get('https://jsonplaceholder.typicode.com/posts');
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return JobData.fromJson(jsonDecode(response.body)[0]);
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load jobs');
}
}
THE JOB MODEL:
import 'package:flutter/foundation.dart';
class JobData extends ChangeNotifier {
final int userId;
final int id;
final String title;
final String body;
JobData({this.userId, this.id, this.title, this.body});
factory JobData.fromJson(Map<String, dynamic> json) {
return JobData(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
AND THE WIDGET WHERE IT IS RENDERED:
import 'package:flutter/material.dart';
import 'package:practice/models/jobs.dart';
import 'package:provider/provider.dart';
import 'package:practice/services/api_calls.dart';
class JobList extends StatefulWidget {
#override
_JobListState createState() => _JobListState();
}
class _JobListState extends State<JobList> {
Future<JobData> futureJobs;
#override
void initState() {
super.initState();
futureJobs = fetchJobs();
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
'Today',
),
SizedBox(
height: 12.0,
),
Container(
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12.0),
topRight: Radius.circular(12.0),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Text(
'Jobs #1',
style: TextStyle(fontWeight: FontWeight.w700),
),
),
FlatButton(
textColor: Colors.blue[700],
minWidth: 0,
child: Text('View'),
onPressed: () {
},
),
],
),
),
Container(
padding: EdgeInsets.all(18.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(12.0),
bottomRight: Radius.circular(12.0),
),
),
child: Consumer<JobData>(
builder: (context, jobData, child) {
return SizedBox(
height: 200,
child: FutureBuilder<JobData>(
future: futureJobs,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: 1,
itemBuilder: (context, index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('${snapshot.data.userId}',
style: TextStyle(color: Colors.black),
Text(
'${snapshot.data.id}',
style: TextStyle(color: Colors.black),
),
Text(
'${snapshot.data.title}',
style: TextStyle(color: Colors.black),
),
Text(
'${snapshot.data.body}',
style: TextStyle(color: Colors.black),
),
],
);
});
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
),
);
},
),
),
SizedBox(
height: 16.0,
),
],
);
}
}
Do you I need to make a list out of the returned API response first and then display it's length?
Thanks in advance!
Try changing this..
Future<List<JobData>> fetchJobs() async {
List<JobData> jobs = new List();
final response = await http.get('https://jsonplaceholder.typicode.com/posts');
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
var jobsJson = jsonDecode(response.body);
for(int i = 0; i < jobsJson.length; i++) {
jobs.add(JobData.fromJson(jobsJson[i]));
}
return jobs;
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load jobs');
}
}
then..
Future<List<JobData>> futureJobs;
inside ListView
itemCount: futureJobs.length,
then take each job from futureJobs list using index and show in ListView()

Uploading and displaying an image from Computer in to a FLUTTER WEB APP during runtime

I am trying to upload an image via a FLUTTER WEB app from my computer internal folder (macos) and truing to display the same image in the same FLUTTER WEB app. Both uploading and displaying is done during run time of the app.
I am getting the following exception. How do I fix this issue?
Thank you.
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
Expected a value of type 'Widget?', but got one of type 'DecorationImage'
main.dart
import 'dart:convert';
import 'dart:html';
import 'dart:typed_data';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ImageUploader(),
);
}
}
class ImageUploader extends StatefulWidget {
#override
_ImageUploaderState createState() => _ImageUploaderState();
}
class _ImageUploaderState extends State<ImageUploader> {
Uint8List imagevalue;
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Scaffold(
body: Container(
width: width,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Text(
'Mah Links',
style: TextStyle(
fontFamily: 'Karla',
fontSize: 20,
fontWeight: FontWeight.w800,
),
),
),
Container(
child: imagevalue == null
? Text('No Image')
: DecorationImage(
fit: BoxFit.cover,
image: Image.memory(imagevalue).image)),
],
),
),
floatingActionButton: FloatingActionButton.extended(
heroTag: 'picker',
elevation: 0,
backgroundColor: Colors.tealAccent[400],
hoverElevation: 0,
label: Row(
children: <Widget>[
Icon(Icons.file_upload),
SizedBox(width: 10),
Text('Upload Image')
],
),
onPressed: () => uploadImage(),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
uploadImage() {
// HTML input element
InputElement uploadInput = FileUploadInputElement();
uploadInput.click();
uploadInput.onChange.listen(
(changeEvent) {
final file = uploadInput.files.first;
final reader = FileReader();
// The FileReader object lets web applications asynchronously read the
// contents of files (or raw data buffers) stored on the user's computer,
// using File or Blob objects to specify the file or data to read.
// Source: https://developer.mozilla.org/en-US/docs/Web/API/FileReader
reader.readAsDataUrl(file);
String imgString = reader.result; //.toString();
Uint8List base64string = Base64Decoder().convert(imgString);
reader.onLoadEnd.listen(
(loadEndEvent) async {
setState(() {
imagevalue = base64string;
});
},
);
},
);
}
}
For anyone looking for a solution to pick an image from the computer internal directory from a flutter web app during runtime:
make sure to insert the file_picker package into pubspec.yaml
import 'dart:html';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ImageUploader(),
);
}
}
class ImageUploader extends StatefulWidget {
#override
_ImageUploaderState createState() => _ImageUploaderState();
}
class _ImageUploaderState extends State<ImageUploader> {
Uint8List imagevalue;
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Scaffold(
body: Container(
width: width,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Text(
'Mah Links',
style: TextStyle(
fontFamily: 'Karla',
fontSize: 20,
fontWeight: FontWeight.w800,
),
),
),
Container(
child: imagevalue == null
? Text('No Image')
: Image.memory(imagevalue))
// : DecorationImage(
// fit: BoxFit.cover,
// image: Image.memory(imagevalue).image)),
],
),
),
floatingActionButton: FloatingActionButton.extended(
heroTag: 'picker',
elevation: 0,
backgroundColor: Colors.tealAccent[400],
hoverElevation: 0,
label: Row(
children: <Widget>[
Icon(Icons.file_upload),
SizedBox(width: 10),
Text('Upload Image')
],
),
onPressed: () => uploadImage(),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
uploadImage() async {
FilePickerResult result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['png', 'jpg', 'svg', 'jpeg']);
if (result != null) {
PlatformFile file = result.files.first;
setState(() {
imagevalue = file.bytes;
});
} else {
// User canceled the picker
}
}
}

i have problem with async function and updated information on screen

When the user comes on this page, the function async get_ticket() need to be called, it returns a ticket id and the revenue user wins (it is scratch game). The first problem with this code is if i want display the value of "gain" and "id_ticket" because at the screen it says "null". When i have finish to scratch the ticket there is a setstate made and the value are updated from null to their real value.
Why this 2 values are not updated ??? The widget seems to be displayed before the function so widget can't display real values (which is updated by the async function). How i can resolve that ? For my game the better solution would be to called the async function and if player can have a ticket we display the ticket and if not we display a message for that "You can't scratch again today" for example.
import 'package:flutter/material.dart';
import 'package:flutter_app/menu_member.dart';
import 'package:scratcher/scratcher.dart';
import 'package:flutter_app/globals.dart' as globals;
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'package:flutter_app/liste_grattage.dart';
class Affiche_Ticket_Grattage extends StatefulWidget {
#override
_Affiche_Ticket_Grattage_State createState() {
return _Affiche_Ticket_Grattage_State();
}
}
// Create a corresponding State class.
// This class holds data related to the form.
class _Affiche_Ticket_Grattage_State extends State<Affiche_Ticket_Grattage> {
#override
void initState() {
Get_Ticket();
super.initState();
}
bool vis=false;
var gain;
var id_ticket;
Future Get_Ticket() async {
// SERVER LOGIN API URL
var url = 'https://www.easytrafic.fr/game_app/get_ticket.php';
// Store all data with Param Name.
var data = {'id_membre':globals.id_membre};
var ticket_encode=jsonEncode(data);
// Starting Web API Call.
var response = await http.post(url, body: ticket_encode,headers: {'content-type': 'application/json','accept': 'application/json','authorization': globals.token});
// Getting Server response into variable.
Map <String,dynamic> map = json.decode(response.body);
gain=map["gain"];
print(gain.toString());
id_ticket=map["id"];
print(id_ticket.toString());
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('GRATTAGE')),
drawer: new DrawerOnly(),
body:
Center(
child:
Column(
children: <Widget>[
Container(
width:400,
height:30,
margin: const EdgeInsets.only(top: 10.0),
child : new Text("Grattez votre ticket N °"+id_ticket.toString(),textAlign: TextAlign.center,style: TextStyle(fontSize: 30.0),),
),
Container(
padding: const EdgeInsets.all(30.0),
child:
Scratcher(
accuracy: ScratchAccuracy.medium,
brushSize: 35,
color: Colors.lightBlueAccent,
threshold: 80,
onThreshold: () {
setState(() {
vis = true;
});
print ("Ticket gratté !!!");
},
child:
Container(
width: 300,
height:300,
color: Colors.black,
alignment: Alignment.center,
child:
Text(gain.toString()+" €",style: TextStyle(fontWeight: FontWeight.bold, fontSize: 50, color: Colors.red),
)
)
)
),
Visibility(
visible: vis,
child: Container(
width:300,
height:45,
margin: const EdgeInsets.only(top: 20.0),
child:
RaisedButton(
color: Colors.green,
textColor: Colors.white,
padding: EdgeInsets.fromLTRB(9, 9, 9, 9),
child: Text('VALIDER VOTRE TICKET'),
onPressed: () {
Valide_Ticket();
},
),
),
),
]
)
)
),
);
}
Future Valide_Ticket() async{
// SERVER LOGIN API URL
var url2 = 'https://www.easytrafic.fr/game_app/valide_ticket.php';
// Store all data with Param Name.
var data2 = {'id_membre':globals.id_membre,'id_ticket':id_ticket,'gain':gain};
var ticket_info=jsonEncode(data2);
// Starting Web API Call.
var response = await http.post(url2, body: ticket_info,headers: {'content-type': 'application/json','accept': 'application/json','authorization': globals.token});
// Getting Server response into variable.
Map <String,dynamic> map2 = json.decode(response.body);
if(map2["status"] == 1)
{
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: new Text(map2["message"]),
actions: <Widget>[
FlatButton(
child: new Text("OK"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Affiche_Liste_Grattage()),
);
},
),
],
);
},
);
}
}
}
You can use FutureBuilder for this issue. It wait for the future function to perform its task and then renders your page.
Another way would be to show an animation while the data is fetched. then use setState() to hide the animation and show the Widgets(based on the condition)

Flutter : The onRefresh callback returned null

Sorry I'm new to flutter and trying to learn it. I have an issue with RefreshIndicator.
When I try pulling it I got an error like below :
════════ Exception caught by material library ══════════════════════════════════════════════════════
The following assertion was thrown when calling onRefresh:
The onRefresh callback returned null.
The RefreshIndicator onRefresh callback must return a Future.
════════════════════════════════════════════════════════════════════════════════════════════════════
Currently I am using flutter_bloc. Here is my sample code
TableList_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:pos_project_bloc/modal/api.dart';
class ModalTableList {
final String Table_Number;
final String Name_Table;
ModalTableList(
this.Table_Number,
this.Name_Table,
);
}
class tablelistbloc extends Bloc<bool, List<ModalTableList>>{
#override
// TODO: implement initialState
List<ModalTableList> get initialState => [];
#override
Stream<List<ModalTableList>> mapEventToState(bool event) async* {
// TODO: implement mapEventToState
List<ModalTableList> tablelist =[];
try {
final response = await http.get(BaseUrl.GetTableList);
final data = jsonDecode(response.body);
if (data.length != 0) {
data.forEach((api) {
tablelist.add(
ModalTableList(
api['Table_Number'],
api['Name_Table'],
)
);
print("test"+api['Table_Number'].toString());
});
print("Get Table List : sukses");
} else {
print('data kosong');
}
} catch (e) {
print("Error GetTableList :");
print(e);
}
yield tablelist;
}
}
TabeList.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pos_project_bloc/bloc/TableList_bloc.dart';
import 'package:pos_project_bloc/modal/SharedPreferences.dart';
class TableList extends StatefulWidget {
#override
_TableListState createState() => _TableListState();
}
class _TableListState extends State<TableList> {
#override
void initState() {
super.initState();
BlocProvider.of<tablelistbloc>(context).add(true);
}
Widget build(BuildContext context) {
final GlobalKey<RefreshIndicatorState> refresh = GlobalKey<RefreshIndicatorState>();
Future<Null> _refresh() async{
BlocProvider.of<tablelistbloc>(context).add(true);
}
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.lightBlueAccent,
title: new Center(
child: new Text('Available Table',
style: new TextStyle(color: Colors.white, fontSize: 15.0)),
)
),
floatingActionButton: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.all(5.0),
child: FloatingActionButton.extended(
heroTag: 1,
icon: Icon(Icons.refresh),
label: Text('Refresh Table List'),
onPressed: () {
BlocProvider.of<tablelistbloc>(context).add(true);
},
),
)
],
),
),
body: RefreshIndicator(
onRefresh: (){
_refresh();
},
key: refresh,
child: BlocBuilder<tablelistbloc,List<ModalTableList>>(
builder: (context,tablelist)=> ListView.builder(
itemCount: tablelist.length,
itemBuilder: (context,index){
final x = tablelist[index];
return GestureDetector(
onTap: (){
print("Selected Available Table On Tap : " + x.Table_Number);
Shared_Preferences().SaveTableNumber(
x.Table_Number
);
Navigator.pushReplacementNamed(context, '/POS');
},
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 2.0,
color: Colors.grey.withOpacity(0.4),
))),
padding: EdgeInsets.all(10.0),
child: Row(
children: <Widget>[
Icon(
Icons.table_chart,
size: 100.0,
color: Colors.lightBlueAccent,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Table Number : ' + x.Table_Number,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Colors.lightBlueAccent),
),
Text(
'Name : ' + x.Name_Table,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Colors.lightBlueAccent),
),
],
)
],
),
),
);
},
),
),
)
);
}
}
Just add async.
onRefresh: () async {
GetTableList.add(true);
},
onRefresh is a RefreshCallback, and RefreshCallback is a Future Function().
So if GetTableList.add(true) not return Future, must to add async.
it is says onRefresh callback must return a Future.
and it is seems like your are not returning Future from onRefresh
onRefresh: _refresh
Future<Null> _refresh() async{
GetTableList.add(true);
}
hope it helps..
You can problably use the following:
Future<bool> refresh() async {
//your refresh code
return true;
}

Flutter Provider access via addPostFrameCallback says widget is outside the widget tree but flutter inspector shows otherwise

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.