Flutter Provider value is null - flutter

I'm trying to pass an index from a widget to another through a provider but the value returns by the provider context seems empty.
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create:(ctx)=> ProvideIDX(),
child: Scaffold(
body: Container(
child: Stack(
children: <Widget>[
Maposm(lieu), //, idcarousel), // Pass index to this class
Align(
alignment: Alignment.bottomCenter,
child: CarouselSlider.builder(
itemCount: lieu.length,
itemBuilder: (BuildContext ctx, index) {
return Container(
color: Colors.yellow, child:Text(lieu[index].name));
},
options: CarouselOptions(
enableInfiniteScroll: true,
scrollDirection: Axis.horizontal,
onPageChanged: (index, reason) {
setState(() {
Index(id: index);
print(index);
});
},
)),
),
],
),
)));
}
}
class Places {
String name;
LatLng coordxy;
Places({this.name, this.coordxy});
}
class Index {
int id;
Index({this.id});
}
class ProvideIDX with ChangeNotifier{
Index _selectedID;
set selectedID(id){
this._selectedID = id;
notifyListeners();
}
get selectedID=>this._selectedID;
}
And the provider is called inside Maposm class like this :
#override
Widget build(BuildContext context) {
final _passIDX= Provider.of<ProvideIDX>(context);
final id = _passIDX.selectedID;
print(id); // show null
and the result I got is "null", which of course is not the value I'd like to use ^^
edit: Even if the code is reduced it appears to be too long for the post. I uploaded the two classes and the yaml on Gdrive

Instead of:
final _passIDX= Provider.of<ProvideIDX>(context);
you should watch your provider:
final _passIDX = context.watch<ProvideIDX>();
Here is a Minimal Working Example:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (ctx) => ProvideIDX(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
),
);
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _Child(),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
Provider.of<ProvideIDX>(context, listen: false).increment(),
child: Icon(Icons.add),
),
);
}
}
class _Child extends StatelessWidget {
#override
Widget build(BuildContext context) {
final _passIDX = context.watch<ProvideIDX>();
return Text(
_passIDX.selectedID.toString(),
style: TextStyle(fontSize: 96.0),
);
}
}
class Places {
String name;
Offset coordxy;
Places({this.name, this.coordxy});
}
class Index {
int id;
Index({this.id});
increment() => id++;
toString() => id.toString();
}
class ProvideIDX with ChangeNotifier {
Index _selectedID = Index(id: 0);
get selectedID => _selectedID;
set selectedID(id) {
_selectedID = id;
notifyListeners();
}
void increment() {
_selectedID.increment();
notifyListeners();
}
}

Related

How to automatically scroll through all the ListTiles in the Listview.seperated in Flutter?

Scroll automatically (without any user interaction) through all the ListTiles in the Listview using a Timer in flutter. The below method makes only one ListTile to animate but I want to animate all the ListTiles from top to bottom one by one and again from bottom to top one by one.
The below is the Listview:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: FutureBuilder(
future: fetchNews(),
builder: (context, snap) {
if (snap.hasData) {
news = snap.data;
return ListView.separated(
//controller: _controller,
scrollDirection: scrollDirection,
controller: controller,
itemBuilder: (context, i) {
final NewsModel _item = news[i];
return AutoScrollTag(
key: ValueKey(i),
controller: controller,
index: i,
child: ListTile(
title: Text('${_item.title}'),
subtitle: Text(
'${_item.description}',
// maxLines: 1,
//overflow: TextOverflow.ellipsis,
),
),
);
},
separatorBuilder: (context, i) => Divider(),
itemCount: news.length,
);
} else if (snap.hasError) {
return Center(
child: Text(snap.error.toString()),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
),
);
}
}
This is the automatic scrolling i have tried:
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 2), (Timer t) async {
await controller.scrollToIndex(1,
preferPosition: AutoScrollPosition.begin);
});
Here is a solution assuming that all your items in the ListView have the same itemExtent.
In this solution, I highlight the current Item as selected. You could also want to stop autoscrolling as soon as you reach the bottom of the list.
Full source code
import 'dart:async';
import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part '66455867.auto_scroll.freezed.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
Future<List<News>> _fetchNews() async => dummyData;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('News')),
body: FutureBuilder(
future: _fetchNews(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return NewsList(newsList: snapshot.data);
} else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
} else {
return Center(child: CircularProgressIndicator());
}
},
),
);
}
}
class NewsList extends StatefulWidget {
final List<News> newsList;
const NewsList({
Key key,
this.newsList,
}) : super(key: key);
#override
_NewsListState createState() => _NewsListState();
}
class _NewsListState extends State<NewsList> {
ScrollController _scrollController = ScrollController();
Timer _timer;
double _itemExtent = 100.0;
Duration _scrollDuration = Duration(milliseconds: 300);
Curve _scrollCurve = Curves.easeInOut;
int _autoScrollIncrement = 1;
int _currentScrollIndex = 0;
#override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 2), (_) async {
_autoScrollIncrement = _currentScrollIndex == 0
? 1
: _currentScrollIndex == widget.newsList.length - 1
? -1
: _autoScrollIncrement;
_currentScrollIndex += _autoScrollIncrement;
_animateToIndex(_currentScrollIndex);
setState(() {});
});
}
void _animateToIndex(int index) {
_scrollController.animateTo(
index * _itemExtent,
duration: _scrollDuration,
curve: _scrollCurve,
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ListView(
controller: _scrollController,
itemExtent: _itemExtent,
children: widget.newsList
.map((news) => ListTile(
title: Text(news.title),
subtitle: Text(
news.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
selected: widget.newsList[_currentScrollIndex].id == news.id,
selectedTileColor: Colors.amber.shade100,
))
.toList(),
);
}
}
#freezed
abstract class News with _$News {
const factory News({int id, String title, String description}) = _News;
}
final faker = Faker();
final dummyData = List.generate(
10,
(index) => News(
id: faker.randomGenerator.integer(99999999),
title: faker.sport.name(),
description: faker.lorem.sentence(),
),
);
Packages used in the solution:
freeze for the News Domain Class
build_runner to generate the freezed code
faker to generate the list of random news
UPDATE : Scroll only once
To stop the autoscrolling at the bottom of the listview, you just need to modify the initState method:
int _currentScrollIndex;
News _selectedNews;
#override
void initState() {
super.initState();
_currentScrollIndex = -1;
_timer = Timer.periodic(Duration(seconds: 2), (_) async {
setState(() {
if (_currentScrollIndex == widget.newsList.length - 1) {
_timer.cancel();
_selectedNews = null;
} else {
_selectedNews = widget.newsList[++_currentScrollIndex];
_animateToIndex(_currentScrollIndex);
}
});
});
}
We don't need the scroll direction defined as _autoScrollIncrement. However, I would introduce a new _selectedNews to easily unselect the last News item when we arrive at the bottom of the list. The selected flag of our ListTile would then become:
#override
Widget build(BuildContext context) {
return ListView(
[...]
children: widget.newsList
.map((news) => ListTile(
[...]
selected: _selectedNews?.id == news.id,
[...]
))
.toList(),
);
}

How to filter list when use FutureBuilder?

I use FutureBuilder in IndexedListView to show a phonebook list,
Widget _buildBody(context) {
String url = api_url_phonebook;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
children: [
TextField(
onTap: () => onTextChanged(_controller.text),
...
),
Expanded(
child: IndexedListView(
100.0,
_getData(context, url),
...
),
),
],
),
);
}
Future<Either<ResponseError, List<dynamic>>> _getData(context, url) {
final result = RequestApi(url).fetchList();
return result;
}
onSearchTextChanged(String text) {
...
}
Now I want filter this phonebook when I input text in TextField, what can I do?
I suggest you to use the Delegate pattern to do that.
Make a class like the following:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mywallet/model/user_account.dart';
import 'package:mywallet/ui/account_item.dart';
class AccountListSearch extends SearchDelegate {
final List<UserAccount> newItems;
String selectedResult = '';
AccountListSearch(this.newItems);
bool _match(UserAccount account, String text) {
return account.name.toLowerCase().contains(text.toLowerCase()) ||
account.user.toLowerCase().contains(text.toLowerCase());
}
#override
List<Widget> buildActions(BuildContext context) => [
IconButton(
icon: Icon(Icons.close),
onPressed: () {
query = '';
},
),
];
#override
Widget buildLeading(BuildContext context) => IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
);
#override
Widget buildResults(BuildContext context) {
return Container(
child: Center(
child: Text(selectedResult),
));
}
#override
Widget buildSuggestions(BuildContext context) {
var suggestionList = <UserAccount>[];
query.isEmpty
? suggestionList = newItems
: suggestionList.addAll(
newItems.where((item) {
return _match(item, query);
}),
);
return ListView.builder(
itemCount: suggestionList.length,
itemBuilder: (context, index) => AccountItem(
account: suggestionList[index],
),
);
}
}
Copy and paste the AccountListSearch class, only change the UserAccount class with your data class ( PhoneBook Item), and change the AccountItem stateless widget with your Widget used to display your PhoneBook item ( something like class PhoneBookItemWidget extends StatelessWidget......)
Then in the main page like this class:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<AccountCubit, AccountState>(
builder: (_, state) {
var currentState = state;
if (currentState is InitializedState) {
var accounts = currentState.userAccounts;
return Scaffold(
appBar: AppBar(
title: Text('All Accounts'),
actions: [
IconButton(
onPressed: () {
showSearch(
context: context,
delegate: AccountListSearch(accounts));
},
icon: Icon(
Icons.search,
),
)
],
),
drawer: AppDrawer(),
body:
ListView.builder(
itemCount: accounts.length,
itemBuilder: (context, counter) => AccountItem(
account: accounts[counter],
)
....
}
As you can see, the 'magic' is in the method :
showSearch( context: context, delegate: AccountListSearch(accounts));
The method showSearch(...) is inherited from StatelessWidget and make all the work for you.
I used the Bloc pattern, but it is not important right now, only to tell where to find the list of items, but it is not directly related to your question.

Flutter Navigator 2.0 pages using with showGeneralDialog to handle clearing pages

I am trying to develop a logout feature with Navigator 2.0 pages & showGeneralDialog. A dialog (created by showGeneralDialog) will handle the logout after a button in the dialog is clicked by the user and the dialog is closed. However, an error is thrown in _RouteEntry.markForComplete (the error is thrown by the assert statement).
I tried to create a dummy project with some simple code:
import 'package:flutter/material.dart';
void main() {
runApp(BooksApp());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => _BooksAppState();
}
class _BooksAppState extends State<BooksApp> {
BookRouterDelegate _routerDelegate = BookRouterDelegate();
BookRouteInformationParser _routeInformationParser =
BookRouteInformationParser();
PlatformRouteInformationProvider _platformRouteInformationProvider =
PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(location: '/'));
#override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
routeInformationProvider: _platformRouteInformationProvider,
);
}
}
class BookRouteInformationParser extends RouteInformationParser<RoutePath> {
#override
Future<RoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
if (uri.pathSegments.length >= 2) {
var remaining = uri.pathSegments[1];
return RoutePath.details(int.tryParse(remaining));
} else if (uri.pathSegments.length > 0 && uri.pathSegments[0] == 'book') {
return RoutePath.home();
} else
return RoutePath.login();
}
#override
RouteInformation restoreRouteInformation(RoutePath path) {
if (path.isLogin) return RouteInformation(location: '/');
if (path.isHomePage) {
return RouteInformation(location: '/book');
}
if (path.isDetailsPage) {
return RouteInformation(location: '/book/${path.id}');
}
return null;
}
}
class BookRouterDelegate extends RouterDelegate<RoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
Book _selectedBook;
List<Book> books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
bool _showLogin = false;
RoutePath get currentConfiguration => _showLogin
? RoutePath.login()
: _selectedBook == null
? RoutePath.home()
: RoutePath.details(books.indexOf(_selectedBook));
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
if (currentConfiguration.isLogin)
MaterialPage(
key: ValueKey('LoginPage'),
child: Scaffold(
appBar: AppBar(
title: Text('Login'),
),
),
),
if (!currentConfiguration.isLogin)
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
if (_selectedBook != null) BookDetailsPage(book: _selectedBook)
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
_selectedBook = null;
notifyListeners();
return true;
},
);
}
#override
Future<void> setNewRoutePath(RoutePath path) async {
if (path.isDetailsPage) {
_selectedBook = books[path.id];
}
}
void _handleBookTapped(Book book) {
_selectedBook = book;
notifyListeners();
}
void handleLogout() {
_showLogin = true;
_selectedBook = null;
notifyListeners();
}
}
class BookDetailsPage extends Page {
final Book book;
BookDetailsPage({
this.book,
}) : super(key: ValueKey(book));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this,
builder: (BuildContext context) {
return BookDetailsScreen(book: book);
},
);
}
}
class RoutePath {
final bool isLogin;
final int id;
RoutePath.login()
: id = null,
isLogin = true;
RoutePath.home()
: id = null,
isLogin = false;
RoutePath.details(this.id) : isLogin = false;
bool get isHomePage => id == null;
bool get isDetailsPage => id != null;
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book> onTapped;
BooksListScreen({
#required this.books,
#required this.onTapped,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => onTapped(book),
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen({
#required this.book,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () async {
final response = await showGeneralDialog<bool>(
context: context,
useRootNavigator: true,
barrierDismissible: false,
transitionDuration: const Duration(milliseconds: 300),
transitionBuilder: (context, animation, __, child) {
return ScaleTransition(
scale: animation,
child: child,
);
},
pageBuilder: (context, _, __) => _CustomDialog(),
);
if (response == null) return;
if (response) {
// await Future.delayed(const Duration(milliseconds: 300));
context
.findAncestorStateOfType<_BooksAppState>()
._routerDelegate
.handleLogout();
}
},
),
],
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (book != null) ...[
ListTile(
title: Text(book.title),
subtitle: Text(book.author,
style: Theme.of(context).textTheme.subtitle1),
),
],
],
),
),
);
}
}
class _CustomDialog extends StatelessWidget {
_CustomDialog({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Clear all pages?'),
RaisedButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop(true);
},
),
],
),
),
);
}
}
However, in this dummy project, the error is randomly thrown, sometimes it is thrown at NavigatorState.finalizeRoute (in this assert statement: assert(_history.where(_RouteEntry.isRoutePredicate(route)).length == 1);), and sometimes it is thrown at exactly the same as the one I mention which is _RouteEntry.markForComplete.
The workaround I can find currently is to delay until the dialog pop transition is completed (delay before context.findAncestorStateOfType<_BooksAppState>()._routerDelegate.handleLogout();).
However, I would like to know the proper fix for this instead of waiting for it to completely pop because I am unsure if there is any hidden trouble I might face.
The issue seems to be fixed on the issue ticket you've filed. No workarounds needed. Just update the Flutter SDK version to fix the issue.

How to do incrementing and decrementing of a particular product in flutter

I'm working on a food delivery app I've tried to make an increment decrement system of a particular product in a list. At the start it works i.e the counter increases but a bit after the counter automatically return to 0 without any button press. I don't know why it's happening
Below is the code I'm trying
This is the class
class ItemData {
final String itemName;
final String itemPrice;
final String image;
int counter = 0;
bool isAdded = false;
ItemData({this.itemName, this.itemPrice, this.image});
}
This is the function for getting data from url
Future<List<ItemData>> _getProducts() async {
var data = await http
.get("https://orangecitycafe.in/app_configs/products_display.php");
var jsonData = json.decode(data.body);
List<ItemData> details = [];
for (var p in jsonData) {
ItemData detail = ItemData(
itemName: p["product_name"],
itemPrice: p["product_price"],
image: p["product_image"]);
details.add(detail);
}
return details;
}
This is the code for fetched products inside future builder
Widget _myCart() {
return FutureBuilder(
future: _getProfile(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(snapshot.data[index].itemName),
leading: Image.network("https://www.orangecitycafe.in/" +
snapshot.data[index].image),
trailing: snapshot.data[index].isAdded
? Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
setState(() {
if (snapshot.data[index].counter > 0) {
snapshot.data[index].counter--;
}
});
},
color: Colors.green,
),
Text(snapshot.data[index].counter.toString()),
IconButton(
icon: Icon(Icons.add),
color: Colors.green,
onPressed: () {
setState(() {
snapshot.data[index].counter++;
});
},
),
],
)
: RaisedButton(
onPressed: (){
setState(() {
snapshot.data[index].isAdded = true;
});
},
child: Text("Add"),
),
);
},
);
} else {
return Container();
}
},
);
}
The rest is working but only when I increase the counter it increases and after sometime it automatically returns to 0
You can copy paste run full code below
You can use the following way to use Future in FutureBuilder to avoid setState cause FutureBuilder rebuild again.
Detail reason https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
didUpdateWidget of the FutureBuilder state is being called every time a rebuild is issued. This function checks if the old future object is different from the new one, and if so, refires the FutureBuilder.
To get past this, we can call the Future somewhere other than in the build function. For example, in the initState, and save it in a member variable, and pass this variable to the FutureBuilder.
code snippet
Future<List<ItemData>> _future;
...
#override
void initState() {
_future = _getProducts();
super.initState();
}
...
Widget _myCart() {
return FutureBuilder(
future: _future,
working demo
full code
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class ItemData {
final String itemName;
final String itemPrice;
final String image;
int counter = 0;
bool isAdded = false;
ItemData({this.itemName, this.itemPrice, this.image});
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<List<ItemData>> _future;
Future<List<ItemData>> _getProducts() async {
var data = await http
.get("https://orangecitycafe.in/app_configs/products_display.php");
var jsonData = json.decode(data.body);
List<ItemData> details = [];
for (var p in jsonData) {
ItemData detail = ItemData(
itemName: p["product_name"],
itemPrice: p["product_price"],
image: p["product_image"]);
details.add(detail);
}
return details;
}
#override
void initState() {
_future = _getProducts();
super.initState();
}
Widget _myCart() {
return FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(snapshot.data[index].itemName),
leading: Image.network("https://www.orangecitycafe.in/" +
snapshot.data[index].image),
trailing: snapshot.data[index].isAdded
? Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
setState(() {
if (snapshot.data[index].counter > 0) {
snapshot.data[index].counter--;
}
});
},
color: Colors.green,
),
Text(snapshot.data[index].counter.toString()),
IconButton(
icon: Icon(Icons.add),
color: Colors.green,
onPressed: () {
setState(() {
snapshot.data[index].counter++;
});
},
),
],
)
: RaisedButton(
onPressed: () {
setState(() {
snapshot.data[index].isAdded = true;
});
},
child: Text("Add"),
),
);
},
);
} else {
return Container();
}
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _myCart());
}
}

Is there an equivalent widget in flutter to the "select multiple" element in HTML

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(),
),
),
);
}
}