Flutter scaffold updated entire page when update on appbar - flutter

So I have a scaffold with body is a list view. And I have an appbar that manage its stage. Here my appbar code :
import 'package:flutter/material.dart';
class HgAppBar extends StatefulWidget implements PreferredSizeWidget {
final String title;
final List<Widget> actions;
HgAppBar({this.title, this.actions, Key key}) : super(key: key);
#override
HgAppBarState createState() => HgAppBarState();
#override
Size get preferredSize => new Size.fromHeight(kToolbarHeight);
}
class HgAppBarState extends State<HgAppBar> {
bool _searchOpenned = false;
void openSeach() {
setState(() {
_searchOpenned = true;
});
}
void closeSearch() {
setState(() {
_searchOpenned = true;
});
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return AppBar(
title: _searchOpenned
? TextField(
decoration: InputDecoration(
filled: true,
border: null,
fillColor: Colors.white,
),
autofocus: true,
)
: Text(widget.title ?? 'No title'),
actions: _searchOpenned
? [
IconButton(
icon: Icon(Icons.close),
onPressed: () {
setState(() {
_searchOpenned = false;
});
},
)
]
: widget.actions,
);
}
}
And here my page code:
class PageSales extends StatefulWidget {
final Store<AppState> store;
final String title;
final bool usePop;
PageSales(this.store, {this.title, this.usePop = false});
#override
State<StatefulWidget> createState() => _PageSales();
}
class _PageSales extends State<PageSales> {
final appBarKey = GlobalKey<HgAppBarState>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: HgAppBar(
key: appBarKey,
title: Localizations.of(context, AppLoc).text('sales_plus'),
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
appBarKey.currentState.openSeach();
},
)
],
),
body: SafeArea(
child: Column(children: <Widget>[
Expanded(
child: FireStoreListView(
snapshot: HgFirestore.instance.productCollection.snapshots(),
itemBuilder: (context, doc) {
return WidgetProductItem(
widget.store, ProductModel.fromDocument(doc));
},
),
),
]),
),
);
}
}
so the problem is when I call the openSearch, my entire scaffold get refresh (I know it because my ListView is flashing). How do I can update my appbar without refreshing entire scaffold?

I tried your code and it seems to be fine. The screen doesn't rebuild, I'm using Flutter 2.2. I suggest adding debugPrint to make sure that the screen does get rebuild, ListView flashing isn't a definite indicator that the entire screen gets rebuild.

Related

When state of widget is created

I have two tabA, tabB and these are switched by user tapping.
What I want to do is
At first tabA opens.
tabB text changes depending on tabA state.
user opens tabB,there comes changed text.
My basic idea is getting the tabB state via object key.
However there is a problem,
Before opening tabB, state of tabB is not created.
These are my source code tabA is playerPage, tabB is settingPage
class _MainFrameState extends State<MainFrame>{
void _onItemTapped(int index) => setState(() => _selectedIndex = index );
int _selectedIndex = 0;
Widget settingPage = Text("");
Widget playerPage = Text("");
GlobalKey<_PlayerPageState> _playerPageKey = new GlobalKey<_PlayerPageState>();
GlobalKey<_SettingPageState> _settingPageKey = new GlobalKey<_SettingPageState>();
#override
void initState() {
print("Main Frame init state");
super.initState();
playerPage = PlayerPage(key:_playerPageKey);
settingPage = SettingPage(key:_settingPageKey);
}
function whenSomethingChanged(){ //this is called by push button or some user action,so initState() is already called.
print(playerPage.currentState!) // it has state and operatable.
print(settingPage.currentState!) // I want to change the tabB text but it returns null.
}
#override
Widget build(BuildContext context) {
print("Main Frame build");
List<Widget> _pages = [
playerPage,
settingPage
];
return Scaffold(
appBar: EmptyAppBar(),
body:Container(
decoration: new BoxDecoration(color: Colors.red),
child:Center(child:_pages.elementAt(_selectedIndex),)),
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Color(0xffC03410),
unselectedItemColor: Color(0xffE96D2F),
backgroundColor: Color(0xffF7BF51),
type: BottomNavigationBarType.fixed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.play_circle),
label: 'Player',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Setting',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
);
}
}
I can tell that when first build is called PlayerPage is created but SettingPage is not.
However I want to control the SettinPage , before it show.
What is the solution for this problem??
Since you didn't tell what kind of state of TabA you want to show on TabB, I will assume it's a String and wrote this. There's a lot of ways of doing what you want to achieve, this is just one of them.
Step 1: Pass the value of PlayerPage state to SettingPage like this:
class SettingPage extends StatefulWidget {
const SettingPage({
Key? key,
required this.text,
}) : super(key: key);
final String text;
#override
_SettingPageState createState() => _SettingPageState();
}
class _SettingPageState extends State<SettingPage> {
#override
Widget build(BuildContext context) {
return Center(child: Text(widget.text));
}
}
Step 2: Let's assume state you want to show on SettingPage is from TextField. Maybe a player name. Then you'll need to pass that textField value to MainFrame() widget whenever it changes.
class PlayerPage extends StatefulWidget {
const PlayerPage({
Key? key,
required this.onTextChanged,
}) : super(key: key);
final Function(String) onTextChanged;
#override
_PlayerPagetate createState() => _PlayerPagetate();
}
class _PlayerPagetate extends State<PlayerPage> {
final TextEditingController _controller = TextEditingController();
#override
Widget build(BuildContext context) {
return Center(
child: TextField(
controller: _controller,
onChanged: (value) {
widget.onTextChanged(value);
},
),
);
}
}
Step 3: On MainFrame widget, now you get playerName into _playerName variable from PlayerPlage and pass it into SettingPage like this. Whenever the value changes _playerName will change too:
class _MainFrameState extends State<MainFrame> {
void _onItemTapped(int index) => setState(() => _selectedIndex = index);
int _selectedIndex = 0;
String _playerName = '';
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
print("Main Frame build");
List<Widget> _pages = [
PlayerPage(onTextChanged: (value) {
setState(() {
_playerName = value;
});
}),
SettingPage(text: _playerName)
];
return Scaffold(
// rest of the codes are same.
)
Since your settingPage is not in the tree, you can't really access its state because it was not created.
Whenever you change your _selectedIndex in the code below, either a new settingPage or a playerPage is inflated, so just create it already with the value it depends on, listening to it if necessary.
body:Container(
decoration: new BoxDecoration(color: Colors.red),
child:Center(child:_pages.elementAt(_selectedIndex),)),
Since you are updating the screen based on the users input in page 1 I have included a code where if the text field is empty it will show a message and if it has value that value is displayed in second page
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MainFrame(),
);
}
}
class MainFrame extends StatefulWidget {
const MainFrame({key});
#override
State<MainFrame> createState() => _MainFrameState();
}
class _MainFrameState extends State<MainFrame> {
void _onItemTapped(int index) => setState(() {
print(playerPageController.text);
_selectedIndex = index;
});
int _selectedIndex = 0;
late TextEditingController playerPageController;
late Widget settingPage;
late Widget playerPage;
late List<Widget> _pages;
#override
void initState() {
playerPageController = TextEditingController();
print("Main Frame init state");
super.initState();
}
#override
Widget build(BuildContext context) {
settingPage = Text((playerPageController.text.isEmpty)
? "Theres no content here"
: playerPageController.text);
playerPage = Center(
child: TextField(
controller: playerPageController,
autofocus: true,
));
_pages = [playerPage, settingPage];
print("Main Frame build");
return Scaffold(
body: Container(
decoration: new BoxDecoration(color: Colors.red),
child: Center(
child: _pages.elementAt(_selectedIndex),
)),
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Color(0xffC03410),
unselectedItemColor: Color(0xffE96D2F),
backgroundColor: Color(0xffF7BF51),
type: BottomNavigationBarType.fixed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.play_circle),
label: 'Player',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Setting',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
);
}
}

Can we change a widgets variable with InkWell onTap function?

I have a custom written stateful widget that wrapped with InkWell and I want to change the widgets variable when onTap function gets activated. Is there any way to achieve that?
Here is my custom written widget
import 'package:flutter/material.dart';
class DrawerListTile extends StatefulWidget {
final tileIcon;
final tileText;
bool isSelected = false;
DrawerListTile({this.tileIcon, this.tileText});
#override
State<DrawerListTile> createState() => _DrawerListTileState();
}
class _DrawerListTileState extends State<DrawerListTile> {
#override
Widget build(BuildContext context) {
return ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
selected: widget.isSelected,
selectedTileColor: Colors.black12,
selectedColor: Colors.black54,
leading: Icon(widget.tileIcon),
title: Text(widget.tileText),
);
}
}
And here is my InkWell widget
InkWell(
onTap: () => setState(() {
//Here is the part that I want to change the DrawerListTile's isSelected value
}),
child: DrawerListTile(
tileText: "Some Text", tileIcon: Icons.credit_card_rounded),
),
I know that I can write the onTap function inside the DrawerListTile but it is not useful in my situation so is there any way to achieve what I want?
You can do something like the below solution ... you can use your isSelected variable for this purpose.
The parent view:
class MainView extends StatefulWidget {
const MainView({Key? key}) : super(key: key);
#override
State<MainView> createState() => _MainViewState();
}
class _MainViewState extends State<MainView> {
String text = DateTime.now().toString();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('App'),
),
body: InkWell(
child: Center(child: TargetWidget(text: text)),
onTap: () {
setState(() {
text = DateTime.now().toString();
});
},
),
);
}
}
The child view:
class TargetWidget extends StatefulWidget {
String text;
TargetWidget({Key? key, required this.text}) : super(key: key);
#override
State<TargetWidget> createState() => _TargetWidgetState();
}
class _TargetWidgetState extends State<TargetWidget> {
#override
Widget build(BuildContext context) {
return Container(
child: Text(widget.text),
);
}
}
You should pass your variable as a parameter to your DrawerListTile(),You can create a model class that will hold all the variables you need and pass them to the widget. Thus, whenever you call the setState function, new parameters are sent to the widget and the widget is updated.
Ex:
InkWell(
onTap: () => setState(() {
//Here is the part that I want to change the DrawerListTile's isSelected value
yourFirstVariable = something;
yourSecondVariable = something;
}),
child: DrawerListTile(
tileText: "Some Text",
tileIcon: Icons.credit_card_rounded,
drawerVariables: DrawerModel(
demoVar1 = yourFirstVariable,
demoVar2 = yourSecondVariable...
),
),
),
class DrawerModel {
final var demoVar1;
final var demoVar2;
DrawerModel ({required this.demoVar1, required this.demoVar1,});
}
class DrawerListTile extends StatefulWidget {
final tileIcon;
final tileText;
final DrawerModel drawerVariables;
bool isSelected = false;
DrawerListTile({this.tileIcon, this.tileText, this.drawerVariables});
#override
State<DrawerListTile> createState() => _DrawerListTileState();
}
class _DrawerListTileState extends State<DrawerListTile> {
#override
Widget build(BuildContext context) {
return ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
selected: widget.isSelected,
selectedTileColor: Colors.black12,
selectedColor: Colors.black54,
leading: Icon(widget.tileIcon),
title: Text(widget.tileText),
);
}
}

Unable to reflect updated parent state in showModalBottomSheet

I am relatively new to Flutter and while I really like it I'm struggling to find a way to have state values in the parent be updated in showModalBottomSheet. I think I understand the issue to be that the values aren't reflecting in showModalBottomSheet when they change in the parent because showModalBottomSheet doesn't get rebuilt when the state updates.
I am storing title and content in the parent because I was also hoping to use it for editing as well as creating todos. I figured the showModalBottomSheet could be shared for both. I am attaching a picture on the simulator. What I am expecting is that when title changes (i.e. is no longer an empty string) then the Add To Do button should become enabled but it currently stays disabled unless I close the modal and re-open it.
Any help or insight would be greatly appreciated. Below is the code in my main.dart file which has showModalBottomSheet and has the state values that need to be passed down. NewToDo contains the text fields in the modal that capture the values and updates the state in main accordingly.
** EDIT **
I have seen this link but it doesn't really explain how to pass state from a parent widget down to a showBottomModalSheet widget, it just shows how to manage state within a showBottomModalSheet widget. My goal is to have the state change from within main to be able to be picked within showBottomModalSheet.
main.dart
import 'package:flutter/material.dart';
import './todoitem.dart';
import './todolist.dart';
import 'classes/todo.dart';
import './newtodo.dart';
void main() {
runApp(const MyApp());
}
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: 'To Do Homie',
theme: ThemeData(
primarySwatch: Colors.deepPurple,
),
home: const MyHomePage(title: "It's To Do's My Guy"),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String content = '';
String title = '';
int maxId = 0;
ToDo? _todo;
final titleController = TextEditingController();
final contentController = TextEditingController();
List<ToDo> _todos = [];
void _addTodo(){
final todo = ToDo (
title: title,
id: maxId,
isDone: false,
content: content
);
if (_todo != null){
setState(() {
_todos[_todos.indexOf(_todo!)] = todo;
});
} else {
setState(() {
_todos.add(todo);
});
}
setState(() {
content = '';
maxId = maxId++;
title = '';
_todo = null;
});
contentController.text = '';
titleController.text = '';
}
#override
void initState() {
super.initState();
titleController.addListener(_handleTitleChange);
contentController.addListener(_handleContentChange);
futureAlbum = fetchAlbum();
}
void _handleTitleChange() {
setState(() {
title = titleController.text;
});
}
void _handleContentChange() {
setState(() {
content = contentController.text;
});
}
void _editTodo(ToDo todoitem){
setState(() {
_todo = todoitem;
content = todoitem.content;
title = todoitem.title;
});
contentController.text = todoitem.content;
titleController.text = todoitem.title;
}
void _deleteToDo(ToDo todoitem){
setState(() {
_todos = List.from(_todos)..removeAt(_todos.indexOf(todoitem));
});
}
void _clear(){
contentController.text = '';
titleController.text = '';
setState(() {
content = '';
title = '';
_todo = null;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SingleChildScrollView(
child: Center(
child: Container(
alignment: Alignment.topCenter,
child: ToDoList(_todos, _editTodo, _deleteToDo)
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
print(context);
return Container(child:NewToDo(titleController, contentController, _addTodo, _clear, _todo),);
});
},
child: const Icon(Icons.add),
backgroundColor: Colors.deepPurple,
),
);
}
}
NewToDo.dart
import 'package:flutter/material.dart';
import './classes/todo.dart';
class NewToDo extends StatelessWidget {
final Function _addTodo;
final Function _clear;
final ToDo? _todo;
final TextEditingController titleController;
final TextEditingController contentController;
const NewToDo(this.titleController, this.contentController, this._addTodo, this._clear, this._todo, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return
Column(children: [
TextField(
decoration: const InputDecoration(
labelText: 'Title',
),
controller: titleController,
autofocus: true,
),
TextField(
decoration: const InputDecoration(
labelText: 'Details',
),
controller: contentController,
autofocus: true,
),
ButtonBar(
alignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: titleController.text.isNotEmpty ? () => _addTodo() : null,
child: Text(_todo != null ? 'Edit To Do' : 'Add To Do'),
style: ButtonStyle(
backgroundColor: titleController.text.isNotEmpty ? MaterialStateProperty.all<Color>(Colors.deepPurple) : null,
overlayColor: MaterialStateProperty.all<Color>(Colors.purple),
),
),
Visibility (
visible: titleController.text.isNotEmpty || contentController.text.isNotEmpty,
child: ElevatedButton(
onPressed: () => _clear(),
child: const Text('Clear'),
)),
])
],
);
}
}
TextControllers are listenable. You can just wrap your Column in two ValueListenables (one for each controller) and that will tell that widget to update whenever their values are updated.
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: contentController,
builder: (context, _content, child) {
return ValueListenableBuilder(
valueListenable: titleController,
builder: (context, _title, child) {
return Column(
children: [
TextField(
decoration: const InputDecoration(
labelText: 'Title',
),
controller: titleController,
autofocus: true,
),
TextField(
decoration: const InputDecoration(
labelText: 'Details',
),
controller: contentController,
autofocus: true,
),
ButtonBar(
alignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed:
titleController.text.isNotEmpty ? () => _addTodo() : null,
child: Text(_todo != null ? 'Edit To Do' : 'Add To Do'),
style: ButtonStyle(
backgroundColor: titleController.text.isNotEmpty
? MaterialStateProperty.all<Color>(Colors.deepPurple)
: null,
overlayColor: MaterialStateProperty.all<Color>(Colors.purple),
),
),
Visibility(
visible: titleController.text.isNotEmpty ||
contentController.text.isNotEmpty,
child: ElevatedButton(
onPressed: () => _clear(),
child: const Text('Clear'),
),
),
],
)
],
);
},
);
},
);
Another more general alternative I can think of is to use Provider (or, if you're familiar enough, regular InheritedWidgets) and the pattern suggested in its readme:
class Example extends StatefulWidget {
const Example({Key key, this.child}) : super(key: key);
final Widget child;
#override
ExampleState createState() => ExampleState();
}
class ExampleState extends State<Example> {
int _count;
void increment() {
setState(() {
_count++;
});
}
#override
Widget build(BuildContext context) {
return Provider.value(
value: _count,
child: Provider.value(
value: this,
child: widget.child,
),
);
}
}
where it suggests reading the count like this:
return Text(context.watch<int>().toString());
Except I'm guessing you can just provide the whole state of the widget to descenents by replacing _count with this to refer to the whole stateful widget. Don't know if this is recommended though.
ValueListenables would be my first choice and then maybe hooks to simplify their use though.

Hover Color for Icon

I am trying to implement a favorite Button in Flutter. I am happy with the properties when clicking the button but I am not happy with the hover animation.
I want that only the icon color changes on a hover. I don't like the Bubble around the IconButton. Here is my approach:
MouseRegion FavoriteButton(int index) {
bool _favoriteHover = false;
return MouseRegion(
onHover: (e) {
setState(() {
_favoriteHover = true;
});
},
child: IconButton(
icon: Icon(
_projects[index].favorite
? Icons.favorite
: Icons.favorite_border_outlined,
color: _favoriteHover ? Colors.yellow : Colors.white,
),
onPressed: () {
setState(() {
_projects[index].favorite
? _projects[index].favorite = false
: _projects[index].favorite = true;
// TODO: Save Favorite state to config
});
},
color: Colors.transparent,
),
);
Unfortunately, the icon color does not change on hover.
Someone can help?
You try:
Don't hover: https://i.stack.imgur.com/oxGUh.png
Hover:https://i.stack.imgur.com/UyW2j.png
/// Flutter code sample for MouseRegion
// This example makes a [Container] react to being entered by a mouse
// pointer, showing a count of the number of entries and exits.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() => runApp(const MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Center(
child: MyStatefulWidget(),
),
),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool _favorite = false;
void _incrementEnter(PointerEvent details) {
setState(() {
_favorite = true;
});
}
void _incrementExit(PointerEvent details) {
setState(() {
_favorite = false;
});
}
Icon setIcon(){
if(_favorite){
return Icon(Icons.favorite);
}
return Icon(Icons.favorite_border_outlined);
}
Color setColor(){
if(_favorite){
return Colors.yellow;
}
return Colors.black;
}
#override
Widget build(BuildContext context) {
return Container(
child: MouseRegion(
onEnter: _incrementEnter,
onExit: _incrementExit,
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: setIcon(),
color: setColor(),
onPressed: (){},
),
],
),
),
),
);
}
}
[1]: https://i.stack.imgur.com/UyW2j.png

How to set the focus on a materialbutton in flutter

I want the focus the focus on the material button so I can press enter or click the button an create an item
final FocusNode _createButtonFocusNode = new FocusNode();
#override
void initState() {
FocusScope.of(context).requestFocus(_createButtonFocusNode);
super.initState();
}
RawKeyboardListener(
focusNode: _createButtonFocusNode,
onKey: (RawKeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.enter) {
_createItem();
}
},
child:RaisedButton(focusNode: _createButtonFocusNode,
onPressed: () {
_createItem();
},
child: Text("Create"))))
Assume also a cancel material button exists with a _cancelItem event that should be able to accept an enter key on focus
You can copy paste run full code below
You can use _node.requestFocus() to request focus and list keyboard event with FocusAttachment and attach
In demo code, when receive Enter will change button color, see working demo below
code snippet
_node.requestFocus();
...
FocusAttachment _nodeAttachment;
_nodeAttachment = _node.attach(context, onKey: _handleKeyPress);
...
bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}');
if (event.logicalKey == LogicalKeyboardKey.enter) {
print('clicked enter');
setState(() {
_color = Colors.deepPurple;
});
return true;
}
}
return false;
}
working demo
full code
// Flutter code sample for FocusNode
// This example shows how a FocusNode should be managed if not using the
// [Focus] or [FocusScope] widgets. See the [Focus] widget for a similar
// example using [Focus] and [FocusScope] widgets.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
/// This Widget is the main application widget.
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: MyStatelessWidget(),
),
);
}
}
class CustomButton extends StatefulWidget {
FocusNode focusNode;
CustomButton({Key key, this.focusNode}) : super(key: key);
#override
_CustomButtonState createState() => _CustomButtonState();
}
class _CustomButtonState extends State<CustomButton> {
bool _focused = false;
FocusAttachment _nodeAttachment;
Color _color = Colors.white;
#override
void initState() {
super.initState();
//widget.focusNode = FocusNode(debugLabel: 'Button');
widget.focusNode.addListener(_handleFocusChange);
_nodeAttachment = widget.focusNode.attach(context, onKey: _handleKeyPress);
}
void _handleFocusChange() {
print(widget.focusNode.hasFocus);
if (widget.focusNode.hasFocus != _focused) {
setState(() {
_focused = widget.focusNode.hasFocus;
_color = Colors.white;
});
}
}
bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}');
if (event.logicalKey == LogicalKeyboardKey.enter) {
print('clicked enter');
setState(() {
_color = Colors.deepPurple;
});
return true;
}
}
return false;
}
#override
void dispose() {
widget.focusNode.removeListener(_handleFocusChange);
// The attachment will automatically be detached in dispose().
widget.focusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
_nodeAttachment.reparent();
return Center(
child: RaisedButton(
focusNode: widget.focusNode,
color: _focused ? _color : Colors.white,
child: Text(_focused ? "focused" : 'Not focus'),
onPressed: () {
print("create item");
},
),
);
}
}
class MyStatelessWidget extends StatefulWidget {
MyStatelessWidget({Key key}) : super(key: key);
#override
_MyStatelessWidgetState createState() => _MyStatelessWidgetState();
}
class _MyStatelessWidgetState extends State<MyStatelessWidget> {
FocusNode _node1 = FocusNode();
FocusNode _node2 = FocusNode();
#override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultTextStyle(
style: textTheme.headline4,
child: Column(
children: [
CustomButton(
focusNode: _node1,
),
CustomButton(
focusNode: _node2,
),
RaisedButton(
onPressed: () {
_node1.requestFocus();
setState(() {});
},
child: Text("request focus button 1")),
RaisedButton(
onPressed: () {
_node2.requestFocus();
setState(() {});
},
child: Text("request focus button 2")),
],
),
);
}
}
If all you want is for the button to be focused by default, you can do that by just specifying autofocus:true on the button, and you don't even need to create a FocusNode:
class MyCustomWidget extends StatelessWidget {
const MyCustomWidget({Key? key}) : super(key: key);
void _createItem() {
print('Item created');
}
#override
Widget build(BuildContext context) {
return TextButton(
autofocus: true,
child: const Text('CREATE'),
onPressed: _createItem,
);
}
}
This will automatically focus the widget when first built, as long as something else doesn't have the focus already.
If you need to set the focus from another control, you can do that with a focus node, but you don't need to use a FocusAttachment (you rarely, if ever, need to use one of those), you can just pass it to the button and call requestFocus() on it.
class MyCustomWidget extends StatefulWidget {
const MyCustomWidget({Key? key}) : super(key: key);
#override
State<MyCustomWidget> createState() => _MyCustomWidgetState();
}
class _MyCustomWidgetState extends State<MyCustomWidget> {
late FocusNode _createButtonFocusNode;
#override
void initState() {
super.initState();
_createButtonFocusNode = FocusNode();
}
#override
void dispose() {
_createButtonFocusNode.dispose();
super.dispose();
}
void _createItem() {
print('Item created');
}
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
child: const Text('FOCUS OTHER BUTTON'),
onPressed: () => _createButtonFocusNode.requestFocus(),
),
TextButton(
focusNode: _createButtonFocusNode,
child: const Text('CREATE'),
onPressed: _createItem,
),
],
),
);
}
}
(When you do create a FocusNode, be sure to dispose of it properly.)