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'),
)),
])
],
);
}
}
CodePudding user response:
TextControllers are listenable
. You can just wrap your Column in two ValueListenable
s (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.
ValueListenable
s would be my first choice and then maybe hooks to simplify their use though.