FlutterFlow's Custom Functions and Custom Actions let you write Dart code that extends the visual builder. Use Custom Functions for pure data transformations — formatting dates, filtering lists, calculating values. Use Custom Actions for side effects — calling APIs, writing to storage, running platform-specific code. Build a reusable snippet library to avoid duplicating logic across pages.
Dart Scripting Patterns in FlutterFlow
FlutterFlow's visual builder handles 80% of common app patterns, but every real-world app eventually needs custom logic. FlutterFlow provides three extension points: Custom Functions (pure Dart functions that take inputs and return a value), Custom Actions (async Dart code that can have side effects and access platform APIs), and Custom Widgets (full Flutter widgets embedded in the canvas). The most common mistake is duplicating the same utility logic inline in multiple Action Flows — when requirements change you must hunt down every instance. This tutorial covers how to structure custom Dart code, which type to choose for different tasks, and how to build a centralized snippet library.
Prerequisites
- FlutterFlow project with at least a few pages built
- Basic understanding of Dart syntax (variables, functions, conditionals)
- FlutterFlow Custom Code panel accessible (available on all plans)
- Familiarity with FlutterFlow Action Flows for wiring custom code to widgets
Step-by-step guide
Understand when to use Custom Functions vs Custom Actions
Understand when to use Custom Functions vs Custom Actions
FlutterFlow has two scripting types. Custom Functions are synchronous, pure functions — they take input parameters, perform a calculation or transformation, and return a value. No async, no Firebase, no external calls. Use them for: formatting currency (formatCurrency(1234.56) -> '$1,234.56'), calculating a user's age from a birthday timestamp, filtering a list by a condition, generating initials from a full name, or building a display string from multiple fields. Custom Actions are async functions — they can await Firestore operations, call HTTP endpoints, use platform channels, write to shared preferences, or launch other apps. Use them for: uploading a file, copying text to clipboard, opening a URL, querying a third-party API, or running code that needs to complete before the next Action Flow step runs. Choosing the wrong type is harmless but confusing — a formatting operation defined as a Custom Action works but cannot be used in widget binding expressions.
Expected result: You can identify which of your pending logic requirements should be Custom Functions vs Custom Actions before writing any code.
Write your first Custom Function for data formatting
Write your first Custom Function for data formatting
In FlutterFlow, open Custom Code > Custom Functions > Add Function. Name it formatRelativeTime. Set the return type to String. Add one parameter: timestamp of type DateTime. In the code editor, write a function that returns 'just now' for timestamps under 60 seconds ago, '5 min ago' for minutes, '3 hours ago' for hours, and 'Yesterday' or the formatted date for older entries. After saving, FlutterFlow validates the Dart syntax. You can now use this function anywhere in the builder: in a Text widget's value binding, inside a Conditional expression, or as a parameter in another action. Custom Functions must be pure — avoid any global state or async operations inside them.
1String formatRelativeTime(DateTime timestamp) {2 final now = DateTime.now();3 final diff = now.difference(timestamp);45 if (diff.inSeconds < 60) return 'just now';6 if (diff.inMinutes < 60) return '${diff.inMinutes} min ago';7 if (diff.inHours < 24) return '${diff.inHours} hour${diff.inHours == 1 ? '' : 's'} ago';8 if (diff.inDays == 1) return 'Yesterday';9 if (diff.inDays < 7) return '${diff.inDays} days ago';10 return '${timestamp.day}/${timestamp.month}/${timestamp.year}';11}Expected result: The function appears in the Custom Functions list and is available in widget property bindings throughout the project.
Create a Custom Action for platform detection and storage
Create a Custom Action for platform detection and storage
In FlutterFlow, open Custom Code > Custom Actions > Add Action. Name it saveToLocalStorage. Add parameters: key (String) and value (String). In the code block, import shared_preferences, get the SharedPreferences instance, and call setString. Create a companion getFromLocalStorage action that takes key (String) and returns String. These actions give you persistent local storage outside of Firebase — useful for user preferences, cached values, or draft content. In your Action Flows, call saveToLocalStorage when a preference changes and getFromLocalStorage in the page's On Page Load action to restore settings. Test by restarting the app and verifying values persist.
1// Custom Action: saveToLocalStorage2import 'package:shared_preferences/shared_preferences.dart';34Future<void> saveToLocalStorage(5 String key,6 String value,7) async {8 final prefs = await SharedPreferences.getInstance();9 await prefs.setString(key, value);10}1112// Custom Action: getFromLocalStorage13// Return type: String14Future<String> getFromLocalStorage(String key) async {15 final prefs = await SharedPreferences.getInstance();16 return prefs.getString(key) ?? '';17}Expected result: Calling saveToLocalStorage then force-quitting and reopening the app shows the saved value returned by getFromLocalStorage.
Add a third-party Dart package to your project
Add a third-party Dart package to your project
Some capabilities require Dart packages not bundled with FlutterFlow. To add one, export your project (Project Settings > Code Export > Download), open pubspec.yaml, and add the package under dependencies (e.g., url_launcher: ^6.2.0 for opening URLs). Run flutter pub get. For the package to be usable in FlutterFlow's online editor, you must either work in the exported project going forward, or use FlutterFlow's Pub.dev integration: in Custom Code, click Add Dependency at the top and type the package name. FlutterFlow will attempt to resolve it. Once added, import the package in your Custom Action or Custom Widget using the standard import statement. Note that FlutterFlow's online package support is limited — complex packages with platform-specific configuration (like camera or Bluetooth) require the exported project workflow.
Expected result: The added package appears in the Dependencies list in FlutterFlow Custom Code and can be imported in custom code files.
Build a reusable snippet library in a Custom Action file
Build a reusable snippet library in a Custom Action file
FlutterFlow allows you to define multiple helper functions inside a single Custom Action file. Create a Custom Action named appUtils and write all your reusable utility functions as top-level Dart functions in the same file. Functions in this file are available to all other Custom Actions via the same compilation unit. Include utilities like: validateEmail(String email) -> bool, truncateText(String text, int maxLength) -> String, sanitizeInput(String input) -> String, generateUUID() -> String, and debounce timers. Centralizing these means that when you need to change the email validation regex or truncation logic, you update one file and every action using it gets the fix automatically.
1// Utility functions — available to all custom actions2import 'dart:math';34bool validateEmail(String email) {5 return RegExp(r'^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}$').hasMatch(email.trim());6}78String truncateText(String text, int maxLength) {9 if (text.length <= maxLength) return text;10 return '${text.substring(0, maxLength - 3)}...';11}1213String sanitizeInput(String input) {14 return input.trim().replaceAll(RegExp(r'[<>"\']'), '');15}1617String generateUUID() {18 final rng = Random.secure();19 final bytes = List<int>.generate(16, (_) => rng.nextInt(256));20 bytes[6] = (bytes[6] & 0x0f) | 0x40;21 bytes[8] = (bytes[8] & 0x3f) | 0x80;22 return bytes23 .map((b) => b.toRadixString(16).padLeft(2, '0'))24 .join()25 .replaceRange(8, 8, '-')26 .replaceRange(13, 13, '-')27 .replaceRange(18, 18, '-')28 .replaceRange(23, 23, '-');29}3031String initials(String fullName) {32 final parts = fullName.trim().split(' ');33 if (parts.isEmpty) return '?';34 if (parts.length == 1) return parts[0][0].toUpperCase();35 return '${parts.first[0]}${parts.last[0]}'.toUpperCase();36}Expected result: The utility functions are accessible from other Custom Actions by calling them directly (no import needed within the same compilation unit).
Complete working example
1import 'dart:math';23/// Format a number as currency with locale-style grouping.4/// formatCurrency(1234567.89, 'USD') -> '$1,234,567.89'5String formatCurrency(double amount, String currencyCode) {6 final symbols = {'USD': '\$', 'EUR': '€', 'GBP': '£', 'JPY': '¥'};7 final symbol = symbols[currencyCode] ?? currencyCode + ' ';8 final isNegative = amount < 0;9 final abs = amount.abs();1011 // Format integer part with commas12 final intPart = abs.truncate().toString();13 final buffer = StringBuffer();14 for (int i = 0; i < intPart.length; i++) {15 if (i > 0 && (intPart.length - i) % 3 == 0) buffer.write(',');16 buffer.write(intPart[i]);17 }1819 // Add decimal part (JPY has no cents)20 String result;21 if (currencyCode == 'JPY') {22 result = '$symbol${buffer.toString()}';23 } else {24 final decPart = (abs - abs.truncate()).toStringAsFixed(2).substring(1);25 result = '$symbol${buffer.toString()}$decPart';26 }2728 return isNegative ? '-$result' : result;29}3031/// Format a file size in bytes to a human-readable string.32/// formatFileSize(1536000) -> '1.5 MB'33String formatFileSize(int bytes) {34 if (bytes < 1024) return '$bytes B';35 if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';36 if (bytes < 1024 * 1024 * 1024) {37 return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';38 }39 return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';40}4142/// Clamp a number between min and max.43double clampValue(double value, double minVal, double maxVal) {44 return value.clamp(minVal, maxVal);45}4647/// Return a deterministic color hex from any string (for avatars, tags, etc.).48/// colorFromString('alice') -> '#E53935'49String colorFromString(String input) {50 const colors = [51 '#E53935', '#8E24AA', '#1E88E5', '#00ACC1',52 '#43A047', '#FB8C00', '#F4511E', '#6D4C41',53 ];54 int hash = 0;55 for (final codeUnit in input.codeUnits) {56 hash = (hash * 31 + codeUnit) & 0xFFFFFFFF;57 }58 return colors[hash % colors.length];59}Common mistakes
Why it's a problem: Writing the same utility logic inline in multiple Custom Actions instead of centralizing it
How to avoid: Write utility functions once in a dedicated appUtils Custom Action file. All other Custom Actions can call these functions directly since they share the same compilation unit.
Why it's a problem: Using a Custom Action for a pure data transformation that should be a Custom Function
How to avoid: Use Custom Functions for any operation that returns a display value (formatted date, truncated text, computed label). Use Custom Actions only when the code has side effects or is async.
Why it's a problem: Not handling exceptions in Custom Actions, causing the whole Action Flow to silently fail
How to avoid: Wrap Custom Action code in try-catch blocks. Return an error string or set an error page state variable so the UI can show a meaningful message.
Best practices
- Use Custom Functions for pure transformations and Custom Actions for async operations with side effects — never mix the two roles.
- Centralize reusable utility code in a single appUtils file rather than duplicating logic across multiple Custom Actions.
- Always add try-catch blocks in Custom Actions and return a status string so Action Flows can handle errors gracefully.
- Validate inputs at the start of every Custom Action before performing any expensive operations.
- Prefix Custom Function names with the domain they belong to (e.g., formatCurrency, formatDate, formatFileSize) for easy discovery.
- Document each Custom Function with a one-line comment explaining inputs, outputs, and an example.
- Test Custom Functions with edge-case inputs (null, empty string, very large numbers) before wiring them to production UI.
- Use const for any values that never change in Custom Functions — it improves Dart's compile-time optimisation.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app and need Custom Functions for common utility operations. Write Dart Custom Functions for: (1) formatRelativeTime(DateTime) returning a string like '5 min ago', (2) formatCurrency(double, String currencyCode) returning '$1,234.56', (3) validateEmail(String) returning bool, (4) generateInitials(String fullName) returning up to 2 uppercase initials, and (5) truncateText(String, int maxLength) adding '...' if truncated. All functions must be synchronous and pure with no imports beyond dart:core.
In FlutterFlow I need to add local storage persistence to my app. Create two Custom Actions — saveToLocalStorage(String key, String value) and getFromLocalStorage(String key) returning String — using the shared_preferences package. Then show me how to wire getFromLocalStorage on the On Page Load event of my settings page to restore the saved theme preference into a page state variable.
Frequently asked questions
Can I import external Dart packages inside a Custom Function?
Only packages that are already included in FlutterFlow's default dependency set can be imported in Custom Functions. For additional packages, you must either add them via the Custom Code > Add Dependency panel, or work in the exported project. Dart core libraries (dart:math, dart:convert, dart:typed_data) are always available.
Why does my Custom Function work in the code editor but show an error in widget binding?
The most common cause is a return type mismatch. If your Custom Function is bound to a Text widget, its return type must be String. If it returns int or double, FlutterFlow's binding will reject it. Change the return type or add a .toString() call at the end of the function.
Can Custom Actions run in the background while the user navigates away from a page?
No. FlutterFlow Custom Actions run on the main Dart isolate tied to the current widget tree. If the user navigates away, the page disposes and any pending async operations may be cancelled. For true background processing, use Firebase Cloud Functions or a Dart Isolate (which requires code export and cannot be done in the visual builder).
How do I share data between a Custom Action and the widget tree?
Custom Actions can update App State variables or Page State variables by returning a value and mapping it to a state variable in the Action Flow editor. They can also directly call setState on page-level state if you pass a callback parameter. App State is the cleanest approach — update it in the action and let FlutterFlow's reactive bindings propagate the value to the UI.
Is there a way to test a Custom Function without running the full app?
FlutterFlow's code editor shows syntax errors in real time. For logic testing, you can export the project and run unit tests locally using the flutter test command with a standard Dart test file. There is no in-browser unit test runner in FlutterFlow's online editor.
Can I use async/await in a Custom Function?
No. Custom Functions must be synchronous — they cannot be marked async and cannot use await. This is by design, because Custom Functions are used in widget bindings, which must return values instantly. If you need async logic, use a Custom Action instead, which can be async and can update state when it completes.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation