Modal Bottom Sheet hidden by keyboard in TabBarView

482

Solution 1

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),
      ),
    );
  }

}

Solution 2

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()
              ],
            ),
          ),
        ),
      );
    },
  );
}
Share:
482
1000Suns
Author by

1000Suns

Freelance developer Java Flutter Python

Updated on December 28, 2022

Comments

  • 1000Suns
    1000Suns over 1 year

    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

  • 1000Suns
    1000Suns about 3 years
    Hey Stewie, This did not work but it helped me come to a solution. I tried what you suggest but keyboardHeight always returned 0.0. It seems that for some reason, MediaQuery.of(context).viewInsets.bottom wasn't registering when the keyboard was displayed.