Related
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:
This is my HomePage.dart Codes. Here I Want to access the _pushSaved() function into the 'onpressed' property from a different file named "randomwords.dart" file (code attached for review). It seems I need to declare the _pushSaved() somewhere but do not have any idea as I am new to dart and flutter. I Will be thankful if somebody could help. Thanks in advance.
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("StartUp Name Generator"),
actions: [
IconButton(icon: Icon(Icons.list), **onPressed: _pushSaved**)
],
),
body: RandomWords(),
);
}
}
The _pushSaved() function code in the randomwords.dart file needs to be accessed in the HomePage.dart as object...
class RandomWords extends StatefulWidget {
#override
_RandomWordsState createState() => _RandomWordsState();
}
class _RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = Set<WordPair>();
final _biggerFont = TextStyle(fontSize: 20.0, color: Colors.pink);
#override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
},
);
}
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
);
}
void **_pushSaved()** {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
final tiles = _saved.map(
(WordPair pair) {
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided =
ListTile.divideTiles(tiles: tiles, context: context).toList();
return Scaffold(
appBar: AppBar(
title: Text("Saved Suggestions"),
),
body: ListView(
children: divided,
),
);
},
),
);
}
}
Although the answer above solves your problem , but thats not a good practice , use absctract classes for defining functions that are supposed to be used at various places.
Your function is defined as a private property. You need to change that first.
pushSaved()
Secondly, You need to define this function as a static function to access it in other classes.
static void pushSaved()
Now call your function in your HomePage class in onPressed function like this:
onPressed: () => RandomWords.pushSaved
Import the HomePage.dart in the Homepage file like this;
import 'package:main.dart' as main; //it doesn't have to be as 'main'
and then acces the function like this:
IconButton(icon: Icon(Icons.list),*onPressed: main._pushSaved)
Note: now when I've done somthing like this and my function startes with a _ it unfortunantly doesn't work, I know that everyone makes functions with _ first, but apperantly it didn't work for me when importing it from another file. So beware of that.
Also the function has to be out of the _RandomWordsState to be accesed by the other file. Just put it down underneath.
I want to load pages from a List and when the user taps on an item from the drawer he can go to that page (if it's already opened) otherwise the Widget will load in the selected page.
But I can't find if that widget is already exists in the List if(myList.contains(Widget1())) => print('it exist'); One guy told me to override hashCode and operator==
class Widget6 extends StatelessWidget {
final String title = 'Widget6';
final Icon icon = Icon(Icons.assessment);
#override
Widget build(BuildContext context) {
return Center(
child: icon,
);
}
#override
bool operator ==(dynamic other) {
final Widget6 typedOther = other;
return title == typedOther.title && icon == typedOther.icon;
}
#override
int get hashCode => hashValues(title, icon);
}
if I do that I can't use any child widget to those widgets. Getting exception like: type 'Center' is not a subtype of type 'Widget6'. I copied this from flutter gallery I didn't find good documentation/guide. Sorry, I am a beginner.
Complete code below
class _MyHomePageState extends State<MyHomePage> {
List pageList = [
Widget1(),
Widget2(),
Widget3(),
Widget4(),
];
PageController _pageController;
int _selectedIndex = 0;
#override
void initState() {
_pageController = PageController(
initialPage: _selectedIndex,
);
super.initState();
}
void navigatePage(Widget widget) {
// problem is here
if (pageList.contains(widget)) {
_pageController.animateToPage(pageList.indexOf(widget, 0),
duration: Duration(milliseconds: 300), curve: Curves.ease);
}
else {
setState(() {
pageList.removeAt(_pageController.page.toInt());
pageList.insert(_pageController.page.toInt(), widget);
});
_pageController.animateToPage(_pageController.page.toInt(),
duration: Duration(milliseconds: 300), curve: Curves.ease);
}
Navigator.pop(context);
}
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: ListView(
children: <Widget>[
ListTile(
title: Text('Widget1'),
onTap: () => navigatePage(
Widget1(),
),
),
ListTile(
title: Text('Widget2'),
onTap: () => navigatePage(
Widget2(),
),
),
ListTile(
title: Text('Widget3'),
onTap: () => navigatePage(
Widget3(),
),
),
ListTile(
title: Text('Widget4'),
onTap: () => navigatePage(
Widget4(),
),
),
ListTile(
title: Text('Widget5'),
onTap: () => navigatePage(
Widget5(),
),
),
ListTile(
title: Text('Widget6'),
onTap: () => navigatePage(
Widget6(),
),
),
],
),
),
appBar: AppBar(
title: Text(widget.title),
),
body: PageView.builder(
onPageChanged: (newPage) {
setState(() {
this._selectedIndex = newPage;
});
},
controller: _pageController,
itemBuilder: (context, index) {
return Container(
child: pageList[index],
);
},
itemCount: pageList.length,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() {
_selectedIndex = index;
_pageController.animateToPage(index,
duration: Duration(milliseconds: 300), curve: Curves.ease);
}),
items: pageList.map((page) {
return BottomNavigationBarItem(
backgroundColor: Colors.deepOrangeAccent,
icon: page.icon,
title: Text(page.title));
}).toList(),
),
);
}
}
Here List of dummy Widgets
class Widget1 extends StatelessWidget {
final String title = 'Widget1';
final Icon icon = Icon(Icons.school);
#override
Widget build(BuildContext context) {
return Center(
child: icon,
);
}
}
class Widget2 extends StatelessWidget {
// only title and icon are changed
}
class Widget3 extends StatelessWidget {
// only title and icon are changed
}
class Widget4 extends StatelessWidget {
// only title and icon are changed
}
class Widget5 extends StatelessWidget {
// only title and icon are changed
}
class Widget6 extends StatelessWidget {
// only title and icon are changed
}
Okay, I found the solution. And it has to do with operator== overriding
I missed this line if (runtimeType != other.runtimeType) return false;
The whole code stays the same.
#override
// ignore: hash_and_equals
bool operator ==(dynamic other) {
if (runtimeType != other.runtimeType) return false;
final Widget6 typedOther = other;
return title == typedOther.title;
}
#Ahmed Sorry for the late reply, I decided to put it in an answer rather than a comment.
One solution is yours, overriding == but I was thinking of using Key and then instead of using contains method, using something like:
if(myList.indexWhere((Widget widget)=> widget.key==_key) != -1)...
Suggestion
You can store icon and title as a map or a module instead of making 6 different Widget.
You can create another file, saying module.dart like this:
class Module {
final String title;
final Icon icon;
Module(this.title, this.icon);
#override
int get hashCode => hashValues(title.hashCode, icon.hashCode);
#override
bool operator ==(other) {
if (!identical(this, other)) {
return false;
}
return other is Module &&
this.title.compareTo(other.title) == 0 &&
this.icon == other.icon;
}
}
Then create another file that builds the page, saying mywidget.dart, like this:
class MyWidget extends StatelessWidget {
final Module module;
MyWidget({Key key,#required this.module}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: module.icon,
);
}
}
Then on each ListTile's onTap, Navigate like this:
...
ListTile(
title: Text('Widget1'),
onTap: () => navigatePage(
MyWidget(module: Module('Widget1', Icon(Icons.school)),)
),
),
...
So instead of storing Widgets, you store a Type(Here Module) that you declared.
You can also use the list's map to build each ListTile of the ListView for each Module, instead of doing it one by one. (if each item on the drawer are similar), Something like this:
List<Module> myTabs = [
Module('Widget1', Icon(Icons.school)),
Module('Widget2', Icon(Icons.home)),
];
...
Drawer(
child: ListView(
children:myTabs.map((Module module)=> ListTile(
title:Text( module.title),
onTap: navigatePage(MyWidget(module: module,)),
)).toList(),
) ,
);
...
I am learning app development on Flutter and can't get my Slider to work within the AlertDialog. It won't change it's value.
I did search the problem and came across this post on StackOverFlow:
Flutter - Why slider doesn't update in AlertDialog?
I read it and have kind of understood it. The accepted answer says that:
The problem is, dialogs are not built inside build method. They are on a different widget tree. So when the dialog creator updates, the dialog won't.
However I am not able to understand how exactly does it have to be implemented as not enough background code is provided.
This is what my current implementation looks like:
double _fontSize = 1.0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(qt.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.format_size),
onPressed: () {
getFontSize(context);
},
),
],
),
body: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 15.0),
itemCount: 3,
itemBuilder: (context, index) {
if (index == 0) {
return _getListTile(qt.scripture, qt.reading);
} else if (index == 1) {
return _getListTile('Reflection:', qt.reflection);
} else {
return _getListTile('Prayer:', qt.prayer);
}
})
);
}
void getFontSize(BuildContext context) {
showDialog(context: context,builder: (context){
return AlertDialog(
title: Text("Font Size"),
content: Slider(
value: _fontSize,
min: 0,
max: 100,
divisions: 5,
onChanged: (value){
setState(() {
_fontSize = value;
});
},
),
actions: <Widget>[
RaisedButton(
child: Text("Done"),
onPressed: (){},
)
],
);
});
}
Widget parseLargeText(String text) {...}
Widget _getListTile(String title, String subtitle) {...}
I understand that I will need to make use of async and await and Future. But I am not able to understand how exactly. I've spent more than an hour on this problem and can't any more. Please forgive me if this question is stupid and noobish. But trust me, I tried my best.
Here is a minimal runnable example. Key points:
The dialog is a stateful widget that stores the current value in its State. This is important because dialogs are technically separate "pages" on your app, inserted higher up in the hierarchy
Navigator.pop(...) to close the dialog and return the result
Usage of async/await
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double _fontSize = 20.0;
void _showFontSizePickerDialog() async {
// <-- note the async keyword here
// this will contain the result from Navigator.pop(context, result)
final selectedFontSize = await showDialog<double>(
context: context,
builder: (context) => FontSizePickerDialog(initialFontSize: _fontSize),
);
// execution of this code continues when the dialog was closed (popped)
// note that the result can also be null, so check it
// (back button or pressed outside of the dialog)
if (selectedFontSize != null) {
setState(() {
_fontSize = selectedFontSize;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Font Size: ${_fontSize}'),
RaisedButton(
onPressed: _showFontSizePickerDialog,
child: Text('Select Font Size'),
)
],
),
),
);
}
}
// move the dialog into it's own stateful widget.
// It's completely independent from your page
// this is good practice
class FontSizePickerDialog extends StatefulWidget {
/// initial selection for the slider
final double initialFontSize;
const FontSizePickerDialog({Key key, this.initialFontSize}) : super(key: key);
#override
_FontSizePickerDialogState createState() => _FontSizePickerDialogState();
}
class _FontSizePickerDialogState extends State<FontSizePickerDialog> {
/// current selection of the slider
double _fontSize;
#override
void initState() {
super.initState();
_fontSize = widget.initialFontSize;
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Font Size'),
content: Container(
child: Slider(
value: _fontSize,
min: 10,
max: 100,
divisions: 9,
onChanged: (value) {
setState(() {
_fontSize = value;
});
},
),
),
actions: <Widget>[
FlatButton(
onPressed: () {
// Use the second argument of Navigator.pop(...) to pass
// back a result to the page that opened the dialog
Navigator.pop(context, _fontSize);
},
child: Text('DONE'),
)
],
);
}
}
You just need to warp the AlertDialog() with a StatefulBuilder()
Link to source on github: https://github.com/dnn1s/flutter_navigationtest
I want to accomplish the same navigation approach as Google does with the Play Store app: the drawer lists the available "root" views (in my case, view1 to view3), while any root view can have any number of subviews and its own navigation stack. The catch is: even when the user is on one of the subviews, the drawer is still accessible by using a swipe gesture, starting from the outer left of screen and going to the center - and this is not possible with my current approach. As for the Play Store app, when you tap on an app to see its details, you can either go back by tapping the arrow on the upper left OR directly invoke the drawer by swiping.
main.dart: nothing fancy
void main() => runApp(new NavigationTestApp());
class NavigationTestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Navigation test',
home: new MainPage(),
);
}
}
mainpage.dart: this view includes the drawer and its items
import 'package:flutter/material.dart';
import 'view1.dart';
import 'view2.dart';
import 'view3.dart';
/// just a wrapper class for drawer items; in my original code, these include
/// icons and other properties
class DrawerItem {
String title;
DrawerItem({this.title});
}
class MainPage extends StatefulWidget {
/// list of items in the drawer
final drawerItems = [
new DrawerItem(title: 'Item 1'),
new DrawerItem(title: 'Item 2'),
new DrawerItem(title: 'Item 3')
];
#override
State<MainPage> createState() => new MainPageState();
}
class MainPageState extends State<MainPage> {
int _selectedPageIndex = 0;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
/// dynamic title, depending on the current view
title: new Text(widget.drawerItems[_selectedPageIndex].title),
),
drawer: new Drawer(
child: new ListView(
padding: EdgeInsets.zero,
children: <Widget>[
new DrawerHeader(child: new Text('Drawer header')),
/// quick and easy way to create the items;
/// in the original
/// code, these items are built in a loop
_buildDrawerItem(0),
_buildDrawerItem(1),
_buildDrawerItem(2),
],
)
),
body: _buildCurrentPage()
);
}
Widget _buildCurrentPage() {
switch(_selectedPageIndex) {
case 0: return new View1();
case 1: return new View2();
case 2: return new View3();
}
return new Text('Invalid page index');
}
Widget _buildDrawerItem(int index) {
return new ListTile(
title: new Text(widget.drawerItems[index].title),
selected: _selectedPageIndex == index,
onTap: () => _handleSelection(index),
);
}
void _handleSelection(int index) {
setState(() {
_selectedPageIndex = index;
});
/// close the drawer
Navigator.of(context).pop();
}
}
This is the first root view:
view1.dart:
class View1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text('Push Subview1'),
onPressed: () {
Navigator.of(context).push(new MaterialPageRoute(
builder: (context) {
return new SubView1();
}
));
},
);
}
}
subview1.dart:
import 'package:flutter/material.dart';
class SubView1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('SubView1'),
),
body: new Text('Pushed from View1')
);
}
}
I know about the Cupertino classes, but I prefer the material design. Can someone point me in the right direction on how to implement the desired behaviour?
Since they are two different pages, drawer in one page will not be available in another page. If you do want that you should create drawer in both pages. I've wrapped everything as much as i can regarding Drawer inside a separate class called drawer.dart with comments everywhere so that it would be helpful to understand.
Plugin used
scoped_model: "^0.2.0"
mainpage.dart
import 'package:commo_drawer/drawer.dart';
import 'package:flutter/material.dart';
import 'view1.dart';
import 'view2.dart';
import 'view3.dart';
MainPageState mainPageState = new MainPageState();
class MainPage extends StatefulWidget {
#override
State<MainPage> createState() => mainPageState;
}
class MainPageState extends State<MainPage> {
MyDrawer myDrawer;
#override
void initState() {
myDrawer =
new MyDrawer(shouldRebuildState: DrawerItemClick.NEED_TO_REBUILD_STATE);
myDrawer.addListener(() {
setState(() {});
});
super.initState();
}
rebuild() {
setState(() {});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(drawerItems[myDrawer.selectedPageIndex].title),
),
drawer: myDrawer.buildDrawer(context),
body: _buildCurrentPage(),
);
}
Widget _buildCurrentPage() {
switch (myDrawer.selectedPageIndex) {
case 0:
return new View1();
case 1:
return new View2();
case 2:
return new View3();
}
return new Text('Invalid page index');
}
}
subview1.dart
import 'package:commo_drawer/drawer.dart';
import 'package:flutter/material.dart';
class SubView1 extends StatelessWidget {
final MyDrawer myDrawer = new MyDrawer(shouldRebuildState: DrawerItemClick.NEED_NOT_REBUILD_STATE);
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('SubView1'),
leading: new IconButton(
icon: new Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
}),
),
body: new Text('Pushed from View1'),
drawer: myDrawer.buildDrawer(context),
);
}
}
drawer.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
int _selectedPageIndex = 0;
BuildContext _context;
class MyDrawer extends Model {
DrawerItemClick shouldRebuildState;
MyDrawer({this.shouldRebuildState});
int get selectedPageIndex => _selectedPageIndex;
Drawer buildDrawer(BuildContext context) {
_context = context;
return new Drawer(
child: new ListView(
padding: EdgeInsets.zero,
children: <Widget>[
new DrawerHeader(child: new Text('Drawer header')),
buildDrawerItem(0),
buildDrawerItem(1),
buildDrawerItem(2),
],
),
);
}
Widget buildDrawerItem(int index) {
return new ListTile(
title: new Text(drawerItems[index].title),
selected: selectedPageIndex == index,
onTap: () => _handleSelection(index),
);
}
void _handleSelection(int index) {
Navigator.of(_context).pop(); // Close drawer
if (shouldRebuildState == DrawerItemClick.NEED_TO_REBUILD_STATE) {
if (_selectedPageIndex != index) {
_selectedPageIndex = index;
notifyListeners();
}
} else {
shouldRebuildState = DrawerItemClick.NEED_TO_REBUILD_STATE;
Navigator.of(_context).pop(); // Close SubView
if (_selectedPageIndex != index) {
_selectedPageIndex = index;
notifyListeners();
}
}
}
}
final drawerItems = [
new DrawerItem(title: 'Item 1'),
new DrawerItem(title: 'Item 2'),
new DrawerItem(title: 'Item 3'),
];
class DrawerItem {
String title;
DrawerItem({this.title});
}
enum DrawerItemClick { NEED_TO_REBUILD_STATE, NEED_NOT_REBUILD_STATE }