I have the dropdownbutton widget below, with a futurebuilder that reads a list from a google spreadsheet. The widget works good as intended.
I want to reuse this widget within the app, and simply pass a different Future
As example, I want to call the same widget, but pass different lists
mydropdownbutton ( futureList1 ),
mydropdownbutton ( futureList2 ),
mydropdownbutton ( futureList3 ),
//========================================================
// WIDGET: FUTURE DROPDOWN MENU + FUTURE LIST
//--------------------------------------------------------
class dropDownButtonWidget extends StatefulWidget {
const dropDownButtonWidget({ Key? key,}) : super(key: key);
#override
State<dropDownButtonWidget> createState() => _dropDownButtonWidgetState();
}
// --------------------------------------------------------
class _dropDownButtonWidgetState extends State<dropDownButtonWidget> {
#override
Widget build(BuildContext context) {
return Center(
child: FutureBuilder(future: futureList(),
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if(snapshot.hasData){ List? futureDataList = snapshot.data;
futureDataList ??= ['Loading']; //if snapshot is null
return buildDropdownButton(dropdownList: futureDataList );
}else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}else {
return Center(child: CircularProgressIndicator());
}
}
)
);
}
//----------------------------------
// DROPDOWNBUTTON EXTRACTED METHOD
DropdownButton<Object> buildDropdownButton({required List dropdownList}) {
String defaultValue = dropdownList.first; //DEFAULT SELECTED ITEM
return DropdownButton(
value: defaultValue,
onChanged: (value) => setState(() => defaultValue = value.toString()),
items: dropdownList.map((items) {
return DropdownMenuItem(value: items, child: Text(items));
}).toList(),
);
}
//----------------------------------
}
//=============================================
//=============================================
//FUTURE LIST FOR THE DROPDOWN MENU
//=============================================
Future<List> futureList() async {
var items = await ScheduleInfo.mx_schedule_WEEKDAYS_as_List(debugONOFF: 1);
return items;}
//=============================================
How can I make this widget modular and reusable?
you can send the future to this widget's constructor. first you declare a variable and set it to constructor. then you can reference that variable in the state of that class by widget.variable keyword. something like this
class dropDownButtonWidget extends StatefulWidget {
final variable;
dropDownButtonWidget(this.variable);
const dropDownButtonWidget({ Key? key,}) : super(key: key);
#override
State<dropDownButtonWidget> createState() => _dropDownButtonWidgetState();
}
class _dropDownButtonWidgetState extends State<dropDownButtonWidget> {
widget.variable //whatever you want to do with it
}
Community... after few hours of reading and testing, I found the solution to my own question. I am posting the solution for anyone else needing it.
Probably, my code can be improved, I welcome suggestions.
My question above has a working code for a dropdown button widget (fully working as today), using a Future
Below, my own answer, the same widget transformed into a reusable modular widget.
( This works only with future lists (async) )
Simple SCREEN WIDGET (with nested dropdownbutton widgets):
class Screen01 extends StatefulWidget {
const Screen01({
Key? key,
}) : super(key: key);
#override
State<Screen01> createState() => _Screen01State();
}
class _Screen01State extends State<Screen01> {
#override
Widget build(BuildContext context) {
return Center(child:
Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children:
[ // each time you call the widget, you can provide a different future list
dropDownButtonWidgetModular(futureList: dropdownFutureList()),
SizedBox(width: 50,),
dropDownButtonWidgetModular(futureList: anotherFutureList())
]
),
);
}
}
DROPDOWNBUTTON WIDGET:
//========================================================
// WIDGET: FUTURE DROPDOWN MENU + FUTURE LIST
//--------------------------------------------------------
class dropDownButtonWidgetModular extends StatefulWidget {
final Future<List> futureList; // ===> ADDED FUTURE LIST
const dropDownButtonWidgetModular({ Key? key, required this.futureList}) : super(key: key); // ===> ADDED REQUIRED FUTURE LIST
#override
State<dropDownButtonWidgetModular> createState() => _dropDownButtonWidgetModularState(chosenFutureList: futureList);
}
// --------------------------------------------------------
class _dropDownButtonWidgetModularState extends State<dropDownButtonWidgetModular> {
Future<List> chosenFutureList; // ===> ADDED FUTURE LIST
String? defaultValue;
_dropDownButtonWidgetModularState({required this.chosenFutureList}); // ===> ADDED REQUIRED FUTURE LIST
#override
Widget build(BuildContext context) {
return Center(child:
FutureBuilder(future: chosenFutureList, // ===> ADDED FUTURE LIST
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if(snapshot.hasData){ List? futureDataList = snapshot.data;
futureDataList ??= ['Loading']; //if snapshot is null
return buildDropdownButton(dropdownList: futureDataList );
}else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}else {
return Center(child: CircularProgressIndicator());
}
}
)
);
}
//----------------------------------
// DROPDOWNBUTTON EXTRACTED METHOD
DropdownButton<Object> buildDropdownButton({required List dropdownList}) {
defaultValue ??= dropdownList.first; //DEFAULT SELECTED ITEM
return DropdownButton(
value: defaultValue,
onChanged: (value) => setState(() => defaultValue = value.toString()),
items: dropdownList.map((items) {
return DropdownMenuItem(value: items, child: Text(items));
}).toList(),
);
}
//----------------------------------
}
//=============================================
I commented some of the changes made from my original question
Related
I need a DropdownButton with items depending on another DropdownButton. Sounds a bit confusing but it isnt. Here is my code with comments at the important parts in order to understand my intention.
Parent
class Parent extends StatefulWidget {
const Parent({ Key? key }) : super(key: key);
#override
State<Parent> createState() => _ParentState();
}
class _ParentState extends State<Parent> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: SizedBox(
width: 500,
height: 500,
child: Column(
children: const [
// Main
DropDownWidget(collection: "MainCollection",),
// Depending
DropDownWidget(collection: ""), // Collection should equals value from Main DropDownWidget
],
),
),
);
}
}
Child
class DropDownWidget extends StatefulWidget {
final String collection;
const DropDownWidget({Key? key, required this.collection}) : super(key: key);
#override
State<DropDownWidget> createState() => _DropDownWidgetState();
}
class _DropDownWidgetState extends State<DropDownWidget> {
var selectedItem;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection(widget.collection)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.hasError) {
return const CircularProgressIndicator();
} else {
var length = snapshot.data?.docs.length;
List<DropdownMenuItem<String>> items = [];
for (int i = 0; i < length!; i++) {
DocumentSnapshot snap = snapshot.data!.docs[i];
items.add(DropdownMenuItem(
child: Text(snap.id),
value: snap.id,
));
}
return DropdownButtonFormField<String>(
onChanged: (value) {
setState(() {
selectedItem = value;
// ********************
// PASS value TO PARENT
// ********************
});
},
value: selectedItem,
items: items);
}
});
}
}
When the Main DropdownButton changes its value, it should pass that to my parent in order to change the focused collection of my depending DropdownButton. I already solved that problem by throwing all the code in one class buts that not the way I want to go.
So maybe you can help me out :)
Thanks
Create an argument ValueChanged<String> onSelectItem in your child. Call the method when the value changes.
Then in your parent, you provide a function that needs to be called when the value changes in your child.
I am new to flutter, so please excuse my experience.
I have 2 classes, both stateful widgets.
One class contains the tiles for a listview.
Each tile class has a checkbox with a state bool for alternating true or false.
The other class (main) contains the body for creating the listview.
What I'd like to do is retrieve the value for the checkbox in the main class, and then update a counter for how many checkbboxes from the listview tiles have been checked, once a checkbox value is updated. I am wondering what the best practices are for doing this.
Tile class
class ListTile extends StatefulWidget {
#override
_ListTileState createState() => _ListTileState();
}
class _ListTileState extends State<ListTile> {
#override
Widget build(BuildContext context) {
bool selected = false;
return Container(
child: Row(
children: [Checkbox(value: selected, onChanged: (v) {
// Do something here
})],
),
);
}
}
Main Class
class OtherClass extends StatefulWidget {
#override
_OtherClassState createState() => _OtherClassState();
}
class _OtherClassState extends State<OtherClass> {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text("Checkbox selected count <count here>"),
ListView.builder(itemBuilder: (context, index) {
// Do something to get the selected checkbox count from the listview
return ListTile();
}),
],
),
);
}
}
Hope this is you are waiting for
class OtherClass extends StatefulWidget {
#override
_OtherClassState createState() => _OtherClassState();
}
class _OtherClassState extends State<OtherClass> {
bool selected = false;
#override
void initState() {
super.initState();
}
var items = [
Animal("1", "Buffalo", false),
Animal("2", "Cow", false),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("title")),
body: Container(
child: ListView.builder(
itemCount: items.length,
shrinkWrap: true,
itemBuilder: (ctx, i) {
return Row(
children: [
Text(items[i].name),
ListTile(
id: items[i].id,
index: i,
)
],
);
}),
));
}
}
ListTileClass
class ListTile extends StatefulWidget {
final String? id;
final int? index;
final bool? isSelected;
const ListTile ({Key? key, this.id, this.index, this.isSelected})
: super(key: key);
#override
_ListTileState createState() => _ListTileState();
}
class _ListTileState extends State<ListTile> {
bool? selected = false;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
width: 20,
child: Checkbox(
value: selected,
onChanged: (bool? value) {
setState(() {
selected = value;
});
}));
}
}
I'd recommend using a design pattern such as BLoC or using the Provider package. I personally use the Provider Package. There are plenty of tutorials on youtube which can help get you started.
I have made the following reusable widget in Flutter. It is used to show a dropdown list of items taken from Cloud Firestore, and when a user selects an option it should be passed to the variable answer.
However when I use my widget Survey Answer the dropdown items show as expected, but I am unable to pass the selected option out from onChanged out of my widget - print(widget.answer); shows the right item in the Dart Console, the item selected from the dropdown menu, but when I use this widget on a different page and try to print out the value of the answer variable I just get null returned.
Can someone shed some light on this please?
Reusable widget:
class SurveyAnswer extends StatefulWidget {
final String collection1;
final String document;
final String collection2;
var answer;
SurveyAnswer(
{Key key,
#required this.collection1,
#required this.document,
#required this.collection2,
#required this.answer})
: super(key: key);
#override
_SurveyAnswerState createState() => _SurveyAnswerState();
}
class _SurveyAnswerState extends State<SurveyAnswer> {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection(widget.collection1)
.document(widget.document)
.collection(widget.collection2)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError)
return Center(child: Text("Error"));
else if (!snapshot.hasData)
return Center(child: Text("Loading..."));
else if (snapshot.data.documents.isEmpty)
return Center(child: Container(width: 0, height: 0));
else {
List<DropdownMenuItem> answerItems = [];
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot snap = snapshot.data.documents[i];
answerItems.add(
DropdownMenuItem(
child: Text(
snap.documentID,
style: TextStyle(color: Color(0xff303841)),
),
value: "${snap.documentID}",
),
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FormBuilderDropdown(
attribute: 'attribute',
hint: Text('Please choose an option'),
validators: [FormBuilderValidators.required()],
items: answerItems,
onChanged: (answerValue) {
setState(() {
widget.answer = answerValue;
print(widget.answer);
});
},
),
],
);
}
});
}
}
How the above is used in my app:
SurveyAnswer(
collection1: 'MySurveys',
document: 'FirestoreTestSurvey',
collection2: 'a01',
answer: 'ans01'),
In this example, I want to create a variable ans01 which returns the value selected in the dropdown menu.
The reason I have made a reusable widget for this is that each page has a lot of survey items and otherwise my code would be a mess.
Thank you.
you should write callback function
class SurveyAnswer extends StatefulWidget {
final String collection1;
final String document;
final String collection2;
final Function(String) onAnswer;
SurveyAnswer(
{Key key,
#required this.collection1,
#required this.document,
#required this.collection2,
#required this.onAnswer})
: super(key: key);
#override
_SurveyAnswerState createState() => _SurveyAnswerState();
}
Change your on change method
onChanged: (answerValue) {
widget.onAnswer(answerValue)
}
and use like this
SurveyAnswer(
collection1: 'MySurveys',
document: 'FirestoreTestSurvey',
collection2: 'a01',
onAnswer: (ans) {
print(ans)
})
I have question how to pass data between pages/screen in flutter without navigator and only using onChanged and streambuilder.
All I want is whenever user write in textfield on first widget, the second widget automatically refresh with the new data from first widget.
Here's my code for first.dart
import 'package:flutter/material.dart';
import 'second.dart';
class First extends StatefulWidget {
First({Key key}) : super(key: key);
#override
_FirstState createState() => _FirstState();
}
class _FirstState extends State<First> {
final TextEditingController _myTextController =
new TextEditingController(text: "");
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
title: Text("Passing Data"),
),
body: Column(
children: <Widget>[
Container(
height: 120.0,
child: Column(
children: <Widget>[
TextField(
controller: _myTextController,
onChanged: (String value) {
// refresh second with new data
},
)
]
)
),
Container(
height: 120.0,
child: Second(
myText: _myTextController.text,
),
)
],
),
);
}
}
and here's my second.dart as second widget to receive data from first widget.
import 'dart:async';
import 'package:flutter/material.dart';
import 'api_services.dart';
class Second extends StatefulWidget {
Second({Key key, #required this.myText}) : super(key: key);
final String myText;
#override
_SecondState createState() => _SecondState();
}
class _SecondState extends State<Second> {
StreamController _dataController;
loadPosts() async {
ApiServices.getDetailData(widget.myText).then((res) async {
_dataController.add(res);
return res;
});
}
#override
void initState() {
_dataController = new StreamController();
loadPosts();
super.initState();
print(widget.myText);
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _dataController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error);
}
if (snapshot.hasData) {
return Container();
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Row(
children: <Widget>[
Text("Please Write A Text"),
],
);
} else if (snapshot.connectionState != ConnectionState.active) {
return CircularProgressIndicator();
}
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return Text('No Data');
} else if(snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
return Text(widget.myText);
}
return null;
});
}
}
You have a couple of options. The two simplest are - passing the text editing controller itself through to the second widget, then listening to it and calling setState to change the text in the second widget.
Example
class Second extends StatefulWidget {
Second({Key key, #required this.textController}) : super(key: key);
final TextEditingController textController;
#override
_SecondState createState() => _SecondState();
}
class _SecondState extends State<Second> {
// made this private
String _myText;
#override
void initState() {
_myText = widget.textController.text
widget.textController.addListener(() {
setState((){_myText = widget.textController.text});
);
});
super.initState();
}
...
// then in your build method, put this in place of return Text(widget.myText);
return Text(_myText);
Option 2 is listening to the controller in your first widget and call setState in there. This will rebuild both the first and second widget though, and I think is not as performant as the first option.
Hope that helps
I'm trying to find the best way to show errors from a Change Notifier Model with Provider through a Snackbar.
Is there any built-in way or any advice you could help me with?
I found this way that is working but I don't know if it's correct.
Suppose I have a simple Page where I want to display a list of objects and a Model where I retrieve those objects from api. In case of error I notify an error String and I would like to display that error with a SnackBar.
page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Page extends StatefulWidget {
Page({Key key}) : super(key: key);
#override
_PageState createState() => _PageState();
}
class _PageState extends State< Page > {
#override
void initState(){
super.initState();
Provider.of<Model>(context, listen: false).load();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
Provider.of< Model >(context, listen: false).addListener(_listenForErrors);
}
#override
Widget build(BuildContext context){
super.build(context);
return Scaffold(
appBar: AppBar(),
body: Consumer<Model>(
builder: (context, model, child){
if(model.elements != null){
...list
}
else return LoadingWidget();
}
)
)
);
}
void _listenForErrors(){
final error = Provider.of<Model>(context, listen: false).error;
if (error != null) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
backgroundColor: Colors.red[600],
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.error),
Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(error) )),
],
),
),
);
}
}
#override
void dispose() {
Provider.of<PushNotificationModel>(context, listen: false).removeListener(_listenForErrors);
super.dispose();
}
}
page_model.dart
import 'package:flutter/foundation.dart';
class BrickModel extends ChangeNotifier {
List<String> _elements;
List<String> get elements => _elements;
String _error;
String get error => _error;
Future<void> load() async {
try{
final elements = await someApiCall();
_elements = [..._elements, ...elements];
}
catch(e) {
_error = e.toString();
}
finally {
notifyListeners();
}
}
}
Thank you
Edit 2022
I ported (and reworked) this package also for river pod if anyone is interested
https://pub.dev/packages/riverpod_messages/versions/1.0.0
EDIT 2020-06-05
I developed a slightly better approach to afford this kink of situations.
It can be found at This repo on github so you can see the implementation there, or use this package putting in your pubspec.yaml
provider_utilities:
git:
url: https://github.com/quantosapplications/flutter_provider_utilities.git
So when you need to present messages to the view you can:
extend your ChangeNotifier with MessageNotifierMixin that gives your ChangeNotifier two properties, error and info, and two methods, notifyError() and notifyInfo().
Wrap your Scaffold with a MessageListener that will present a Snackbar when it gets called notifyError() or NotifyInfo()
I'll give you an example:
ChangeNotifier
import 'package:flutter/material.dart';
import 'package:provider_utilities/provider_utilities.dart';
class MyNotifier extends ChangeNotifier with MessageNotifierMixin {
List<String> _properties = [];
List<String> get properties => _properties;
Future<void> load() async {
try {
/// Do some network calls or something else
await Future.delayed(Duration(seconds: 1), (){
_properties = ["Item 1", "Item 2", "Item 3"];
notifyInfo('Successfully called load() method');
});
}
catch(e) {
notifyError('Error calling load() method');
}
}
}
View
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_utilities/provider_utilities.dart';
import 'notifier.dart';
class View extends StatefulWidget {
View({Key key}) : super(key: key);
#override
_ViewState createState() => _ViewState();
}
class _ViewState extends State<View> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: MessageListener<MyNotifier>(
child: Selector<MyNotifier, List<String>>(
selector: (ctx, model) => model.properties,
builder: (ctx, properties, child) => ListView.builder(
itemCount: properties.length,
itemBuilder: (ctx, index) => ListTile(
title: Text(properties[index])
),
),
)
)
);
}
}
OLD ANSWER
thank you.
Maybe I found a simpler way to handle this, using the powerful property "child" of Consumer.
With a custom stateless widget (I called it ErrorListener but it can be changed :))
class ErrorListener<T extends ErrorNotifierMixin> extends StatelessWidget {
final Widget child;
const ErrorListener({Key key, #required this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<T>(
builder: (context, model, child){
//here we listen for errors
if (model.error != null) {
WidgetsBinding.instance.addPostFrameCallback((_){
_handleError(context, model); });
}
// here we return child!
return child;
},
child: child
);
}
// this method will be called anytime an error occurs
// it shows a snackbar but it could do anything you want
void _handleError(BuildContext context, T model) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
backgroundColor: Colors.red[600],
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.error),
Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(model.error) )),
],
),
),
);
// this will clear the error on model because it has been handled
model.clearError();
}
}
This widget must be put under a scaffold if you want to use a snackbar.
I use a mixin here to be sure that model has a error property and a clarError() method.
mixin ErrorNotifierMixin on ChangeNotifier {
String _error;
String get error => _error;
void notifyError(dynamic error) {
_error = error.toString();
notifyListeners();
}
void clearError() {
_error = null;
}
}
So for example we can use this way
class _PageState extends State<Page> {
// ...
#override
Widget build(BuildContext context) =>
ChangeNotifierProvider(
builder: (context) => MyModel(),
child: Scaffold(
body: ErrorListener<MyModel>(
child: MyBody()
)
)
);
}
You can create a custom StatelessWidget to launch the snackbar when the view model changes. For example:
class SnackBarLauncher extends StatelessWidget {
final String error;
const SnackBarLauncher(
{Key key, #required this.error})
: super(key: key);
#override
Widget build(BuildContext context) {
if (error != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _displaySnackBar(context, error: error));
}
// Placeholder container widget
return Container();
}
void _displaySnackBar(BuildContext context, {#required String error}) {
final snackBar = SnackBar(content: Text(error));
Scaffold.of(context).hideCurrentSnackBar();
Scaffold.of(context).showSnackBar(snackBar);
}
}
We can only display the snackbar once all widgets are built, that's why we have the WidgetsBinding.instance.addPostFrameCallback() call above.
Now we can add SnackBarLauncher to our screen:
class SomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Title',
),
),
body: Stack(
children: [
// Other widgets here...
Consumer<EmailLoginScreenModel>(
builder: (context, model, child) =>
SnackBarLauncher(error: model.error),
),
],
),
);
}
}