I have a listview that I want to enable shortcuts like Ctrl+c, Enter, etc this improves user experience.
The issue is after I click/tap on an item, it loses focus and the shortcut keys no longer work.
Is there a fix or a workaround for this?
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
void main() {
runApp(const MyApp());
}
class SomeIntent extends Intent {}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetBuilder<Controller>(
init: Get.put(Controller()),
builder: (controller) {
final List<MyItemModel> myItemModelList = controller.myItemModelList;
return Scaffold(
appBar: AppBar(
title: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (event) {
if (event.logicalKey.keyLabel == 'Arrow Down') {
FocusScope.of(context).nextFocus();
}
},
child: const TextField(
autofocus: true,
),
),
),
body: myItemModelList.isEmpty
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemBuilder: (context, index) {
final MyItemModel item = myItemModelList[index];
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): SomeIntent(),
},
child: Actions(
actions: {
SomeIntent: CallbackAction<SomeIntent>(
// this will not launch if I manually focus on the item and press enter
onInvoke: (intent) => print(
'SomeIntent action was launched for item ${item.name}'),
)
},
child: InkWell(
focusColor: Colors.blue,
onTap: () {
print('clicked item $index');
controller.toggleIsSelected(item);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: myItemModelList[index].isSelected
? Colors.green
: null,
height: 50,
child: ListTile(
title: Text(myItemModelList[index].name),
subtitle: Text(myItemModelList[index].detail),
),
),
),
),
),
);
},
itemCount: myItemModelList.length,
),
);
},
);
}
}
class Controller extends GetxController {
List<MyItemModel> myItemModelList = [];
#override
void onReady() {
myItemModelList = buildMyItemModelList(100);
update();
super.onReady();
}
List<MyItemModel> buildMyItemModelList(int count) {
return Iterable<MyItemModel>.generate(
count,
(index) {
return MyItemModel('$index - check debug console after pressing Enter.',
'$index - click me & press Enter... nothing happens\nfocus by pressing TAB/Arrow Keys and press Enter.');
},
).toList();
}
toggleIsSelected(MyItemModel item) {
for (var e in myItemModelList) {
if (e == item) {
e.isSelected = !e.isSelected;
}
}
update();
}
}
class MyItemModel {
final String name;
final String detail;
bool isSelected = false;
MyItemModel(this.name, this.detail);
}
Tested with Windows 10 and flutter 3.0.1
Using Get State manager.
In Flutter, a ListView or GridView containing a number of ListTile widgets, you may notice that the selection and the focus are separate. We also have the issue of tap() which ideally sets both the selection and the focus - but by default tap does nothing to affect focus or selection.
The the official demo of ListTile selected property https://api.flutter.dev/flutter/material/ListTile/selected.html
shows how we can manually implement a selected ListTile and get tap() to change the selected ListTile. But this does nothing for us in terms of synchronising focus.
Note: As that demo shows, tracking the selected ListTile needs to
be done manualy, by having e.g. a selectedIndex variable, then setting the
selected property of a ListTile to true if the index matches the
selectedIndex.
Here are a couple of solutions to the problem of to the syncronising focus, selected and tap in a listview.
Solution 1 (deprecated, not recommended):
The main problem is accessing focus behaviour - by default we have no access
to each ListTile's FocusNode.
UPDATE: Actually it turns out that there is a way to access a focusnode, and thus allocating our own focusnodes is not necessary - see Solution 2 below. You use the Focus widget with a child: Builder(builder: (BuildContext context) then you can access the focusnode with FocusScope.of(context).focusedChild. I am leaving this first solution here for study, but recommend solution 2 instead.
But by allocating a focus node for each ListTile item in the
ListView, we then do. You see, normally a ListTile item allocates its own focus
node, but that's bad for us because we want to access each focus node from
the outside. So we allocate the focus nodes ourselves and pass them to the
ListTile items as we build them, which means a ListTile no longer has to
allocate a FocusNode itself - note: this is not a hack - supplying custom
FocusNodes is supported in the ListTile API. We now get access to the
FocusNode object for each ListTile item, and
invoke its requestFocus()
method whenever selection changes.
we also listen in the FocusNode
objects for changes in focus, and update the selection whenever focus
changes.
The benefits of custom focus node which we supply ourselves to each ListTile
are:
We can access the focus node from outside the ListTile widget.
We can use the focus node to request focus.
We can listen to changes in focus.
BONUS: We can wire shortcuts directly into the focus node without the usual Flutter shortcut complexity.
This code synchronises selection, focus and tap behaviour, as well as supporting up and down arrow changing the selection.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// Enhancements to the official ListTile 'selection' demo
// https://api.flutter.dev/flutter/material/ListTile/selected.html to
// incorporate Andy's enhancements to sync tap, focus and selected.
// This version includes up/down arrow key support.
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title =
'Synchronising ListTile selection, focus and tap - with up/down arrow key support';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatefulWidget(),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _selectedIndex = 0;
late List _focusNodes; // our custom focus nodes
void changeSelected(int index) {
setState(() {
_selectedIndex = index;
});
}
void changeFocus(int index) {
_focusNodes[index].requestFocus(); // this works!
}
// initstate
#override
void initState() {
super.initState();
_focusNodes = List.generate(
10,
(index) => FocusNode(onKeyEvent: (node, event) {
print(
'focusnode detected: ${event.logicalKey.keyLabel} ${event.runtimeType} $index ');
// The focus change that happens when the user presses TAB,
// SHIFT+TAB, UP and DOWN arrow keys happens on KeyDownEvent (not
// on the KeyUpEvent), so we ignore the KeyDownEvent and let
// Flutter do the focus change. That way we don't need to worry
// about programming manual focus change ourselves, say, via
// methods on the focus nodes, which would be an unecessary
// duplication.
//
// Once the focus change has happened naturally, all we need to do
// is to change our selected state variable (which we are manually
// managing) to the new item position (where the focus is now) -
// we can do this in the KeyUpEvent. The index of the KeyUpEvent
// event will be item we just moved focus to (the KeyDownEvent
// supplies the old item index and luckily the corresponding
// KeyUpEvent supplies the new item index - where the focus has
// just moved to), so we simply set the selected state value to
// that index.
if (event.runtimeType == KeyUpEvent &&
(event.logicalKey == LogicalKeyboardKey.arrowUp ||
event.logicalKey == LogicalKeyboardKey.arrowDown ||
event.logicalKey == LogicalKeyboardKey.tab)) {
changeSelected(index);
}
return KeyEventResult.ignored;
}));
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return ListTile(
focusNode: _focusNodes[
index], // allocate our custom focus node for each item
title: Text('Item $index'),
selected: index == _selectedIndex,
onTap: () {
changeSelected(index);
changeFocus(index);
},
);
},
);
}
}
Important Note: The above solution doesn't work when changing the number of items, because all the focusnodes are allocated during initState which only gets called once. For example if the number of items increases then there are not enough focusnodes to go around and the build step will crash.
The next solution (below) does not explicitly allocate focusnodes and is a more robust solution which supports rebuilding and adding and removing items dynamically.
Solution 2 (allows rebuilds, recommended)
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:developer' as developer;
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Flutter selectable listview - solution 2';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: HomeWidget(),
);
}
}
// ╦ ╦┌─┐┌┬┐┌─┐╦ ╦┬┌┬┐┌─┐┌─┐┌┬┐
// ╠═╣│ ││││├┤ ║║║│ │││ ┬├┤ │
// ╩ ╩└─┘┴ ┴└─┘╚╩╝┴─┴┘└─┘└─┘ ┴
class HomeWidget extends StatefulWidget {
const HomeWidget({super.key});
#override
State<HomeWidget> createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
// generate a list of 10 string items
List<String> _items = List<String>.generate(10, (int index) => 'Item $index');
String currentItem = '';
int currentIndex = 0;
int redrawTrigger = 0;
// clear items method inside setstate
void _clearItems() {
setState(() {
currentItem = '';
_items.clear();
});
}
// add items method inside setstate
void _rebuildItems() {
setState(() {
currentItem = '';
_items.clear();
_items.addAll(List<String>.generate(5, (int index) => 'Item $index'));
});
}
// set currentItem method inside setstate
void _setCurrentItem(String item) {
setState(() {
currentItem = item;
currentIndex = _items.indexOf(item);
});
}
// set currentindex method inside setstate
void _setCurrentIndex(int index) {
setState(() {
currentIndex = index;
if (index < 0 || index >= _items.length) {
currentItem = '';
} else {
currentItem = _items[index];
}
});
}
// delete current index method inside setstate
void _deleteCurrentIndex() {
// ensure that the index is valid
if (currentIndex >= 0 && currentIndex < _items.length) {
setState(() {
String removedValue = _items.removeAt(currentIndex);
if (removedValue.isNotEmpty) {
print('Item index $currentIndex deleted, which was $removedValue');
// calculate new focused index, if have deleted the last item
int newFocusedIndex = currentIndex;
if (newFocusedIndex >= _items.length) {
newFocusedIndex = _items.length - 1;
}
_setCurrentIndex(newFocusedIndex);
print('setting new newFocusedIndex to $newFocusedIndex');
} else {
print('Failed to remove $currentIndex');
}
});
} else {
print('Index $currentIndex is out of range');
}
}
#override
Widget build(BuildContext context) {
// print the current time
print('HomeView build at ${DateTime.now()} $_items');
return Scaffold(
body: Column(
children: [
// display currentItem
Text(currentItem),
Text(currentIndex.toString()),
ElevatedButton(
child: Text("Force Draw"),
onPressed: () => setState(() {
redrawTrigger = redrawTrigger + 1;
}),
),
ElevatedButton(
onPressed: () {
_setCurrentItem('Item 0');
redrawTrigger = redrawTrigger + 1;
},
child: const Text('Set to Item 0'),
),
ElevatedButton(
onPressed: () {
_setCurrentIndex(1);
redrawTrigger = redrawTrigger + 1;
},
child: const Text('Set to index 1'),
),
// button to clear items
ElevatedButton(
onPressed: _clearItems,
child: const Text('Clear Items'),
),
// button to add items
ElevatedButton(
onPressed: _rebuildItems,
child: const Text('Rebuild Items'),
),
// button to delete current item
ElevatedButton(
onPressed: _deleteCurrentIndex,
child: const Text('Delete Current Item'),
),
Expanded(
key: ValueKey('${_items.length} $redrawTrigger'),
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
// print(' building listview index $index');
return FocusableText(
_items[index],
autofocus: index == currentIndex,
updateCurrentItemParentCallback: _setCurrentItem,
deleteCurrentItemParentCallback: _deleteCurrentIndex,
);
},
itemCount: _items.length,
),
),
],
),
);
}
}
// ╔═╗┌─┐┌─┐┬ ┬┌─┐┌─┐┌┐ ┬ ┌─┐╔╦╗┌─┐─┐ ┬┌┬┐
// ╠╣ │ ││ │ │└─┐├─┤├┴┐│ ├┤ ║ ├┤ ┌┴┬┘ │
// ╚ └─┘└─┘└─┘└─┘┴ ┴└─┘┴─┘└─┘ ╩ └─┘┴ └─ ┴
class FocusableText extends StatelessWidget {
const FocusableText(
this.data, {
super.key,
required this.autofocus,
required this.updateCurrentItemParentCallback,
required this.deleteCurrentItemParentCallback,
});
/// The string to display as the text for this widget.
final String data;
/// Whether or not to focus this widget initially if nothing else is focused.
final bool autofocus;
final updateCurrentItemParentCallback;
final deleteCurrentItemParentCallback;
#override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.keyX): () {
print('X pressed - attempting to delete $data');
deleteCurrentItemParentCallback();
},
},
child: Focus(
autofocus: autofocus,
onFocusChange: (value) {
print(
'$data onFocusChange ${FocusScope.of(context).focusedChild}: $value');
if (value) {
updateCurrentItemParentCallback(data);
}
},
child: Builder(builder: (BuildContext context) {
// The contents of this Builder are being made focusable. It is inside
// of a Builder because the builder provides the correct context
// variable for Focus.of() to be able to find the Focus widget that is
// the Builder's parent. Without the builder, the context variable used
// would be the one given the FocusableText build function, and that
// would start looking for a Focus widget ancestor of the FocusableText
// instead of finding the one inside of its build function.
developer.log('build $data', name: '${Focus.of(context)}');
return GestureDetector(
onTap: () {
Focus.of(context).requestFocus();
// don't call updateParentCallback('data') here, it will be called by onFocusChange
},
child: ListTile(
leading: Icon(Icons.map),
selectedColor: Colors.red,
selected: Focus.of(context).hasPrimaryFocus,
title: Text(data),
),
);
}),
),
);
}
}
Edit:
this works to regain focus, however, the focus starts again from the top widget and not from the widget that was clicked on. I hope this answer still helps
Edit 2 I found a solution, you'll have to create a separate FocusNode() for each element on your listview() and requestFocus() on that in your inkwell. Complete updated working example (use this one, not the one in the original answer):
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class SomeIntent extends Intent {}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final myItemModelList = List.generate(10, (index) => Text('${index + 1}'));
final _focusNodes = List.generate(myItemModelList.length, (index) => FocusNode());
return Scaffold(
appBar: AppBar(),
body: myItemModelList.isEmpty
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemBuilder: (context, index) {
final item = myItemModelList[index];
return RawKeyboardListener(
focusNode: _focusNodes[index],
onKey: (event) {
if (event.logicalKey.keyLabel == 'Arrow Down') {
FocusScope.of(context).nextFocus();
}
},
child: Actions(
actions: {
SomeIntent: CallbackAction<SomeIntent>(
// this will not launch if I manually focus on the item and press enter
onInvoke: (intent) => print(
'SomeIntent action was launched for item ${item}'),
)
},
child: InkWell(
focusColor: Colors.blue,
onTap: () {
_focusNodes[index].requestFocus();
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.blue,
height: 50,
child: ListTile(
title: myItemModelList[index],
subtitle: myItemModelList[index]),
),
),
),
),
);
},
itemCount: myItemModelList.length,
),
);
}
}
Edit 3:
To also detect the up key you can try:
onKey: (event) {
if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) {
FocusScope.of(context).nextFocus();
} else if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) {
FocusScope.of(context).previousFocus();
}
},
Original answer (you should still read to understand the complete answer).
First of all, your adding RawKeyboardListener() within your appBar() don't do that, instead add it to the Scaffold().
Now, create a FocusNode() outside of your Build method:
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key}) : super(key: key);
final _focusNode = FocusNode();
#override
Widget build(BuildContext context) {}
...
...
And assing the _focusNode to the RawKeyboardListener():
RawKeyboardListener(focusNode: _focusNode,
...
And here's the key point. Since you don't want to lose focus in the ListView(), in the onTap of your inkWell you'll have to request focus again:
InkWell(
focusColor: Colors.blue,
onTap: () {
_focusNode.requestFocus();
print('clicked item $index');
},
...
That's it.
Here is a complete working example based on your code. (I needed to modify some things, since I don't have all your data):
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class SomeIntent extends Intent {}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key}) : super(key: key);
final _focusNode = FocusNode();
#override
Widget build(BuildContext context) {
final myItemModelList = List.generate(10, (index) => Text('${index + 1}'));
return Scaffold(
appBar: AppBar(),
body: myItemModelList.isEmpty
? const Center(child: CircularProgressIndicator())
: RawKeyboardListener(
focusNode: _focusNode,
onKey: (event) {
if (event.logicalKey.keyLabel == 'Arrow Down') {
FocusScope.of(context).nextFocus();
}
},
child: ListView.builder(
itemBuilder: (context, index) {
final item = myItemModelList[index];
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): SomeIntent(),
},
child: Actions(
actions: {
SomeIntent: CallbackAction<SomeIntent>(
// this will not launch if I manually focus on the item and press enter
onInvoke: (intent) => print(
'SomeIntent action was launched for item ${item}'),
)
},
child: InkWell(
focusColor: Colors.blue,
onTap: () {
_focusNode.requestFocus();
print('clicked item $index');
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.blue,
height: 50,
child: ListTile(
title: myItemModelList[index],
subtitle: myItemModelList[index]),
),
),
),
),
);
},
itemCount: myItemModelList.length,
),
),
);
}
}
Demo:
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.
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.
I'm using photo_view_gallery class and I would like to show dynamically the value of current image index of PhotoViewGallery.builder into the appBar.
I'm new in Flutter, I googled a lot but I can't found any solution for this
body: PhotoViewGallery.builder(
itemCount: listaPagine.length,
builder: (context, index) {
saveIndex(index);
String myImg =
'http://www.attilofficina.altervista.org/phpbackend/JOB/000004/fullsize/' +
listaPagine[index].nomefile.toString() +
'.jpg';
return PhotoViewGalleryPageOptions(
imageProvider: AdvancedNetworkImage(myImg,
retryLimit: 1, timeoutDuration: Duration(seconds: 30)),
);
},
),
I also try a function that save index to another variable, but the value is still unavailable in appBar
Here the code and position of this function (in appBar is shown null)
class GalleryPageState extends State<GalleryPage> {
int curr;
...
#override
Widget build(BuildContext context) {
...
saveIndex(int index) {
int curr = index;
print('*** curr = ' + curr.toString()); /// PRINTS CORRECT VALUE
return curr;
}
...
return Scaffold(
appBar: AppBar(
title: Text(curr.toString(), /// BUT IT SHOWS NULL
),
),
body: PhotoViewGallery.builder(
itemCount: listaPagine.length,
builder: (context, index) {
salvaIndex(index);
String myImg =
'http://www.attilofficina.altervista.org/phpbackend/JOB/000004/fullsize/' +
listaPagine[index].nomefile.toString() +
'.jpg';
return PhotoViewGalleryPageOptions(
imageProvider: AdvancedNetworkImage(myImg,
retryLimit: 1, timeoutDuration: Duration(seconds: 30)),
);
},
),
);
}
}
Can someone help me?
special thanks mario
I think you can use onPageChanged
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
final List<String> listaPagine = [
'https://picsum.photos/id/451/200/300',
'https://picsum.photos/id/200/200/300'
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('# $_currentIndex'),
),
body: PhotoViewGallery.builder(
itemCount: listaPagine.length,
builder: (BuildContext context, int index) {
String myImg = listaPagine[index];
return PhotoViewGalleryPageOptions(
imageProvider: NetworkImage(myImg),
);
},
onPageChanged: (int index) {
setState(() {
_currentIndex = index;
});
},
),
);
}
}
There's three things wrong in your following code:
saveIndex(int index) {
int curr = index;
print('*** curr = ' + curr.toString()); /// PRINTS CORRECT VALUE
return curr;
}
Here you are creating new int curr variable which should be curr to use the existing global scope variable.
You should create the saveIndex method outside of build function.
Also you should update the variable in setState like
setState((){
curr=index;
});
This will update the variable and recreate the widget tree with AppBar with updated values.
Also you don't need the return statement in saveIndex
Editing for #attila's help
The basic about the state is, it just keep the current state or values in memory. You can always change any value & it will be updated in the state but it will not reflected on the UI. Because your application or view still don't know that some value is updated & it needs to re-render the UI.
That's where the setState come in picture. What it does is while updating the value it also call the build function so new UI with updated values could be re-rendered on screen.
To test it, you can just update the values & call setState((){}); after that. It will still work as same.
Hope this helps you.