I'm learning Flutter and Is it possible to create an reorderable list which has items that are an reorderable List itself (picture below). I'll be really appreciated if someone can suggest me or help me. thanks
This is my code
class _ReorderItemsState extends State<ReorderItems> {
List<String> topTen = [
"EPL",
"MLS",
"LLG",
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: ReorderableListView(
onReorder: onReorder,
children: getListItem(),
),
);
}
List<ExpansionTile> getListItem() => topTen.asMap()
.map((index, item) => MapEntry(index, buildTenableListTile(item, index)))
.values.toList();
ExpansionTile buildTenableListTile(String item, int index) => ExpansionTile(
key: ValueKey(item),
title: Text(item),
leading: Icon(Icons.list),
backgroundColor: Colors.blue,
);
void onReorder(int oldIndex, int newIndex){
if(newIndex > oldIndex){
newIndex -=1;
}
setState(() {
String game = topTen[oldIndex];
topTen.removeAt(oldIndex);
topTen.insert(newIndex, game);
});
}
}
You can achieve this by setting the children attribute of ExpansionTile widget.
The approach is as follows.
You need a common Data handler or some sort of state management to keep the state of the parent and children outside the widget to avoid rebuilds when the child list changes. For brevity I am using a singleton to hold the common data. In real case this should be some ChangeNotifier or BLoc based approach. Not however if you mutate either the parent or child list you need a full rebuild because Flutter, widgets are immutable.
/// Holding the common data as a singleton to avoid excessive rebuilds.
/// Usually this should be replaced with a manager or bloc or changenotifier class
class DataHolder {
List<String> parentKeys;
Map<String, List<String>> childMap;
DataHolder._privateConstructor();
static final DataHolder _dataHolder = DataHolder._privateConstructor();
static DataHolder get instance => _dataHolder;
factory DataHolder.initialize({#required parentKeys}) {
_dataHolder.parentKeys = parentKeys;
_dataHolder.childMap = {};
for (String key in parentKeys) {
_dataHolder.childMap.putIfAbsent(
key, () => ['${key}child_1', '${key}child_2', '${key}child_3']);
}
return _dataHolder;
}
}
Create a widget which returns a child ReorderableListView with unique ScrollController for each of these widget. For e.g. a ReorderList widget. Its almost identical to what you wrote except I return a ListTile instead of ExpansionTile and set scrollController attribute. Current stable release doesn't have this attribute. So in this solution it is wrapped with a PrimaryScrollController widget to avoid duplicate usage of scrollController.
class ReorderList extends StatefulWidget {
final String parentMapKey;
ReorderList({this.parentMapKey});
#override
_ReorderListState createState() => _ReorderListState();
}
class _ReorderListState extends State<ReorderList> {
#override
Widget build(BuildContext context) {
return PrimaryScrollController(
controller: ScrollController(),
child: ReorderableListView(
// scrollController: ScrollController(),
onReorder: onReorder,
children: DataHolder.instance.childMap[widget.parentMapKey]
.map(
(String child) => ListTile(
key: ValueKey(child),
leading: Icon(Icons.done_all),
title: Text(child),
),
)
.toList(),
),
);
}
void onReorder(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
List<String> children = DataHolder.instance.childMap[widget.parentMapKey];
String game = children[oldIndex];
children.removeAt(oldIndex);
children.insert(newIndex, game);
DataHolder.instance.childMap[widget.parentMapKey] = children;
// Need to set state to rebuild the children.
setState(() {});
}
}
In the Parent ExpansionTile widget set this new widget one of the children. This children and the parent are both built from the value of the DataHolder singleton class.
Note I am setting a constant height to avoid conflicts of layout. You have to play with this for dynamic sizes.
class ReorderItems extends StatefulWidget {
final List<String> topTen;
ReorderItems({this.topTen});
#override
_ReorderItemsState createState() => _ReorderItemsState();
}
class _ReorderItemsState extends State<ReorderItems> {
#override
void initState() {
super.initState();
// initialize the children for the Expansion tile
// This initialization can be replaced with any logic like network fetch or something else.
DataHolder.initialize(parentKeys: widget.topTen);
}
#override
Widget build(BuildContext context) {
return PrimaryScrollController(
key: ValueKey(widget.topTen.toString()),
controller: ScrollController(),
child: ReorderableListView(
onReorder: onReorder,
children: getListItem(),
),
);
}
List<ExpansionTile> getListItem() => DataHolder.instance.parentKeys
.asMap()
.map((index, item) => MapEntry(index, buildTenableListTile(item, index)))
.values
.toList();
ExpansionTile buildTenableListTile(String mapKey, int index) => ExpansionTile(
key: ValueKey(mapKey),
title: Text(mapKey),
leading: Icon(Icons.list),
backgroundColor: Colors.blue,
children: [
Container(
key: ValueKey('$mapKey$index'),
height: 400,
child: Container(
padding: EdgeInsets.only(left: 20.0),
child: ReorderList(
parentMapKey: mapKey,
),
),
),
],
);
void onReorder(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
setState(() {
String game = DataHolder.instance.parentKeys[oldIndex];
DataHolder.instance.parentKeys.removeAt(oldIndex);
DataHolder.instance.parentKeys.insert(newIndex, game);
});
}
}
A fully working solution is available in this codepen.
I changed the code to accept the list of items dynamically from the parent widget. You will have to play with how to maintain the data and reduce rebuilds. But in general a child ReorderableListView works as long as the list is maintainted.
Hope this helps.
Related
I have a view which switches between a ListView and a ReorderableListView.
Widget _buildList(
BuildContext context,
List<ExerciseTemplate> exerciseTemplates,
EditWorkoutModel dao,
) {
if (_isInEditingMode) {
return ReorderableListView(
key: ObjectKey('reordeableListView'),
onReorder: ((oldIndex, newIndex) {
dao.reorderIndexes(
oldIndex,
(oldIndex < newIndex) ? newIndex - 1 : newIndex,
);
}),
padding: const EdgeInsets.only(bottom: 120),
children: [
for (var exerciseTemplate in exerciseTemplates)
Provider(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: CompactExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow'),
),
),
],
);
} else {
return ListView.builder(
key: ObjectKey('listView'),
itemCount: exerciseTemplates.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
final exerciseTemplate = exerciseTemplates[index];
return Provider(
// Key is needed here to properly handle deleted rows in the ui.
// Without the key, deleted rows are being shown.
key: ObjectKey(
'${exerciseTemplate.id}_exerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: ExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_exerciseTemplateRow'),
onDelete: () async {
await dao.deleteExercise(exerciseTemplate);
return true;
},
),
);
},
);
}
}
Both lists show the same data, but tapping a button, switches to a ReorderableListView which shows the data with different widgets. Tapping the button again switches back to the ListView.
However, switching forth and back results that I am not able to interact with elements within the row of the ListView. This issue appeared after I added a globalKey for each element in the ListView. I need this key, to properly handle deleting rows, so I can not just remove the key again.
How can I make it work, that I can interact with widgets within the row after I switched to the ReorderableListView and back to the ListView?
Copied from Provider document:
https://pub.dev/packages/provider
DON'T create your object from variables that can change over time.
In such a situation, your object would never update when the value changes.
int count;
Provider(
create: (_) => MyModel(count),
child: ...
)
If you want to pass variables that can change over time to your object, consider using ProxyProvider:
int count;
ProxyProvider0(
update: (_, __) => MyModel(count),
child: ...
)
It's ok to use Global key and switch between ListView and ReorderableListView, see example below:
https://dartpad.dev/?id=fd39a89b67448d86e682dd2c5ec77453
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool reOrder = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(reOrder ? "ReoderableListView" : "ListView"),
),
floatingActionButton: FloatingActionButton(onPressed: () {
setState(() {
reOrder = !reOrder;
});
}),
body: MyListView(reOrder));
}
}
final data = List.generate(10, (index) => {"title": 'item $index', "value": false});
class MyListView extends StatefulWidget {
final bool reOrder;
const MyListView(this.reOrder, {super.key});
#override
State<MyListView> createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
#override
Widget build(BuildContext context) {
if (widget.reOrder) {
return ReorderableListView(
key: const ObjectKey('reordeableListView'),
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = data.removeAt(oldIndex);
data.insert(newIndex, item);
});
},
children: [
for (var item in data)
ListTile(
key: ObjectKey('${item["title"]}_compactExerciseTemplateRow_provider'),
title: Text(item["title"] as String),
trailing: Text((item["value"] as bool).toString()),
),
],
);
} else {
return ListView.builder(
key: const ObjectKey('listView'),
itemCount: data.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
return CheckboxListTile(
key: ObjectKey('${data[index]["title"]}_exerciseTemplateRow_provider'),
title: Text(data[index]["title"] as String),
value: (data[index]["value"] as bool),
onChanged: (bool? value) {
setState(() => data[index]["value"] = value!);
},
);
},
);
}
}
}
So the issue was that I was using ObjectKey instead of ValueKey.
The difference between those two is that ObjectKey checks if the identity (the instance) is the same. ValueKey checks the underlying value with the == operator.
My guess here is that by using ObjectKey in my case, flutter is not able to properly replace the old widget with the new one, since the new widget always have a different key. By using ValueKey flutter can distinguish between old and new widgets. Widgets will be in my case replaced after I switch between the lists, because the row widget won't be visible and therefor disposed.
Because the widgets were not properly replaced, somehow the old widgets are still being rendered, but all gesture listeners were already disposed. Therefor no interaction was possible anymore.
These are just my assumption, let me know if I am completely wrong here.
Apologies in advance for posting Pseudo code. Real code would be too long.
I have a screen where I have a drop down at the top where a user can select an option. The rest of the page updates based on that option. Something like this:
// state variable
String idFromDropdown;
Column(
children: [
DropDownWidget(),
ChildWidget1(myId: idFromDropDown),
ChildWidget2(myId: idFromDropDown),
ChildWidget3(myId: idFromDropDown),
]
)
In the child widgets, I am using widget.myId to pass into a backend service and read new data.
Expectation is that when the dropdown changes and I call
setState((val)=>{idFromDropdown = val});
then the value would cascade into the three child widgets. Somehow trigger the widgets to reconnect to the backend service based on the new value of widget.myId.
How do I trigger a state update on the child widgets?
I ended up using a ValueNotifier. Instead of directly using a string and passing that into the child widgets. I ended up doing something like:
ValueNotifier<String> idFromDropdown;
...
setState((val)=>{idFromDropdown.value = val});
Then in each widget, I am adding a listener onto the ValueNotifier coming in and retriggering the read to the backend service.
While this works, I feel like I'm missing something obvious. My childwidgets now take in a ValueNotifier instead of a value. I'm afraid this is going to make my ChildWidgets more difficult to use in other situations.
Is this the correct way of doing this?
Use provider package your problem will solved easily
Here is example of Riverpod.
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final fetureDataForChild =
FutureProvider.family<List<String>, String>((ref, id) {
return Future.delayed(Duration(seconds: 1), () {
return <String>["active", "${id}", "got it "];
});
});
class MainWidgetR extends StatefulWidget {
#override
_MainWidgetState createState() => _MainWidgetState();
}
class _MainWidgetState extends State<MainWidgetR> {
String id = "id 0";
final items = List.generate(
4,
(index) => DropdownMenuItem(
child: Text("Company $index"),
value: "id $index",
),
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DropdownButton(
items: items,
value: id,
onChanged: (value) {
setState(() {
id = value as String;
});
},
),
RiverPodRespone(
id: id,
),
],
),
),
);
}
}
class RiverPodRespone extends ConsumerWidget {
final String id;
RiverPodRespone({
required this.id,
});
#override
Widget build(BuildContext context, watch) {
final futureData = watch(fetureDataForChild("$id"));
return futureData.map(
data: (value) {
final items = value.value;
return Column(
children: [
...items.map((e) => Text("$e")).toList(),
],
);
},
loading: (value) => CircularProgressIndicator(),
error: (value) => Text(value.toString()),
);
}
}
Hello fellow developers,
For my first Flutter project I need to use a list with sticky headers and infinite scroll. I found a very nice library for this purpose.
https://pub.dev/packages/flutter_sticky_header
Final goal is to fetch new items into the list from my database by scrolling further down.
For testing purposes I added a button to add a random item to my list. However the UI is not updated when the function is called. I am very new to Flutter. Below my code. How can I update the UI every time an item is added to the list without recreating the widget.
class AppScaffold2 extends StatefulWidget {
#override
_AppScaffold2State createState() => _AppScaffold2State();
}
class _AppScaffold2State extends State<AppScaffold2> {
final CustomScrollView x = CustomScrollView(
slivers: new List<Widget>(),
reverse: false,
);
int counter = 0;
add(Widget w){
x.slivers.add(w);
}
#override
Widget build(BuildContext context) {
return DefaultStickyHeaderController(
child: Scaffold(
body: Container(child: Column(children: <Widget>[
Expanded(child: x),
MaterialButton(
onPressed: () => fetch(),
child: Text('add to list')
)
],),)
),
);
}
fetch() {
x.slivers.add(_StickyHeaderList(index: counter));
counter++;
}
}
class _StickyHeaderList extends StatelessWidget {
const _StickyHeaderList({
Key key,
this.index,
}) : super(key: key);
final int index;
#override
Widget build(BuildContext context) {
return SliverStickyHeader(
header: Header(index: index),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => ListTile(
leading: CircleAvatar(
child: Text('$index'),
),
title: Image.network(
"https://i.ytimg.com/vi/ixkoVwKQaJg/hqdefault.jpg?sqp=-oaymwEZCNACELwBSFXyq4qpAwsIARUAAIhCGAFwAQ==&rs=AOn4CLDrYjizQef0rnqvBc0mZyU3k13yrg",
),
),
childCount: 6,
),
),
);
}
}
Try using setState() in fetch Method.
like this.
fetch() {
x.slivers.add(_StickyHeaderList(index: counter));
setState(() {
_counter++;
});
}```
Update state using setState.
fetch() {
x.slivers.add(_StickyHeaderList(index: counter));
setState(() {
_counter++;
});
}
so I am trying to build up a list in my provider from a Future Call.
So far, I have the following ChangeNotifier class below:
class MainProvider extends ChangeNotifier {
List<dynamic> _list = <dynamic>[];
List<dynamic> get list => _list;
int count = 0;
MainProvider() {
initList();
}
initList() async {
var db = new DatabaseHelper();
addToList(Consumer<MainProvider>(
builder: (_, provider, __) => Text(provider.count.toString())));
await db.readFromDatabase(1).then((result) {
result.forEach((item) {
ModeItem _modelItem= ModeItem.map(item);
addToList(_modelItem);
});
});
}
addToList(Object object) {
_list.add(object);
notifyListeners();
}
addCount() {
count += 1;
notifyListeners();
}
}
However, this is what happens whenever I use the list value:
I can confirm that my initList function is executing properly
The initial content from the list value that is available is the
Text() widget that I firstly inserted through the addToList function, meaning it appears that there is only one item in the list at this point
When I perform Hot Reload, the rest of the contents of the list seems to appear now
Notes:
I use the value of list in a AnimatedList widget, so I am
supposed to show the contents of list
What appears initially is that the content of my list value is only one item
My list value doesn't seem to automatically update during the
execution of my Future call
However, when I try to call the addCount function, it normally
updates the value of count without needing to perform Hot Reload -
this one seems to function properly
It appears that the Future call is not properly updating the
contents of my list value
My actual concern is that on initial loading, my list value doesn't
properly initialize all it's values as intended
Hoping you guys can help me on this one. Thank you.
UPDATE: Below shows how I use the ChangeNotifierClass above
class ParentProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MainProvider>(
create: (context) => MainProvider(),
),
],
child: ParentWidget(),
);
}
}
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
var mainProvider = Provider.of<MainProvider>(context);
buildItem(BuildContext context, int index, Animation animation) {
print('buildItem');
var _object = mainProvider.list[index];
var _widget;
if (_object is Widget) {
_widget = _object;
} else if (_object is ModelItem) {
_widget = Text(_object.unitNumber.toString());
}
return SizeTransition(
key: ValueKey<int>(index),
axis: Axis.vertical,
sizeFactor: animation,
child: InkWell(
onTap: () {
listKey.currentState.removeItem(index,
(context, animation) => buildItem(context, index, animation),
duration: const Duration(milliseconds: 300));
mainProvider.list.removeAt(index);
mainProvider.addCount();
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: _widget,
),
),
),
);
}
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: mainProvider.list == null
? Container()
: AnimatedList(
key: listKey,
initialItemCount: mainProvider.list.length,
itemBuilder:
(BuildContext context, int index, Animation animation) =>
buildItem(context, index, animation),
),
),
),
);
}
}
You are retrieving your provider from a StatelessWidget. As such, the ChangeNotifier can't trigger your widget to rebuild because there is no state to rebuild. You have to either convert ParentWidget to be a StatefulWidget or you need to get your provider using Consumer instead of Provider.of:
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
return Consumer<MainProvider>(
builder: (BuildContext context, MainProvider mainProvider, _) {
...
}
);
}
As an aside, the way you are using provider is to add the MainProvider to its provider and then retrieve it from within its immediate child. If this is the only place you are retrieving the MainProvider, this makes the provider pattern redundant as you can easily just declare it within ParentWidget, or even just get your list of images using a FutureBuilder. Using provider is a good step toward proper state management, but also be careful of over-engineering your app.
If you need more info then please comment.Thank you
How can I create flutter staggered grid view like
this with headerTile + image in each grid tile + centered texts
First, create a child and parent model class, ParentModel contains a header text as well list of its children,
class ParentModel {
String title;
List<ChildModel> list;
ParentModel(this.title, this.list);
}
class ChildModel {
String text;
ChildModel(this.text);
}
Then create a ListView, this list will contain title as well as a grid of its children.
class ComplexList extends StatefulWidget {
#override
_ComplexListState createState() => _ComplexListState();
}
class _ComplexListState extends State<ComplexList> {
List<ParentModel> parentList = List();
#override
void initState() {
super.initState();
// this list is just to add dummy data, replace this with your list from api
List<ChildModel> childList = List();
childList.add(ChildModel('Child1'));
childList.add(ChildModel('Child2'));
childList.add(ChildModel('Child3'));
childList.add(ChildModel('Child4'));
List<ChildModel> childList1 = List();
childList1.add(ChildModel('Child5'));
childList1.add(ChildModel('Child6'));
childList1.add(ChildModel('Child7'));
childList1.add(ChildModel('Child8'));
parentList.add(ParentModel('Title1', childList));
parentList.add(ParentModel('Title2', childList1));
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: parentList.length,
itemBuilder: (context, index) {
ParentModel parentModel = parentList[index];
return Column(
children: <Widget>[Text('${parentModel.title}',style: TextStyle(fontSize: 16),),
GridView.count(
shrinkWrap: true,
// Create a grid with 2 columns. If you change the scrollDirection to
// horizontal, this produces 2 rows.
crossAxisCount: 2,
// Generate 100 widgets that display their index in the List.
children: List.generate(parentModel.list.length, (index) {
ChildModel childModel = parentModel.list[index];
return Card(
child: Center(
child: Text(
'Item ${childModel.text}',
style: Theme
.of(context)
.textTheme
.headline,
),
),
);
}),
),
],
);
});
}
}
The output of this code,