Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Create a Custom Landing Page Builder Within FlutterFlow

Build a meta-builder in FlutterFlow where your users can create their own landing pages. Store all sections in a single Firestore document as an ordered Array of section objects (not separate documents) to make reordering a single write. Use ReorderableListView for drag-and-drop section ordering, a BottomSheet editor per section type, a ColorFiltered preview, and a published boolean toggle for one-click deployment.

What you'll learn

  • How to model landing page sections as an ordered Array in a single Firestore document
  • How to build drag-to-reorder section management using ReorderableListView
  • How to create BottomSheet editors for each section type (hero, text, image, CTA)
  • How to implement a publish toggle that makes pages live with a single Firestore update
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner11 min read70-90 minFlutterFlow Pro+ (Custom Widgets required for ReorderableListView)March 2026RapidDev Engineering Team
TL;DR

Build a meta-builder in FlutterFlow where your users can create their own landing pages. Store all sections in a single Firestore document as an ordered Array of section objects (not separate documents) to make reordering a single write. Use ReorderableListView for drag-and-drop section ordering, a BottomSheet editor per section type, a ColorFiltered preview, and a published boolean toggle for one-click deployment.

Let Your Users Build Their Own Pages

A landing page builder inside your FlutterFlow app lets end users create and customize their own promotional pages, product listings, or profile pages without needing a developer. The critical design decision is data modeling: storing each section as a separate Firestore document makes reordering require N writes and creates race conditions. Instead, store all sections as an ordered Array on a single page document — reordering becomes one atomic write, and the entire page loads in a single Firestore read.

Prerequisites

  • FlutterFlow Pro plan (Custom Widgets required for drag-and-drop)
  • Firebase and Firestore configured in your project
  • Authentication set up so pages belong to specific users
  • Basic Dart knowledge for Custom Widget implementation

Step-by-step guide

1

Design the Page Document Schema in Firestore

Create a 'pages' Firestore collection. Each page document contains: user_id, title (String), slug (String — URL identifier), published (Boolean), created_at, updated_at, and sections (Array of Maps). Each section Map in the Array has: id (UUID), type (String — 'hero', 'text', 'image', 'cta', 'spacer'), order (Integer), and a content Map whose keys vary by type. For a 'hero' section, content has heading, subheading, background_color, and button_text. For 'text', it has body and alignment. For 'image', it has image_url and caption. For 'cta', it has headline, button_text, and button_url. Storing sections as an Array on the parent document means the entire page loads in one read and reordering is one write.

Expected result: Firestore shows a 'pages' collection where each document contains a sections Array with multiple section Maps, each with a type and content object.

2

Build the Section List with ReorderableListView

Create a Custom Widget called SectionReorderList that wraps Flutter's ReorderableListView. Each item in the list renders a section preview card based on the section type, with a drag handle icon on the right side. When the user finishes dragging, the ReorderableListView's onReorder callback fires with the old and new index. In the callback, create a new List with the section moved to the new position, regenerate the order integers, and write the updated sections Array to Firestore in one update() call. Show a drag-mode indicator bar at the top when dragging is active.

section_reorder_list.dart
1import 'package:flutter/material.dart';
2
3class SectionReorderList extends StatefulWidget {
4 final List<Map<String, dynamic>> sections;
5 final ValueChanged<List<Map<String, dynamic>>> onReorder;
6 final ValueChanged<Map<String, dynamic>> onEditSection;
7 final ValueChanged<String> onDeleteSection;
8
9 const SectionReorderList({
10 super.key,
11 required this.sections,
12 required this.onReorder,
13 required this.onEditSection,
14 required this.onDeleteSection,
15 });
16
17 @override
18 State<SectionReorderList> createState() => _SectionReorderListState();
19}
20
21class _SectionReorderListState extends State<SectionReorderList> {
22 late List<Map<String, dynamic>> _sections;
23
24 @override
25 void initState() {
26 super.initState();
27 _sections = List.from(widget.sections);
28 }
29
30 @override
31 Widget build(BuildContext context) {
32 return ReorderableListView.builder(
33 shrinkWrap: true,
34 physics: const NeverScrollableScrollPhysics(),
35 itemCount: _sections.length,
36 onReorder: (oldIndex, newIndex) {
37 setState(() {
38 if (newIndex > oldIndex) newIndex--;
39 final item = _sections.removeAt(oldIndex);
40 _sections.insert(newIndex, item);
41 // Reindex order field
42 for (int i = 0; i < _sections.length; i++) {
43 _sections[i] = {..._sections[i], 'order': i};
44 }
45 });
46 widget.onReorder(_sections);
47 },
48 itemBuilder: (context, index) {
49 final section = _sections[index];
50 return SectionPreviewCard(
51 key: ValueKey(section['id']),
52 section: section,
53 onEdit: () => widget.onEditSection(section),
54 onDelete: () => widget.onDeleteSection(section['id'] as String),
55 );
56 },
57 );
58 }
59}

Expected result: Long-pressing and dragging a section card moves it to a new position in the list. Releasing saves the new order to Firestore as a single write.

3

Create BottomSheet Editors for Each Section Type

When a user taps the Edit button on a section card, show a BottomSheet with the appropriate editor for that section type. Use a switch on section type to determine which editor to show. The Hero BottomSheet contains a TextField for heading, a TextField for subheading, a ColorPicker for background color, and a TextField for button text. The Text BottomSheet has a multiline TextField and an alignment selector. The Image BottomSheet has an image upload button and a TextField for caption. Each BottomSheet has Save and Cancel buttons. On Save, find the section by its id in the sections Array, replace it with the updated version, and write the whole sections Array back to Firestore.

update_section.dart
1import 'package:cloud_firestore/cloud_firestore.dart';
2
3Future<void> updateSection(
4 String pageId,
5 String sectionId,
6 Map<String, dynamic> updatedContent,
7) async {
8 final docRef = FirebaseFirestore.instance.collection('pages').doc(pageId);
9
10 await FirebaseFirestore.instance.runTransaction((tx) async {
11 final snap = await tx.get(docRef);
12 final sections = List<Map<String, dynamic>>.from(
13 (snap.data()?['sections'] as List? ?? []).map((s) => Map<String, dynamic>.from(s as Map)),
14 );
15
16 final index = sections.indexWhere((s) => s['id'] == sectionId);
17 if (index == -1) return;
18
19 sections[index] = {
20 ...sections[index],
21 'content': updatedContent,
22 'updated_at': DateTime.now().millisecondsSinceEpoch,
23 };
24
25 tx.update(docRef, {
26 'sections': sections,
27 'updated_at': FieldValue.serverTimestamp(),
28 });
29 });
30}

Expected result: Tapping Edit on a Hero section opens a bottom sheet with the current heading and subheading pre-populated. Saving updates only that section in the Firestore document.

4

Add New Sections with a Section Type Picker

Add an Add Section button at the bottom of the sections list. Tapping it shows a BottomSheet with a grid of section type options: Hero, Text, Image, and CTA — each with an icon and label. Tapping a type creates a new section Map with default content for that type, assigns a UUID as the id, sets order to sections.length, and appends it to the sections Array using FieldValue.arrayUnion(). Show a brief animation (AnimatedList or FadeTransition) as the new section appears. New sections always appear at the bottom and can be dragged to the desired position.

Expected result: Tapping Add Section shows a type picker. Choosing Hero adds a new hero section card at the bottom of the list with placeholder text visible.

5

Implement the Publish Toggle and Live Preview

Add a Publish toggle switch at the top of the page editor, linked to the page document's published Boolean field. When toggled on, update published to true and set published_at to the current timestamp. Your public-facing page renderer reads the page by slug and checks published — unpublished pages show a 404 or a preview-only message. Add a Preview button that opens the page in a WebView or a Flutter route that renders the sections as real UI (not just the editor cards). The preview renders each section type using its actual content: a full-width Container with color for Hero, a Text widget for Text sections, and an Image.network for Image sections.

toggle_publish.dart
1import 'package:cloud_firestore/cloud_firestore.dart';
2
3Future<void> togglePublish(String pageId, bool publish) async {
4 final update = <String, dynamic>{
5 'published': publish,
6 'updated_at': FieldValue.serverTimestamp(),
7 };
8
9 if (publish) {
10 update['published_at'] = FieldValue.serverTimestamp();
11 }
12
13 await FirebaseFirestore.instance
14 .collection('pages')
15 .doc(pageId)
16 .update(update);
17}

Expected result: Toggling Published on makes the page accessible at its slug URL. Toggling off removes it from public access. The toggle state reflects the current Firestore value instantly via the real-time listener.

Complete working example

page_builder_service.dart
1// Complete landing page builder service for FlutterFlow
2import 'package:cloud_firestore/cloud_firestore.dart';
3import 'package:firebase_auth/firebase_auth.dart';
4import 'package:uuid/uuid.dart';
5
6const _uuid = Uuid();
7
8class PageBuilderService {
9 final _db = FirebaseFirestore.instance;
10 String? get _uid => FirebaseAuth.instance.currentUser?.uid;
11
12 // ─── Create a new blank page ──────────────────────────────────────────────
13
14 Future<String?> createPage(String title, String slug) async {
15 if (_uid == null) return null;
16
17 final docRef = await _db.collection('pages').add({
18 'user_id': _uid,
19 'title': title,
20 'slug': slug.toLowerCase().replaceAll(' ', '-'),
21 'published': false,
22 'sections': [
23 _defaultSection('hero'),
24 ],
25 'created_at': FieldValue.serverTimestamp(),
26 'updated_at': FieldValue.serverTimestamp(),
27 });
28 return docRef.id;
29 }
30
31 // ─── Add a section ────────────────────────────────────────────────────────
32
33 Future<void> addSection(String pageId, String type) async {
34 final section = _defaultSection(type);
35 await _db.collection('pages').doc(pageId).update({
36 'sections': FieldValue.arrayUnion([section]),
37 'updated_at': FieldValue.serverTimestamp(),
38 });
39 }
40
41 // ─── Reorder sections (one atomic write) ─────────────────────────────────
42
43 Future<void> reorderSections(
44 String pageId,
45 List<Map<String, dynamic>> reorderedSections,
46 ) async {
47 await _db.collection('pages').doc(pageId).update({
48 'sections': reorderedSections,
49 'updated_at': FieldValue.serverTimestamp(),
50 });
51 }
52
53 // ─── Update a section by ID ──────────────────────────────────────────────
54
55 Future<void> updateSection(
56 String pageId,
57 String sectionId,
58 Map<String, dynamic> newContent,
59 ) async {
60 final ref = _db.collection('pages').doc(pageId);
61 await _db.runTransaction((tx) async {
62 final snap = await tx.get(ref);
63 final sections = _parseSections(snap.data()?['sections']);
64 final idx = sections.indexWhere((s) => s['id'] == sectionId);
65 if (idx == -1) return;
66 sections[idx] = {...sections[idx], 'content': newContent};
67 tx.update(ref, {'sections': sections, 'updated_at': FieldValue.serverTimestamp()});
68 });
69 }
70
71 // ─── Delete a section ────────────────────────────────────────────────────
72
73 Future<void> deleteSection(String pageId, String sectionId) async {
74 final ref = _db.collection('pages').doc(pageId);
75 await _db.runTransaction((tx) async {
76 final snap = await tx.get(ref);
77 final sections = _parseSections(snap.data()?['sections'])
78 .where((s) => s['id'] != sectionId).toList();
79 tx.update(ref, {'sections': sections, 'updated_at': FieldValue.serverTimestamp()});
80 });
81 }
82
83 // ─── Publish / Unpublish ──────────────────────────────────────────────────
84
85 Future<void> setPublished(String pageId, bool published) async {
86 await _db.collection('pages').doc(pageId).update({
87 'published': published,
88 if (published) 'published_at': FieldValue.serverTimestamp(),
89 'updated_at': FieldValue.serverTimestamp(),
90 });
91 }
92
93 // ─── Helpers ──────────────────────────────────────────────────────────────
94
95 Map<String, dynamic> _defaultSection(String type) {
96 final defaults = <String, Map<String, dynamic>>{
97 'hero': {'heading': 'Your Headline Here', 'subheading': 'Supporting text', 'background_color': '#4F46E5', 'button_text': 'Get Started'},
98 'text': {'body': 'Add your paragraph text here.', 'alignment': 'left'},
99 'image': {'image_url': '', 'caption': ''},
100 'cta': {'headline': 'Ready to get started?', 'button_text': 'Sign Up Now', 'button_url': ''},
101 };
102 return {'id': _uuid.v4(), 'type': type, 'order': 0, 'content': defaults[type] ?? {}};
103 }
104
105 List<Map<String, dynamic>> _parseSections(dynamic raw) =>
106 ((raw as List?) ?? []).map((s) => Map<String, dynamic>.from(s as Map)).toList();
107}

Common mistakes when creating a Custom Landing Page Builder Within FlutterFlow

Why it's a problem: Storing each section as a separate Firestore document instead of an Array

How to avoid: Store all sections as an ordered Array in a single page document. Reordering is one atomic array replacement. Loading is one document read. The 1MB document limit supports hundreds of rich text sections before becoming a concern.

Why it's a problem: Not using a transaction when updating a single section in the Array

How to avoid: Wrap all Array-modify-and-write operations in a Firestore transaction. The transaction retries automatically on contention and guarantees the read and write are atomic.

Why it's a problem: Updating the published URL without checking for slug uniqueness

How to avoid: Before saving a new slug, query Firestore for existing pages with that slug and show a validation error if one exists. Consider scoping slugs by user ID in the URL: /page/{userId}/{slug}.

Best practices

  • Always include a unique id field inside each section Map — you need it to identify which section to update without relying on fragile Array index positions.
  • Cap the maximum number of sections per page (e.g., 20) to prevent the document from approaching the 1MB Firestore limit.
  • Add a lastSaved indicator showing when the page was last saved, so users know their changes are persisted.
  • Implement autosave: debounce content changes by 2 seconds and save automatically rather than requiring users to tap a Save button.
  • Store a screenshot or thumbnail of the published page in Firebase Storage for the page list view — generating it client-side with RepaintBoundary is the same technique as the photo editor guide.
  • Add a duplicate page feature: copy the entire page document with a new ID and a modified slug suffix (-copy) for fast page cloning.
  • Gate the publish action behind a completion check: validate that required fields (heading, at least one section) are populated before allowing publish.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I am building a landing page builder feature in FlutterFlow where end users create their own pages. Show me the correct Firestore data model storing sections as an ordered Array, how to implement drag-to-reorder with ReorderableListView in a Custom Widget, how to update a single section in a Firestore Array using a transaction, and how to implement a publish toggle. Include Dart code for all components.

FlutterFlow Prompt

In my FlutterFlow app, create a Custom Action called updatePageSection(pageId, sectionId, newContent) that reads the current sections Array from the Firestore pages collection, finds the section with the matching sectionId, replaces its content map with newContent, and writes the updated sections Array back to Firestore using a transaction. Also create addPageSection(pageId, sectionType) that appends a default section Map to the Array using FieldValue.arrayUnion.

Frequently asked questions

What is the maximum number of sections I can store in a single Firestore document?

Firestore documents have a 1MB size limit. A typical section Map with text content is 500 bytes to 2KB. This means you can store 500-2,000 sections before hitting the limit. For a landing page builder, capping at 20-30 sections per page is a reasonable UX limit that keeps the document well within bounds even with image URLs and rich text.

How do I give pages a public URL that anyone can view without logging in?

Create a PublicPageView route in FlutterFlow that accepts a slug as a URL parameter. This page uses a Firestore query with no authentication requirement to fetch the page by slug (where published == true). Configure Firestore Security Rules to allow unauthenticated reads of published page documents. In FlutterFlow, set this page as accessible without login in the authentication settings.

Can I add video sections to the landing page builder?

Yes. Add 'video' as a section type with a video_url field in the content Map. In the section renderer, detect the YouTube or Vimeo URL format and use the youtube_player_flutter or webview_flutter package to embed the video. Add a poster_image_url field for the thumbnail shown before the video plays.

How do I let users preview their page before publishing?

Create a PagePreviewRoute that accepts the pageId and renders all sections using the same rendering logic as the public page view, but fetches unpublished pages and shows a 'Preview Mode' banner. In FlutterFlow, add a Preview button on the editor page that navigates to this route with the current pageId. This shows the user exactly what their published page will look like.

What happens to existing pages if I add a new section type later?

Existing pages simply do not have any sections of the new type — they are unaffected. New section types only appear when users add them. In your section renderer, add a default case that shows a simple 'Unsupported section type' placeholder for any unknown types, so old pages do not break if you ever remove a section type.

How do I implement undo/redo for section edits?

Maintain a Page State variable editHistory as a List of complete sections Array snapshots (the whole Array, not diffs). Before each edit, push the current state to the history stack. Undo pops the last snapshot and writes it back to Firestore. Limit the history to 10-20 snapshots to control memory usage. This simple snapshot-based undo works for landing page editors where sections change infrequently.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.