Create a visual page builder using a Custom Widget with Flutter's Draggable and DragTarget widgets. Users drag elements from a palette onto a canvas where they are positioned at the drop coordinates. Each placed element is stored in Component State as a list of JSON objects tracking type, position, and properties. Save layouts to Firestore as serialized JSON and render them dynamically from the saved data.
Building a Visual Drag-and-Drop Interface Builder in FlutterFlow
Form builders, page editors, and dashboard configurators all need drag-and-drop functionality. This tutorial creates a visual builder where users drag elements from a palette onto a canvas, position them freely, edit their properties, and save the layout as JSON to Firestore. The saved layout can then be rendered dynamically in a separate viewer widget.
Prerequisites
- A FlutterFlow project on the Pro plan or higher
- Firebase project with Firestore enabled
- Familiarity with FlutterFlow Custom Widgets and Component State
- Basic understanding of Flutter's Draggable and DragTarget widgets
Step-by-step guide
Create the element palette with Draggable widgets
Create the element palette with Draggable widgets
Create a Custom Widget named InterfaceBuilder. Define an enum of element types: text, image, button, and textField. In the build method, create a Column on the left side (width 120) containing a list of Draggable widgets, one for each element type. Each Draggable wraps a Container showing an icon and label (e.g., text_fields icon + 'Text'). Set the data property to the element type string. The feedback parameter shows a semi-transparent version of the element being dragged. The childWhenDragging parameter dims the palette item to indicate it is being moved.
Expected result: A vertical palette of draggable element types appears on the left side of the widget, and each item can be picked up and dragged.
Build the DragTarget canvas that accepts dropped elements
Build the DragTarget canvas that accepts dropped elements
Next to the palette, add an Expanded widget containing a DragTarget wrapped in a Stack. The DragTarget accepts String data (the element type). In the onAcceptWithDetails callback, capture the drop position relative to the canvas using details.offset and the canvas's RenderBox globalToLocal conversion. Create a new element map with keys: id (unique), type, fractionalX (drop x / canvas width), fractionalY (drop y / canvas height), width (0.2 default), and height (0.1 default). Add this map to a List stored in a StatefulWidget state variable called _elements. Use setState to trigger a rebuild.
1// Inside DragTarget onAcceptWithDetails:2final RenderBox box = _canvasKey.currentContext!3 .findRenderObject() as RenderBox;4final local = box.globalToLocal(details.offset);5final fracX = (local.dx / box.size.width).clamp(0.0, 1.0);6final fracY = (local.dy / box.size.height).clamp(0.0, 1.0);78setState(() {9 _elements.add({10 'id': DateTime.now().millisecondsSinceEpoch.toString(),11 'type': details.data,12 'fracX': fracX,13 'fracY': fracY,14 'width': 0.2,15 'height': 0.1,16 'label': 'New ${details.data}',17 });18});Expected result: Dropping an element from the palette onto the canvas adds it at the drop position, and it appears as a rendered widget in the Stack.
Render placed elements with Positioned widgets in the Stack
Render placed elements with Positioned widgets in the Stack
Inside the Stack, iterate over _elements and create a Positioned widget for each one. Calculate the left and top values by multiplying fracX and fracY by the canvas dimensions using a LayoutBuilder. Each Positioned child is a GestureDetector wrapping a Container styled according to the element type: Text elements show a text label, Image elements show a placeholder icon, Button elements show an ElevatedButton, and TextField elements show an outlined input. Add a blue border around the currently selected element (tracked via _selectedId state variable). The GestureDetector onTap sets _selectedId to the tapped element's id.
Expected result: All placed elements render at their correct positions on the canvas. Tapping an element highlights it with a blue selection border.
Add a properties editor panel for the selected element
Add a properties editor panel for the selected element
Below the canvas or in a BottomSheet, build a properties panel that appears when _selectedId is not null. The panel shows TextFields for editing the selected element's properties: label text, width (as fraction 0.0-1.0), and height. Add a Delete button that removes the element from _elements by id. Add a color picker using a Wrap of colored CircleAvatars that updates a backgroundColor property on the element map. Every change calls setState to update the canvas rendering in real time. The panel hides when the user taps an empty area of the canvas (set _selectedId to null in the DragTarget's onTap).
Expected result: Selecting an element opens the properties panel where the user can edit its label, size, color, or delete it. Changes reflect immediately on the canvas.
Save the layout to Firestore as serialized JSON
Save the layout to Firestore as serialized JSON
Add a Save button above the canvas. On tap, serialize the _elements list to a JSON string using jsonEncode. Create or update a document in a Firestore `layouts` collection with fields: userId (current user), name (from a TextField prompt), layoutJson (the serialized string), and updatedAt (timestamp). To load a saved layout, query the layouts collection for the current user, display results in a ListView dialog, and on selection, parse the layoutJson with jsonDecode back into the _elements list. Call setState to render the loaded layout on the canvas.
1// Save layout2final jsonStr = jsonEncode(_elements);3await FirebaseFirestore.instance4 .collection('layouts')5 .doc(layoutId)6 .set({7 'userId': currentUser.uid,8 'name': layoutName,9 'layoutJson': jsonStr,10 'updatedAt': FieldValue.serverTimestamp(),11});1213// Load layout14final doc = await FirebaseFirestore.instance15 .collection('layouts')16 .doc(layoutId)17 .get();18final list = jsonDecode(doc['layoutJson']) as List;19setState(() {20 _elements = list.map((e) =>21 Map<String, dynamic>.from(e)).toList();22});Expected result: Layouts are saved to Firestore and can be loaded back, restoring all element positions, types, and properties exactly as configured.
Build a layout renderer widget for displaying saved layouts
Build a layout renderer widget for displaying saved layouts
Create a second Custom Widget named LayoutRenderer with a Component Parameter layoutJson (String). In the build method, decode the JSON string into a list of element maps. Use a LayoutBuilder wrapping a Stack with Positioned children, just like the builder canvas but without drag-and-drop or selection capabilities. Each element renders as a read-only widget matching its type. This renderer can be placed on any page in your FlutterFlow app to display user-created layouts dynamically. Bind the layoutJson parameter to a Backend Query that fetches the appropriate layout document.
Expected result: Saved layouts render correctly in the read-only renderer widget, allowing user-created designs to appear throughout the app.
Complete working example
1// Custom Widget: InterfaceBuilder2import 'dart:convert';3import 'package:flutter/material.dart';45class InterfaceBuilder extends StatefulWidget {6 final double width;7 final double height;8 const InterfaceBuilder({Key? key, required this.width, required this.height}) : super(key: key);9 @override10 State<InterfaceBuilder> createState() => _InterfaceBuilderState();11}1213class _InterfaceBuilderState extends State<InterfaceBuilder> {14 List<Map<String, dynamic>> _elements = [];15 String? _selectedId;16 final _types = ['text', 'image', 'button', 'textField'];1718 @override19 Widget build(BuildContext context) {20 return SizedBox(21 width: widget.width,22 height: widget.height,23 child: Row(children: [24 // Palette25 SizedBox(26 width: 100,27 child: ListView(28 children: _types.map((t) => Draggable<String>(29 data: t,30 feedback: Material(child: Chip(label: Text(t))),31 child: ListTile(leading: Icon(Icons.widgets), title: Text(t)),32 )).toList(),33 ),34 ),35 // Canvas36 Expanded(37 child: LayoutBuilder(builder: (ctx, constraints) {38 return DragTarget<String>(39 onAcceptWithDetails: (details) {40 final box = ctx.findRenderObject() as RenderBox;41 final local = box.globalToLocal(details.offset);42 setState(() {43 _elements.add({44 'id': DateTime.now().millisecondsSinceEpoch.toString(),45 'type': details.data,46 'fracX': (local.dx / constraints.maxWidth).clamp(0.0, 0.8),47 'fracY': (local.dy / constraints.maxHeight).clamp(0.0, 0.8),48 'label': 'New ${details.data}',49 });50 });51 },52 builder: (ctx, candidates, rejects) {53 return Container(54 color: Colors.grey[100],55 child: Stack(56 children: _elements.map((el) {57 final selected = el['id'] == _selectedId;58 return Positioned(59 left: el['fracX'] * constraints.maxWidth,60 top: el['fracY'] * constraints.maxHeight,61 child: GestureDetector(62 onTap: () => setState(() => _selectedId = el['id']),63 child: Container(64 padding: const EdgeInsets.all(8),65 decoration: BoxDecoration(66 border: Border.all(67 color: selected ? Colors.blue : Colors.grey,68 width: selected ? 2 : 1,69 ),70 color: Colors.white,71 ),72 child: Text(el['label'] ?? ''),73 ),74 ),75 );76 }).toList(),77 ),78 );79 },80 );81 }),82 ),83 ]),84 );85 }8687 // Save layout to Firestore88 Future<void> saveLayout(String layoutId, String name) async {89 final jsonStr = jsonEncode(_elements);90 // Write to Firestore layouts collection91 // await FirebaseFirestore.instance.doc('layouts/$layoutId').set(...);92 }9394 // Load layout from Firestore95 Future<void> loadLayout(String layoutId) async {96 // Read from Firestore, decode JSON, setState97 }98}Common mistakes when developing a Drag-and-Drop Interface Builder in FlutterFlow
Why it's a problem: Storing element positions in absolute pixel values
How to avoid: Use fractional positions (0.0-1.0 of canvas width and height) so the layout scales proportionally on any device. Multiply by the LayoutBuilder constraints at render time.
Why it's a problem: Not using a GlobalKey or RenderBox for accurate drop position calculation
How to avoid: Use the canvas RenderBox's globalToLocal method to convert the global drop offset to canvas-relative coordinates before calculating fractional positions.
Why it's a problem: Forgetting to serialize element data as plain JSON-compatible maps
How to avoid: Convert all element properties to JSON-safe primitives (strings, numbers, booleans) before saving. Store colors as hex strings and positions as doubles.
Best practices
- Clamp fractional positions between 0.0 and a maximum like 0.8 to prevent elements from being placed partially off-canvas
- Highlight the selected element with a visible border color so users always know which element they are editing
- Provide a Delete button in the properties panel for every element to allow easy removal
- Show a grid overlay on the canvas as a visual guide for element alignment
- Add undo functionality by keeping a history stack of _elements states and popping on undo
- Use a LayoutBuilder to get actual canvas dimensions rather than hardcoding pixel values
- Test saved layouts on multiple screen sizes to verify fractional positioning works correctly
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to build a drag-and-drop interface builder in FlutterFlow. I need a Custom Widget with a palette of element types (text, image, button, text field) on the left and a DragTarget canvas on the right. Placed elements should store fractional positions and be selectable with a properties editor. Show me the full Dart code.
Create a Custom Widget with a left sidebar palette of draggable elements (text, image, button, text field) and a main canvas area. When I drag an element onto the canvas, it should appear at the drop position. Tapping an element should show a properties editor. Add save and load functionality using Firestore.
Frequently asked questions
Can users resize placed elements by dragging their corners?
Yes. Add small GestureDetector handles at each corner of the selected element. On pan update, adjust the element's width and height fractional values based on the drag delta divided by the canvas dimensions.
How do I support drag-to-reposition for already placed elements?
Wrap each placed element in a GestureDetector with onPanUpdate. Calculate the new fractional position from the pan delta and update the element's fracX and fracY in setState.
Can I add snapping to a grid when elements are dropped?
Yes. Round the fractional position to the nearest grid increment, for example Math.round(fracX * 20) / 20 for a 20-column grid. This snaps elements to the nearest 5% position.
How do I render the saved layout on a different page without edit functionality?
Create a separate LayoutRenderer Custom Widget that reads the layoutJson from Firestore, decodes it, and renders elements in a Stack using LayoutBuilder — without the DragTarget, palette, or selection logic.
Is there a limit to how many elements the builder can handle?
Flutter's rendering engine handles hundreds of positioned widgets efficiently. For practical use, keep layouts under 100 elements. Beyond that, consider grouping elements into containers.
Can RapidDev help build a production-grade visual builder?
Yes. RapidDev can implement advanced features like nested containers, responsive breakpoints, undo/redo history, collaborative editing, and template libraries for your drag-and-drop builder.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation