Home > Back-end >  Unable to reflect updated parent state in showModalBottomSheet
Unable to reflect updated parent state in showModalBottomSheet

Time:10-08

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 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.

  • Related