FlutterFlow supports three types of custom Dart code: Custom Actions (async logic like API calls), Custom Functions (synchronous data transformation), and Custom Widgets (full Flutter UI components). Access them all from the left sidebar under Custom Code. Each type serves a different purpose — choosing the right one saves hours of debugging.
Extending FlutterFlow with Dart Code
FlutterFlow's visual builder covers most app needs, but sometimes you need logic or UI that the drag-and-drop editor cannot express. Custom Code lets you drop into raw Dart/Flutter for exactly those cases. There are three distinct types, each with its own access pattern and constraints. Understanding which type to use — and why — is the key skill this tutorial teaches.
Prerequisites
- A FlutterFlow project already created (Free plan works for writing; Pro for code export)
- Basic familiarity with FlutterFlow's widget canvas
- No prior Dart experience required, but helpful
- Understanding of what Actions and Functions mean in FlutterFlow's visual builder
Step-by-step guide
Navigate to Custom Code and choose your code type
Navigate to Custom Code and choose your code type
In the FlutterFlow editor, look at the far left sidebar. You will see icons for Pages, Components, and several others. Click the icon that looks like angle brackets (</>) — this opens the Custom Code panel. Inside, you will see three sections: Custom Actions, Custom Functions, and Custom Widgets. Each has a '+' button to create a new item. Take a moment to read the brief description FlutterFlow shows for each type before creating anything. This orientation step prevents the most common mistake: creating a Custom Action when you actually need a Custom Function.
Expected result: The Custom Code panel is open showing three sections: Custom Actions, Custom Functions, and Custom Widgets.
Create a Custom Action for Async Logic
Create a Custom Action for Async Logic
Click '+' next to Custom Actions. Name it 'fetchWeatherData'. Custom Actions are async Dart functions — they can await HTTP calls, read device storage, call Firebase, or do anything that requires waiting. In the code editor that appears, you will see a pre-generated function signature. The function receives typed parameters you define in the Arguments panel on the right, and returns a value you declare in the Return Value section. Add a String parameter named 'city'. Set the return type to String. Write your async logic inside the function body. Custom Actions are invoked from Action Flows — drag them from the Action palette onto a button's On Tap event.
1// Custom Action: fetchWeatherData2// Arguments: city (String)3// Return Value: String45Future<String> fetchWeatherData(String city) async {6 final response = await http.get(7 Uri.parse(8 'https://api.openweathermap.org/data/2.5/weather?q=$city&appid=YOUR_KEY',9 ),10 );1112 if (response.statusCode == 200) {13 final data = jsonDecode(response.body);14 final temp = data['main']['temp'];15 final description = data['weather'][0]['description'];16 return '$description, ${(temp - 273.15).toStringAsFixed(1)}°C';17 } else {18 return 'Could not fetch weather for $city';19 }20}Expected result: A Custom Action named fetchWeatherData appears in the Custom Actions list with a green checkmark indicating no compile errors.
Create a Custom Function for Synchronous Transformations
Create a Custom Function for Synchronous Transformations
Click '+' next to Custom Functions. Name it 'formatCurrency'. Custom Functions are SYNCHRONOUS pure Dart functions — they take inputs and immediately return an output with no waiting, no async, no await. They are ideal for formatting numbers, parsing strings, calculating values, and transforming data for display in widgets. In the editor, define your parameters (a double named 'amount' and a String named 'currencyCode') and a String return type. Write the pure transformation logic. Custom Functions appear in the Expression Editor throughout FlutterFlow — you can use them directly in widget Text bindings, Condition expressions, and anywhere you type a formula.
1// Custom Function: formatCurrency2// Arguments: amount (double), currencyCode (String)3// Return Value: String4// IMPORTANT: No async/await — this is synchronous only56String formatCurrency(double amount, String currencyCode) {7 final symbols = {'USD': '\$', 'EUR': '€', 'GBP': '£', 'JPY': '¥'};8 final symbol = symbols[currencyCode] ?? currencyCode;910 if (amount >= 1000000) {11 return '$symbol${(amount / 1000000).toStringAsFixed(1)}M';12 } else if (amount >= 1000) {13 return '$symbol${(amount / 1000).toStringAsFixed(1)}K';14 }15 return '$symbol${amount.toStringAsFixed(2)}';16}Expected result: The formatCurrency function appears under Custom Functions and is available in the Expression Editor when binding widget properties.
Create a Custom Widget for Flutter UI
Create a Custom Widget for Flutter UI
Click '+' next to Custom Widgets. Name it 'GradientProgressBar'. Custom Widgets are full Flutter StatefulWidget or StatelessWidget classes. They appear as draggable components in the FlutterFlow widget palette, just like built-in widgets. Use them when you need UI that FlutterFlow's visual tools cannot produce — custom painters, complex animations, third-party package widgets, or any rendering logic requiring raw Flutter code. Define widget parameters in the Parameters panel on the right (e.g., a double named 'progress' and a Color named 'startColor'). These parameters become configurable Properties in the FlutterFlow UI when the widget is placed on a canvas.
1// Custom Widget: GradientProgressBar2// Parameters: progress (double, 0.0-1.0), startColor (Color), endColor (Color)34class GradientProgressBar extends StatelessWidget {5 final double progress;6 final Color startColor;7 final Color endColor;8 final double height;910 const GradientProgressBar({11 Key? key,12 required this.progress,13 required this.startColor,14 required this.endColor,15 this.height = 12.0,16 }) : super(key: key);1718 @override19 Widget build(BuildContext context) {20 return ClipRRect(21 borderRadius: BorderRadius.circular(height / 2),22 child: SizedBox(23 height: height,24 child: Stack(25 children: [26 Container(color: Colors.grey.shade200),27 FractionallySizedBox(28 widthFactor: progress.clamp(0.0, 1.0),29 child: Container(30 decoration: BoxDecoration(31 gradient: LinearGradient(32 colors: [startColor, endColor],33 ),34 ),35 ),36 ),37 ],38 ),39 ),40 );41 }42}Expected result: The GradientProgressBar widget appears in the widget palette under Custom Widgets and can be dragged onto any page canvas.
Wire a Custom Action to a Button
Wire a Custom Action to a Button
Navigate to a page in your app. Select a Button widget on the canvas. In the right panel, click the Actions tab, then click '+' to add an action. In the Action Flow Editor, click '+' on the flow and search for your Custom Action by name — type 'fetchWeather' and it will appear under the Custom Actions section. Select it. FlutterFlow will prompt you to bind the 'city' argument — tap the binding icon and choose a TextField's value, an App State variable, or type a literal string. Set the Action Output Name (e.g., 'weatherResult') so you can reference the return value in subsequent actions. Add a second action: Update App State → set a variable to 'weatherResult'. Now a Text widget bound to that variable will update after the button tap.
Expected result: Tapping the button in the FlutterFlow preview triggers the Custom Action, and the Text widget updates with the returned weather string.
Test and Debug Custom Code
Test and Debug Custom Code
FlutterFlow compiles your Dart code in real time — red underlines in the editor mean syntax errors, and the panel shows a compile error banner. Fix all errors before leaving the Custom Code panel. For runtime debugging, use the FlutterFlow Run Mode (the play button, top-right) to test in the browser. For deeper debugging — print statements, breakpoints — click the '</> View Code' button (top-right) to open the full generated Flutter project, then run it in your local VS Code or Android Studio with a connected device. Custom Functions can also be tested inline: the Expression Editor shows a live preview of the return value when you type test inputs into the argument fields.
Expected result: No compile errors in the Custom Code panel, and the feature works end-to-end in Run Mode.
Complete working example
1// ============================================================2// FlutterFlow Custom Code — Three Types Demonstrated3// ============================================================45// --- 1. CUSTOM ACTION (async) ---6// File: custom_actions/fetch_weather_data.dart7// Use from: Action Flow Editor → Custom Actions89Future<String> fetchWeatherData(String city) async {10 final response = await http.get(11 Uri.parse(12 'https://api.openweathermap.org/data/2.5/weather?q=$city&appid=YOUR_KEY',13 ),14 );15 if (response.statusCode == 200) {16 final data = jsonDecode(response.body);17 final temp = data['main']['temp'];18 final desc = data['weather'][0]['description'];19 return '$desc, ${(temp - 273.15).toStringAsFixed(1)}°C';20 }21 return 'Weather unavailable';22}2324// --- 2. CUSTOM FUNCTION (synchronous only) ---25// File: custom_functions/format_currency.dart26// Use from: Expression Editor anywhere in FlutterFlow2728String formatCurrency(double amount, String currencyCode) {29 final symbols = {'USD': '\$', 'EUR': '€', 'GBP': '£', 'JPY': '¥'};30 final symbol = symbols[currencyCode] ?? currencyCode;31 if (amount >= 1000000) {32 return '$symbol${(amount / 1000000).toStringAsFixed(1)}M';33 } else if (amount >= 1000) {34 return '$symbol${(amount / 1000).toStringAsFixed(1)}K';35 }36 return '$symbol${amount.toStringAsFixed(2)}';37}3839// --- 3. CUSTOM WIDGET (Flutter UI component) ---40// File: custom_widgets/gradient_progress_bar.dart41// Drag from the widget palette onto any page4243class GradientProgressBar extends StatelessWidget {44 final double progress;45 final Color startColor;46 final Color endColor;47 final double height;4849 const GradientProgressBar({50 Key? key,51 required this.progress,52 required this.startColor,53 required this.endColor,54 this.height = 12.0,55 }) : super(key: key);5657 @override58 Widget build(BuildContext context) {59 return ClipRRect(60 borderRadius: BorderRadius.circular(height / 2),61 child: SizedBox(62 height: height,63 child: Stack(64 children: [65 Container(color: Colors.grey.shade200),66 FractionallySizedBox(67 widthFactor: progress.clamp(0.0, 1.0),68 child: Container(69 decoration: BoxDecoration(70 gradient: LinearGradient(71 colors: [startColor, endColor],72 ),73 ),74 ),75 ),76 ],77 ),78 ),79 );80 }81}Common mistakes when creating Custom Code in FlutterFlow
Why it's a problem: Writing async/await inside a Custom Function
How to avoid: If your logic needs to fetch data, call an API, or wait for anything, create a Custom Action instead. Move the async logic there, and use the Action Output to pass the result back to the UI via App State or Page State.
Why it's a problem: Declaring a Custom Widget with no default parameter values for optional parameters
How to avoid: Add sensible default values for all parameters that are not strictly required, for example: 'this.height = 12.0' and 'this.startColor = Colors.blue'. Mark only truly required parameters with the 'required' keyword.
Why it's a problem: Adding package imports in Custom Code without adding the package to pubspec dependencies
How to avoid: Go to Settings (gear icon, top-right) → Pubspec Dependencies → search for the package name (e.g., 'http') → toggle it on. The dependency is then available in all Custom Code files.
Why it's a problem: Using a Custom Action where a Custom Function would work
How to avoid: For pure data transformations (format a string, calculate a value, combine two strings), use a Custom Function. Its return value is available directly in the Expression Editor throughout the entire FlutterFlow UI.
Why it's a problem: Hardcoding API keys directly in Custom Action code
How to avoid: Store secrets in Firebase Remote Config or a backend environment variable. Call a Cloud Function that holds the key server-side, and have the Custom Action call that Cloud Function instead of the external API directly.
Best practices
- Name Custom Actions with verb phrases (fetchUserProfile, sendPushNotification) and Custom Functions with noun phrases (formattedDate, userDisplayName) so their type is clear from usage.
- Keep Custom Functions under 20 lines. If the logic is complex, break it into multiple focused functions rather than one large one.
- Always handle null and error cases in Custom Actions — unhandled exceptions in async code crash the app silently in FlutterFlow Run Mode.
- Use the Arguments panel to type-check Custom Action inputs rather than accepting dynamic/Object and casting inside the function body.
- Test Custom Functions using the Expression Editor's inline preview before wiring them to a widget — type sample inputs to verify the output instantly.
- Store sensitive configuration (API base URLs, keys) in App State constants or Firebase Remote Config, not as hardcoded strings in Custom Code.
- Add a brief comment at the top of every Custom Code file explaining what it does and which widget or action flow uses it — FlutterFlow has no automatic documentation.
- For Custom Widgets that use third-party packages, pin the package version in pubspec.yaml to avoid breaking changes when FlutterFlow auto-updates dependencies.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a FlutterFlow app and need to write a Custom Action in Dart that calls an external REST API and returns a parsed result. The function should handle errors gracefully and return a fallback string on failure. Show me a complete Custom Action template with the correct FlutterFlow function signature, error handling, and HTTP call.
Write a FlutterFlow Custom Function in Dart that takes a DateTime and returns a human-readable relative time string like '2 hours ago' or 'Yesterday'. It must be synchronous with no async/await.
Frequently asked questions
What is the difference between a Custom Action and a Custom Function in FlutterFlow?
Custom Actions are async Dart functions invoked from Action Flows (e.g., on button tap). They can await network calls, device APIs, and Firebase. Custom Functions are synchronous pure functions available in the Expression Editor for data transformation — they cannot use async/await. Use Actions for 'do something', Functions for 'transform something'.
Do I need to know Dart to use Custom Code in FlutterFlow?
Basic Dart syntax helps but is not mandatory for simple cases. FlutterFlow generates boilerplate for you, and most Custom Functions only require a return statement with straightforward logic. For complex Custom Widgets involving animations or custom painters, intermediate Dart/Flutter knowledge is recommended.
Can I use external Flutter packages in Custom Code?
Yes. Go to Settings → Pubspec Dependencies, search for the package (e.g., 'http', 'intl', 'fl_chart'), and enable it. The package is then importable in your Custom Code files. FlutterFlow Pro or higher is required to export and run the full project locally.
Why does my Custom Widget show a red error box on the FlutterFlow canvas?
This usually means the widget threw an error during construction — most commonly because a required parameter has no default value and FlutterFlow passes null when the widget is first placed. Add default values to all optional parameters. You can also check the error details by hovering over the red box.
Can Custom Functions access App State or Firestore?
No. Custom Functions are pure synchronous functions with no access to Flutter's context, FlutterFlow's app state, or any async data source. If you need to read App State or Firestore, use a Custom Action instead, which has access to the FFAppState singleton.
How do I pass data from a Custom Action back to the UI?
In the Action Flow Editor, set an 'Action Output' name when you configure the Custom Action. This output is available as a local variable in subsequent actions in the same flow. Use an 'Update App State' or 'Update Page State' action immediately after to store the value, then bind a widget to that state variable.
Can I use Custom Widgets on both mobile and web in FlutterFlow?
Yes, as long as the packages your Custom Widget uses support all platforms. Most Flutter UI packages are cross-platform. If you use a package that is mobile-only (e.g., camera or GPS), the widget will fail on web. Check pub.dev platform support badges before adding a package.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation