flutterdartbottom-sheetstatelessstateful

Why does a change in state of a child bottom sheet trigger a rebuild of parent widget?


I have a Scaffold screen (ListsScreen). Which has a Button(AddNewListButton) that opens up a Modal Bottom Sheet (ListScreenBottomSheetWidget). Bottom Sheet has TextField (ListTitleInputTextFieldWidget).

When i tap the TextField to open the keyboard, the parent screen rebuilds itself, due to which ofcourse all its child widgets are rebuilt as well.

Why is this happening? I was under the impression that state changes only rebuild themselves or their children, not their parents. And i also added const constructors almost everywhere to avoid rebuilds but this is still happening.

The Parent ListsScreen:

class ListsScreen extends StatelessWidget {
  const ListsScreen();
  static const routeName = '/lists-screen';

  @override
  Widget build(BuildContext context) {
    final user = Provider.of<AuthProvider>(context, listen: false).getUser;
    print('stateless rebuilding');
    return Scaffold(
      appBar: AppBar(
        centerTitle: false,
        title: Text(
          '${user['name']}\'s Lists',
          style: TextStyle(
            color: Theme.of(context).primaryColorLight,
          ),
        ),
        actions: <Widget>[
          const SignOutButton(),
        ],
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            children: <Widget>[
              SizeConfig.smallDevice
                  ? const SizedBox(
                      height: 30,
                    )
                  : const SizedBox(
                      height: 40,
                    ),
              SizeConfig.smallDevice
                  ? Text(
                      'Welcome to TODOS',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontSize: 20,
                        color: Colors.grey[700],
                      ),
                    )
                  : Text(
                      'Welcome to TODOS',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontSize: 25,
                        color: Colors.grey[700],
                      ),
                    ),
              SizeConfig.smallDevice
                  ? const SizedBox(
                      height: 30,
                    )
                  : const SizedBox(
                      height: 40,
                    ),
              const AddNewListButton(),
              SizeConfig.smallDevice
                  ? const SizedBox(
                      height: 30,
                    )
                  : const SizedBox(
                      height: 40,
                    ),
              const UserListsListViewWidget(),
            ],
          ),
        ),
      ),
    );
  }
}

class SignOutButton extends StatelessWidget {
  const SignOutButton();

  Future<void> _submitRequest(BuildContext context) async {
    _showLoadingAlert(context);

    try {
      await Provider.of<AuthProvider>(context, listen: false)
          .submitLogOutRequest();
      Navigator.of(context).pop();
      Navigator.of(context).pushReplacementNamed(LoginScreen.routeName);
    } on HttpExceptions catch (error) {
      Navigator.of(context).pop();
      _showErrorDialogue(error.getErrorList, context);
    }
  }

  void _showErrorDialogue(List<dynamic> errorMessages, BuildContext context) {
    showDialog(
      context: context,
      builder: (ctx) => Dialog(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(20.0),
        ),
        child: ErrorListWidget(errorMessages: errorMessages),
      ),
    );
  }

  void _showLoadingAlert(BuildContext context) {
    showDialog(
      context: context,
      builder: (ctx) => const LoadingWidget(),
    );
  }

  @override
  Widget build(BuildContext context) {
    return FlatButton(
      onPressed: () => _submitRequest(context),
      child: Row(
        children: <Widget>[
          Text(
            'Sign Out',
            style: TextStyle(
              color: Theme.of(context).primaryColorLight,
            ),
          ),
          Icon(
            Icons.exit_to_app,
            color: Theme.of(context).primaryColorLight,
          ),
        ],
      ),
    );
  }
}

class AddNewListButton extends StatelessWidget {
  const AddNewListButton();

  void _modalBottomSheetMenu(BuildContext context) {
    showModalBottomSheet(
      context: context,
      backgroundColor: Colors.transparent,
      isScrollControlled: true,
      builder: (builder) {
        return const ListScreenBottomSheetWidget();
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(5),
      ),
      elevation: 10,
      color: Theme.of(context).primaryColor,
      onPressed: () => _modalBottomSheetMenu(context),
      child: Text(
        '+ Add List',
        style: TextStyle(
          color: Colors.white,
          fontSize: SizeConfig.smallDevice ? 10 : 15,
        ),
      ),
    );
  }
}

The Modal Bottom Sheet:

import 'package:flutter/material.dart';
import 'package:todo_spicotech/helpers/size_config.dart';

class ListScreenBottomSheetWidget extends StatelessWidget {
  const ListScreenBottomSheetWidget();
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        FocusScopeNode currentFocus = FocusScope.of(context);
        if (!currentFocus.hasPrimaryFocus) {
          currentFocus.unfocus();
        }
        currentFocus.unfocus();
      },
      child: Container(
        margin: const EdgeInsets.all(20.0),
        padding: EdgeInsets.only(
          bottom: MediaQuery.of(context).viewInsets.bottom,
        ),
        child: Material(
          borderRadius: BorderRadius.all(Radius.circular(15)),
          elevation: 10,
          child: Padding(
            padding: const EdgeInsets.all(20.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[

                SizeConfig.smallDevice
                    ? Text(
                        'Create a new List',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontSize: 20,
                          color: Colors.grey[700],
                        ),
                      )
                    : Text(
                        'Create a new List',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontSize: 25,
                          color: Colors.grey[700],
                        ),
                      ),
                SizeConfig.smallDevice
                    ? const SizedBox(
                  height: 20,
                )
                    : const SizedBox(
                  height: 30,
                ),
                const ListTitleInputTextFieldWidget(),
                SizeConfig.smallDevice
                    ? const SizedBox(
                  height: 20,
                )
                    : const SizedBox(
                  height: 30,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: <Widget>[
                    InkWell(
                      borderRadius: BorderRadius.circular(5),
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                      child: Ink(
                        padding: EdgeInsets.all(10),
                        child: const Text('CANCEL'),
                      ),
                    ),
                    RaisedButton(
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(5),
                      ),
                      elevation: 10,
                      color: Theme.of(context).primaryColor,
                      onPressed: () {},
                      child: Text(
                        'Create',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: SizeConfig.smallDevice ? 10 : 15,
                        ),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class ListTitleInputTextFieldWidget extends StatefulWidget {
  const ListTitleInputTextFieldWidget();
  @override
  _ListTitleInputTextFieldWidgetState createState() => _ListTitleInputTextFieldWidgetState();
}

class _ListTitleInputTextFieldWidgetState extends State<ListTitleInputTextFieldWidget> {
  @override
  Widget build(BuildContext context) {
    return TextFormField(
      decoration: const InputDecoration(
        enabledBorder: OutlineInputBorder(
          borderSide: BorderSide(
            color: Colors.lightBlue,
          ),
        ),
        focusedBorder: OutlineInputBorder(
          borderSide: BorderSide(
            color: Colors.lightBlue,
          ),
        ),
        labelText: 'List Title',
        contentPadding: EdgeInsets.all(10),
      ),
    );
  }
}


Solution

  • when you call showModalBottomSheet , it actually use Navigator inside

    return Navigator.of(context, rootNavigator: useRootNavigator).push(_ModalBottomSheetRoute<T>(
    builder: builder,
    

    source code of showModalBottomSheet https://github.com/flutter/flutter/blob/17079f26b54c8517678699a0cefe5f7bfec67b3f/packages/flutter/lib/src/material/bottom_sheet.dart#L635

    Flutter teams' reply of issue Pages on Navigator stack rebuild when a new page is pushed https://github.com/flutter/flutter/issues/11655#issuecomment-348287396

    This is working as intended. In general, you should assume that all widgets can rebuild at any time, that they don't is mostly just an optimisation.

    In particular, routes will rebuild because their navigator state has changed so they might need to update how they draw back buttons and the like.