Created
June 18, 2026 02:14
-
-
Save mercen-lee/76ec193f6ac2f2367bcae84d5acfa0d4 to your computer and use it in GitHub Desktop.
Safearea reacting scrollview implementation for Flutter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| How to use ScrollSafeAreaPadding | |
| This helper prevents ScrollView content from being hidden behind: | |
| - top safe area: status bar, notch, Dynamic Island | |
| - bottom safe area: iOS home indicator, Android navigation gesture area | |
| The pattern is based on: | |
| MediaQuery.of(context).padding.top | |
| MediaQuery.of(context).padding.bottom | |
| Instead of wrapping the whole ScrollView with SafeArea, this adds the safe-area | |
| values to the scrollable content padding. | |
| This is useful when you want: | |
| - the ScrollView to keep occupying the full screen | |
| - the first item to avoid the top unsafe area | |
| - the last item to avoid the bottom unsafe area | |
| 1) ListView usage | |
| ListView.builder( | |
| padding: ScrollSafeAreaPadding.fromLTRB( | |
| context, | |
| left: 16, | |
| top: 12, | |
| right: 16, | |
| bottom: 24, | |
| includeTop: true, | |
| includeBottom: true, | |
| ), | |
| itemCount: items.length, | |
| itemBuilder: (context, index) { | |
| return Text(items[index]); | |
| }, | |
| ) | |
| 2) CustomScrollView / Sliver usage | |
| CustomScrollView( | |
| slivers: [ | |
| ScrollSafeAreaSliverPadding( | |
| padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), | |
| includeTop: true, | |
| includeBottom: true, | |
| sliver: SliverList( | |
| delegate: SliverChildBuilderDelegate( | |
| (context, index) => Text(items[index]), | |
| childCount: items.length, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ) | |
| 3) Top only | |
| ListView( | |
| padding: ScrollSafeAreaPadding.only( | |
| context, | |
| top: 12, | |
| includeTop: true, | |
| includeBottom: false, | |
| ), | |
| children: const [ | |
| Text('Top safe area protected'), | |
| ], | |
| ) | |
| 4) Bottom only | |
| ListView( | |
| padding: ScrollSafeAreaPadding.only( | |
| context, | |
| bottom: 24, | |
| includeTop: false, | |
| includeBottom: true, | |
| ), | |
| children: const [ | |
| Text('Bottom safe area protected'), | |
| ], | |
| ) | |
| 5) Fixed bottom button | |
| For fixed bottom buttons, use SafeArea directly. | |
| Scaffold( | |
| body: ..., | |
| bottomNavigationBar: SafeArea( | |
| top: false, | |
| child: Padding( | |
| padding: const EdgeInsets.all(16), | |
| child: ElevatedButton( | |
| onPressed: () {}, | |
| child: const Text('Continue'), | |
| ), | |
| ), | |
| ), | |
| ) | |
| Notes: | |
| - Use MediaQuery.padding for normal safe-area scroll padding. | |
| - Use MediaQuery.viewPadding only when you want to preserve the physical safe | |
| area even while the keyboard is visible. | |
| - Do not double-apply top safe area when using AppBar, SliverAppBar, or | |
| CupertinoNavigationBar, because they may already handle it. | |
| */ | |
| import 'package:flutter/material.dart'; | |
| class ScrollSafeAreaPadding { | |
| const ScrollSafeAreaPadding._(); | |
| static EdgeInsets _safeInsetsOf( | |
| BuildContext context, { | |
| bool maintainViewPadding = false, | |
| }) { | |
| final mediaQuery = MediaQuery.of(context); | |
| return maintainViewPadding ? mediaQuery.viewPadding : mediaQuery.padding; | |
| } | |
| static double top( | |
| BuildContext context, { | |
| double top = 0, | |
| bool maintainViewPadding = false, | |
| }) { | |
| final safeInsets = _safeInsetsOf( | |
| context, | |
| maintainViewPadding: maintainViewPadding, | |
| ); | |
| return safeInsets.top + top; | |
| } | |
| static double bottom( | |
| BuildContext context, { | |
| double bottom = 0, | |
| bool maintainViewPadding = false, | |
| }) { | |
| final safeInsets = _safeInsetsOf( | |
| context, | |
| maintainViewPadding: maintainViewPadding, | |
| ); | |
| return safeInsets.bottom + bottom; | |
| } | |
| static EdgeInsets only( | |
| BuildContext context, { | |
| double left = 0, | |
| double top = 0, | |
| double right = 0, | |
| double bottom = 0, | |
| bool includeTop = false, | |
| bool includeBottom = true, | |
| bool maintainViewPadding = false, | |
| }) { | |
| final safeInsets = _safeInsetsOf( | |
| context, | |
| maintainViewPadding: maintainViewPadding, | |
| ); | |
| return EdgeInsets.only( | |
| left: left, | |
| top: includeTop ? safeInsets.top + top : top, | |
| right: right, | |
| bottom: includeBottom ? safeInsets.bottom + bottom : bottom, | |
| ); | |
| } | |
| static EdgeInsets fromLTRB( | |
| BuildContext context, { | |
| double left = 0, | |
| double top = 0, | |
| double right = 0, | |
| double bottom = 0, | |
| bool includeTop = false, | |
| bool includeBottom = true, | |
| bool maintainViewPadding = false, | |
| }) { | |
| final safeInsets = _safeInsetsOf( | |
| context, | |
| maintainViewPadding: maintainViewPadding, | |
| ); | |
| return EdgeInsets.fromLTRB( | |
| left, | |
| includeTop ? safeInsets.top + top : top, | |
| right, | |
| includeBottom ? safeInsets.bottom + bottom : bottom, | |
| ); | |
| } | |
| static EdgeInsets symmetric( | |
| BuildContext context, { | |
| double horizontal = 0, | |
| double vertical = 0, | |
| bool includeTop = false, | |
| bool includeBottom = true, | |
| bool maintainViewPadding = false, | |
| }) { | |
| final safeInsets = _safeInsetsOf( | |
| context, | |
| maintainViewPadding: maintainViewPadding, | |
| ); | |
| return EdgeInsets.fromLTRB( | |
| horizontal, | |
| includeTop ? safeInsets.top + vertical : vertical, | |
| horizontal, | |
| includeBottom ? safeInsets.bottom + vertical : vertical, | |
| ); | |
| } | |
| static EdgeInsets all( | |
| BuildContext context, { | |
| double value = 0, | |
| bool includeTop = false, | |
| bool includeBottom = true, | |
| bool maintainViewPadding = false, | |
| }) { | |
| final safeInsets = _safeInsetsOf( | |
| context, | |
| maintainViewPadding: maintainViewPadding, | |
| ); | |
| return EdgeInsets.fromLTRB( | |
| value, | |
| includeTop ? safeInsets.top + value : value, | |
| value, | |
| includeBottom ? safeInsets.bottom + value : value, | |
| ); | |
| } | |
| static EdgeInsets add( | |
| BuildContext context, | |
| EdgeInsets padding, { | |
| bool includeTop = false, | |
| bool includeBottom = true, | |
| bool maintainViewPadding = false, | |
| }) { | |
| final safeInsets = _safeInsetsOf( | |
| context, | |
| maintainViewPadding: maintainViewPadding, | |
| ); | |
| return EdgeInsets.fromLTRB( | |
| padding.left, | |
| includeTop ? padding.top + safeInsets.top : padding.top, | |
| padding.right, | |
| includeBottom ? padding.bottom + safeInsets.bottom : padding.bottom, | |
| ); | |
| } | |
| } | |
| /// SliverPadding version of ScrollSafeAreaPadding. | |
| /// | |
| /// Use this inside CustomScrollView. | |
| class ScrollSafeAreaSliverPadding extends StatelessWidget { | |
| const ScrollSafeAreaSliverPadding({ | |
| super.key, | |
| required this.sliver, | |
| this.padding = EdgeInsets.zero, | |
| this.includeTop = false, | |
| this.includeBottom = true, | |
| this.maintainViewPadding = false, | |
| }); | |
| final Widget sliver; | |
| final EdgeInsets padding; | |
| final bool includeTop; | |
| final bool includeBottom; | |
| final bool maintainViewPadding; | |
| @override | |
| Widget build(BuildContext context) { | |
| return SliverPadding( | |
| padding: ScrollSafeAreaPadding.add( | |
| context, | |
| padding, | |
| includeTop: includeTop, | |
| includeBottom: includeBottom, | |
| maintainViewPadding: maintainViewPadding, | |
| ), | |
| sliver: sliver, | |
| ); | |
| } | |
| } | |
| /// Builder helper when you want to calculate safe-area-aware padding once and | |
| /// pass it into another widget. | |
| class ScrollSafeAreaPaddingBuilder extends StatelessWidget { | |
| const ScrollSafeAreaPaddingBuilder({ | |
| super.key, | |
| required this.builder, | |
| this.padding = EdgeInsets.zero, | |
| this.includeTop = false, | |
| this.includeBottom = true, | |
| this.maintainViewPadding = false, | |
| }); | |
| final EdgeInsets padding; | |
| final bool includeTop; | |
| final bool includeBottom; | |
| final bool maintainViewPadding; | |
| final Widget Function(BuildContext context, EdgeInsets padding) builder; | |
| @override | |
| Widget build(BuildContext context) { | |
| final resolvedPadding = ScrollSafeAreaPadding.add( | |
| context, | |
| padding, | |
| includeTop: includeTop, | |
| includeBottom: includeBottom, | |
| maintainViewPadding: maintainViewPadding, | |
| ); | |
| return builder(context, resolvedPadding); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment