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),
),
),
);
}
}
Set resizeToAvoidBottomInset: false in Scaffold.
Used Stack with Positioned outside the Scaffold.
Checked for keyboard visibility using MediaQuery.of(context).viewInsets.bottom inside a Visibility widget.
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 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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With