flutter return multiple fields from custom widget - dart

I got the below geolocation.dart file, that works perfectly as stand alone widget:
import 'package:flutter/material.dart';
import 'package:location/location.dart';
import 'package:flutter/services.dart';
import 'package:simple_permissions/simple_permissions.dart';
class LocationField extends StatefulWidget {
const LocationField({
this.fieldKey,
this.onSaved,
});
final Key fieldKey;
final FormFieldSetter<String> onSaved;
#override
_LocationFieldState createState() => _LocationFieldState();
}
class _LocationFieldState extends State<LocationField> {
Location _location = new Location();
final lat = TextEditingController();
final lon = TextEditingController();
// #override
// void initState() {
// super.initState();
// lat.addListener(_addLatValue);
// lon.addListener(_addLonValue);
//}
#override
Widget build(BuildContext context) {
return Row(
textDirection: TextDirection.rtl,
children: <Widget>[
FlatButton(
child: const Icon(Icons.my_location),
onPressed: () => _getLocation(),
),
Expanded(child: Column(
textDirection: TextDirection.ltr,
children: <Widget>[
TextFormField(
controller: lat,
decoration: InputDecoration(
prefixIcon: Text("Latittude: ")
),
),
TextFormField(
controller: lon,
decoration: InputDecoration(
prefixIcon: Text("Longitude: ")
),
)
])
)
],
);
}
_getLocation() async {
Map<String, double> location;
var error = null;
try {
await SimplePermissions.requestPermission(Permission.AccessFineLocation);
location = await _location.getLocation();
} on PlatformException catch (e) {
if (e.code == 'PERMISSION_DENIED') {
error = 'Permission denied';
} else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {
error =
'Permission denied - please ask the user to enable it from the app settings';
}
location = null;
}
print("error $error");
setState(() {
lat.text = ('${location["latitude"]}');
lon.text = ('${location["longitude"]}');
});
}
}
And display the below, at which the location coordinate appear upon clicking the location icon, as below:
I can also insert it as a widget in my main app, as:
class _SignUpPageState extends State<SignUpPage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
body: Form(
key: _formKey,
child: Column(
children: <Widget>[
LocationField(),
RaisedButton(
onPressed: signUp,
child: Text('Sign up'),
),
],
)
),
);
}
void signUp() {
// what to write here to get the geolocation points lat/lon printed?
}
My question, is: How can I get the geolocation points lat/lon printed upon clicking the signup button, how can I get the value of the 2 fields from the sub-widget?

In Flutter, passing state down the widget tree is quite easy using InheritedWidget & co., while passing data upwards actually involves some thinking.
Similar to the TextEditingControllers you're using, you could create a LocationController that holds the location data:
class LocationController {
Location _location = Location();
get location => _location;
set location(Location val) {
_location = val;
if (onChanged != null) _onChanged(val);
}
VoidCallback _onChanged;
}
This controller can then be passed to the LocationField like this:
class LocationField extends StatefulWidget {
LocationField({
this.fieldKey,
#required this.controller,
this.onSaved,
});
final Key fieldKey;
final LocationController controller;
final FormFieldSetter<String> onSaved;
#override
_LocationFieldState createState() => _LocationFieldState();
}
class _LocationFieldState extends State<LocationField> {
final lat = TextEditingController();
final lon = TextEditingController();
#override
void initState() {
super.initState();
widget.controller._onChanged = (location) => setState(() {
lat.text = ('${location["latitude"]}');
lon.text = ('${location["longitude"]}');
});
lat.addListener(_addLatValue);
lon.addListener(_addLonValue);
}
#override
Widget build(BuildContext context) { ... }
_getLocation() async {
String error;
try {
await SimplePermissions.requestPermission(Permission.AccessFineLocation);
widget.controller.location = await _location.getLocation();
} on PlatformException catch (e) {
if (e.code == 'PERMISSION_DENIED') {
error = 'Permission denied';
} else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {
error =
'Permission denied - please ask the user to enable it from the app settings';
}
location = null;
}
print("error $error");
}
}
Then, in your widget up the tree, you can access the controller to retrieve the location:
class _SignUpPageState extends State<SignUpPage> {
LocationController controller = LocationController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Form(
key: _formKey,
child: Column(
children: <Widget>[
LocationField(controller: controller),
RaisedButton(onPressed: signUp, child: Text('Sign up')),
],
)
),
);
}
void signUp() {
final location = controller.location;
}
}
An added benefit is that you could set the controller's location from the widget up in the tree and the LocationField will automatically rebuild to reflect that change.

Related

How to set state on after a method call has been completed

I'm trying to change the state of isSyncing then rebuild the widget with set state once await api.fetchProducts() is completed. api.fetchProducts() is what i used to fetch from API then store local using sqflite.
I tried using cloudSyn.then() but it wont work.
class SyncProgress extends StatefulWidget {
#override
_SyncProgressState createState() => _SyncProgressState();
}
class _SyncProgressState extends State<SyncProgress> {
bool isSyncing = true;
String progressString = 'Syncing your data....';
final db = DatabaseHelper();
final bloc = ProductBloc();
#override
void initState() {
super.initState();
}
Future cloudSync() async{
await api.fetchProducts();
//Here is the challenge
setState(() {
isSyncing = false;
progressString = 'Syncing complete....';
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: isSyncing ? _indicateProgress() : _syncDone()
);
}
Widget _indicateProgress(){
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(),
SizedBox(height: 50.0,),
Text(progressString, style: TextStyle(
fontSize: 16.0,
),),
],
),
);
}
_syncDone(){
print('Syncing completed');
//return Navigator.push(context, MaterialPageRoute(builder: (context) => HomePage()));
}
}
Use then to force setState function to execute only after fetchProducts() is finished:
Future cloudSync() async{
await api.fetchProducts().then(
setState(() {
isSyncing = false;
progressString = 'Syncing complete....';
});
);
}

How do I make RefreshIndicator disappear?

I have this code that has the parent widget Homepage and the child widget CountryList. In CountryList, I have created a function that uses an API to get a list of countries. I felt like enabling a RefreshIndicator in the app, so I had to modify the Homepage widget and add GlobalKey to access getCountryData() function of CountryList widget. The RefreshIndicator has done its job well. But the problem now is that when I pull and use the RefreshIndicator in the app, the getCountryData() function is called, but even after showing all data in the list, the circular spinner doesn't go (shown in the screenshot).
So, could anyone please suggest me a way to make the spinner go?
The code of main.dart containing Homepage widget is given below:
import 'package:flutter/material.dart';
import 'country_list.dart';
GlobalKey<dynamic> globalKey = GlobalKey();
void main() => runApp(MaterialApp(home: Homepage()));
class Homepage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("List of countries"), actions: <Widget>[
IconButton(icon: Icon(Icons.favorite), onPressed: (){},)
],),
body: RefreshIndicator(child: CountryList(key:globalKey), onRefresh: (){globalKey.currentState.getCountryData();},),
);
}
}
And the code of country_list.dart containing CountryList widget is:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter_svg/flutter_svg.dart';
class CountryList extends StatefulWidget {
CountryList({Key key}) : super(key: key);
#override
_CountryListState createState() => _CountryListState();
}
class _CountryListState extends State<CountryList> {
List<dynamic> _countryData;
bool _loading = false;
#override
void initState() {
// TODO: implement initState
super.initState();
this.getCountryData();
}
Future<String> getCountryData() async {
setState(() {
_loading = true;
});
var response =
await http.get(Uri.encodeFull("https://restcountries.eu/rest/v2/all"));
var decodedResponse = json.decode(response.body);
setState(() {
_countryData = decodedResponse;
_loading = false;
});
}
#override
Widget build(BuildContext context) {
return _loading?Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[CircularProgressIndicator(), Padding(padding: EdgeInsets.all(5.0),), Text("Loading data...", style: TextStyle(fontSize: 20.0),)],)):ListView.builder(
itemCount: _countryData.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: ListTile(
leading: SvgPicture.network(_countryData[index]['flag'], width: 60.0,),
title: Text(_countryData[index]['name']),
trailing: IconButton(
icon: Icon(Icons.favorite_border),
onPressed: () {},
),
),
);
},
);
}
}
You need to add return here:
Future<String> getCountryData() async {
setState(() {
_loading = true;
});
var response =
await http.get(Uri.encodeFull("https://restcountries.eu/rest/v2/all"));
var decodedResponse = json.decode(response.body);
setState(() {
_countryData = decodedResponse;
_loading = false;
});
return 'success';
}
and here:
body: RefreshIndicator(
child: CountryList(key: globalKey),
onRefresh: () {
return globalKey.currentState.getCountryData();
},
),
The onRefresh callback is called. The callback is expected to update the scrollable's contents and then complete the Future it returns. The refresh indicator disappears after the callback's Future has completed, I think you should return Future<String> from getCountryData.

Flutter close a Dialog inside a condition

I am trying to close a Dialog dynamically.
What I am actually trying to do is to change the content of the dialog depending on the information I have at the moment.
Starts with loading info and no button and after a few seconds could be an error with the OK button to close the Dialog Box.
class Dialogs{
loginLoading(BuildContext context, String type, String description){
var descriptionBody;
if(type == "error"){
descriptionBody = CircleAvatar(
radius: 100.0,
maxRadius: 100.0,
child: new Icon(Icons.warning),
backgroundColor: Colors.redAccent,
);
} else {
descriptionBody = new Center(
child: new CircularProgressIndicator(),
);
}
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context){
return AlertDialog(
title: descriptionBody,
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Center(child: Text(description))
],
),
),
);
}
);
}
}
So after creating the instance os the dialog and opening it
Dialogs _dialog = new Dialogs();
_dialog.loginLoading(context, "loading", "loading...");
// Close the dialog code here
don't know how to do it
// Call again the AlertDialog with different content.
https://docs.flutter.io/flutter/material/showDialog.html
The dialog route created by this method is pushed to the root navigator. If the application has multiple Navigator objects, it may be necessary to call Navigator.of(context, rootNavigator: true).pop(result) to close the dialog rather than just Navigator.pop(context, result).
So any one of the below should work for you
Navigator.of(context, rootNavigator: true).pop(result)
Navigator.pop(context, result)
You don't need to close and reopen the dialog. Instead let flutter handle the dialog update. The framework is optimised for just that.
Here is a working example app that you can use as a starting point (just add your own Dialogs class):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MyApp',
home: Login(
child: Home(),
),
);
}
}
class Home extends StatefulWidget {
final Dialogs dialog = Dialogs();
#override
State<StatefulWidget> createState() => HomeState();
}
class HomeState extends State<Home> {
#override
void didChangeDependencies() {
super.didChangeDependencies();
Future.delayed(Duration(milliseconds: 50)).then((_) {
widget.dialog.loginLoading(
context,
LoginStateProvider.of(context).type,
LoginStateProvider.of(context).description,
);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Updating Dialog'),
),
body: Container(),
);
}
}
class Login extends StatefulWidget {
final Widget child;
Login({#required this.child});
#override
State<StatefulWidget> createState() => LoginState();
}
class LoginState extends State<Login> {
String type = 'wait';
String description = 'foo';
#override
void didChangeDependencies() {
super.didChangeDependencies();
Future.delayed(Duration(milliseconds: 2000)).then((_) {
setState(() {
type = 'error';
description = 'bar';
});
});
}
#override
Widget build(BuildContext context) {
return LoginStateProvider(widget.child, type, description);
}
}
class LoginStateProvider extends InheritedWidget {
final String type;
final String description;
LoginStateProvider(Widget child, this.type, this.description)
: super(child: child);
#override
bool updateShouldNotify(LoginStateProvider old) {
return type != old.type || description != old.description;
}
static LoginStateProvider of(BuildContext context) =>
context.inheritFromWidgetOfExactType(LoginStateProvider);
}

Call FutureBuilder from RaisedButton

i would love to call the Future fetchPost from a RaisedButton or in other words i don't wan't the FutureBuilder to do anything until i click the button, i tried calling fetchPost from the button but it won't work and I'm stuck.
PS: I used the example from this page https://flutter.io/cookbook/networking/fetch-data/
Your help is appreciated.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Post> fetchPost() async {
final response =
await http.get('https://jsonplaceholder.typicode.com/posts/1');
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return Post.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
class Post {
final int userId;
final int id;
final String title;
final String body;
Post({this.userId, this.id, this.title, this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
class FirstFragment extends StatelessWidget {
FirstFragment(this.usertype,this.username);
final String usertype;
final String username;
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final Size screenSize = MediaQuery.of(context).size;
return new SingleChildScrollView(
padding: new EdgeInsets.all(5.0),
child: new Padding(
padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
child: new RaisedButton(
child: new Text('Call'),
onPressed: (){
fetchPost();
},
),
),
new Container(
child: FutureBuilder<Post>(
future: fetchPost(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.title);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner
return CircularProgressIndicator();
},
)
)
],
),
),
);
}
}
As Dhiraj explained above calling fetchPost alone won't change UI, so you need to reset UI by calling setState.
Below is how your code should look like
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Post> fetchPost() async {
final response =
await http.get('https://jsonplaceholder.typicode.com/posts/1');
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return Post.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
class Post {
final int userId;
final int id;
final String title;
final String body;
Post({this.userId, this.id, this.title, this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
class FirstFragment extends StatefulWidget {
FirstFragment(this.usertype,this.username);
final String usertype;
final String username;
#override
_FirstFragmentState createState() => new _FirstFragmentState(usertype, username);
}
class _FirstFragmentState extends State<FirstFragment> {
_FirstFragmentState(this.usertype,this.username);
final String usertype;
final String username;
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final Size screenSize = MediaQuery.of(context).size;
return new SingleChildScrollView(
padding: new EdgeInsets.all(5.0),
child: new Padding(
padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
child: new RaisedButton(
child: new Text('Call'),
onPressed: (){
fetchPost();
setState(() {
});
},
),
),
new Container(
child: FutureBuilder<Post>(
future: fetchPost(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.title);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner
return CircularProgressIndicator();
},
)
)
],
),
),
);
}
}
Calling fetchPost alone wont do changes in UI.
At first inside build your futurebuilder is execcuted which gets data from fetchPost.
Further then to fetchPost agiain you need to rebuild.
To do so inside onPressed of raised button:
onPressed: (){
setState((){})
},
And to fetch post only on button click (not for first time) you should use then()
Details here : https://www.dartlang.org/tutorials/language/futures

How to write clickable functions for slider images of image_carousel package in flutter?

I am using imagecarousel package for displaying images from the network. I want to keep onPressed function for images in the slide.
new ImageCarousel(
<ImageProvider>[
new NetworkImage('http://www.hilversum.ferraridealers.com/siteasset/ferraridealer/54f07ac8c35b6/961/420/selected/0/0/0/54f07ac8c35b6.jpg'),
new NetworkImage('http://auto.ferrari.com/en_EN/wp-content/uploads/sites/5/2017/08/ferrari-portofino-reveal-2017-featured-new.jpg'),
new NetworkImage('http://www.hilversum.ferraridealers.com/siteasset/ferraridealer/54f07ac8c35b6/961/420/selected/0/0/0/54f07ac8c35b6.jpg'),
],
interval: new Duration(seconds: 1),
)
After making some modifications to Image Carousel, I was able to implement click event (other events also possible). Here is the sample code.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class ImageCarousel extends StatefulWidget {
final List<ImageProvider> imageProviders;
final double height;
final TargetPlatform platform;
final Duration interval;
final TabController tabController;
final BoxFit fit;
// Images will shrink according to the value of [height]
// If you prefer to use the Material or Cupertino style activity indicator set the [platform] parameter
// Set [interval] to let the carousel loop through each photo automatically
// Pinch to zoom will be turned on by default
ImageCarousel(this.imageProviders,
{this.height = 250.0, this.platform, this.interval, this.tabController, this.fit = BoxFit.cover});
#override
State createState() => new _ImageCarouselState();
}
TabController _tabController;
class _ImageCarouselState extends State<ImageCarousel> with SingleTickerProviderStateMixin {
#override
void initState() {
super.initState();
_tabController = widget.tabController ?? new TabController(vsync: this, length: widget.imageProviders.length);
if (widget.interval != null) {
new Timer.periodic(widget.interval, (_) {
_tabController.animateTo(_tabController.index == _tabController.length - 1 ? 0 : ++_tabController.index);
});
}
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new SizedBox(
height: widget.height,
child: new TabBarView(
controller: _tabController,
children: widget.imageProviders.map((ImageProvider provider) {
return new CarouselImageWidget(widget, provider, widget.fit, widget.height);
}).toList(),
),
);
}
}
class CarouselImageWidget extends StatefulWidget {
final ImageCarousel carousel;
final ImageProvider imageProvider;
final BoxFit fit;
final double height;
CarouselImageWidget(this.carousel, this.imageProvider, this.fit, this.height);
#override
State createState() => new _CarouselImageState();
}
class _CarouselImageState extends State<CarouselImageWidget> {
bool _loading = true;
Widget _getIndicator(TargetPlatform platform) {
if (platform == TargetPlatform.iOS) {
return new CupertinoActivityIndicator();
} else {
return new Container(
height: 40.0,
width: 40.0,
child: new CircularProgressIndicator(),
);
}
}
#override
void initState() {
super.initState();
widget.imageProvider.resolve(new ImageConfiguration()).addListener((i, b) {
if (mounted) {
setState(() {
_loading = false;
});
}
});
}
#override
Widget build(BuildContext context) {
return new Container(
height: widget.height,
child: _loading
? _getIndicator(widget.carousel.platform == null ? defaultTargetPlatform : widget.carousel.platform)
: new GestureDetector(
child: new Image(
image: widget.imageProvider,
fit: widget.fit,
),
onTap: () {
int index = int.parse(_tabController.index.toString());
switch(index){
//Implement you case here
case 0:
case 1:
case 2:
default:
print(_tabController.index.toString());
}
},
),
);
}
}
void main(){
runApp(new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Demo"),
),
body: new ImageCarousel(
<ImageProvider>[
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-2.jpg'),
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-10.jpg'),
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-4.jpg'),
],
interval: new Duration(seconds: 5),
)
),
));
}
Hope it helps..!!

Resources