My use case: I show a search UI the user can pick an item with - when the user taps an item, something async happens and depending on the result, close (https://api.flutter.dev/flutter/material/SearchDelegate/close.html) is called with either the picked item or a null. In my snippet below the async something is simply asking the user for confirmation with an AlertDialog.
This works without an issue but I updated the lint rules recently, and turned this one on: https://dart-lang.github.io/linter/lints/use_build_context_synchronously.html. Now the linter complains about the BuildContext being used after an await in the call to close. Here is the snippet (full reproducible sample below, written with Flutter 2.10.4):
onTap: () async {
final confirmed = await _confirm(context, item) ?? false;
// Triggers 'Do not use BuildContexts across async gaps.'
// https://dart-lang.github.io/linter/lints/use_build_context_synchronously.html
close(context, confirmed ? item : null);
},
I guess this makes sense and could be dangerous in some scenarios, so I'd better fix it. My question is: how do I implement my use case the 'right' way? One solution that works is to extract suggestions to a separate StatefulWidget and guard the call with a isMounted check, but this has the following drawbacks:
It requires a separate StatefulWidget that I would otherwise not need (boilerplate I don't want).
I need to pass a callback to the widget to call close (it belongs to SearchDelegate but now it will be called by code in the widget).
As close requires a BuildContext, I either have to pass the one that the SearchDelegate has to the widget to use (yuck) or just use the one from my widget - in this case it works (as close just uses Navigator.of(context)), but what if it were necessary to pass exactly the one from SearchDelegate?
Full code:
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: _Home(),
);
}
}
class _Home extends StatefulWidget {
#override
State<_Home> createState() => _HomeState();
}
class _HomeState extends State<_Home> {
String? _picked;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
onPressed: () => _maybePick(context),
child: const Text('Maybe pick'),
),
Center(
child: Text(_picked != null ? 'Last pick: $_picked' : 'No pick'),
),
],
),
);
}
Future<void> _maybePick(BuildContext context) async {
final result = await showSearch<String?>(
context: context,
delegate: _PickerDelegate(),
);
if (result != null) {
setState(() {
_picked = result;
});
}
}
}
class _PickerDelegate extends SearchDelegate<String?> {
final _allItems = List.generate(200, (index) => 'Item $index');
#override
Widget buildLeading(BuildContext context) {
return IconButton(
onPressed: () => close(context, null),
icon: const BackButtonIcon(),
);
}
#override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
onPressed: () => query = '',
icon: const Icon(Icons.clear),
),
];
}
#override
Widget buildSuggestions(BuildContext context) {
final items = _allItems.where((element) => element.contains(query));
return ListView(
children: items.map((item) {
return ListTile(
title: Text(item),
onTap: () async {
final confirmed = await _confirm(context, item) ?? false;
// Triggers 'Do not use BuildContexts across async gaps.'
// https://dart-lang.github.io/linter/lints/use_build_context_synchronously.html
close(context, confirmed ? item : null);
},
);
}).toList(growable: false),
);
}
#override
Widget buildResults(BuildContext context) {
// Keep it simple for the snippet.
throw UnimplementedError('results are not supported');
}
Future<bool?> _confirm(BuildContext context, String item) async {
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
content: Text("Pick '$item'?"),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Yes'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('No'),
),
],
);
},
);
}
}
Related
I'm new utilising Flutter, and I'd like to know how to display a text box based on what the user chooses from a drop-down menu, for example, if the drop-down menu has options of burgers, vegetables, and fruits.
So, once the user chooses burger, the app will provide a text box with the message, "Do not eat that much." then the user will click okay and will close the text box
Thank you and I hope you can understand.
You can follow this snippet But must check DropdownButton StatefulWidget showDialog
void main(List<String> args) {
runApp(
MaterialApp(
home: DFR(),
),
);
}
class DFR extends StatefulWidget {
DFR({Key? key}) : super(key: key);
#override
State<DFR> createState() => _DFRState();
}
class _DFRState extends State<DFR> {
String? selected;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
DropdownButton<String?>(
items: ["burgers", "vegetables", "fruits"]
.map(
(e) => DropdownMenuItem<String>(
child: Text(e),
value: e,
),
)
.toList(),
onChanged: (v) async {
if (v == "burgers") {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text("Do not eat that much."),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("Ok"),
)
],
);
},
);
}
selected = v;
setState(() {});
},
),
Text(selected ?? ""),
],
),
);
}
}
I have create a Alert Dialog for OTP Verification after verifying OTP I close it and then I had created a another dialog which is for data processing... and then I close it.
Result:-
First OTP Dialog closed after OTP verification by calling Navigator.of(context).pop(); and then second dialog just pops up but It does not closed after calling Navigator.of(context).pop();
What I want to do:
Close OTP Dialog after verifying OTP (Works)
Open Progress dialog (Works)
Close it after uploading profile in firebase storage (Does not Works)
Please help me solve this issue.
Thanks in Advance !
You probably forgetting await somewhere in your code.
Try this,
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController _otpCtrl = TextEditingController();
void dispose() {
_otpCtrl.dispose();
super.dispose();
}
Future<void> _verifyOTP() async {
final String otp = await _inputOtp();
String otpValidationError;
if (otp != null) otpValidationError = await _sendOtpVerifyRequest();
print(otpValidationError);
}
Future<String> _sendOtpVerifyRequest() async {
showDialog(
context: context,
builder: (context) {
return Center(child: CircularProgressIndicator());
},
);
await Future.delayed(Duration(seconds: 2)); //TODO: Do post request here
Navigator.pop(context);
return null;
}
Future<String> _inputOtp() async {
final flag = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text("Enter OTP"),
content: TextField(
controller: _otpCtrl,
decoration: InputDecoration(
hintText: "x x x x x x",
),
),
actions: <Widget>[
FlatButton(
child: Text("Cancel"),
onPressed: () {
Navigator.pop(context, false);
},
),
FlatButton(
child: Text("Confirm"),
onPressed: () {
Navigator.pop(context, true);
},
),
],
);
},
);
if (flag == true)
return _otpCtrl.text;
else
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: _verifyOTP,
child: Text("Click Here"),
),
),
);
}
}
I want to use showSearch to get the search text( or query) from the user. I also want to show the recent searches as suggestions and filter search history based on the text entered.
So how do I achieve this?
custom_search_delgates.dart
import 'package:flutter/material.dart';
typedef OnSearchChanged = Future<List<String>> Function(String);
class SearchWithSuggestionDelegate extends SearchDelegate<String> {
///[onSearchChanged] gets the [query] as an argument. Then this callback
///should process [query] then return an [List<String>] as suggestions.
///Since its returns a [Future] you get suggestions from server too.
final OnSearchChanged onSearchChanged;
///This [_oldFilters] used to store the previous suggestions. While waiting
///for [onSearchChanged] to completed, [_oldFilters] are displayed.
List<String> _oldFilters = const [];
SearchWithSuggestionDelegate({String searchFieldLabel, this.onSearchChanged})
: super(searchFieldLabel: searchFieldLabel);
///
#override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
);
}
#override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: Icon(Icons.clear),
onPressed: () => query = "",
),
];
}
///OnSubmit in the keyboard, returns the [query]
#override
void showResults(BuildContext context) {
close(context, query);
}
///Since [showResults] is overridden we can don't have to build the results.
#override
Widget buildResults(BuildContext context) => null;
#override
Widget buildSuggestions(BuildContext context) {
return FutureBuilder<List<String>>(
future: onSearchChanged != null ? onSearchChanged(query) : null,
builder: (context, snapshot) {
if (snapshot.hasData) _oldFilters = snapshot.data;
return ListView.builder(
itemCount: _oldFilters.length,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.restore),
title: Text("${_oldFilters[index]}"),
onTap: () => close(context, _oldFilters[index]),
);
},
);
},
);
}
}
Usage:
import 'package:flutter/material.dart';
import 'package:flutter_app/custom_search_delgates.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(MaterialApp(home: Home()));
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
Future<void> _showSearch() async {
final searchText = await showSearch<String>(
context: context,
delegate: SearchWithSuggestionDelegate(
onSearchChanged: _getRecentSearchesLike,
),
);
//Save the searchText to SharedPref so that next time you can use them as recent searches.
await _saveToRecentSearches(searchText);
//Do something with searchText. Note: This is not a result.
}
Future<List<String>> _getRecentSearchesLike(String query) async {
final pref = await SharedPreferences.getInstance();
final allSearches = pref.getStringList("recentSearches");
return allSearches.where((search) => search.startsWith(query)).toList();
}
Future<void> _saveToRecentSearches(String searchText) async {
if (searchText == null) return; //Should not be null
final pref = await SharedPreferences.getInstance();
//Use `Set` to avoid duplication of recentSearches
Set<String> allSearches =
pref.getStringList("recentSearches")?.toSet() ?? {};
//Place it at first in the set
allSearches = {searchText, ...allSearches};
pref.setStringList("recentSearches", allSearches.toList());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Search Demo"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: _showSearch,
),
],
),
);
}
}
import 'package:flutter/material.dart';
class Searching extends SearchDelegate {
#override
Widget? buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => close(context, null),
);
}
#override
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
icon: const Icon(Icons.clear),
// color: Colors.grey[800],
onPressed: () {
if (query.isEmpty) {
close(context, null);
} else {
query = '';
}
},
),
];
}
#override
Widget buildResults(BuildContext context) => SearchResult(query: query);
// search history
#override
Widget buildSuggestions(BuildContext context) {
List suggestions = localUser.searches!.where((search) {
return search.toLowerCase().contains(query.toLowerCase());
}).toList();
return ListView.builder(
itemCount: suggestions.length,
itemBuilder: (context, index) {
final suggestion = suggestions[index];
return ListTile(
leading: Icon(Icons.history, size: 22),
title: Text(suggestion, style: TextStyle(fontSize: 16)),
onTap: () {
query = suggestion;
showResults(context);
},
trailing: IconButton(
icon: Icon(Icons.close),
iconSize: 15,
onPressed: () {
localUser.removeSearched(suggestion);
UserPreferences.setUser(localUser);
buildSuggestions(context);
},
),
);
},
);
}
}
There is no clear answer on how to implement a checkbox tile in a dialog and set the state to work.
A print statement is working in setting the state of the checkbox is not changing, but other statements are working. Where can I find the answer?
I am using a dialog with multiple check boxes for multi select. Is there another of implementing multiselect in Flutter?
child: TextFormField(
decoration: InputDecoration(
labelText: 'Team Leader',
labelStyle: TextStyle(color: Colors.black)),
controller: teamLeaderController,
enabled: false,
style: TextStyle(color: Colors.black),
),
onTap: () {
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return CheckBoxDialog(context, teamLeader,
"Choose Team Leader", teamLeaderController, onSubmit);
});
}),
class CheckBoxState extends State<CheckBoxDialog> {
BuildContext context;
List<String> places;
String title;
TextEditingController con;
bool state;
CheckBoxState(this.context, this.places, this.title, this.con);
#override
void initState() {
super.initState();
state = false;
}
#override
Widget build(BuildContext context) {
return new AlertDialog(
title: new Text(title),
content:
Column(children: getMultiSelectOption(context, places, con, state)),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
FlatButton(
child: Text('Ok'),
onPressed: () {
widget.onSubmit("");
Navigator.of(context).pop();
})
],
);
}
List<Widget> getMultiSelectOption(BuildContext context, List<String> places,
TextEditingController con, bool state) {
List<Widget> options = [];
List<String> selectedList = [];
for (int i = 0; i < places.length; i++) {
options.add(CheckboxListTile(
title: Text(places[i]),
value: selectedList.contains(places[i]),
onChanged: (bool value) {
print("on change: $value title: ${places[i]}");
setState(() {
if (value) {
selectedList.add(places[i]);
} else {
selectedList.remove(places[i]);
}
print("contains: ${selectedList.contains(places[i])}");
print("status: $value");
});
}));
}
return options;
}
}
Suppose you have a Dialog with some Widgets such as RadioListTile, DropdowButton… or anything that might need to be updated WHILE the dialog remains visible, how to do it?
Look at this example here.
https://www.didierboelens.com/2018/05/hint-5-how-to-refresh-the-content-of-a-dialog-via-setstate/
Suppose you have a Dialog with some Widgets such as RadioListTile, DropdowButton… or anything that might need to be updated WHILE the dialog remains visible, how to do it?
Difficulty: Beginner
Background
Lately I had to display a Dialog to let the user select an item from a list and I wanted to display a list of RadioListTile.
I had no problem to show the Dialog and display the list, via the following source code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class Sample extends StatefulWidget {
#override
_SampleState createState() => new _SampleState();
}
class _SampleState extends State<Sample> {
List<String> countries = <String>['Belgium','France','Italy','Germany','Spain','Portugal'];
int _selectedCountryIndex = 0;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){_showDialog();});
}
_buildList(){
if (countries.length == 0){
return new Container();
}
return new Column(
children: new List<RadioListTile<int>>.generate(
countries.length,
(int index){
return new RadioListTile<int>(
value: index,
groupValue: _selectedCountryIndex,
title: new Text(countries[index]),
onChanged: (int value) {
setState((){
_selectedCountryIndex = value;
});
},
);
}
)
);
}
_showDialog() async{
await showDialog<String>(
context: context,
builder: (BuildContext context){
return new CupertinoAlertDialog(
title: new Text('Please select'),
actions: <Widget>[
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: (){Navigator.of(context).pop('Cancel');},
child: new Text('Cancel'),
),
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: (){Navigator.of(context).pop('Accept');},
child: new Text('Accept'),
),
],
content: new SingleChildScrollView(
child: new Material(
child: _buildList(),
),
),
);
},
barrierDismissible: false,
);
}
#override
Widget build(BuildContext context) {
return new Container();
}
}
I was surprised to see that despite the setState in lines #34-36, the selected RadioListTile was not refreshed when the user tapped one of the items.
Explanation
After some investigation, I realized that the setState() refers to the stateful widget in which the setState is invoked. In this example, any call to the setState() rebuilds the view of the Sample Widget, and not the one of the content of the dialog. Therefore, how to do?
Solution
A very simple solution is to create another stateful widget that renders the content of the dialog. Then, any invocation of the setState will rebuild the content of the dialog.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class Sample extends StatefulWidget {
#override
_SampleState createState() => new _SampleState();
}
class _SampleState extends State<Sample> {
List<String> countries = <String>['Belgium','France','Italy','Germany','Spain','Portugal'];
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){_showDialog();});
}
_showDialog() async{
await showDialog<String>(
context: context,
builder: (BuildContext context){
return new CupertinoAlertDialog(
title: new Text('Please select'),
actions: <Widget>[
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: (){Navigator.of(context).pop('Cancel');},
child: new Text('Cancel'),
),
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: (){Navigator.of(context).pop('Accept');},
child: new Text('Accept'),
),
],
content: new SingleChildScrollView(
child: new Material(
child: new MyDialogContent(countries: countries),
),
),
);
},
barrierDismissible: false,
);
}
#override
Widget build(BuildContext context) {
return new Container();
}
}
class MyDialogContent extends StatefulWidget {
MyDialogContent({
Key key,
this.countries,
}): super(key: key);
final List<String> countries;
#override
_MyDialogContentState createState() => new _MyDialogContentState();
}
class _MyDialogContentState extends State<MyDialogContent> {
int _selectedIndex = 0;
#override
void initState(){
super.initState();
}
_getContent(){
if (widget.countries.length == 0){
return new Container();
}
return new Column(
children: new List<RadioListTile<int>>.generate(
widget.countries.length,
(int index){
return new RadioListTile<int>(
value: index,
groupValue: _selectedIndex,
title: new Text(widget.countries[index]),
onChanged: (int value) {
setState((){
_selectedIndex = value;
});
},
);
}
)
);
}
#override
Widget build(BuildContext context) {
return _getContent();
}
}
I am searching for a widget in flutter that is equal to
<select multiple=""></select>
in flutter.
An example implementation (for the web) is MaterializeCSS Select Multiple
As seen above I should be able to provide a list of items (with some of them preselected) and at the end retrieve a list of selected items or a map or something else.
An example implementation or a link to a documentation is very appreciated.
I don't think that a widget like that currently exists in Flutter, but you can build one yourself.
On mobile phones with limited screen space it would probably make sense to display a dialog with a submit button, like this native Android dialog.
Here is a rough sketch how to implement such a dialog in less than 100 lines of code:
class MultiSelectDialogItem<V> {
const MultiSelectDialogItem(this.value, this.label);
final V value;
final String label;
}
class MultiSelectDialog<V> extends StatefulWidget {
MultiSelectDialog({Key key, this.items, this.initialSelectedValues}) : super(key: key);
final List<MultiSelectDialogItem<V>> items;
final Set<V> initialSelectedValues;
#override
State<StatefulWidget> createState() => _MultiSelectDialogState<V>();
}
class _MultiSelectDialogState<V> extends State<MultiSelectDialog<V>> {
final _selectedValues = Set<V>();
void initState() {
super.initState();
if (widget.initialSelectedValues != null) {
_selectedValues.addAll(widget.initialSelectedValues);
}
}
void _onItemCheckedChange(V itemValue, bool checked) {
setState(() {
if (checked) {
_selectedValues.add(itemValue);
} else {
_selectedValues.remove(itemValue);
}
});
}
void _onCancelTap() {
Navigator.pop(context);
}
void _onSubmitTap() {
Navigator.pop(context, _selectedValues);
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Select animals'),
contentPadding: EdgeInsets.only(top: 12.0),
content: SingleChildScrollView(
child: ListTileTheme(
contentPadding: EdgeInsets.fromLTRB(14.0, 0.0, 24.0, 0.0),
child: ListBody(
children: widget.items.map(_buildItem).toList(),
),
),
),
actions: <Widget>[
FlatButton(
child: Text('CANCEL'),
onPressed: _onCancelTap,
),
FlatButton(
child: Text('OK'),
onPressed: _onSubmitTap,
)
],
);
}
Widget _buildItem(MultiSelectDialogItem<V> item) {
final checked = _selectedValues.contains(item.value);
return CheckboxListTile(
value: checked,
title: Text(item.label),
controlAffinity: ListTileControlAffinity.leading,
onChanged: (checked) => _onItemCheckedChange(item.value, checked),
);
}
}
You can use it like this:
void _showMultiSelect(BuildContext context) async {
final items = <MultiSelectDialogItem<int>>[
MultiSelectDialogItem(1, 'Dog'),
MultiSelectDialogItem(2, 'Cat'),
MultiSelectDialogItem(3, 'Mouse'),
];
final selectedValues = await showDialog<Set<int>>(
context: context,
builder: (BuildContext context) {
return MultiSelectDialog(
items: items,
initialSelectedValues: [1, 3].toSet(),
);
},
);
print(selectedValues);
}
Is this what you want?
In case you need a short and ready to use code, follow this article
import 'package:flutter/material.dart';
import 'package:multiple_selection_dialogue_app/widgets/multi_select_dialog.dart';
/// A demo page that displays an [ElevatedButton]
class DemoPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
/// Stores the selected flavours
List<String> flavours = [];
return ElevatedButton(
child: Text('Flavours'),
onPressed: () async {
flavours = await showDialog<List<String>>(
context: context,
builder: (_) => MultiSelectDialog(
question: Text('Select Your Flavours'),
answers: [
'Chocolate',
'Caramel',
'Vanilla',
'Peanut Butter'
])) ??
[];
print(flavours);
// Logic to save selected flavours in the database
});
}
}
import 'package:flutter/material.dart';
/// A Custom Dialog that displays a single question & list of answers.
class MultiSelectDialog extends StatelessWidget {
/// List to display the answer.
final List<String> answers;
/// Widget to display the question.
final Widget question;
/// List to hold the selected answer
/// i.e. ['a'] or ['a','b'] or ['a','b','c'] etc.
final List<String> selectedItems = [];
/// Map that holds selected option with a boolean value
/// i.e. { 'a' : false}.
static Map<String, bool> mappedItem;
MultiSelectDialog({this.answers, this.question});
/// Function that converts the list answer to a map.
Map<String, bool> initMap() {
return mappedItem = Map.fromIterable(answers,
key: (k) => k.toString(),
value: (v) {
if (v != true && v != false)
return false;
else
return v as bool;
});
}
#override
Widget build(BuildContext context) {
if (mappedItem == null) {
initMap();
}
return SimpleDialog(
title: question,
children: [
...mappedItem.keys.map((String key) {
return StatefulBuilder(
builder: (_, StateSetter setState) => CheckboxListTile(
title: Text(key), // Displays the option
value: mappedItem[key], // Displays checked or unchecked value
controlAffinity: ListTileControlAffinity.platform,
onChanged: (value) => setState(() => mappedItem[key] = value)),
);
}).toList(),
Align(
alignment: Alignment.center,
child: ElevatedButton(
style: ButtonStyle(visualDensity: VisualDensity.comfortable),
child: Text('Submit'),
onPressed: () {
// Clear the list
selectedItems.clear();
// Traverse each map entry
mappedItem.forEach((key, value) {
if (value == true) {
selectedItems.add(key);
}
});
// Close the Dialog & return selectedItems
Navigator.pop(context, selectedItems);
}))
],
);
}
}
import 'package:flutter/material.dart';
import 'package:multiple_selection_dialogue_app/pages/demo_page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: DemoPage(),
),
),
);
}
}