Modal Bottom Sheet hidden by keyboard in TabBarView

Viewed 676

I am having an issue where the keyboard covers and hides the bottom sheet.

I tried a number of solutions but nothing seems to work. I am not too sure what is causing the problem. My guess is that it has something to do with either nested Scaffold and/or using bottom sheet in TabBarView.

Any help would be greatly appreciated.

I tried to include as much detail as possible. If you need anything else, please let me know.

Code below:

DetailsPage.dart

@override
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: 2, 
        child: Scaffold(
          key: _scaffoldKey,
          appBar: AppBar(
            centerTitle: true,
            title: UtilWidget.getLogo(),
            bottom: TabBar(
              labelColor: Colors.deepPurpleAccent,
              unselectedLabelColor: Colors.white,
              indicatorSize: TabBarIndicatorSize.label,
              indicator: BoxDecoration(
                  borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(10),
                      topRight: Radius.circular(10)),
                  color: Colors.white),
              tabs: [
                UtilWidget.addTab('Details'),
                UtilWidget.addTab('Procedures'),
              ],
            ),
          ),
          body: TabBarView(
            children: <Widget>[
              EditServiceFragment(contractorServiceId: widget.contractorServiceId),
              ServiceProceduresFragment(contractorServiceId: widget.contractorServiceId)
            ],
          ),
        )
    );
  }

ServiceProceduresFragment.dart

@override
  Widget build(BuildContext context) {
    return Scaffold(
      primary: false,
      resizeToAvoidBottomInset: true,
      key: _scaffoldKey,
      body: _buildPage(),
      floatingActionButton: new FloatingActionButton.extended(
        onPressed: () {
          _addProcedureBottomSheet(context);
        },
        label: Text('Add Procedure'),
        icon: new Icon(Icons.add),
      ),
    );
  }

Widget _buildPage() {
    return FutureBuilder(
      future: _loadedContractorService,
      builder: (BuildContext context, AsyncSnapshot<ContractorService> serviceRes) {
        if(serviceRes.hasError) {
          print('Error while loading Asset - EditService');
          print(serviceRes.error);

          return UtilWidget.pageLoadError(
              "SERVICE PROCEDURES",
              "An Error has occurred",
              "Got an error getting Contractor Service from the API."
          );
        }

        if (serviceRes.connectionState == ConnectionState.waiting)
          return UtilWidget.progressIndicator();

        ContractorService loadedService = serviceRes.data;
        List<Procedure> procedures = loadedService.procedures;

        if(procedures.length == 0)
          return UtilWidget.emptyListView('SERVICE PROCEDURES', 'No procedures were found. Add one.');

        return SingleChildScrollView(
          child: ListView.builder(
            shrinkWrap: true,
            itemCount: procedures.length + 1,
            itemBuilder: (context, index) {
              if(index == 0){
                return UtilWidget.pageHeader('SERVICE PROCEDURES');
              }

              int listIndex = index - 1;
              Procedure procedure = procedures[listIndex];

              return Card(
                elevation: 2,
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
                margin: EdgeInsets.only(top: 10, bottom: 10, left: 20, right: 20),
                child: Theme(
                    data: ThemeData().copyWith(
                        dividerColor: Colors.transparent,
                        accentColor: Colors.deepPurpleAccent
                    ),
                    child: ExpansionTile(
                        title: ListTile(
                          title: Text(
                            procedure.name,
                            style: TextStyle(fontWeight: FontWeight.bold)
                          ),
                          subtitle: Text(
                            'Requirements: ' +
                            procedure.requirements.length.toString()
                          ),
                        ),
                        childrenPadding: EdgeInsets.only(left: 20, right: 20),
                        children: [
                          UtilWidget.addRowWithText('Notes', FontWeight.bold),
                          UtilWidget.addRowWithText(procedure.notes, FontWeight.normal),

                          _deleteProcedureBtn(procedure.id),

                          Padding(
                            padding: const EdgeInsets.only(top: 10),
                            child: UtilWidget.addRowWithText('Requirements', FontWeight.bold),
                          ),
                          for(var req in procedure.requirements)
                            UtilWidget.addRowWithText(req.name, FontWeight.normal)
                          ,
                          _addRequirementBtn(procedure.id)
                        ]
                    )
                )
              );
            }
          )
        );
      }
    );
  }

void _addProcedureBottomSheet(BuildContext context) {
    showModalBottomSheet(
      isScrollControlled: true,
      context: context,
      builder: (BuildContext bc) {
        return SafeArea(
          child: SingleChildScrollView(
            padding: EdgeInsets.only(
                top: 10, left:15, right:15,
                bottom: MediaQuery.of(context).viewInsets.bottom
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Center(
                  child: Text(
                    'Add Procedure to Service',
                    style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 18
                    ),
                  ),
                ),
                _addProcedureForm()
              ],
            ),
          )
        );
      }
    );
  }

Form _addProcedureForm() {
    return Form(
      key: _formKey,
      child: Column(
        children: <Widget>[
          TextFormField(
              validator: (value) {
                if(value.isEmpty) {
                  return 'Service name is required.';
                }
                return null;
              },
              controller: _name,
              decoration: InputDecoration(
                  labelText: 'Name',
                  hintText: "Specific/applicable procedure",
                  icon: Icon(Icons.edit, color: Colors.deepPurple)
              )
          ),
          TextFormField(
              validator: (value) {
                if(value.isEmpty) {
                  return 'Service description is required.';
                }
                return null;
              },
              maxLines: null,
              keyboardType: TextInputType.multiline,
              controller: _notes,
              decoration: InputDecoration(
                  labelText: 'Description',
                  hintText: "Applicable procedure description.",
                  icon: Icon(Icons.notes, color: Colors.deepPurple)
              )
          ),

          Container(
            margin: const EdgeInsets.only(top: 20.0, bottom: 10),
            child: SizedBox(
              width: double.maxFinite,
              child: RaisedButton(
                color: Colors.deepPurple,
                textColor: Colors.white,
                padding: EdgeInsets.all(5),
                child: Text('Save Procedure',
                    style: TextStyle(fontSize: 15)
                ),
                onPressed: () {
                  if(_formKey.currentState.validate()) {
                    _submitProcedureForm();
                  }
                },
              ),
            ),
          )
        ],
      ),
    );
  }

Widget _addRequirementBtn(int procedureId) {
    return Padding(
      padding: const EdgeInsets.only(top: 10.0, bottom: 10),
      child: RaisedButton(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
        color: Colors.deepPurple,
        textColor: Colors.white,
        child: Text('Add Requirement',
            style: TextStyle(fontSize: 14)
        ),
        onPressed: () {
          _addProcedureRequirementBottomSheet(procedureId, context);
        }
      )
    );
  }

Outputs

Before clicking on text field

After clicking on text field

3 Answers

You can check the value of MediaQuery.of(context).viewInsets.bottom to know whether the keyboard is active and put some space to the bottom of the modal.

Also, you can use AnimatedPadding to make it look smooth.

void _addProcedureBottomSheet(BuildContext context) {
  showModalBottomSheet(
    isScrollControlled: true,
    context: context,
    builder: (BuildContext context) {
      var keyboardHeight = MediaQuery.of(context).viewInsets.bottom ?? 0.0;

      return AnimatedPadding(
        padding: EdgeInsets.only(bottom: keyboardHeight),
        duration: Duration(milliseconds: 300),
        child: SafeArea(
          bottom: keyboardHeight <= 0.0,
          child: SingleChildScrollView(
            padding: EdgeInsets.only(
              top: 10,
              left: 15,
              right: 15,
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Center(
                  child: Text(
                    'Add Procedure to Service',
                    style:
                    TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
                  ),
                ),
                _addProcedureForm()
              ],
            ),
          ),
        ),
      );
    },
  );
}

Did you try to wrap your SafeArea widget with Padding and adding padding like :

=> ShowBottomSheet.instance.showBottomSheet(
   isScrollControlled: true,
   context: context,
   widget: Padding(
    padding: MediaQuery.of(context).viewInsets,
    child: Container(
      height: MediaQuery.of(context).size.height * 0.60,
      child: DefaultTabController(
        length: 2,
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: MySize.size12),
          child: Column(children: <Widget>[
            const TabsOfTabbar(),
            ....... );

Thanks to Stewie, I realized that MediaQuery.of(context).viewInsets.bottom wasn't registering the appearance of the keyboard.

I was able to solve the issue by passing in the BuildContext from the page with DefaultTabController to the TabView page. Using this BuildContext resolved the issue.

Code below:

ContractorServiceDetailsPage.dart

@override
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: 2, 
        child: Scaffold(
          key: _scaffoldKey,
          appBar: AppBar(
            centerTitle: true,
            title: UtilWidget.getLogo(),
            bottom: TabBar(
              labelColor: Colors.deepPurpleAccent,
              unselectedLabelColor: Colors.white,
              indicatorSize: TabBarIndicatorSize.label,
              indicator: BoxDecoration(
                  borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(10),
                      topRight: Radius.circular(10)),
                  color: Colors.white),
              tabs: [
                UtilWidget.addTab('Details'),
                UtilWidget.addTab('Procedures'),
              ],
            ),
          ),
          body: TabBarView(
            children: <Widget>[
              EditServiceFragment(contractorServiceId: widget.contractorServiceId),
              ServiceProceduresFragment(
                baseContext: context,
                contractorServiceId: widget.contractorServiceId
              )
            ],
          ),
        )
    );
  }

ServiceProceduresFragment.dart

class ServiceProceduresFragment extends StatefulWidget {
  final BuildContext baseContext;
  final int contractorServiceId;

  const ServiceProceduresFragment({
    Key key,
    this.contractorServiceId,
    this.baseContext}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _ServiceProceduresState();
  }
}

class _ServiceProceduresState extends State<ServiceProceduresFragment> {
...

@override
  Widget build(BuildContext context) {
    return Scaffold(
      primary: false,
      resizeToAvoidBottomPadding: false,
      key: _scaffoldKey,
      body: _buildPage(),
      floatingActionButton: new FloatingActionButton.extended(
        onPressed: () {
          _addProcedureBottomSheet(widget.baseContext);
        },
        label: Text('Add Procedure'),
        icon: new Icon(Icons.add),
      ),
    );
  }

}
Related