People never behave how you expect them to and if your app isn’t prepared for this, the results can be ugly.
One way to restrict user input is to limit the number of characters they can enter in a TextField. For example, lets say you have a zip code field that users can use to search their location on a map. Zip codes are generally only 5 characters and anything more (or less) will likely caues your search feature to breakdown. You can cap the character limit on the zip code field by adding the maxLength property like this:
This generates a simple TextField with a counter in the bottom right:
But what if you don’t want to show a counter? After all, most people know that a zip code is 5 digits long. In that case, you can replace the counter property like this:
This is a two-part solution. We can accomplish the same thing with one less step by using a LengthLimitingTextInputFormatter like this:
Who else likes code golf? ⛳️
Barrel Files
Creating one widget per dart file can drastically improve the overall organization of your Flutter project at the expense of increasing the total number of import statements required to compose your widget trees. To combat this downside, you can create what are know as “barrel files” to export multiple files from a single location. Files that need to use the same set of widgets can then import one barrel file instead of each separate widget file.
For instance, say you’re building an app meant to describe the nature of the universe and you’re frequently using 3 widgets: Reality, Simulation, and Nothing. On a page where you need all 3 widgets, you would typically import them like this:
To avoid doing this everywhere, you can instead create a fourth file in the same directory as these widgets called widgets.dart and export each of these widgets like this:
Now, wherever you need one, two, or three of these widgets, you can use a single import statement:
You can install the[Dart Barrel File Generator plugins for VS code or Android Studio to automate the creation of these files, too.
Don’t Push, Navigate
Navigating through the pages of a Flutter web apps without spamming the console with errors
requires a bit of finesse. Unlike native Android or iOS applications which manage a literal
stack of routes or pages, web apps treat each route as a standalone page that can be accessed
directly from a URL and refreshed ad nauseam. Further, pressing the back arrow in a browser
doesn’t “pop” the top route off your stack - it pushes the previous route 🤔
With all of this in mind, its easy to see how you can wind up with errors on top of errors as
multiple widgets with the same key pile onto your router.
How can you fix this?
There are multiple techniques you can employ but here I want to focus on the concept of
“navigating to” routes instead of always pushing them onto your stack.
If a route is already in your stack, pop until you get to it
If the route is not in your stack, push it as you normally would
That’s it! By doing this, you can avoid having two of the same PostDetailsRoutes stacked on top
of each other and simultaneously avoid elusive bugs. The go_router
package is designed to use declarative routing so if you’re already using that, you might be
fine. The auto_route package on the other hand is older
and more open to imperative pushing and popping. Fortunately, it has a navigate() method that
will pop to a page if its already in the stack and push it if not. Exactly what we want.
The base Flutter Navigator doesn’t let you view the current stack
but you can use a custom NavigatorObserver
to manually track the stack and then push or pop as appropriate.
Easy MediaQuery
The MediaQuery widget is an InheritedWidget (more specifically an InheritiedModel) that passes a MediaQueryData object to its subtree. This object includes information about the screen size, default insets, and accessibility data like the text scaling factor.
The MaterialApp class includes a MediaQuery widget at the root of your app’s widget tree. You typically access data from the nearest MediaQuery like this:
Conveniently, MediaQuery has a long list of static methods that can be used to access data in the nearest MediaQuery. Save yourself a bit of typing ✌️
Global Message
In some situations, you may want to display a message to your users regardless of where they are in your app. For example:
A newer version of your app is available to download and you want users to update
The app will be undergoing maintenance soon and you want to warn users that service will be unavailable
You want to share a message about current events with your audience
To accomplish this, you can use the builder property on the MaterialApp class and the Stack widget to conditionally display an informational banner. First, you’ll need a way to determine if the user acknowledged the banner:
Then you can use that variable inside the builder of your MaterialApp:
This banner will display on all screens in your app until the user dismisses it by swiping left or right or updating their app.
If you want to make the message fixed (so users can’t dismiss it), wrap the Dismissible in an IgnorePointer widget:
The HeroMode widget can be used to programmatically enable or disable a hero animation.
To start, determine how you want to track if the Hero is enabled. It’s fairly easy to do this with a ValueNotifier:
Then wrap the Hero you want to control in a HeroMode. In this example, I use a ValueListenableBuilder to bind the HeroMode to the showAnimations flag above:
This approach can be useful for implementing a setting in your app that allows users to turn off animations. Some folks don’t enjoy UI components flying across the screen and that’s fine.
IconButton Constructors
Adding an icon to a Material button can give your design that extra shine. Luckily, each of the built in Material button types have a .icon constructor which you can use to tack on a leading icon:
The ListenableBuilder will rebuild everytime the Listenable changes so you can use it to enable a submission button when a TextField is populated or show an up arrow when the user hits the bottom of a scroll window:
Loading Google Fonts
On Flutter web, the google_fonts package can take 100-300ms to load the text theme’s your app needs. During this time, Flutter will use the default text theme to render text. When the Google Font you need is loaded, the text is updated and it’s possible for your app’s layout to shift.
To avoid this, you can use the GoogleFonts.pendingFonts() method which was added in June, 2023. This method returns a future that resolves when the requested fonts are finished loading.
You can await all required fonts like this:
Or only a subset of fonts by specifying them in list format:
This future can be plugged into a FutureBuilder to display the text only when the fonts are ready:
Removing Page Transitions
Most modern day websites don’t animate from one page to the next like a mobile application. When
a new page is loaded, it typically pops onto screen as soon as its loaded instead of sliding in
from the left or scaling up from the center. It’s just how things work.
ValueNotifiers are extremely useful for managing state in your Flutter application but they might not work how you’d expect them to.
From the docs 📘:
Because this class only notifies listeners when the value’s identity changes, listeners will not be notified when mutable state within the value itself changes.
This means that for some data types (custom classes, Lists, and Maps) listeners will only be notified if you change the identity of your value.
Lists
Maps
Custom Classes
Updating the value inside a ValueNotifier directly without changing the value’s identity will still change the value. This can lead to some interesting bugs where the UI is out of date with the value in the ValueNotifier. To avoid this in the List and Map cases, you can construct your lists and maps using the List.unmodifiable and Map.unmodifiable constructors.
Pinned SliverHeader
Flutter 3.13 was released this week and it added a sweet new widget: SliverMainAxisGroup. This widget lets you group multiple sliver children in a list. The only way to do this before the 3.13 release was with the sliver_tools package (#RIP).
If you want to create a pinned header inside a CustomScrollView or SliverList, now you can easily do that. First, create a new class that extends SliverPersistentHeaderDelegate:
Then, add a SliverMainAxisGroup to your scrolling sliver widget (for example, CustomScrollView). Inside the SliverMainAxisGroup you can add a SliverPersistentHeader widget that uses the delegate you created above. Don’t forget to set pinned to true:
When the SliverPersistentHeader reaches the top of the screen, it will stick until the entire SliverMainAxisGroup is scrolled through.
You can accomplish a similar effect by adding a SliverAppBar to the SliverMainAxisGroup but note that this widget will behave like an AppBar with a leading icon, scrolled under color change, and default size.
Adding or removing multiple widgets from a Row or Column is frequently a requirement in many Flutter applications. Use the Spread Operator (…) to group several widgets and show or hide them based on a condition.
The spread operator can be used inside of any widget that accepts a list of children, including Rows, Columns, ListViews, and GridViews. You can also use it in lists that are not related to your app’s UI.
Staggered Animations
One of the oldest UI tricks in the book is to stagger your entry animations so the components on screen roll or pop in one after the other. You can accomplish this easily using the flutter_animate package and the “delay” property on the Animate widget:
The key here is to use the index property from your ListView.builder to make each item take just a little longer to show up.
What’s the catch?
For small lists, the approach above is fine. For larger lists, when the index value might be 10, 20, or a 100 times larger than what can appear on the screen at one time, items towards the bottom of the list will take forever to animate in.
To fix this, use the min function from the dart:math
library to cap the delay at a certain value (typically a number that fits nicely on screen).
You can see examples of each of these code snippets in this Tweet.
Text Brightness
You can determine the relative brightness of a color in Flutter two ways.
computeLuminance
Call the computeLuminance method on your color to get a value between 0 (dark) and 1 (light). You can update the color of text on top of this background based on your preferred threshold.
estimateBrightnessForColor
Use ThemeData’s static estimateBrightnessForColor method to determine if a color is considered “light” or “dark” according to the Material design specifications.
Find the Current URL
There are situations while developing a Flutter web app when knowing the current URL is beneficial. For example, maybe you want to know if the user is on the home page or if they are inside of a certain workflow. This is straightforward using the Uri class in Flutter.
Anywhere within your application, simply access the Uri.base property.
From the docs 📘
The natural base URI for the current platform.
When running in a browser, this is the current URL of the current page (from window.location.href).
When not running in a browser, this is the file URI referencing the current working directory.
The Uri.base proprty contains all data relevant to the current URL: