I'm trying to keep the FloatingActionButton (FAB) fixed at the bottom when the keyboard opens in my Flutter app, but it keeps moving up when the keyboard appears.
class RecentDmConversationsPageBody extends StatefulWidget {
const RecentDmConversationsPageBody({super.key});
@override
State<RecentDmConversationsPageBody> createState() => _RecentDmConversationsPageBodyState();
}
class _RecentDmConversationsPageBodyState extends State<RecentDmConversationsPageBody> with PerAccountStoreAwareStateMixin<RecentDmConversationsPageBody>{
RecentDmConversationsView? model;
Unreads? unreadsModel;
final TextEditingController _searchController = TextEditingController();
List<DmNarrow> _filteredConversations = [];
bool _isSearching = false;
@override
void onNewStore() {
model?.removeListener(_modelChanged);
model = PerAccountStoreWidget.of(context).recentDmConversationsView
..addListener(_modelChanged);
unreadsModel?.removeListener(_modelChanged);
unreadsModel = PerAccountStoreWidget.of(context).unreads
..addListener(_modelChanged);
_applySearchFilter();
}
@override
void initState() {
super.initState();
_searchController.addListener(_applySearchFilter);
}
@override
void dispose() {
model?.removeListener(_modelChanged);
unreadsModel?.removeListener(_modelChanged);
_searchController.dispose();
super.dispose();
}
void _modelChanged() {
setState(() {
// The actual state lives in [model] and [unreadsModel].
// This method was called because one of those just changed. _applySearchFilter();
});
}
void _applySearchFilter() {
final query = _searchController.text.toLowerCase();
if (query.isEmpty) {
_filteredConversations = List.from(model!.sorted);
} else {
_filteredConversations = model!.sorted.where((narrow) {
final store = PerAccountStoreWidget.of(context);
final selfUser = store.users[store.selfUserId]!;
final otherRecipientIds = narrow.otherRecipientIds;
if (otherRecipientIds.isEmpty) {
return selfUser.fullName.toLowerCase().contains(query);
} else {
return otherRecipientIds.any((id) {
final user = store.users[id];
return user?.fullName.toLowerCase().contains(query) ?? false;
});
}
}).toList();
}
setState(() {
_isSearching = query.isNotEmpty;
});
}
@override
Widget build(BuildContext context) {
// Check if there are any DMs at all in the original model
if (model!.sorted.isEmpty) {
return const EmptyDmState();
}
return Scaffold(
backgroundColor: DesignVariables.of(context).mainBackground,
resizeToAvoidBottomInset: true,
body: SafeArea(
// Don't pad the bottom here; we want the list content to do that.
bottom: false,
child: Stack(
children: [Column(
children: [
SearchRow(controller: _searchController),
Expanded(
child: ListView.builder(
itemCount: _filteredConversations.length + (_isSearching ? 1 : 0),
itemBuilder: (context, index) {
if(index < _filteredConversations.length) {
final narrow = _filteredConversations[index];
return RecentDmConversationsItem(
narrow: narrow,
unreadCount: unreadsModel!.countInDmNarrow(narrow),
searchQuery: _searchController.text,
);
}
else{
return const NewDirectMessageButton();
}
}),
)
],
),
],
)),
floatingActionButton: Visibility(
visible: !_isSearching,
child: const NewDmButton(),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
}
}
class NewDirectMessageButton extends StatelessWidget {
const NewDirectMessageButton({super.key});
@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);
return Container(
margin: const EdgeInsets.fromLTRB(24, 8.0, 24, 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), // Match the button's shape
color: designVariables.contextMenuItemBg.withAlpha(30) //12% opacity
),
child: FilledButton.icon(
style: FilledButton.styleFrom(
minimumSize: const Size(137, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
backgroundColor: Colors.transparent,
),
onPressed: (){
Navigator.of(context).push(
NewDmScreen.buildRoute(context: context)
);
},
icon: Icon(Icons.add, color: designVariables.contextMenuItemIcon, size: 24),
label: Text(
'New Direct Message',
style: TextStyle(color: designVariables.contextMenuItemText, fontSize: 20, fontWeight: FontWeight.w600),
),
),
);
}
}
class NewDmButton extends StatelessWidget {
const NewDmButton({super.key});
@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);
return Container(
padding: const EdgeInsets.fromLTRB(12, 8.0, 16.0, 16),
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: Color(0x662B0E8A), // 40% opacity for #2B0E8A
offset: Offset(0, 4), // X: 0, Y: 4
blurRadius: 16, // Blur: 16
spreadRadius: 0, // Spread: 0
),
],
borderRadius: BorderRadius.circular(28), // Match the button's shape
),
child: FilledButton.icon(
style: FilledButton.styleFrom(
minimumSize: const Size(137, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
backgroundColor: designVariables.fabBg,
),
onPressed: (){
Navigator.of(context).push(
NewDmScreen.buildRoute(context: context)
);
},
icon: const Icon(Icons.add, color: Colors.white, size: 24),
label: Text(
'New DM',
style: TextStyle(color: designVariables.fabLabel, fontSize: 20, fontWeight: FontWeight.w500),
),
),
);
}
}
What I Have Tried:
Set
resizeToAvoidBottomInset: false
inScaffold
.- This prevents the body from resizing, but the FAB still moves.
Used
Stack
withPositioned
outside theScaffold
.- The FAB still shifts when the keyboard opens.
Checked for keyboard visibility using
MediaQuery.of(context).viewInsets.bottom
inside aVisibility
widget.- While this hides the FAB when the keyboard is open, it does not prevent the movement when visible.
Expected Behavior:
The FAB should always remain at the bottom of the screen, even when the keyboard opens.
The keyboard should not push the FAB up.
I'm trying to keep the FloatingActionButton (FAB) fixed at the bottom when the keyboard opens in my Flutter app, but it keeps moving up when the keyboard appears.
class RecentDmConversationsPageBody extends StatefulWidget {
const RecentDmConversationsPageBody({super.key});
@override
State<RecentDmConversationsPageBody> createState() => _RecentDmConversationsPageBodyState();
}
class _RecentDmConversationsPageBodyState extends State<RecentDmConversationsPageBody> with PerAccountStoreAwareStateMixin<RecentDmConversationsPageBody>{
RecentDmConversationsView? model;
Unreads? unreadsModel;
final TextEditingController _searchController = TextEditingController();
List<DmNarrow> _filteredConversations = [];
bool _isSearching = false;
@override
void onNewStore() {
model?.removeListener(_modelChanged);
model = PerAccountStoreWidget.of(context).recentDmConversationsView
..addListener(_modelChanged);
unreadsModel?.removeListener(_modelChanged);
unreadsModel = PerAccountStoreWidget.of(context).unreads
..addListener(_modelChanged);
_applySearchFilter();
}
@override
void initState() {
super.initState();
_searchController.addListener(_applySearchFilter);
}
@override
void dispose() {
model?.removeListener(_modelChanged);
unreadsModel?.removeListener(_modelChanged);
_searchController.dispose();
super.dispose();
}
void _modelChanged() {
setState(() {
// The actual state lives in [model] and [unreadsModel].
// This method was called because one of those just changed. _applySearchFilter();
});
}
void _applySearchFilter() {
final query = _searchController.text.toLowerCase();
if (query.isEmpty) {
_filteredConversations = List.from(model!.sorted);
} else {
_filteredConversations = model!.sorted.where((narrow) {
final store = PerAccountStoreWidget.of(context);
final selfUser = store.users[store.selfUserId]!;
final otherRecipientIds = narrow.otherRecipientIds;
if (otherRecipientIds.isEmpty) {
return selfUser.fullName.toLowerCase().contains(query);
} else {
return otherRecipientIds.any((id) {
final user = store.users[id];
return user?.fullName.toLowerCase().contains(query) ?? false;
});
}
}).toList();
}
setState(() {
_isSearching = query.isNotEmpty;
});
}
@override
Widget build(BuildContext context) {
// Check if there are any DMs at all in the original model
if (model!.sorted.isEmpty) {
return const EmptyDmState();
}
return Scaffold(
backgroundColor: DesignVariables.of(context).mainBackground,
resizeToAvoidBottomInset: true,
body: SafeArea(
// Don't pad the bottom here; we want the list content to do that.
bottom: false,
child: Stack(
children: [Column(
children: [
SearchRow(controller: _searchController),
Expanded(
child: ListView.builder(
itemCount: _filteredConversations.length + (_isSearching ? 1 : 0),
itemBuilder: (context, index) {
if(index < _filteredConversations.length) {
final narrow = _filteredConversations[index];
return RecentDmConversationsItem(
narrow: narrow,
unreadCount: unreadsModel!.countInDmNarrow(narrow),
searchQuery: _searchController.text,
);
}
else{
return const NewDirectMessageButton();
}
}),
)
],
),
],
)),
floatingActionButton: Visibility(
visible: !_isSearching,
child: const NewDmButton(),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
}
}
class NewDirectMessageButton extends StatelessWidget {
const NewDirectMessageButton({super.key});
@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);
return Container(
margin: const EdgeInsets.fromLTRB(24, 8.0, 24, 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), // Match the button's shape
color: designVariables.contextMenuItemBg.withAlpha(30) //12% opacity
),
child: FilledButton.icon(
style: FilledButton.styleFrom(
minimumSize: const Size(137, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
backgroundColor: Colors.transparent,
),
onPressed: (){
Navigator.of(context).push(
NewDmScreen.buildRoute(context: context)
);
},
icon: Icon(Icons.add, color: designVariables.contextMenuItemIcon, size: 24),
label: Text(
'New Direct Message',
style: TextStyle(color: designVariables.contextMenuItemText, fontSize: 20, fontWeight: FontWeight.w600),
),
),
);
}
}
class NewDmButton extends StatelessWidget {
const NewDmButton({super.key});
@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);
return Container(
padding: const EdgeInsets.fromLTRB(12, 8.0, 16.0, 16),
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: Color(0x662B0E8A), // 40% opacity for #2B0E8A
offset: Offset(0, 4), // X: 0, Y: 4
blurRadius: 16, // Blur: 16
spreadRadius: 0, // Spread: 0
),
],
borderRadius: BorderRadius.circular(28), // Match the button's shape
),
child: FilledButton.icon(
style: FilledButton.styleFrom(
minimumSize: const Size(137, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
backgroundColor: designVariables.fabBg,
),
onPressed: (){
Navigator.of(context).push(
NewDmScreen.buildRoute(context: context)
);
},
icon: const Icon(Icons.add, color: Colors.white, size: 24),
label: Text(
'New DM',
style: TextStyle(color: designVariables.fabLabel, fontSize: 20, fontWeight: FontWeight.w500),
),
),
);
}
}
What I Have Tried:
Set
resizeToAvoidBottomInset: false
inScaffold
.- This prevents the body from resizing, but the FAB still moves.
Used
Stack
withPositioned
outside theScaffold
.- The FAB still shifts when the keyboard opens.
Checked for keyboard visibility using
MediaQuery.of(context).viewInsets.bottom
inside aVisibility
widget.- While this hides the FAB when the keyboard is open, it does not prevent the movement when visible.
Expected Behavior:
The FAB should always remain at the bottom of the screen, even when the keyboard opens.
The keyboard should not push the FAB up.
- 2 resizeToAvoidBottomInset: false shouldn't move the fab, Can you simplify the snippet and share minimal code-snippet that can be run in dartPad – Md. Yeasin Sheikh Commented Feb 17 at 3:03
2 Answers
Reset to default 1I have faced same issue on my side as well and I apply below code and its working well when keyboard is open then float button not goes up.
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0.0,
child: Container(),// add your widget here
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
Here is a minimal code example with your expected behavior of not pushing the FAB when the keyboard is opening/opened.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: const Example(),
);
}
}
class Example extends StatelessWidget {
const Example({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: Scaffold(
body: GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
decoration: const InputDecoration(border: OutlineInputBorder(), labelText: 'Enter your username'),
),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
print("Hello World");
},
child: const Icon(Icons.add),
),
// This does the magic.
resizeToAvoidBottomInset: false,
),
);
}
}
The clue is to use resizeToAvoidBottomInset: false
on the Scaffold of which the FAB is attached to.
Please reconsider testing it and if needed, update your Flutter Version.
Also update your code and maybe attach a video/gif next time!