Flutter 3.27.0 arrived with a bang, bringing a wave of improvements to both performance and developer experience. This release shows how Flutter is always getting better and helping developers make great apps.
In this article, we’ll explore the key updates of the Cupertino and Material widgets in Flutter 3.27.0
Ready? Let’s dive in!
Just like Google Photos, The SliverFloatingHeader
is a widget designed to create headers that appear when the user scrolls forward and gracefully disappear when scrolling in the opposite direction.
CustomScrollView(
slivers: [
SliverFloatingHeader(
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.orange,
),
child: const Text(
"Sliver Floating Header like Google photos",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
)),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text("Item $index"),
tileColor: index.isEven? Colors.grey.shade300 : Colors.grey.shade100,
);
},
childCount: 100,
),
),
],
);
Have you ever faced UI misalignment when displaying monospaced text like DateTime
, such as 01:00 PM
or 02:30 PM
? I encountered this issue while building the Canbook app, where I needed to display time slots. Initially, I tried calculating the width based on the thickest character (0
) to fix the alignment.
How can we improve the UI for rendering monospaced text like DateTime
and time
, and maintain alignment and consistency for dynamic text updates?
In the old UI implementation, digits had variable widths due to the default font settings. This resulted in inconsistent alignment.
SingleChildScrollView(
child: Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8,
runSpacing: 8,
children: List.generate(30, (index) {
final time = DateTime.now().add(Duration(minutes: (index) * 15));
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
DateFormat('hh:mm a').format(time),
style: const TextStyle(
fontFeatures: [FontFeature.tabularFigures()],
color: Colors.white),
),
);
}),
),
);
FontFeature.tabularFigures()
feature ensures that each character occupies the same width.
This feature enables the use of tabular figures for fonts with both proportional (varying width) and tabular figures. Tabular figures are monospaced, meaning all characters have the same width, making them ideal for aligning text in tables, lists, or dynamic UI elements like time slots or numeric data.
Text(
DateFormat('hh:mm a').format(time),
style: const TextStyle(
fontFeatures: [FontFeature.tabularFigures()],
color: Colors.white),
),
With the latest update, both CupertinoNavigationBar
and CupertinoSliverNavigationBar
now feature transparent backgrounds until content scrolls underneath them.
This enables,
CustomScrollView(
slivers: [
const CupertinoSliverNavigationBar(
largeTitle : Text(
'Cupertino Transparent Navigation Bar',
style: TextStyle(color: Colors.white, fontSize: 20),
),
backgroundColor: Colors.transparent,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text("Item $index"),
);
},
childCount: 100,
),
),
],
);
Flutter’s repeat
method allows animations to run infinitely. But what if you want the animation to run for a specific number of times?
Now, with the addition of the count
parameter, the repeat
method can restart the animation and perform a set number of iterations before stopping.
If no value is provided for count
, the animation will repeat indefinitely.
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
animation = Tween<double>(begin: 100, end: 300).animate(controller)
..addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
width: animation.value,
height: animation.value,
color: Colors.yellow),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
controller.repeat(count: 2);
},
label: Text("Start Animation"),
),
);
}
With the release of Flutter 3.27.0, Row
and Column
widgets now support the spacing
parameter, eliminating the need for manual SizedBox
or Padding
to add spacing between child widgets.
Now, with Flutter 3.27, you can simplify the layout by directly using the spacing
property.
Using SizedBox
for spacing between children,
@override
Widget build(BuildContext context) {
return Column(
children: [
_item(),
const SizedBox(height: 16),
_item(),
const SizedBox(height: 16),
_item(),
const SizedBox(height: 16),
_item(),
const SizedBox(height: 16),
_item(),
],
);
}
With Updates,
@override
Widget build(BuildContext context) {
return Column(
spacing: 16,
children: [
_item(),
_item(),
_item(),
_item(),
],
);
}
CupertinoPicker
now supports tapping on an item to automatically scroll to it, to navigate directly to the selected item.
Similar to Android’s tab indicator behavior, you can set the mode by which the selection indicator should animate when moving between destinations, Linear or Elastic.
TabBar.indicatorAnimation
property, which allows you to customize the animation behavior of the tab indicator when switching between tabs.
TabIndicatorAnimation.linear
: A smooth, linear animation (default when indicatorAnimation
is null and indicatorSize
is TabBarIndicatorSize.tab
).TabIndicatorAnimation.elastic
: A bouncing, elastic animation (when indicatorSize
is TabBarIndicatorSize.label
)DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('Tab Indicator Animation'),
bottom: const TabBar(
indicatorAnimation: TabIndicatorAnimation.elastic,
tabs: <Widget>[
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2 with long text'),
],
),
),
body: TabBarView(),
),
);
RefreshIndicator.noSpinner
widget to create a refresh indicator without the default spinner. This allows you to handle the refresh action with custom UI elements while still using the drag-to-refresh functionality.
Stack(
children: [
RefreshIndicator.noSpinner(
onRefresh: () async {
isRefreshing = true;
setState(() {});
await Future.delayed(const Duration(seconds: 2));
isRefreshing = false;
setState(() {});
},
child: ListView()
),
if (isRefreshing)
Align(
child: CircularProgressIndicator(
color: Colors.orange,
),
),
],
),
The elevation
property determines the shadow depth of the RefreshIndicator
. The default value is 2.0.
RefreshIndicator(
elevation: 10,
backgroundColor: Colors.orange,
onRefresh: () async {
await Future.delayed(const Duration(seconds: 2));
}, // Callback function for refresh action
child: ListView()
),
One of the most useful features is CarouselView.weighted
. It allows you to assign relative weights to each carousel item by passing an array of integers called flexWeights
(e.g., [3, 2, 1]
). These integers represent the relative size of each item in the carousel.
The CarouselView
then calculates the total weight of all items, and divides the available carousel width according to the ratio of these weights.
|
|
|
enableSplash
Parameter The enableSplash
parameter in CarouselView
allows you to enable or disable the ink splash effect when an item is tapped. When set to true
, tapping an item will create the splash effect as defined by the ThemeData.splashFactory
.
Flutter has introduced a new improvement to text selection with the SelectionArea
widget. It now supports the Shift + Click
gesture to move the extent of the selection to the clicked position on Linux, macOS, and Windows. This enhances the interactivity of text selection in Flutter apps across these platforms.
Additionally, the clearSelection
method to remove the ongoing text selection.
GlobalKey<SelectionAreaState> _selectionAreaKey = GlobalKey();
class TextSelectionWidget extends StatelessWidget {
const TextSelectionWidget({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Text Selection")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
SelectionArea(
key: _selectionAreaKey,
child: const Text(
"How are you? select me.\n"
"Press Shift + Click gesture to move the extent of the selection to \nthe clicked position on Linux, macOS, and Windows.",
selectionColor: Colors.orange,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
),
)),
const SizedBox(
height: 16,
),
ElevatedButton(
onPressed: () {
_selectionAreaKey.currentState?.selectableRegion
.clearSelection();
},
child: const Text("Reset"))
],
),
),
);
}
}
The default value for Flutter’s deep linking option has changed from false
to true
, meaning deep linking is now enabled by default. This simplifies the process for many developers, but if you're using a third-party plugin for deep links, such as:
You must manually disable Flutter’s deep linking to avoid conflicts with these plugins.
Add the following metadata to your AndroidManifest.xml
file to explicitly disable Flutter's default deep linking,
<manifest>
<application
<activity>
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
</activity>
</application>
</manifest>
Modify your Info.plist
file to disable Flutter's deep linking with the following key,
<key>FlutterDeepLinkingEnabled</key>
<false/>
I’m a huge fan of native apps where the content extends behind system bars (like the status bar and navigation bar), creating a more immersive and modern experience. This is exactly what you see in many native Android apps, where the UI fills the entire screen, even behind the system bars.
Starting with Android 15 (API level 31) and above, Flutter apps now default to this edge-to-edge mode, allowing them to run in full-screen, extending content behind the system bars.
We can set it like this,
void main() {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.edgeToEdge,
overlays: [SystemUiOverlay.top], // Show the top system bar (status bar)
);
runApp(MyApp());
}
If you don’t want to add edge-to-edge mode, then add the following attribute in your_app/android/app/src/main/res/values/styles.xml
.
<?xml version="1.0" encoding="utf-8"?>
<resources>
…
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
…
<! - Add the following line: →
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>
fillColor
property has been added to the CupertinoCheckbox
.CupertinoCheckbox
fidelity.semanticLabel
to CupertinoCheckbox
.mouseCursor
property to CupertinoCheckbox
.inactiveColor
from CupertinoCheckbox
.CupertinoRadio
fidelity.CupertinoSwitch
activeColor
and trackColor
to activeTrackColor
and inactiveTrackColor
.Switch.adaptive
changes into CupertinoSwitch
.setEnabled
so that segments can be disabled now.CupertinoButton
now has a size
property (type enum CupertinoButtonSize
.CupertinoButton
now has a .tinted
constructor that renders a translucent background.CupertinoButton
now uses the actionTextStyle
TextStyle from the given theme.CupertinoContextMenu
now support scrolling if its actions overflow the screen.CupertinoDatePicker
no longer clipping long contents in its columns.CupertinoButton
interactive by keyboard shortcuts.SegmentedButton
directionality can be vertical or horizontal instead of just horizontal position by adding direction
argument.borderRadius
property to PopupMenuButton
.selectableDayPredicate
for showDateRangePicker
.The API for the Color
class in dart:ui
is changing to support wide gamut color spaces.
Color.opacity
// Before: Access the alpha channel as a (converted) floating-point value.
final x = color.opacity;
// After: Access the alpha channel directly.
final x = color.a;
Color.withOpacity
to Color.withValues
// Before: Create a new color with the specified opacity.
final x = color.withOpacity(0.0);
// After: Create a new color with the specified alpha channel value,
// accounting for the current or specified color space.
final x = color.withValues(alpha: 0.0);
When working with large numbers, it can be difficult to read and understand their values at a glance.
Dart 3.6 (Flutter 3.27) introduces a helpful feature that adds support for digit separators using the underscore (_
) character. You can’t use (_
) as prefix or suffix.
const int largeNumber = 10_00_000_000;
TimeOfDay
comparison methods. You can compare the TimeOfDay
same like DateTime
.DialogRoute
, CupertinoDialogRoute
and show dialog methods. This release is packed with improvements that refine both the visual fidelity and developer tools, ensuring a more smooth Flutter experience across all platforms. 🚀.
Are you ready to dive into the new version? We’d love to hear your thoughts! Which feature excites you the most? Or is there a specific part you’d like more details on? Let us know! 😄