Flutter: popup menu inside popup menu - flutter

I need to achieve a list of PopUpMenu nested inside a PopUpMenu.
On click on one of the itmes I want to get another PopUpMenu with it's own items.
We could say it maight look similar to classic windows options.
Is it possible to achievie in flutter?

Sure you can:
This show two subMenu with two items each.
I used an enum for the demo:
enum Item { i1, i2, i3, i4 }
Make sure to call Navigator.pop(context) in the onSelected to close the first menu.
PopupMenuButton(
child: Text('MENU'),
itemBuilder: (BuildContext context) => <PopupMenuEntry<PopupMenuButton>>[
PopupMenuItem(
child: PopupMenuButton(
child: Text('SUBMENU A'),
onSelected: (Item result) {
setState(() { _selection = result; });
Navigator.pop(context); },
itemBuilder: (BuildContext context) => <PopupMenuEntry<Item>>[
const PopupMenuItem<Item>(
value: Item.i1,
child: Text('i1'),
),
const PopupMenuItem<Item>(
value: Item.i2,
child: Text('i2'),
),
],
),
),
PopupMenuItem(
child: PopupMenuButton(
child: Text('SUBMENU B'),
onSelected: (Item result) {
setState(() { _selection = result; });
Navigator.pop(context); },
itemBuilder: (BuildContext context) => <PopupMenuEntry<Item>>[
const PopupMenuItem<Item>(
value: Item.i3,
child: Text('i3'),
),
const PopupMenuItem<Item>(
value: Item.i4,
child: Text('i4'),
),
],
),
),
],
),

you can get nested pop up menus like this :
PopupMenuButton(
itemBuilder: (_) {
return [
PopupMenuItem(child: Text("Item1")),
PopupMenuItem(
child: PopupMenuButton(
child: Text("Nested Items"),
itemBuilder: (_) {
return [
PopupMenuItem(child: Text("Item2")),
PopupMenuItem(child: Text("Item3"))
];
},
),
),
];
},
)

You can do like this, add it to code.
class PopupMenuChildrenItem<T> extends PopupMenuEntry<T> {
const PopupMenuChildrenItem({
super.key,
this.height = kMinInteractiveDimension,
this.padding,
this.enable = true,
this.textStyle,
this.onTap,
required this.itemBuilder,
required this.child,
});
final TextStyle? textStyle;
final EdgeInsets? padding;
final bool enable;
final void Function()? onTap;
final List<PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
final Widget child;
#override
final double height;
#override
bool represents(T? value) => false;
#override
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() => MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
}
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>> extends State<W> {
#protected
void handleTap(T value) {
widget.onTap?.call();
Navigator.pop<T>(context, value);
}
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
TextStyle style = widget.textStyle ??
popupMenuTheme.textStyle ??
theme.textTheme.subtitle1!;
return PopupMenuButton<T>(
enabled: widget.enable,
onSelected: handleTap,
itemBuilder: widget.itemBuilder,
child: AnimatedDefaultTextStyle(
style: style,
duration: kThemeChangeDuration,
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: widget.height),
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
child: widget.child,
),
),
);
}
}
Then, use it like
PopupMenuButton<int>(
child: const Text('MENU'),
onSelected: (result) {
setState(() {
_selection = result;
});
},
itemBuilder: (BuildContext context) => [
PopupMenuChildrenItem(
child: const Text('SUBMENU A'),
itemBuilder: (BuildContext context) => [
const PopupMenuItem(
value: 1,
child: Text('1'),
),
const PopupMenuItem(
value: 2,
child: Text('2'),
),
],
),
PopupMenuChildrenItem(
child: const Text('SUBMENU B'),
itemBuilder: (BuildContext context) => [
const PopupMenuItem(
value: 3,
child: Text('3'),
),
const PopupMenuItem(
value: 4,
child: Text('4'),
),
],
),
const PopupMenuItem(
value: 5,
child: Text('5'),
),
const PopupMenuItem(
value: 6,
child: Text('6'),
),
],
)

Related

Flutter - Provider - Clearing Data itself when navigate

When I in a page and added some quantity and navigate to another page and come back to added more quantity data is cleared.
Here's my code.
add_inv_stream.dart
class AddInvStream extends StatelessWidget {
const AddInvStream({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
FirebaseServices services = FirebaseServices();
final provider = Provider.of<InventoryProvider>(context);
return StreamBuilder<QuerySnapshot>(
stream: services.inventory
.where('fishType', isEqualTo: provider.inventoryData['fishType'])
.where('status', isEqualTo: true)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text('Something wrong!'));
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.data!.size == 0) {
return const Center(
child: Text('No inventories'),
);
}
return AddInvData(snapshot: snapshot, services: services);
},
);
}
}
add_inv_data.dart
class AddInvData extends StatefulWidget {
final AsyncSnapshot<QuerySnapshot<Object?>> snapshot;
final FirebaseServices services;
const AddInvData({Key? key, required this.snapshot, required this.services})
: super(key: key);
#override
State<AddInvData> createState() => _AddInvDataState();
}
class _AddInvDataState extends State<AddInvData> {
final _qty = TextEditingController();
List sellerList = [];
#override
Widget build(BuildContext context) {
final providerr = Provider.of<InventoryProvider>(context);
return ListView.builder(
padding: const EdgeInsets.all(15.0),
physics: const ScrollPhysics(),
shrinkWrap: true,
itemCount: widget.snapshot.data!.size,
itemBuilder: (context, index) {
Map<String, dynamic> sellerData =
widget.snapshot.data!.docs[index].data() as Map<String, dynamic>;
int sellerQty = int.parse(sellerData['qty']);
return InkWell(
onTap: () {
showDialog(
context: context,
useRootNavigator: false,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
elevation: 16,
child: Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Quantity in kg: '),
const SizedBox(width: 10),
Container(
height: 60,
width: 60,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
),
child: TextFormField(
controller: _qty,
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
border: InputBorder.none,
),
),
),
],
),
const SizedBox(height: 50),
TextButton(
onPressed: () async {
Map<String, dynamic> seller = {
'date': sellerData['date'],
'qty': _qty.text,
};
sellerList.add(seller);
providerr.getData(sellerList: sellerList);
print(providerr.inventoryData['sellerList']);
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const InvDetails(),
));
},
child: const Text('ASSIGN'),
),
],
),
),
);
},
);
},
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
sellerData['date'],
style: const TextStyle(fontSize: 18),
),
Text(
'${sellerData['qty']} kg',
style: const TextStyle(fontSize: 18),
),
],
),
),
);
},
);
}
}
inv_details.dart
class InvDetails extends StatelessWidget {
const InvDetails({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: TextButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const AddInv(),
));
},
child: const Text('Add Inventory'),
),
),
);
}
}
inv_provider.dart
class InventoryProvider with ChangeNotifier {
Map<String, dynamic> inventoryData = {};
getData({List? sellerList}) {
if (sellerList != null) {
inventoryData['sellerList'] = sellerList;
}
notifyListeners();
}
}
Here printing at the first
[{date: 10/31/2022, qty: 1}]
When I navigate to InvDetails from AddInvData and when the button is clicked it's printing
[{date: 11/25/2022, qty: 1}]
Data is not updating
But if I didn't navigate.
i.e If I stay in AddInvData and added quantity it is updating.
inv_data.dart (Added textbutton part only ) removed navigate
TextButton(
onPressed: () async {
Map<String, dynamic> seller = {
'date': sellerData['date'],
'qty': _qty.text,
};
sellerList.add(seller);
providerr.getData(sellerList: sellerList);
print(providerr.inventoryData['sellerList']);
},
prints
[{date: 10/31/2022, qty: 1}]
[{date: 10/31/2022, qty: 1}, {date: 11/25/2022, qty: 1}]
Why this is not hapenning when navigate and come back to previous page?

Changing Text regarding to the TextField Simultaneously

I have a custom Text widget Named Dynamic_Game_Preview , and also a TextField.
I want the Dynamic_Game_Preview to be changed with the change of the TextField.
I used onChanged method for the TextField but all the letters are shown separately in the Dynamic_Game_Preview. How can I handle this changes to be applied in the same Dynamic_Game_Preview simultaneously with changing the TextField?
Here is my code:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:pet_store/widgets/dynamic_game_preview.dart';
import 'main.dart';
class Dynamic_Game extends StatefulWidget {
const Dynamic_Game({Key? key}) : super(key: key);
#override
State<Dynamic_Game> createState() => _Dynamic_GameState();
}
class _Dynamic_GameState extends State<Dynamic_Game> {
TextEditingController nameController = TextEditingController();
List<String> names = [];
bool isLoading = false;
List<Dynamic_Game_Preview> dynamicList = [];
void initState() {
super.initState();
dynamicList = [];
names = [];
}
void addNames() {
if (names.length == 1) {
names = [];
}
names.add(nameController.text);
nameController.clear();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.indigo,
title: const Text('Dynamic Game'),
leading: GestureDetector(
child: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
),
onTap: () {
// Navigator.pop(context);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (BuildContext context) => const HomePage(),
),
(route) => false,
);
},
),
),
body: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Center(
child: Column(
children: <Widget>[
SizedBox(height: 20),
Container(
margin: const EdgeInsets.symmetric(horizontal: 30),
child: TextField(
controller: nameController,
onChanged: (value) {
setState(() {
addNames();
});
},
decoration: const InputDecoration(
labelText: 'Enter a Pet Name',
),
),
),
SizedBox(height: 10),
Flexible(
fit: FlexFit.loose,
child: ListView.builder(
shrinkWrap: true,
itemCount: names.length,
itemBuilder: (_, index) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5.0, vertical: 3.0),
child: Dynamic_Game_Preview(nameController.text),
);
},
),
),
],
),
),
),
);
}
}
Problem
Your code clears the nameController:
void addNames() {
...
nameController.clear();
}
Then the code is trying to display nameController.text, which just got cleared:
Dynamic_Game_Preview(nameController.text)
Solution
Something along these lines should work:
itemBuilder: (_, index) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5.0, vertical: 3.0),
child: Dynamic_Game_Preview(names[index]),
);
},
Last but not least
This probably is not needed:
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},

Flutter : Adding item from one list view to another list view

I am trying to select one item from phone contacts list (List view widget)
class PhoneContacts extends StatefulWidget {
const PhoneContacts({Key? key}) : super(key: key);
#override
State<PhoneContacts> createState() => _PhoneContactsState();
}
class _PhoneContactsState extends State<PhoneContacts> {
List<Contact> _contacts = [];
late PermissionStatus _permissionStatus;
late Customer _customer;
#override
void initState(){
super.initState();
getAllContacts();
}
void getAllContacts() async {
_permissionStatus = await Permission.contacts.request();
if(_permissionStatus.isGranted) {
List<Contact> contacts = await ContactsService.getContacts(withThumbnails: false);
setState(() {
_contacts = contacts;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Phone Contacts"),
backgroundColor: Colors.indigo[600],
),
body: Container(
padding: const EdgeInsets.all(5),
child: ListView.builder(
itemCount: _contacts.length,
itemBuilder: (BuildContext context, int index) {
Contact contact = _contacts[index];
return contactItem(contact);
}
),
),
);
}
Widget contactItem(Contact contact){
return ListTile(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context)=>Dashboard(contact)));
},
leading: const CircleAvatar(
backgroundColor: Colors.pinkAccent,
child: Icon(Icons.person_outline_outlined)),
title : Text(contact.displayName.toString()),
subtitle: Text(contact.phones!.first.value.toString()),
);
}
}
and insert and display it to dashboard list (another List view widget)
class Dashboard extends StatefulWidget {
final Contact? contact;
const Dashboard([this.contact]);
#override
State<Dashboard> createState() => _DashboardState();
}
class _DashboardState extends State<Dashboard> {
final Color? themeColor = Colors.indigo[600];
late GlobalKey<RefreshIndicatorState> refreshKey;
late List<CardGenerator> existingCustomerContactList = getCustomerContactList();
#override
void initState(){
super.initState();
refreshKey=GlobalKey<RefreshIndicatorState>();
}
void addCustomerContact() {
existingCustomerContactList.add(
CardGenerator(
Text(widget.contact!.displayName.toString()),
const Icon(Icons.account_circle),
Text(widget.contact!.phones!.first.value.toString())));
}
List<CardGenerator> getCustomerContactList () {
existingCustomerContactList = [
CardGenerator(
const Text('Dave', style: TextStyle(fontSize: 24.0), textAlign: TextAlign.start,),
const Icon(Icons.account_circle, size: 100, color: Colors.white,),
const Text('Address 1')),
CardGenerator(
const Text('John', style: TextStyle(fontSize: 24.0)),
const Icon(Icons.account_circle, size: 100, color: Colors.white),
const Text('Address 2')),
CardGenerator(
const Text('Richard', style: TextStyle(fontSize: 24.0)),
const Icon(Icons.account_circle, size: 100, color: Colors.white),
const Text('Address 3')),
];
return existingCustomerContactList;
}
Future<void> refreshList() async {
await Future.delayed(const Duration(seconds: 1));
setState(() => {
addCustomerContact(),
getCustomerContactList()
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
title: const Text("Dashboard"),
backgroundColor: themeColor,
),
body: RefreshIndicator(
key: refreshKey,
onRefresh: () async {
await refreshList();
},
child: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: existingCustomerContactList.length,
key: UniqueKey(),
itemBuilder: (BuildContext context, int index) {
return OpenContainer(
closedColor: Colors.transparent,
closedElevation: 0.0,
openColor: Colors.transparent,
openElevation: 0.0,
transitionType: ContainerTransitionType.fadeThrough,
closedBuilder: (BuildContext _, VoidCallback openContainer) {
return Card(
color: Colors.white,
child: GestureDetector(
onTap: openContainer,
child: SizedBox(
height: 140,
child: Row(
children: [
Container(
decoration: const BoxDecoration(
color: Colors.indigo,
borderRadius: BorderRadius.only(topLeft: Radius.circular(7.0),bottomLeft: Radius.circular(7.0))
),
height: 140,
width: 120,
child: existingCustomerContactList[index].icon,
),
Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: existingCustomerContactList[index].title,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: existingCustomerContactList[index].address,
),
],
)
],
),
),
),
);
},
openBuilder: (BuildContext _, VoidCallback openContainer) {
return ConsumerHome();
}
);
}),
),
],
),
),
);
}
}
I found the
selected item has been added to the Dashboard items list but when I refresh it it doesn't newly added item in the dashboard list view.
I am a newcomer in flutter please bare with me. I already did my search for this problem unfortunately, no luck.
Change the order of execution. You are adding the item in the list and then making a new list again in the current order
addCustomerContact(),
getCustomerContactList()
change this to
getCustomerContactList()
addCustomerContact(),

How to make just one ExpansionPanel, in an ExpansionPanelList different to the others? flutter

As the question suggests I have an ExpansionPanelList, one ExpansionPanel (the last one or the 7th one) should have 2 additional buttons, but how can I add them just in this one last panel & not in all the others as well?
This is the code of my whole Expansion panel, as Im not sure where you have to add the behaviour, but guessing in the body of the ExpansionPanel (close to line 40):
class ExpansionList extends StatefulWidget {
final Info info;
const ExpansionList({
Key key,
this.info,
}) : super(key: key);
#override
_ExpansionListState createState() => _ExpansionListState();
}
class _ExpansionListState extends State<ExpansionList> {
Widget _buildListPanel() {
return Container(
child: Theme(
data: Theme.of(context).copyWith(
cardColor: Color(0xffDDBEA9),
),
child: ExpansionPanelList(
dividerColor: Colors.transparent,
elevation: 0,
expansionCallback: (int index, bool isExpanded) {
setState(() {
infos[index].isExpanded = !isExpanded;
});
},
children: infos.map<ExpansionPanel>((Info info) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: !isExpanded
? Text(
info.headerValue,
) //code if above statement is true
: Text(
info.headerValue,
textScaleFactor: 1.3,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
);
},
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
color: Color(0xffFFE8D6),
borderRadius: BorderRadius.circular(25)),
child: Column(
children: [
ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.only(left: 40.0,),
itemCount: info.expandedValueData.length,
itemBuilder: (context, index) {
return CheckboxListTile(
title: Text(info.expandedValueData[index].title,
style: TextStyle(
decoration: info.expandedValueData[index]
.completed
? TextDecoration.lineThrough
: null)),
value: info.expandedValueData[index].completed,
onChanged: (value) {
setState(() {
// Here you toggle the checked item state
infos.firstWhere(
(currentInfo) => info == currentInfo)
..expandedValueData[index].completed =
value;
});
});
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(
height: 20,
);
},
),
Row(children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.16),
Text("Abschnitt bis zum Neustart löschen"),
SizedBox(
width: MediaQuery.of(context).size.width * 0.11),
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
infos.removeWhere(
(currentInfo) => info == currentInfo);
});
},
)
]),
],
),
),
),
isExpanded: info.isExpanded);
}).toList(),
),
),
);
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Container(
child: _buildListPanel(),
),
);
}
}
Thanks for suggestions!
Hi Just add a field (if you already do not have one) in the info object that will allow you to change the widget that is inflated based on that field.
For example
...
children: infos.map<ExpansionPanel>((Info info) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return info.type == TYPE_A ? TypeAWidgetHeader(info) : TypeBWidgetHeader(info);
body: info.type == TYPE_A ? TypeAWidgetBody(info) : TypeBWidgetBody(info);
...

Flutter how to save position of the reorderables package?

i tried the reorderables package https://pub.dev/packages/reorderables
I successed to move my dashboard blocs but when I restart app, my moves are removed.
So the solution can only be a sharedpref solution.
But I dont found how to save this information
I tried to save and load newIndex but without success
I tried to save and load List _tiles; but sharedpref can't save List
Here is my code example
List<Widget> _tiles;
void _onReorder(int oldIndex, int newIndex) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
Widget row = _tiles.removeAt(oldIndex);
_tiles.insert(newIndex, row);
//prefs.setListWidget('indexList', _tiles); not working
// prefs.setInt('index', newIndex ); not working
});
}
#override
void initState() {
super.initState();
_tiles = <Widget>[
//my widget1
//my widget2
//my widget3
//my widget4
//my widget5
//my widget6
//my widget7
//my widget8
//my widget9
]
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ReorderableWrap(
spacing: 0.0,
runSpacing:0,
maxMainAxisCount: 3,
minMainAxisCount: 3,
padding: const EdgeInsets.all(5),
children:_tiles,
onReorder: _onReorder,
onNoReorder: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} reorder cancelled. index:$index');
},
onReorderStarted: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} reorder started: index:$index');
}
)
]
);
}
}
edit : here is Widget 1. other widget are same
new Container (
width: SizeConfig.safeBlockHorizontal * 32,
height: 160,
child :
new Card(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
color: Colors.white,
child:
Ink(
child: InkResponse(
splashFactory: InkRipple.splashFactory,
radius: 100,
onTap: () {
},
child:
Padding(
padding: const EdgeInsets.only(top:10),
child:
new Container(
padding: const EdgeInsets.all(0),
child :
new Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Container(
height:25,
child :
new Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.keyboard_arrow_right, color: Colors.white, size: 15.0),
Text('Planning',textAlign:TextAlign.center, style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: SizeConfig.safeBlockHorizontal * 4),
),
Icon(Icons.keyboard_arrow_right, color: Colors.grey, size: 15.0),
]
),
),
Padding(
padding: const EdgeInsets.all(5),),
Icon(Icons.event_available, color:Color(0xffff9a7b), size: 70.0),
],
),
))
),
)),
)
Here is my design
Edit #2
Now I tried to add my assets in the model, but I don't know how to do
void initState() {
// default models list
models = [
Model(index: 0, new Container (
width:90,
height: 90,
child :new FlareActor("assets/pierre.flr", alignment:Alignment.center, fit:BoxFit.contain, animation:"pierre")
), title: 'Coach'),
Model(index: 1, new Image.asset(
"assets/cup.png",
width: 50,
), title: 'Victories'),
Model(index: 2, icon: Icon(Icons.card_giftcard), title: 'Jeux'),
Model(index: 3, icon: Icon(Icons.wb_sunny), title: 'Sunny'),
Model(index: 4, icon: Icon(Icons.cloud), title: 'Cloud'),
Model(index: 5, icon: Icon(Icons.tv), title: 'TV'),
Model(index: 6, icon: Icon(Icons.place), title: 'Location'),
Model(index: 8, icon: Icon(Icons.music_note), title: 'Music'),
// More customization
Model(
index: 7,
icon: Icon(Icons.event_available, color: Color(0xffff9a7b)),
title: 'Planning'),
];
config();
super.initState();
}
This solution isn't a good one and I don't like it, but it works :D
I really appreciate it if anyone would refer a better one
Maybe for this project, it's good to use DB instead of SharedPreferences but here I used SharedPreferences.
The question is how to save the order of some widget(each time on reordering, the order of widget changes and we want to save the order of them, after restarting the app the saved order should be fetched).
SharedPreferences can also save a list of string, so what I did here was:
In the beginning, there should be a default list, that contains the initial order of widget's of the app.
Because widgets are somehow the same and only some of their info is different, I decided to define a model and work with models, instead of a whole complicated widget, I mean when I want to remove or change indexes I do it for a list of models rather than a list of widgets.
Here I supposed the model only contains a title, I also defined an index for it, so all I do is that when I reorder the widget, it reorders the list of models, to save the order, I save the index of models in any order they are now,
for example, if the initial order was [0, 1, 2, 3] let's say after reordering it's now [3, 0, 1, 2], I save this order, and for the next boot, I fetch the saved order([3, 0, 1, 2]) and then reorder the default list based on this fetched order.
Another solution would be to change the model's index and then show an ordered list of models based on their index.
Here is the code:
import 'package:flutter/material.dart';
import 'package:reorderables/reorderables.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: Page(),
),
),
),
);
class Page extends StatefulWidget {
const Page({Key key}) : super(key: key);
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
SharedPreferences prefs;
List<Model> models;
#override
void initState() {
// default models list
models = [
Model(index: 0, title: 'Item 0'),
Model(index: 1, title: 'Item 1'),
Model(index: 2, title: 'Item 2'),
Model(index: 3, title: 'Item 3'),
];
config();
super.initState();
}
void config() async {
// Here we reset the default model based on saved order
await SharedPreferences.getInstance().then((pref) {
prefs = pref;
List<String> lst = pref.getStringList('indexList');
List<Model> list = [];
if (lst != null && lst.isNotEmpty) {
list = lst
.map(
(String indx) => models
.where((Model item) => int.parse(indx) == item.index)
.first,
)
.toList();
models = list;
}
setState(() {});
});
}
void _onReorder(int oldIndex, int newIndex) async {
Model row = models.removeAt(oldIndex);
models.insert(newIndex, row);
setState(() {
prefs.setStringList(
'indexList', models.map((m) => m.index.toString()).toList());
});
}
#override
Widget build(BuildContext context) {
return ReorderableWrap(
scrollDirection: Axis.vertical,
direction: Axis.vertical,
spacing: 0.0,
runSpacing: 0,
maxMainAxisCount: 3,
minMainAxisCount: 3,
padding: const EdgeInsets.all(5),
children: models
.map((m) => Card(
child: Container(
child: Text('${m.index} - ${m.title}'),
padding: EdgeInsets.all(24.0),
),
))
.toList(),
onReorder: _onReorder,
onNoReorder: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} ' +
'reorder cancelled. index:$index');
},
onReorderStarted: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} ' +
'reorder started: index:$index');
});
}
}
class Model {
int index;
String title;
Model({this.index, this.title});
#override
String toString() {
return '$index : $title';
}
}
Changes After Edit to the main question:
This version is based on the editing to the main question, I decided to keep the first answer unchanged because it's a more simple version and may help another viewer.
For the new model, as far as I could get, it has an icon, title, and an onTap functionality, I changed the model to have icon and title, but for the onTap, I wrote my own card version that gets a model and onTap functionality, I could add onTap to the model, but I thought it's better for future use or to use in other places, so I separated the onTap from the model, I also chose Icon for the model, it could be IconData (benefit of IconData is that you can choose customization for each icon and etc).
On my Card version (MyCard), I simply used a GestureDetector and Card to simulate the taps and card.
I wrote a FakePage that gets a model and if you Tap on each card it navigates to this page and shows some message based on the received model.
To clean the previously saved model in SharedPreferences, you should comment the part that fetches models order in config() and on the next refresh, you should uncomment it again.
Here is the new version of code:
import 'package:flutter/material.dart';
import 'package:reorderables/reorderables.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: Page(),
),
),
),
);
class Page extends StatefulWidget {
const Page({Key key}) : super(key: key);
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
SharedPreferences prefs;
List<Model> models;
#override
void initState() {
// default models list
models = [
Model(index: 0, icon: Icon(Icons.people), title: 'Coach'),
Model(index: 1, icon: Icon(Icons.wb_incandescent), title: 'Victories'),
Model(index: 2, icon: Icon(Icons.card_giftcard), title: 'Jeux'),
Model(index: 3, icon: Icon(Icons.wb_sunny), title: 'Sunny'),
Model(index: 4, icon: Icon(Icons.cloud), title: 'Cloud'),
Model(index: 5, icon: Icon(Icons.tv), title: 'TV'),
Model(index: 6, icon: Icon(Icons.place), title: 'Location'),
Model(index: 8, icon: Icon(Icons.music_note), title: 'Music'),
// More customization
Model(
index: 7,
icon: Icon(Icons.event_available, color: Color(0xffff9a7b)),
title: 'Planning'),
];
config();
super.initState();
}
void config() async {
// Here we reset the default model based on saved order
await SharedPreferences.getInstance().then((pref) {
prefs = pref;
List<String> lst = pref.getStringList('indexList');
List<Model> list = [];
if (lst != null && lst.isNotEmpty) {
list = lst
.map(
(String indx) => models
.where((Model item) => int.parse(indx) == item.index)
.first,
)
.toList();
models = list;
}
setState(() {});
});
}
void _onReorder(int oldIndex, int newIndex) async {
Model row = models.removeAt(oldIndex);
models.insert(newIndex, row);
setState(() {
prefs.setStringList(
'indexList', models.map((m) => m.index.toString()).toList());
});
}
#override
Widget build(BuildContext context) {
return ReorderableWrap(
spacing: 0.0,
runSpacing: 0,
maxMainAxisCount: 3,
minMainAxisCount: 3,
padding: const EdgeInsets.all(5),
children: <Widget>[
MyCard(
model: models[0],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[0]),
),
),
),
MyCard(
model: models[1],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[1]),
),
),
),
MyCard(
model: models[2],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[2]),
),
),
),
MyCard(
model: models[3],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[3]),
),
),
),
MyCard(
model: models[4],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[4]),
),
),
),
MyCard(
model: models[5],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[5]),
),
),
),
MyCard(
model: models[6],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[6]),
),
),
),
MyCard(
model: models[7],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[7]),
),
),
),
MyCard(
model: models[8],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[8]),
),
),
),
],
onReorder: _onReorder,
onNoReorder: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} ' +
'reorder cancelled. index:$index');
},
onReorderStarted: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} ' +
'reorder started: index:$index');
});
}
}
// ---------------------- Model --------------------------
class Model {
int index;
String title;
Icon icon;
Model({this.index, this.title, this.icon});
#override
String toString() {
return '$index : $title';
}
}
// ------------------------ MyCard ----------------------------
class MyCard extends StatelessWidget {
final Model model;
final void Function() onTap;
const MyCard({Key key, this.onTap, #required this.model})
: assert(model != null),
super(key: key);
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return GestureDetector(
onTap: onTap,
child: Card(
elevation: 8.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: _child(width),
),
);
}
Widget _child(double width) {
return Container(
width: width / 4,
height: width / 3,
margin: EdgeInsets.all(5.0),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
flex: 3,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
model.title,
maxLines: 1,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: 15.0,
),
overflow: TextOverflow.ellipsis,
),
Icon(
Icons.arrow_forward_ios,
color: Colors.grey.shade400,
size: 15.0,
),
],
),
),
Expanded(
flex: 5,
child: Padding(
padding: EdgeInsets.all(8.0),
child: FittedBox(
fit: BoxFit.contain,
child: model.icon,
),
),
),
],
),
);
}
}
// ----------------------- FAKE PAGE ---------------------------
class FakePage extends StatelessWidget {
final Model model;
const FakePage({Key key, this.model}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepOrangeAccent,
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'You Clicked on Card : ${model.title}',
style: TextStyle(fontSize: 20.0),
),
Padding(
padding: EdgeInsets.only(top: 24.0),
child: Icon(
model.icon.icon,
size: 70.0,
),
),
],
),
),
);
}
}