flutterdartbottom-sheetmaterial-componentsflutter-showmodalbottomsheet

Enable/Disable button when TextField has input inside a BottomSheet


I want to have a simple TextField and a TextButton inside a BottomSheet. The TextButton should only be enabled if there is some text in the TextField. However it only enables/disables the button when I click the screen or enter. I want it to change in real time.

Here is my full code:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyBottomSheet(),
    );
  }
}
class MyBottomSheet extends StatefulWidget {
  @override
  _MyBottomSheetState createState() => _MyBottomSheetState();
}

class _MyBottomSheetState extends State<MyBottomSheet> {
  late TextEditingController _textController;
  bool _isButtonDisabled = true;

  @override
  void initState() {
    super.initState();
    _textController = TextEditingController();
    _textController.addListener(_onTextChanged);
  }

  void _onTextChanged() {
    setState(() {
      _isButtonDisabled = _textController.text.isEmpty;
    });
  }

  @override
  void dispose() {
    _textController.dispose();
    super.dispose();
  }
  void _showBottomSheet() {
    showModalBottomSheet(
      context: context,
      builder: (BuildContext context) {
        return Padding(
          padding: EdgeInsets.only(
            bottom: MediaQuery.of(context).viewInsets.bottom
          ),
          child: Container(
            height: 300,
            width: double.infinity,
            child: Column(
              children: [
                TextField(
                  controller: _textController,
                  autofocus: true,
                  decoration: const InputDecoration(
                    contentPadding: EdgeInsets.symmetric(horizontal: 15),
                    hintText: 'Enter name...',
                  ),
                ),
                TextButton(
                  onPressed: _isButtonDisabled ? null : () {
                    
                  },
                  child: const Text('Save'),
                )
              ],
            ),
          ),
        );
      }
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TEST'),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked ,
        floatingActionButton: FloatingActionButton(
          shape: CircleBorder(),
          onPressed: (_showBottomSheet),
          child: Icon(Icons.add_rounded),
        ),
        bottomNavigationBar: BottomAppBar(
          height: 60,
          shape: CircularNotchedRectangle(),
          notchMargin: 8.0,
          clipBehavior: Clip.antiAlias,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              IconButton(
                onPressed: () {},
                icon: Icon(Icons.more_horiz_rounded),
              ),
              IconButton(
                onPressed: () {},
                icon: Icon(Icons.menu_rounded)
              )
            ],
          ),
        ),
    );
  }

  void _onSubmit() {
    // Handle form submission here
  }
}

I tried the same approach outside the BottomSheet and it works fine.


Solution

  • You must move Modal content into a separated StatefulWidget like bellow

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyBottomSheet(),
        );
      }
    }
    class MyBottomSheet extends StatefulWidget {
      @override
      State<MyBottomSheet> createState() => _MyBottomSheetState();
    }
    
    class _MyBottomSheetState extends State<MyBottomSheet> {
      
      late TextEditingController _textController;
      
      @override
      void initState() {
        super.initState();
        _textController = TextEditingController();
      }
      
      @override
      void dispose() {
        _textController.dispose();
        super.dispose();
      }
        
      void _showBottomSheet() {
        showModalBottomSheet(
          context: context,
          builder: (BuildContext context) {
            return ModalContent(controller: _textController);
          }
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('TEST'),
          ),
          floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked ,
            floatingActionButton: FloatingActionButton(
              shape: const CircleBorder(),
              onPressed: (_showBottomSheet),
              child: const Icon(Icons.add_rounded),
            ),
            bottomNavigationBar: BottomAppBar(
              height: 60,
              shape: const CircularNotchedRectangle(),
              notchMargin: 8.0,
              clipBehavior: Clip.antiAlias,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  IconButton(
                    onPressed: () {},
                    icon: const Icon(Icons.more_horiz_rounded),
                  ),
                  IconButton(
                    onPressed: () {},
                    icon: const Icon(Icons.menu_rounded)
                  )
                ],
              ),
            ),
        );
      }
    
      void _onSubmit() {
        // Handle form submission here
      }
    }
    
    class ModalContent extends StatefulWidget {
      final TextEditingController controller;
      const ModalContent({super.key, required this.controller});
      
      @override
      State<ModalContent> createState() => _ModalContentState();
    }
    
    class _ModalContentState extends State<ModalContent> {
      bool _isButtonDisabled = true;
      
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: EdgeInsets.only(
            bottom: MediaQuery.of(context).viewInsets.bottom
          ),
          child: Container(
            height: 300,
            width: double.infinity,
            child: Column(
              children: [
                TextField(
                  controller: widget.controller,
                  autofocus: true,
                  decoration: const InputDecoration(
                    contentPadding: EdgeInsets.symmetric(horizontal: 15),
                    hintText: 'Enter name...',
                  ),
                  onChanged: (value) {
                    print("Value is empty : ${value.isEmpty}");
                    setState(() {
                      _isButtonDisabled = value.isEmpty;
                    });
                  }
                ),
                TextButton(
                  onPressed: _isButtonDisabled ? null : () {
    
                  },
                  child: const Text('Save'),
                )
              ],
            ),
          ),
        );
      }
    }