I am using the ReorderableListView widget to display a list in my Flutter application..
Everything works fine and the ListTiles easily get dragged and dropped to new positions but the only thing is that they don't stay in the new position after restarting the application.
How do I maintain their positions? I tried using PageStorage but it doesn't maintain the new positions
Any ideas?
Thanks in advance
ReorderableListView(
physics: const ClampingScrollPhysics(),
shrinkWrap: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) newIndex--;
final item = tasks.removeAt(oldIndex);
tasks.insert(newIndex, item);
});
},
children: [
for (final item in tasks)
Container(
key: ValueKey(item),
child: ExpansionTile(
title: Text(item['name'])
)])
I would suggest creating a Task class and adding an id field that uniquely identifies the task:
class Task{
final String name;
final int id;
Task(this.name, this.id);
}
Then you'll need to use a storage package like shared_preferences to save the index of each task whenever they are reordered:
late SharedPreferences sharedPreferences;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
sharedPreferences = await SharedPreferences.getInstance();
runApp(const MyApp());
}
The code is a little complicated and it will only be suitable for small lists. In short, whenever a item in the list moves, you need to save its new index to shared preferences. The trick is that newIndex is always one greater than the actual new index.
You also need to handle forward and backward movement differently. On forward moves, all of the items between the old and new index need to be updated since their order value would have decreased. For backward moves, it is the opposite.
class ReorderExample extends StatefulWidget {
const ReorderExample({super.key});
#override
State<ReorderExample> createState() => _ReorderExampleState();
}
List<Task> tasks = [
Task('Task one', 4),
Task('Task two', 5),
Task('Task three', 6),
];
class _ReorderExampleState extends State<ReorderExample> {
#override
void initState() {
tasks.sort((a, b) {
int indexA = (sharedPreferences.getInt(a.id.toString()) ?? 0);
int indexB = (sharedPreferences.getInt(b.id.toString()) ?? 0);
debugPrint('${a.name}: $indexA');
debugPrint('${b.name}: $indexB');
return (indexA > indexB) ? 1 : 0;
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ReorderableListView(
physics: const ClampingScrollPhysics(),
shrinkWrap: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
debugPrint('newIndex: $newIndex');
debugPrint('oldIndex: $oldIndex');
if (newIndex > oldIndex) {
Task movingTask = tasks[oldIndex];
tasks.removeAt(oldIndex);
tasks.insert(newIndex - 1, movingTask);
sharedPreferences.setInt(movingTask.id.toString(), newIndex - 1);
for (Task task in tasks.sublist(oldIndex, newIndex-1)) {
int index = sharedPreferences.getInt(task.id.toString()) ?? (oldIndex + 1);
debugPrint('${task.name}: ${index - 1}');
sharedPreferences.setInt(task.id.toString(), index - 1);
}
} else {
Task movingTask = tasks[oldIndex];
tasks.removeAt(oldIndex);
tasks.insert(newIndex, movingTask);
sharedPreferences.setInt(movingTask.id.toString(), newIndex);
for (Task task in tasks.sublist(newIndex+1)) {
int index = sharedPreferences.getInt(task.id.toString()) ?? (oldIndex - 1);
debugPrint('${task.name}: ${index + 1}');
sharedPreferences.setInt(task.id.toString(), index + 1);
}
}
});
},
children: [
for (final task in tasks)
Container(
key: UniqueKey(),
child: ExpansionTile(
title: Text(task.name),
),
),
]),
));
}
}
Related
I am trying to add animation to a list of widgets inside of a stack. When ever I remove a Widget or add a Widget to the List I Want to have a scale up/down transition as if the widget pops up from no where and shrinks to nothing. Any idea on how I can achieve this?
You can use AnimatedList widget as solution.
Example video here
Flutter documentation example
import 'package:flutter/material.dart';
void main() {
runApp(const AnimatedListSample());
}
class AnimatedListSample extends StatefulWidget {
const AnimatedListSample({super.key});
#override
State<AnimatedListSample> createState() => _AnimatedListSampleState();
}
class _AnimatedListSampleState extends State<AnimatedListSample> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
late ListModel<int> _list;
int? _selectedItem;
late int
_nextItem; // The next item inserted when the user presses the '+' button.
#override
void initState() {
super.initState();
_list = ListModel<int>(
listKey: _listKey,
initialItems: <int>[0, 1, 2],
removedItemBuilder: _buildRemovedItem,
);
_nextItem = 3;
}
// Used to build list items that haven't been removed.
Widget _buildItem(
BuildContext context, int index, Animation<double> animation) {
return CardItem(
animation: animation,
item: _list[index],
selected: _selectedItem == _list[index],
onTap: () {
setState(() {
_selectedItem = _selectedItem == _list[index] ? null : _list[index];
});
},
);
}
// Used to build an item after it has been removed from the list. This
// method is needed because a removed item remains visible until its
// animation has completed (even though it's gone as far this ListModel is
// concerned). The widget will be used by the
// [AnimatedListState.removeItem] method's
// [AnimatedListRemovedItemBuilder] parameter.
Widget _buildRemovedItem(
int item, BuildContext context, Animation<double> animation) {
return CardItem(
animation: animation,
item: item,
// No gesture detector here: we don't want removed items to be interactive.
);
}
// Insert the "next item" into the list model.
void _insert() {
final int index =
_selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
_list.insert(index, _nextItem++);
}
// Remove the selected item from the list model.
void _remove() {
if (_selectedItem != null) {
_list.removeAt(_list.indexOf(_selectedItem!));
setState(() {
_selectedItem = null;
});
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('AnimatedList'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.add_circle),
onPressed: _insert,
tooltip: 'insert a new item',
),
IconButton(
icon: const Icon(Icons.remove_circle),
onPressed: _remove,
tooltip: 'remove the selected item',
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: AnimatedList(
key: _listKey,
initialItemCount: _list.length,
itemBuilder: _buildItem,
),
),
),
);
}
}
typedef RemovedItemBuilder<T> = Widget Function(
T item, BuildContext context, Animation<double> animation);
/// Keeps a Dart [List] in sync with an [AnimatedList].
///
/// The [insert] and [removeAt] methods apply to both the internal list and
/// the animated list that belongs to [listKey].
///
/// This class only exposes as much of the Dart List API as is needed by the
/// sample app. More list methods are easily added, however methods that
/// mutate the list must make the same changes to the animated list in terms
/// of [AnimatedListState.insertItem] and [AnimatedList.removeItem].
class ListModel<E> {
ListModel({
required this.listKey,
required this.removedItemBuilder,
Iterable<E>? initialItems,
}) : _items = List<E>.from(initialItems ?? <E>[]);
final GlobalKey<AnimatedListState> listKey;
final RemovedItemBuilder<E> removedItemBuilder;
final List<E> _items;
AnimatedListState? get _animatedList => listKey.currentState;
void insert(int index, E item) {
_items.insert(index, item);
_animatedList!.insertItem(index);
}
E removeAt(int index) {
final E removedItem = _items.removeAt(index);
if (removedItem != null) {
_animatedList!.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return removedItemBuilder(removedItem, context, animation);
},
);
}
return removedItem;
}
int get length => _items.length;
E operator [](int index) => _items[index];
int indexOf(E item) => _items.indexOf(item);
}
/// Displays its integer item as 'item N' on a Card whose color is based on
/// the item's value.
///
/// The text is displayed in bright green if [selected] is
/// true. This widget's height is based on the [animation] parameter, it
/// varies from 0 to 128 as the animation varies from 0.0 to 1.0.
class CardItem extends StatelessWidget {
const CardItem({
super.key,
this.onTap,
this.selected = false,
required this.animation,
required this.item,
}) : assert(item >= 0);
final Animation<double> animation;
final VoidCallback? onTap;
final int item;
final bool selected;
#override
Widget build(BuildContext context) {
TextStyle textStyle = Theme.of(context).textTheme.headline4!;
if (selected) {
textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
}
return Padding(
padding: const EdgeInsets.all(2.0),
child: SizeTransition(
sizeFactor: animation,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: SizedBox(
height: 80.0,
child: Card(
color: Colors.primaries[item % Colors.primaries.length],
child: Center(
child: Text('Item $item', style: textStyle),
),
),
),
),
),
);
}
}
I want to preserve favorite list with Hive in Flutter application.
The user can change the order of the list with ReorderableListView.
But Hive seems to be sorted with its key and we cannot store the order of the list.
Basically we implement ReorderableListView with following code.
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final int item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
},
However Hive does not support insert.
What is the best practice when we want to store list data, which is reordered, with hive?
There is a similar question, but its answer will conflict with keys and flutter will output error.
Flutter & Hive - How to Save Reordered List in Hive
class SortStudentsPage extends StatefulWidget {
final String title;
final List<int> students;
const SortStudentsPage({
Key? key,
required this.title,
required this.students,
}) : super(key: key);
#override
State<SortStudentsPage> createState() => _SortStudentsPageState();
}
class _SortStudentsPageState extends State<SortStudentsPage> {
List<int> students=[];
#override
void initState() {
super.initState();
students=widget.students;
}
#override
Widget build(BuildContext context) {
int groupId = (ModalRoute.of(context)!.settings.arguments
as Map<String, dynamic>)['id'];
return WillPopScope(
onWillPop: () async {
StorageService myStorage = StorageService();
await myStorage.setGroup(
groupId,
students.map((e) => e).toList(),
);
return Future.value(true);
},
child: Scaffold(
appBar: AppBarWidget(
title: widget.title,
isNavigate: true,
),
body: ReorderableListView.builder(
itemBuilder: (context, index) {
return Card(
key: Key(students[index].toString()),
child: ListTile(
title: Text(students[index].toString()),
),
);
},
itemCount: students.length,
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
int student = students[oldIndex];
students.removeAt(oldIndex);
students.insert(newIndex, student);
setState(() {});
},
),
),
);
}
}
Storage Service
class StorageService {
Future<void> setGroup(int groupId, List<int> contacts) async {
await Hive.box("groups").put("$groupId", contacts);
}
List<int>? getGroup(int groupId) {
return Hive.box("groups").get('$groupId');
}
static Future<void> init() async {
final appDocumentDir = await getApplicationDocumentsDirectory();
Hive.init(appDocumentDir.path);
await Hive.openBox("groups");
}
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await StorageService.init();
runApp(const MyApp());
}
Flutter newbie here. I'm trying to make a simple to-do list that I'm using to build up my skills. The idea is there is a to-do list that you can move the properties up and down and once you complete a task you check it off and it should display a checkmark and maybe eventually play an animation and remove it. For now, I'm stuck on just removing it, I had a few implementation ideas, but a lot of them will require me to restart my code. Is there a way to make it so that once I am done with a specific widget I can delete it or replace it with another, noting the fact that these widgets are in a list? code for reference:
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
var checkboxes = [
Checkboxstate(title: "test"),
Checkboxstate(title: "test2"),
Checkboxstate(title: "test3"),
Checkboxstate(title: "tes4t"),
];
bool value = true;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("To-Do"),
backgroundColor: Colors.purple,
),
backgroundColor: Colors.blue,
body: Container(
padding: EdgeInsets.all(10),
child: ReorderableListView(
shrinkWrap: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
var item = checkboxes.removeAt(oldIndex);
checkboxes.insert(newIndex, item);
});
},
children: [
...checkboxes.map(buildBox).toList(),
],
),
),
);
}
Card buildBox(Checkboxstate checkbox) {
return Card(
key: UniqueKey(),
child: CheckboxListTile(
title: Text(checkbox.title),
key: UniqueKey(),
controlAffinity: ListTileControlAffinity.leading,
value: checkbox.value,
onChanged: (value) {
setState(() {
checkboxes.removeAt();
checkbox.value = value;
});
},
),
);
}
}
class Checkboxstate {
String title;
bool value;
Checkboxstate({this.title, this.value = false});
}
EDIT:
As suggested by Prabhanshu I followed his steps and instead used the item builder; however, there is a new issue: the reorderablelistview now doesn't work. My idea was that the reason was that the index was different for the reorderable list than the checkbox widget I create, but I am still unable to find a solution.
new code:
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
var checkboxes = [
Checkboxstate(title: "test"),
Checkboxstate(title: "test2"),
Checkboxstate(title: "test3"),
Checkboxstate(title: "tes4t"),
];
bool value = true;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("To-Do"),
backgroundColor: Colors.purple,
),
backgroundColor: Colors.blue,
body: Container(
padding: EdgeInsets.all(10),
child: ReorderableListView.builder(
shrinkWrap: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
var item = checkboxes.removeAt(oldIndex);
checkboxes.insert(newIndex, item);
});
},
itemCount: checkboxes.length,
itemBuilder: (context, index) {
Checkboxstate box = checkboxes.elementAt(index);
return buildBox(box, index);
},
),
),
);
}
Card buildBox(Checkboxstate checkbox, int index) {
return Card(
key: UniqueKey(),
child: CheckboxListTile(
title: Text(checkbox.title),
key: UniqueKey(),
controlAffinity: ListTileControlAffinity.leading,
value: checkbox.value,
onChanged: (value) {
setState(() {
checkbox.value = value;
checkboxes.removeAt(index);
});
},
),
);
}
}
class Checkboxstate {
String title;
bool value;
Checkboxstate({this.title, this.value = false});
}
If drag to dismiss is an option:
What you're looking for here is the Dismissible Widget, which as the docs describe is a widget that can be dismissed by dragging in the indicated direction.
Check out the flutter cookbook tutorial for a detailed explanation
on how to use this widget.
This will definitely help you.
replace the build method body with this
Scaffold(
appBar: AppBar(
title: Text("To-Do"),
backgroundColor: Colors.purple,
),
backgroundColor: Colors.blue,
body: Container(
padding: EdgeInsets.all(10),
child: ReorderableListView.builder(
shrinkWrap: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
var item = checkboxes.removeAt(oldIndex);
checkboxes.insert(newIndex, item);
});
},
itemCount: checkboxes.length,
itemBuilder: (context, index) {
Checkboxstate box = checkboxes.elementAt(index);
return buildBox(box, index);
},
),
),
);
and
Replace this method also
Card buildBox(Checkboxstate checkbox, int index) {
return Card(
key: UniqueKey(),
child: CheckboxListTile(
title: Text(checkbox.title),
key: UniqueKey(),
controlAffinity: ListTileControlAffinity.leading,
value: checkbox.value,
onChanged: (value) {
setState(() {
checkboxes.removeAt(index);
checkbox.value = value;
});
},
),
);
}
Okay so Its kinda tough for me to explain but I will try my best. I am trying to make an app that follows Bloc state management. Right now, I have a bloc class that controls a bottom navigation bar and the widgets/fragments that it displays, one of those screens has a list view builder, now the state of the list view is also managed by a bloc builder. The bloc builder of the list is responsible for showing the loading state of the list (in the beginning) while the bloc class of this list view is responsible for fetching the data from the server by communicating with the API handler. my issue is that while the data is loading if the user clicks on the bottom navigation bar to show another screen and comes back to the original screen (the one that has the list view) then the data never gets loaded and the list view builder never creates the list items.
What I'm trying to do is when the user clicks on the bottom navigation bar to switch the current widget being viewed, I want the data to be loaded and the list to be built even when the tab/bottom navigation is not the one that has the list
The feed list widget class
// ignore: must_be_immutable
class FeedList extends StatelessWidget {
final ScrollController _scrollController = new ScrollController();
final int _incrementValue = 5;
final FeedListBloc _feedListBloc = FeedListBloc();
int _start = 0;
int _end = 10;
bool fromHome;
int get start => _start;
int get end => _end;
List<Tournament> tournaments = new List<Tournament>();
FeedList({this.fromHome:false}) {
print("feed list constructor");
_feedListBloc.fetchTournaments(start, end, isHomeFeed: fromHome);
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_feedListBloc.fetchTournaments(start, end, isHomeFeed: true);
}
});
}
#override
Widget build(BuildContext context) {
return BlocListener(
bloc: _feedListBloc,
listener: (context, FeedListState state) {
if (state is ErrorState)
Scaffold.of(context).showSnackBar(SnackBar(
action: SnackBarAction(
textColor: Theme.of(context).accentColor,
label: "dismiss",
onPressed: () {
Scaffold.of(context).hideCurrentSnackBar();
},
),
duration: Duration(seconds: 3),
content: Text(
state.errorMSG,
style:
Theme.of(context).textTheme.headline1.copyWith(fontSize: 12),
),
elevation: 0,
backgroundColor: Color(0xff363636),
behavior: SnackBarBehavior.floating,
));
else if (state is DataLoadedState) {
if (state.tournaments.length > 0) {
_incrementStartEnd();
tournaments.addAll(state.tournaments);
}
}
},
child: BlocBuilder(
bloc: _feedListBloc,
builder: (context, FeedListState state) {
if (state is ShimmerState) {
return ListView.builder(
itemCount: 3,
itemBuilder: (context, index) => Shimmer.fromColors(
child: FeedListItem(null),
baseColor: Color(0xFF1D1D1D),
highlightColor: Color(0xff202020)));
} else {
return RefreshIndicator(
onRefresh: () async {
await refreshFeed();
},
child: ListView.builder(
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemCount: tournaments.length + 1,
itemBuilder: (context, index) => _getListItems(context, index),
),
);
}
},
),
);
}
_getListItems(BuildContext context, int index) {
if (index >= tournaments.length && tournaments.isNotEmpty)
return Padding(
padding: const EdgeInsets.only(bottom: 35),
child: Center(child: CircularProgressIndicator()),
);
else if (index == tournaments.length - 1)
return Padding(
padding: const EdgeInsets.only(bottom: 30),
child: FeedListItem(tournaments[index]),
);
else if (tournaments.isNotEmpty) return FeedListItem(tournaments[index]);
}
void dispose() {
_feedListBloc.close();
}
void _incrementStartEnd() {
print("incremented");
_start = _end;
_end += _incrementValue;
}
refreshFeed() async {
tournaments.clear();
_start = 0;
_end = 10;
await _feedListBloc.fetchTournaments(start, end, isHomeFeed: fromHome);
}
}
the feed list bloc class
class FeedListBloc extends Bloc<FeedListEvent, FeedListState> {
Future<void> fetchTournaments(int start, int end,
{bool isHomeFeed: false}) async {
await Future.delayed(Duration(seconds: 3));
dynamic reply =
await API.getHomeFeedTournaments(start, end, isHomeFeed: isHomeFeed);
if (reply == null)
this.add(ErrorEvent("could not load "));
else if (reply is String)
this.add(ErrorEvent(reply));
else
this.add(DataLoadedEvent(reply as List<Tournament>));
}
#override
FeedListState get initialState => FeedListState.initState();
#override
Stream<FeedListState> mapEventToState(FeedListEvent event) async* {
if (event is ErrorEvent)
yield FeedListState.errorState(event.errorMsg);
else if (event is DataLoadedEvent)
yield FeedListState.dataLoaded(event.tournaments);
}
}
please tell me if what I'm trying to do is wrong or if there is a better way to do it. essentially I am doing it like this because I don't want an API call to be made every time the user switches screens but I do want the list to be built even if the user switches screens, the 3 seconds delay in the bloc fetch more method was for me to see understand what was exactly happening
If you're just trying to update the data displayed on your widgets using flutter_bloc, you can either use BlocListener or BlocBuilder. Once the Bloc has been updated from your action, changes on data can be handled inside those widgets.
I'm making a question answer application to pass time.
There are questions in a question list. The code below shows for each question. A question can have multiple choices but only 1 answer.
I'm having trouble where everytime I type in the TextFormField, the cursor resets to the beginning. How can i avoid that? I need to use setState so that the Text Widget updates in realtime.
class Question{
int answer;
List<Answer> answers;
}
class Answer{
int index;
String answer;
}
class EditQuestionWidget extends StatefulWidget {
final List<Question> questions;
final int index;
EditQuestionWidget(this.questions, this.index);
#override
_EditQuestionWidgetState createState() => _EditQuestionWidgetState();
}
class _EditQuestionWidgetState extends State<EditQuestionWidget> {
#override
Widget build(BuildContext context) {
return Card( //For Each Question
child: ExpansionTile(
title: Text(widget.questions[widget.index].answer != null ? widget.questions[widget.index].answers[widget.questions[widget.index].answer].answer ?? 'Untitled Answer' : 'Untitled Answer'),
children: <Widget>[
ListView.builder( //For Each answer choice
itemCount: widget.questions[widget.index].answers.length,
itemBuilder: (context, i) {
TextEditingController answerController = TextEditingController(text: widget.questions[widget.index].answers[i].answer);
return Row(
children: <Widget>[
Expanded(
child: RadioListTile( //Select Answer
value: widget.questions[widget.index].answers[i].index,
groupValue: widget.questions[widget.index].answer,
onChanged: (int value) => setState(() => widget.questions[widget.index].answer = value),
title: TextFormField( //change answer choice text
controller: answerController,
onChanged: (text) {
setState(() {widget.questions[widget.index].answers[i].answer = text;}); // The cursor returns to beginning after executing this.
},
),
),
),
IconButton( //Remove Answer Function
onPressed: () {
//This method Works as intended.
if (widget.questions[widget.index].answers[i].index == widget.questions[widget.index].answer) widget.questions[widget.index].answer = null;
String answer = widget.questions[widget.index].answer != null ? widget.questions[widget.index].answers[widget.questions[widget.index].answer].answer : '';
widget.questions[widget.index].answers.remove(widget.questions[widget.index].answers[i]);
if (widget.questions[widget.index].answers.isEmpty) {
widget.questions[widget.index].answer = widget.questions[widget.index].answers.length;
widget.questions[widget.index].answers.add(new Answer(index: widget.questions[widget.index].answers.length));
}
for (int ii = 0; ii < widget.questions[widget.index].answers.length; ii++) {
widget.questions[widget.index].answers[ii].index = ii;
if (widget.questions[widget.index].answer != null && answer == widget.questions[widget.index].answers[ii].answer) widget.questions[widget.index].answer = ii;
}
setState(() {});
},
),
],
);
},
),
],
),
);
}
}
You're updating the value of answerController every time the text changes, and when this update happens the cursor goes to the beginning.
To solve that you can create a list of TextEditingControllers outside the builder:
List<TextEditingController> controllers = [];
And then add a new Controller to the list when a new field is created:
itemBuilder: (context, i) {
if(controllers.length != widget.questions[widget.index].answers.length){
controllers.add(TextEditingController()); //condition to prevent the creation of infinity Controllers
}
And finally pass the controller to the TextField:
controller: controllers[i],