Flutter Snapshot of Streambuilder has no data after condition is met - flutter

I have a class Home with a PageView as body and a BottomNavigationBar.
In this class the current user and the current location of the user is loaded.
When the user and the location is known, a global variable is set to true
On the first tab icon of the BottomNavigationBar there is a feed of nearby locations coded in class Feed
Now the issue.
When I start the app for the first time or make a hot reload geoQuery() returns the circular spinner. When the current user is loaded it returns the text "No Data" instead of showing the events. The user needs to change the tab of BottomNavigationBar from feed to something else and back to feed to refresh the streambuilder. After that it works as expected.
When I use the streambuilder without the condition (currentLocationloaded && currentUserloaded == true) it works as expected but sometimes it throws an error as the user is not loaded fast enough.
What can I do to get it work with condition?
Update
Workflow logged in:
RootPage -> Logged in? -> Home
RootPage
enum AuthStatus {
NOT_DETERMINED,
NOT_LOGGED_IN,
LOGGED_IN,
}
class RootPage extends StatefulWidget {
RootPage({this.auth});
final BaseAuth auth;
#override
State<StatefulWidget> createState() => new _RootPageState();
}
class _RootPageState extends State<RootPage> {
AuthStatus authStatus = AuthStatus.NOT_DETERMINED;
String _userID = "";
#override
void initState() {
super.initState();
widget.auth.getCurrentUser().then((user) {
setState(() {
if (user != null) {
_userID = user?.uid;
}
authStatus =
user?.uid == null ? AuthStatus.NOT_LOGGED_IN : AuthStatus.LOGGED_IN;
});
});
}
void loginCallback() {
widget.auth.getCurrentUser().then((user) {
setState(() {
_userID = user.uid.toString();
});
});
setState(() {
authStatus = AuthStatus.LOGGED_IN;
});
}
void logoutCallback() {
setState(() {
authStatus = AuthStatus.NOT_LOGGED_IN;
_userID = "";
});
}
Widget buildWaitingScreen() {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: CircularProgressIndicator(),
),
);
}
#override
Widget build(BuildContext context) {
switch (authStatus) {
case AuthStatus.NOT_DETERMINED:
return buildWaitingScreen();
break;
case AuthStatus.NOT_LOGGED_IN:
return new StartPage(
auth: widget.auth,
loginCallback: loginCallback,
);
break;
case AuthStatus.LOGGED_IN:
if (_userID.length > 0 && _userID != null) {
return new Home(
userID: _userID,
auth: widget.auth,
logoutCallback: logoutCallback,
);
} else
return buildWaitingScreen();
break;
default:
return buildWaitingScreen();
}
}
}
Home
User currentUser;
bool currentUserloaded = false;
bool currentLocationloaded = false;
class Home extends StatefulWidget {
final BaseAuth auth;
final VoidCallback logoutCallback;
final String userID;
const Home({Key key, this.auth, this.logoutCallback, this.userID})
: super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
PageController pageController;
int pageIndex = 0;
double longitude;
double latitude;
//INIT
#override
void initState() {
super.initState();
loadCurrentUser();
getCurrentLocation();
pageController = PageController();
}
//LOAD current user
loadCurrentUser() async {
print("Current User ${widget.userID}");
DocumentSnapshot doc = await userRef.document(widget.userID).get();
currentUser = User.fromDocument(doc);
setState(() {
currentUserloaded = true;
print("User loaded $currentUserloaded");
});
}
//get current location
getCurrentLocation() async {
var currentLocationCoordinates = await Geolocator()
.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
List<Placemark> place = await Geolocator().placemarkFromCoordinates(
currentLocationCoordinates.latitude,
currentLocationCoordinates.longitude);
latitude = currentLocationCoordinates.latitude;
longitude = currentLocationCoordinates.longitude;
setState(() {
currentLocationloaded = true;
print("Got location $currentLocationloaded");
});
}
//DISPOSE
#override
void dispose() {
pageController.dispose();
super.dispose();
}
//Pageview
onPageChanged(int pageIndex) {
setState(() {
this.pageIndex = pageIndex;
});
}
//On Tap of ButtomTabbar => Jump to next Page
onTap(int pageIndex) {
if (currentUserloaded && currentLocationloaded) {
pageController.jumpToPage(pageIndex);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: PageView(
children: <Widget>[
Feed(userID: widget.userID, latitude: latitude, longitude: longitude),
SearchView(),
ChatHome(),
Profile(
uid: currentUser?.uid,
auth: widget.auth,
logoutCallback: widget.logoutCallback),
],
controller: pageController,
onPageChanged: onPageChanged,
physics: NeverScrollableScrollPhysics(),
),
bottomNavigationBar: CupertinoTabBar(
currentIndex: pageIndex,
inactiveColor: Colors.white,
backgroundColor: Colors.blue,
activeColor: Colors.orange,
onTap: onTap,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home, size: 20),
title: Text("Home"),
),
BottomNavigationBarItem(
icon: Icon(Icons.search, size: 20),
title: Text("Search"),
),
BottomNavigationBarItem(
icon: Icon(Icons.chat, size: 20),
title: Text("chat"),
),
BottomNavigationBarItem(
icon: Icon(Icons.profil, size: 20),
title: Text("Profil"),
),
]),
);
}
}
Feed
class Feed extends StatefulWidget {
final String userID;
final double latitude;
final double longitude;
const Feed({Key key, this.userID, this.latitude, this.longitude})
: super(key: key);
#override
_FeedState createState() => _FeedState();
}
class _FeedState extends State<Feed> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
List<Event> events = [];
var radius = BehaviorSubject<double>.seeded(50.0);
Stream<List<DocumentSnapshot>> stream;
Geoflutterfire geo;
#override
void initState() {
super.initState();
geo = Geoflutterfire();
GeoFirePoint center = geo.point(
latitude: widget.latitude,
longitude: widget
.longitude);
stream = radius.switchMap((rad) {
var collectionReference =
eventRef.where("event", isEqualTo: "festival");
return geo.collection(collectionRef: collectionReference).within(
center: center, radius: rad, field: 'position', strictMode: true);
});
}
//GEOQUERY
Widget geoQuery() {
if (currentLocationloaded && currentUserloaded) {
return Column(
children: <Widget>[
StreamBuilder(
stream: stream,
builder: (BuildContext context,
AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
if (!snapshot.hasData) {
Text("No data");
}
events =
snapshot.data.map((doc) => Event.fromDocument(doc)).toList();
events.sort((a, b) {
var aDate = a.timestamp;
var bDate = b.timestamp;
return aDate.compareTo(bDate);
});
if (events.isEmpty) {
return Text("No events");
}
return Flexible(
child: ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return buildEvent(index);
},
),
);
},
)
],
);
} else {
return circularProgress();
}
}
#override
Widget build(BuildContext context) {
SizeConfig().init(context);
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
centerTitle: true,
title: Text("Feed"),
backgroundColor: Colors.blue,
),
body: geoQuery(),
);
}
}
Update 2
If I use hard coded latitude and longitude for
GeoFirePoint center = geo.point(latitude: 37.773972, longitude: -122.431297);
it works!
Looks like an issue with passing the current user location.
Any suggestions?

The issue was that the location of current user was not passed on time.
Just put
GeoFirePoint center = geo.point(
latitude: widget.latitude,
longitude: widget
.longitude);
stream = radius.switchMap((rad) {
var collectionReference =
eventRef.where("event", isEqualTo: "festival");
return geo.collection(collectionRef: collectionReference).within(
center: center, radius: rad, field: 'position', strictMode: true);
});
from initState to geoQuery()

Related

Connection lost - OnTap Text to play file => getting connection lost

I am trying to get the user to press a record button, to then speak. His memo is recorded and then when the user tap on the Text widget displaying the name of the audio file, the file should be read so he can listen again to what he has recorded.
When I try, I am getting connection lost and the simulator crash. I do not find what is the problem in my code. I guess it is because I am still new with Flutter. Your help is welcome as I am stuck with this for several days. Many thanks.
import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_audio_recorder2/flutter_audio_recorder2.dart';
import 'package:path_provider/path_provider.dart';
enum RecordingState {
UnSet,
Set,
Recording,
Stopped,
}
void main66() => runApp(MyTest());
bool isRecording = false;
class MyTest extends StatelessWidget {
const MyTest({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
// backgroundColor: Colors.white,
body: HomeViewRecorder(),//HomeViewRecorder(),
),
);
}
}
class HomeViewRecorder extends StatefulWidget {
final String _title;
const HomeViewRecorder({Key key, String title}) : _title = title,
super(key: key);
#override
_HomeViewRecorderState createState() => _HomeViewRecorderState();
}
class _HomeViewRecorderState extends State<HomeViewRecorder> {
Directory appDirectory;
String records = '';
#override
void initState() {
super.initState();
getApplicationDocumentsDirectory().then((value) {
appDirectory = (value);
setState(() {
print(appDirectory);
});
});
}
void dispose() {
appDirectory.delete();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold (
appBar: AppBar(
title: Text('My recorder Test')),
body: Column(
children: [
Expanded(child:
InkWell(child: RecordListView(records:records)),
),
Center(child: Record_Widget(
onSaved:
_onRecordComplete)),
]
)
);
}
_onRecordComplete() {
records ='';
appDirectory.list().listen((onData) {
if (onData.path.contains('.aac')) records=(onData.path);
}).onDone(() {
setState(() {});
});
}
}
class RecordListView extends StatefulWidget {
final String records;
const RecordListView({
Key key,
this.records,
}) : super(key: key);
#override
_RecordListViewState createState() => _RecordListViewState();
}
class _RecordListViewState extends State<RecordListView> {
int _totalDuration;
int _currentDuration;
double _completedPercentage = 0.0;
bool _isPlaying = false;
int _selectedIndex = -1;
#override
Widget build(BuildContext context){
return Column(
children: [
widget.records.isEmpty?
Text('No records yet'): InkWell(child: Text(widget.records.split("/").last+DateTime.now().toString()),
onTap: _onPlay,),
],
);
}
Future<void> _onPlay({ String filePath, int index}) async {
AudioPlayer audioPlayer = AudioPlayer();
if (!_isPlaying) {
audioPlayer.play(filePath, isLocal: true);
setState(() {
_selectedIndex = index;
_completedPercentage = 0.0;
_isPlaying = true;
});
audioPlayer.onPlayerCompletion.listen((_) {
setState(() {
_isPlaying = false;
_completedPercentage = 0.0;
});
});
audioPlayer.onDurationChanged.listen((duration) {
setState(() {
_totalDuration = duration.inMicroseconds;
});
});
audioPlayer.onAudioPositionChanged.listen((duration) {
setState(() {
_currentDuration = duration.inMicroseconds;
_completedPercentage =
_currentDuration.toDouble() / _totalDuration.toDouble();
});
});
}
}
String _getDateFromFilePath({ String filePath}) {
String fromEpoch = filePath.substring(
filePath.lastIndexOf('/') + 1, filePath.lastIndexOf('.'));
DateTime recordedDate =
DateTime.fromMillisecondsSinceEpoch(int.parse(fromEpoch));
int year = recordedDate.year;
int month = recordedDate.month;
int day = recordedDate.day;
return ('$year-$month-$day');
}
}
class Record_Widget extends StatefulWidget {
final Function onSaved;
const Record_Widget({Key key,
this.onSaved,}) : super(key: key);
#override
_Record_WidgetState createState() => _Record_WidgetState();
}
class _Record_WidgetState extends State<Record_Widget> {
Directory appDirectory;
String records = '';
RecordingState _recordingState = RecordingState.UnSet;
// Recorder properties
FlutterAudioRecorder2 audioRecorder;
#override
void initState() {
super.initState();
FlutterAudioRecorder2.hasPermissions.then((hasPermission) {
if (hasPermission) {
_recordingState = RecordingState.Set;
}
});
}
#override
void dispose() {
_recordingState = RecordingState.UnSet;
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(100.0),
child: _RecordOrStopButton());
}
Container _RecordOrStopButton() {
return Container(
height: 90,
width: 90,
decoration: BoxDecoration(
border: Border.all(
width: 4.0,
color: Colors.grey,
),
shape: BoxShape.circle,
),
child: Padding(
padding: EdgeInsets.all(4.0),
child: isRecording == false ?
_createRecordButton() : _createStopButton()),);
}
//Widget Button Record
Container _createRecordButton({IconData icon, Function onPressFunc}) {
return Container(child: ElevatedButton(
onPressed: () async {
await _onRecordButtonPressed();
setState(() {
_recordingState = RecordingState.Recording;
isRecording = true;
});
},
style: ButtonStyle(
shape: MaterialStateProperty.all(CircleBorder()),
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
backgroundColor: MaterialStateProperty.all(
Colors.red), // <-- Button color
)));
}
//Widget Button Stop
Container _createStopButton() {
return Container(
padding: EdgeInsets.all(16),
width: 30.0,
height: 30.0,
child: ElevatedButton(
onPressed: () {
_onRecordButtonPressed();
setState(() {
isRecording = false;
});
},
style: ButtonStyle(
fixedSize: MaterialStateProperty.all(Size(10, 10)),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: Colors.red))),
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
backgroundColor: MaterialStateProperty.all(Colors.red),
),
));
}
Future<void> _onRecordButtonPressed() async {
switch (_recordingState) {
case RecordingState.Set:
await _recordVoice();
break;
case RecordingState.Recording:
await _stopRecording();
_recordingState = RecordingState.Stopped;
break;
case RecordingState.Stopped:
await _recordVoice();
break;
case RecordingState.UnSet:
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Please allow recording from settings.'),
));
break;
}
}
_initRecorder() async {
Directory appDirectory = await getApplicationDocumentsDirectory();
String filePath = appDirectory.path +
'/' +
DateTime.now().millisecondsSinceEpoch.toString() +
'.aac';
audioRecorder =
FlutterAudioRecorder2(filePath, audioFormat: AudioFormat.AAC);
await audioRecorder.initialized;
}
_startRecording() async {
isRecording = true;
await audioRecorder.start();
await audioRecorder.current(channel: 0);
}
_stopRecording() async {
isRecording = false;
await audioRecorder.stop();
widget.onSaved();
}
Future<void> _recordVoice() async {
final hasPermission = await FlutterAudioRecorder2.hasPermissions;
if (hasPermission ?? false) {
await _initRecorder();
await _startRecording();
_recordingState = RecordingState.Recording;
} else {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Please allow recording from settings.'),
));
}
}
}

Multiple showDialog for validation

On my project I need to use several showDialog one after the other.
For user creation, I use a SearchField widget to retrieve info from a table related to the user.
If the SearchField value does not exist I would like to propose the creation. Depending on the choice either the form is in error or I propose to register the user.
For this I use a showDialog in the validator of the SearchField and an if validator is correct.
My problem is that my second dialog box is displayed before validating the first one and even above that of the SearchField.
What is the correct way to do this?
Thank you,
class InformationsPage extends StatefulWidget {
const InformationsPage({
required Key key,
required this.user,
required this.type,
}) : super(key: key);
final User user;
final FenType type;
#override
InformationsPageState createState() => InformationsPageState();
}
class InformationsPageState extends State<InformationsPage>
with AutomaticKeepAliveClientMixin {
InformationsPageState({this.user});
final User? user;
late UserApi _api;
#override
bool get wantKeepAlive => true;
bool _familyIsCreated = false;
late User userSaved;
late FenType type;
//Info Form
var _pseudoController = TextEditingController();
var _familyController = TextEditingController();
#override
void initState() {
super.initState();
_api = UserApi();
_pseudoController = TextEditingController(text: widget.user.pseudo);
_familyController = TextEditingController(text: widget.user.familyName);
userSaved = User.fromUser();
type = widget.type;
}
#override
void dispose() {
_pseudoController.dispose();
_familyController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Column(
children: <Widget>[
FutureBuilder(
future: _api.getFamilies(),
builder: (context, AsyncSnapshot<List<Family>> snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(
"Something wrong with message: ${snapshot.error.toString()}"));
} else if (snapshot.connectionState == ConnectionState.done) {
List<Family> _list = snapshot.data!;
return _buildDropdownSearchFamilies(_list);
} else {
return const Center(child: CircularProgressIndicator());
}
}),
TextFormField(
readOnly: type == FenType.read ? true : false,
inputFormatters: [LowerCaseTextFormatter()],
controller: _pseudoController,
onSaved: (value) => userSaved.pseudo = value,
decoration: const InputDecoration(
icon: Icon(Icons.person),
hintText: 'Pseudo',
labelText: 'Pseudo',
),
validator: (value) =>
value!.isEmpty ? 'Obligatory' : null),
],
);
}
int? _contains(List<Family> list, String? name) {
int? res = -1;
for (Family element in list) {
if (element.name == name) {
res = element.id;
break;
}
}
return res;
}
Widget _buildDropdownSearchFamilies(List<Family> _list) {
return SearchField(
controller: _familyController,
suggestions: _list
.map((e) =>
SearchFieldListItem(e.name!, child: Text(e.name!), item: e.id))
.toList(),
hint: 'Family',
validator: (x) {
if (x!.isEmpty) {
userSaved.familyId = null;
userSaved.familyName = null;
return null;
}
int? id = _contains(_list, x);
if (id == -1) {
userSaved.familyId == null;
showDiaglog(x);
if (userSaved.familyId != null) {
return null;
} else {
return 'Family not exist';
}
} else {
userSaved.familyId = id;
userSaved.familyName = x;
return null;
}
},
searchInputDecoration: const InputDecoration(
labelText: 'Family', icon: Icon(Icons.groups)),
itemHeight: 50,
onTap: (x) {
userSaved.familyId = x.item as int?;
userSaved.familyName = x.child.toString();
});
}
showDiaglog(String family) async {
String title = "Family";
String message =
"Family $family not exist. Create ?";
String textKoButton = "no";
String textOkButton = "yes";
MyDialog alert = MyDialog(
title: title,
message: message,
onPressedKo: koButtonPressed(),
onPressedOk: okButtonPressed(family),
textKoButton: textKoButton,
textOkButton: textOkButton);
await showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
void Function() koButtonPressed() => () {
_familyIsCreated = false;
Navigator.of(context).pop(false);
};
void Function() okButtonPressed(family) => () {
_save(family);
Navigator.of(context).pop();
};
void _save(family) async {
UserApi apiUser = UserApi();
Family oldF = Family.empty();
Family newF = Family.empty();
newF.name = family;
newF.createdAt = oldF.createdAt;
newF.deletedAt = newF.deletedAt;
Map<String, dynamic> data = oldF.toJson(newF);
int res = -1;
res = await apiUser.createFamily(data);
SnackBar snackBar;
if (res != -1) {
snackBar = MyWidget.okSnackBar('Family created');
userSaved.familyId = res;
userSaved.familyName = family;
} else {
snackBar = MyWidget.koSnackBar(
'Family not created');
userSaved.familyId = null;
userSaved.familyName = null;
}
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
My form :
class UserFormPage extends StatefulWidget {
static const String routeName = '/admin/user-form';
final User? user;
final FenType fenType;
const UserFormPage({Key? key, required this.user, required this.fenType})
: super(key: key);
#override
_UserFormPageState createState() => _UserFormPageState();
}
class _UserFormPageState extends State<UserFormPage>
with SingleTickerProviderStateMixin {
static final GlobalKey<FormState> _formKey =
GlobalKey<FormState>(debugLabel: '_appState');
static final GlobalKey<InformationsPageState> _infoKey =
GlobalKey<InformationsPageState>();
late TabController _controller;
late User _user;
late User _userSaved;
#override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 2);
_user = widget.user!;
_userSaved = widget.user!;
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () =>
Navigator.pushReplacementNamed(context, Routes.admUserList),
),
title: const Text('Member'),
actions: <Widget>[
Visibility(
visible: widget.fenType != FenType.read ? true : false,
child: IconButton(
icon: const Icon(Icons.save),
onPressed: () {
if (!_formKey.currentState!.validate()) {
return;
}
showDiaglog();
},
))
],
bottom: TabBar(
controller: _controller,
tabs: const [
Tab(text: 'Info'),
Tab(text: 'Others'),
],
),
),
body: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Flexible(
child: TabBarView(
controller: _controller,
children: <Widget>[
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InformationsPage(
user: _user,
key: _infoKey,
type: widget.fenType),
])),
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DetailsPage(
user: _user,
key: _detailsKey,
type: widget.fenType)
],
)),
],
))
],
))),
);
}
void _save() async {
final infoState = _infoKey.currentState;
_userSaved = infoState?.userSaved ?? _user;
_userSaved.pseudo = infoState?.userSaved.pseudo ?? _user.pseudo;
Map<String, dynamic> data = _user.userToJsonClean(_userSaved);
if (!_userSaved.userIsUpdated()) {
final outSnackBar = MyWidget.okSnackBar('Not update');
ScaffoldMessenger.of(context).showSnackBar(outSnackBar);
} else {
UserApi apiUser = UserApi();
bool res = false;
res = widget.fenType == FenType.update
? await apiUser.update(data)
: await apiUser.create(data);
SnackBar snackBar;
res
? snackBar = MyWidget.okSnackBar('Member saved')
: snackBar = MyWidget.koSnackBar(
'Member not saved');
ScaffoldMessenger.of(context).showSnackBar(snackBar);
_user = _userSaved;
if (widget.fenType == FenType.create) {
Navigator.of(context).popAndPushNamed(Routes.admUserList);
}
}
}
void showDiaglog() {
String pseudo = _userSaved.pseudo!;
String title = "Save";
String message = widget.fenType == FenType.create
? "Create member $pseudo ?"
: "Save meber $pseudo ?";
String textKoButton = "no";
String textOkButton = "yes";
MyDialog alert = MyDialog(
title: title,
message: message,
onPressedKo: koButtonPressed(),
onPressedOk: okButtonPressed(),
textKoButton: textKoButton,
textOkButton: textOkButton);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
void Function() koButtonPressed() => () {
Navigator.of(context).pop(false);
};
void Function() okButtonPressed() => () {
_formKey.currentState!.save();
_save();
Navigator.of(context).pop();
};
}
I resolve this problem to modified the widget SearchField to a DropdownSearch.

Getting null safety errors when tried to get autocomplete location of places in Flutter and display details on tapped place

I tried to migrate the no null safety code to null safety and I ended up with errors. I want to get autocomplete location of places in Flutter and display details on the tapped place.
Screenshots of errors:
The code:
main.dart
import 'package:flutter/material.dart';
import 'package:google_places_flutter/address_search.dart';
import 'package:google_places_flutter/place_service.dart';
import 'package:uuid/uuid.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Google Places Demo',
home: MyHomePage(title: 'Places Autocomplete Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, this.title}) : super(key: key);
final String? title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _controller = TextEditingController();
String? _streetNumber = '';
String? _street = '';
String? _city = '';
String? _zipCode = '';
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Container(
margin: EdgeInsets.only(left: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextField(
controller: _controller,
readOnly: true,
onTap: () async {
// generate a new token here
final sessionToken = Uuid().v4();
final Suggestion? result = await showSearch(
context: context,
delegate:AddressSearch(sessionToken),
);
// This will change the text displayed in the TextField
if (result != null) {
final placeDetails = await PlaceApiProvider(sessionToken)
.getPlaceDetailFromId(result.placeId);
setState(() {
_controller.text = result.description!;
_streetNumber = placeDetails.streetNumber;
_street = placeDetails.street;
_city = placeDetails.city;
_zipCode = placeDetails.zipCode;
});
}
},
decoration: InputDecoration(
icon: Container(
width: 10,
height: 10,
child: Icon(
Icons.home,
color: Colors.black,
),
),
hintText: "Enter address",
border: InputBorder.none,
contentPadding: EdgeInsets.only(left: 8.0, top: 16.0),
),
),
SizedBox(height: 20.0),
Text('Street Number: $_streetNumber'),
Text('Street: $_street'),
Text('City: $_city'),
Text('ZIP Code: $_zipCode'),
],
),
),
);
}
}
address_search.dart
import 'package:flutter/material.dart';
import 'package:google_places_flutter/place_service.dart';
class AddressSearch extends SearchDelegate<Suggestion?> {
AddressSearch(this.sessionToken) {
apiClient = PlaceApiProvider(sessionToken);
}
final sessionToken;
late PlaceApiProvider apiClient;
#override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
tooltip: 'Clear',
icon: Icon(Icons.clear),
onPressed: () {
query = '';
},
)
];
}
#override
Widget buildLeading(BuildContext context) {
return IconButton(
tooltip: 'Back',
icon: Icon(Icons.arrow_back),
onPressed: () {
close(context, null);
},
);
}
#override
Widget buildResults(BuildContext context) {
return Container();
}
#override
Widget buildSuggestions(BuildContext context) {
return FutureBuilder(
future: query == ""
? null
: apiClient.fetchSuggestions(
query, Localizations.localeOf(context).languageCode),
builder: (context, snapshot) => query == ''
? Container(
padding: EdgeInsets.all(16.0),
child: Text('Enter address'),
)
: snapshot.hasData
? ListView.builder(
itemBuilder: (context, index) =>
ListTile(
title:
Text((snapshot.data[index] as Suggestion).description!),
onTap: () {
close(context, snapshot.data[index] as Suggestion?);
},
),
itemCount: snapshot.data.length,
)
: Container(child: Text('Loading...')),
);
}
}
place_service.dart
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
class Place {
String? streetNumber;
String? street;
String? city;
String? zipCode;
Place({
this.streetNumber,
this.street,
this.city,
this.zipCode,
});
#override
String toString() {
return 'Place(streetNumber: $streetNumber, street: $street, city: $city, zipCode: $zipCode)';
}
}
class Suggestion {
final String? placeId;
final String? description;
Suggestion(this.placeId, this.description);
#override
String toString() {
return 'Suggestion(description: $description, placeId: $placeId)';
}
}
class PlaceApiProvider {
final client = Client();
PlaceApiProvider(this.sessionToken);
final sessionToken;
static final String androidKey = 'YOUR_API_KEY_HERE';
static final String iosKey = 'YOUR_API_KEY_HERE';
final apiKey = Platform.isAndroid ? androidKey : iosKey;
Future<List<Suggestion>?> fetchSuggestions(String input, String lang) async {
final request =
'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$input&key=$apiKey&sessiontoken=$sessionToken';
final response = await client.get(Uri.parse(request));
if (response.statusCode == 200) {
final result = json.decode(response.body);
if (result['status'] == 'OK') {
// compose suggestions in a list
return result['predictions']
.map<Suggestion>((p) => Suggestion(p['place_id'], p['description']))
.toList();
}
if (result['status'] == 'ZERO_RESULTS') {
return [];
}
throw Exception(result['error_message']);
} else {
throw Exception('Failed to fetch suggestion');
}
}
Future<Place> getPlaceDetailFromId(String? placeId) async {
final request =
'https://maps.googleapis.com/maps/api/place/details/json?place_id=$placeId&fields=address_component&key=$apiKey&sessiontoken=$sessionToken';
final response = await client.get(Uri.parse(request));
if (response.statusCode == 200) {
final result = json.decode(response.body);
if (result['status'] == 'OK') {
final components =
result['result']['address_components'] as List<dynamic>;
// build result
final place = Place();
components.forEach((c) {
final List type = c['types'];
if (type.contains('street_number')) {
place.streetNumber = c['long_name'];
}
if (type.contains('route')) {
place.street = c['long_name'];
}
if (type.contains('locality')) {
place.city = c['long_name'];
}
if (type.contains('postal_code')) {
place.zipCode = c['long_name'];
}
});
return place;
}
throw Exception(result['error_message']);
} else {
throw Exception('Failed to fetch suggestion');
}
}
}
The solution is probably this:
builder: (context, AsyncSnapshot<List<Suggestion>> snapshot)
instead of this:
builder: (context, snapshot)
then you can do something like:
List<Suggestion>? suggestions = snapshot.data;
if ( suggestions != null && suggestions.length > 0) {

How to sync a resource or a model object across different screens/widgets when it is updated?

I have a REST API which allows the user to update a Book model
GET /api/books.json # list of books
PUT /api/books/1.json # update the book with id=1
I have corresponding screens for these actions (an Index screen to list books; an Edit screen to edit the book details) in my flutter application. When creating the form edit a Book,
I pass a Book object to the Edit form
In the Edit form, I make a copy of the book object. I create a copy and not edit the original object to ensure that the object is not changed if the Update fails at the server
If the update is successful, I display an error message.
However, when I go back to the Index view, the book title is still the same (as this object has not changed). Also, I found that even if I make changes to the original object, instead of making a copy, the build method is not called when I go 'back'. I am wondering if there is a pattern that I can use to have this object updated across the application on successful updates.
I have the following classes
class Book {
final int id;
final String title;
Book(this.id, this.title);
static Book fromJson(json) {
return Book(
json['id'],
json['title']);
}
Map<String, dynamic> toJson() => {
'title': title
};
Future<bool> update() {
var headers = {
'Content-Type': 'application/json'
};
return http
.put(
"$HOST/api/books/${id}.json",
headers: headers,
body: jsonEncode(this.toJson()),
)
.then((response) => response.statusCode == 200);
}
}
Here is the Index view
class BooksIndex extends StatefulWidget {
static final tag = "books-index";
#override
_BooksIndexState createState() => _BooksIndexState();
}
class _BooksIndexState extends State<BooksIndex> {
final Future<http.Response> _getBooks = http.get("$HOST/api/books.json", headers: headers);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _getBooks,
builder: (context, snapshot) {
if (snapshot.hasData) {
var response = snapshot.data as http.Response;
if (response.statusCode == 200) {
List<dynamic> booksJson = jsonDecode(response.body);
List<Book> books = booksJson.map((bookJson) {
return Book.fromJson(bookJson);
}).toList();
return _buildMaterialApp(ListView.builder(
itemCount: books.length,
itemBuilder: (context, index) {
var book = books[index];
return ListTile(
title: Text(book.title),
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => BooksEdit(book: book)
));
},
);
},
));
} else {
return _buildMaterialApp(Text(
"An error occured while trying to retrieve the books. Status=${response.statusCode}"));
}
} else if (snapshot.hasError) {
return _buildMaterialApp(Text(
"Could not load books. Please check your internet connection."));
} else {
return _buildMaterialApp(Text("Loading"));
}
});
}
_buildMaterialApp(widget) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Books"),
),
body: widget,
),
);
}
}
Here is the Edit form
class BooksEdit extends StatelessWidget {
final Book book;
BooksEdit({Key key, #required this.book}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Edit ${book.title}"),
),
body: Column(
children: <Widget>[
BookForm(
book: book,
)
],
),
);
}
}
class BookForm extends StatefulWidget {
Book book;
BookForm({Key key, #required this.book}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _BookFormState();
}
}
class _BookFormState extends State<BookForm> {
TextEditingController _titleField;
RaisedButton _submitBtn;
bool isError = false;
String formMessage = "";
#override
Widget build(BuildContext context) {
_titleField = TextEditingController(text: widget.book.title);
_submitBtn = RaisedButton(
child: Text(
"Update",
style: Theme
.of(context)
.textTheme
.button,
),
color: Theme
.of(context)
.primaryColor,
onPressed: () {
var book = Book(
widget.book.id,
_titleField.text
);
book.update().then((success) {
if (success) {
setState(() {
isError = false;
formMessage = "Successfully updated";
widget.book = book;
});
} else {
setState(() {
isError = true;
formMessage = "Book could not be updated";
});
}
}, onError: (error) {
setState(() {
isError = true;
formMessage =
"An unexpected error occured. It has been reported to the administrator.";
});
});
},
);
var formMessageColor = isError ? Colors.red : Colors.green;
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
formMessage,
style: TextStyle(color: formMessageColor),
),
TextFormField(
controller: _titleField,
),
_submitBtn
],
),
);
}
}
Here the main file
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final routes = <String, WidgetBuilder>{
'/': (context) => BooksIndex(),
};
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "BooksApp",
theme: ThemeData(primarySwatch: Colors.green),
routes: routes,
initialRoute: '/',
);
}
}
ALSO, I am new to Flutter. So, I would appreciate it if I get any feedback about any other places in my code that I can improve upon.
You can copy paste run full code below
I use fixed json string to simulate http, when update be called, only change json string
You can also reference official example https://flutter.dev/docs/cookbook/networking/fetch-data
Step 1 : You can await Navigator.push and do setState after await to refresh BooksIndex
Step 2 : Move parse json logic to getBooks
code snippet
return ListTile(
title: Text(book.title),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BooksEdit(book: book)));
setState(() {});
},
Future<List<Book>> httpGetBooks() async {
print("httpGetBooks");
var response = http.Response(jsonString, 200);
if (response.statusCode == 200) {
print("200");
List<dynamic> booksJson = jsonDecode(response.body);
List<Book> books = booksJson.map((bookJson) {
return Book.fromJson(bookJson);
}).toList();
print(books[1].title.toString());
return books;
}
}
working demo
full code
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final routes = <String, WidgetBuilder>{
'/': (context) => BooksIndex(),
};
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "BooksApp",
theme: ThemeData(primarySwatch: Colors.green),
routes: routes,
initialRoute: '/',
);
}
}
class BooksEdit extends StatelessWidget {
final Book book;
BooksEdit({Key key, #required this.book}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Edit ${book.title}"),
),
body: Column(
children: <Widget>[
BookForm(
book: book,
)
],
),
);
}
}
class BookForm extends StatefulWidget {
Book book;
BookForm({Key key, #required this.book}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _BookFormState();
}
}
class _BookFormState extends State<BookForm> {
TextEditingController _titleField;
RaisedButton _submitBtn;
bool isError = false;
String formMessage = "";
#override
Widget build(BuildContext context) {
_titleField = TextEditingController(text: widget.book.title);
_submitBtn = RaisedButton(
child: Text(
"Update",
style: Theme.of(context).textTheme.button,
),
color: Theme.of(context).primaryColor,
onPressed: () {
var book = Book(widget.book.id, _titleField.text);
book.update().then((success) {
if (success) {
setState(() {
isError = false;
formMessage = "Successfully updated";
widget.book = book;
});
} else {
setState(() {
isError = true;
formMessage = "Book could not be updated";
});
}
}, onError: (error) {
setState(() {
isError = true;
formMessage =
"An unexpected error occured. It has been reported to the administrator.";
});
});
},
);
var formMessageColor = isError ? Colors.red : Colors.green;
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
formMessage,
style: TextStyle(color: formMessageColor),
),
TextFormField(
controller: _titleField,
),
_submitBtn
],
),
);
}
}
class BooksIndex extends StatefulWidget {
static final tag = "books-index";
#override
_BooksIndexState createState() => _BooksIndexState();
}
String jsonString = '''
[{
"id" : 1,
"title" : "t"
}
,
{
"id" : 2,
"title" : "t1"
}
]
''';
class _BooksIndexState extends State<BooksIndex> {
Future<List<Book>> httpGetBooks() async {
print("httpGetBooks");
var response = http.Response(jsonString, 200);
if (response.statusCode == 200) {
print("200");
List<dynamic> booksJson = jsonDecode(response.body);
List<Book> books = booksJson.map((bookJson) {
return Book.fromJson(bookJson);
}).toList();
print(books[1].title.toString());
return books;
}
}
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
print("build ${jsonString}");
return FutureBuilder<List<Book>>(
future: httpGetBooks(),
builder: (context, snapshot) {
if (snapshot.hasData) {
print("hasData");
return _buildMaterialApp(ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
var book = snapshot.data[index];
print(book.title);
return ListTile(
title: Text(book.title),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BooksEdit(book: book)));
setState(() {});
},
);
},
));
} else if (snapshot.hasError) {
return _buildMaterialApp(Text(
"Could not load books. Please check your internet connection."));
} else {
return _buildMaterialApp(Text("Loading"));
}
});
}
_buildMaterialApp(widget) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Books"),
),
body: widget,
),
);
}
}
class Book {
final int id;
final String title;
Book(this.id, this.title);
static Book fromJson(json) {
return Book(json['id'], json['title']);
}
Map<String, dynamic> toJson() => {'title': title};
Future<bool> update() {
print("update");
var headers = {'Content-Type': 'application/json'};
/*return http
.put(
"$HOST/api/books/${id}.json",
headers: headers,
body: jsonEncode(this.toJson()),
)
.then((response) => response.statusCode == 200);*/
jsonString = '''
[{
"id" : 1,
"title" : "t"
}
,
{
"id" : 2,
"title" : "test"
}
]
''';
return Future.value(true);
}
}
setState(() {
});
},
);

How to place a key in ListView to maintain state on page change

I am trying to build a steam of posts (think twitter or instagram type posts) that a user is able to scroll through. As they are scrolling, they can click one of the posts and navigate to a new page. When they go navigate back from that page, I want them to remain at the same position on the position that they were previously on within the ListView.
PROBLEM
I can not currently keep the stream widget from rebuilding and returning to the scroll position. I know that one of the solutions to this is to include a key; however, I have tried to including the key in the ListView.builder, but it has not worked.
QUESTION
Where should I include the key? Am I using the right type of key?
class Stream extends StatefulWidget {
Stream({Key key, this.user}) : super(key: key);
final User user;
#override
_StreamState createState() => new _StreamState(
user: user
);
}
class _StreamState extends State<Stream> {
_StreamState({this.user});
final User user;
Firestore _firestore = Firestore.instance;
List<DocumentSnapshot> _posts = [];
bool _loadingPosts = true;
int _per_page = 30;
DocumentSnapshot _lastPosts;
ScrollController _scrollController = ScrollController();
bool _gettingMorePosts = false;
bool _morePostsAvailable = true;
_getPosts() async {
Query q = _firestore
.collection('posts')
.document(user.user_id)
.collection('posts')
.orderBy("timePosted", descending: true)
.limit(_per_page);
setState(() {
_loadingPosts = true;
});
QuerySnapshot querySnapshot = await q.getDocuments();
_posts = querySnapshot.documents;
if (_posts.length == 0) {
setState(() {
_loadingPosts = false;
});
}
else {
_lastPosts = querySnapshot.documents[querySnapshot.documents.length - 1];
setState(() {
_loadingPosts = false;
});
}
}
_getMorePosts() async {
if (_morePostsAvailable == false) {
return;
}
if (_gettingMorePosts == true) {
return;
}
if (_posts.length == 0) {
return;
}
_gettingMorePosts = true;
Query q = _firestore
.collection('posts')
.document(user.user_id)
.collection('posts')
.orderBy("timePosted", descending: true)
.startAfter([_lastPosts.data['timePosted']]).limit(_per_page);
QuerySnapshot querySnapshot = await q.getDocuments();
if (querySnapshot.documents.length == 0) {
_morePostsAvailable = false;
}
if(querySnapshot.documents.length > 0) {
_lastPosts = querySnapshot.documents[querySnapshot.documents.length - 1];
}
_posts.addAll(querySnapshot.documents);
setState(() {});
_gettingMorePosts = false;
}
#override
void initState() {
super.initState();
_getPosts();
_scrollController.addListener(() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
double delta = MediaQuery.of(context).size.height * 0.25;
if (maxScroll - currentScroll < delta) {
_getMorePosts();
}
});
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
new Expanded(
child: _loadingPosts == true
? Container(
child: Center(
child: Text(" "),
),
)
: Container(
child: Center(
child: _posts.length == 0
? Center(
child: Text("Follow friends", style: TextStyle(fontSize: 15),),
)
: ListView.builder(
key: widget.key,
controller: _scrollController,
itemCount: _posts.length,
itemBuilder: (BuildContext ctx, int index) {
return new Widget(
//paramenters to build the post widget here
);
}),
),
),
),
],
);
}
One thing to note, since I don't want to return all pages (due to Firestore expenses calling so many posts), the build logic is created such that more posts are loaded upon scroll. I realize this may impact it.
Short answer:
You need to provide key to your ListView.builder like this:
ListView.builder(
key: PageStorageKey("any_text_here"),
// ...
)
Long answer:
You can see that when you come back from screen 2 to screen 1, the item 30 remains on the top.
Sorry it was difficult to reproduce your code due to limited availability to the variables you're using. I created a simple example to demonstrate what you're looking for.
Full code:
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
key: PageStorageKey("any_text_here"), // this is the key you need
itemCount: 50,
itemBuilder: (_, i) {
return ListTile(
title: Text("Item ${i}"),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(index: i))),
);
},
),
);
}
}
class DetailPage extends StatefulWidget {
final int index;
const DetailPage({Key key, this.index}) : super(key: key);
#override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text(
"You clicked ${widget.index}",
style: Theme.of(context).textTheme.headline,
),
),
);
}
}
This kind of key works:
key: PageStorageKey('Your Key Name'),