Feature spec
IntermediateCategory
forms-data
Build with AI
1–2 days with FlutterFlow
Custom build
3–7 days custom dev
Running cost
$0/mo at launch · $25–$75/mo at 10K users
Works on
Everything it takes to ship Data Collection Forms with Conditional Logic — parts, prompts, and real costs.
A mobile conditional-logic form needs a rule evaluator (flutter_form_builder with visibility conditions), Supabase upsert for auto-save drafts using a session ID, and a server-side Edge Function to validate the final payload. With FlutterFlow you can ship a working smart form in 1–2 days for $0–$25/month. Supabase handles both draft storage and file uploads; Resend handles confirmation emails at $0/month for the first 3,000 sends.
What Conditional Logic Forms Actually Are
A conditional logic form shows or hides fields based on what a user has already answered. A health intake form that only asks about pregnancy if the patient selects 'female'. A loan application that reveals income verification fields only after the user selects 'employed'. A field inspection checklist that asks follow-up questions only for items marked 'fail'. The field list adapts in real time as answers change, making long forms feel shorter and preventing irrelevant data from cluttering the submission. The conditional evaluation runs client-side on every field change so the response is instant — no round trips to the server. Auto-save on each field blur means a user who closes the app mid-form can resume exactly where they left off.
What users consider table stakes in 2026
- Fields appear and disappear instantly, without a page reload or visible transition delay, the moment the triggering answer changes
- A progress indicator (Step 2 of 5 or a fill-bar) that updates as the user advances, accounting for fields that are currently hidden and therefore not required
- Inline validation messages that appear when a field loses focus (on blur), not all at once on form submit, so users fix errors before continuing
- Auto-save of partial responses after each field interaction so users can close the app and resume later without losing any work
- Accessible field labels and error messages that screen readers announce correctly, with sufficient tap target sizes (minimum 44pt) on iOS and Android
- Keyboard-aware scrolling so the active text field is always visible above the soft keyboard on both iOS and Android
Anatomy of the Feature
Six components — FlutterFlow handles the field types and auto-save well, but the conditional rule evaluator and the hidden-field cleanup on submit are the two places where first builds break.
Form engine with conditional rule evaluator
UIflutter_form_builder provides the field declarations and a GlobalKey<FormBuilderState> for programmatic value access. Conditional rules are stored as JSON objects ({"if": "field_a", "op": "equals", "value": "yes", "show": "field_b"}) and evaluated client-side in a function called on every onChange event. Fields whose show condition evaluates to false are wrapped in an AnimatedSize + Visibility widget for a smooth collapse.
Note: FlutterFlow's Conditional Visibility action is the visual equivalent — add a condition on the parent Container of each conditional field. For simple rules (show field if other field equals a value), the visual editor is sufficient. For nested AND/OR logic, write a custom Dart action.
Dynamic field renderer
UIA Dart switch block or FlutterFlow SwitchCase widget renders the correct input widget based on field type from the form schema: FormBuilderTextField, FormBuilderDropdown, FormBuilderDateTimePicker, FormBuilderCheckboxGroup, FormBuilderFilePicker, or a SignatureView widget for signatures.
Note: Define the complete field type list in your prompt so the AI generates the correct switch cases. Missing a field type (like file upload) forces a manual code addition after generation.
Auto-save to Supabase
BackendSupabase upsert on the form_responses table using a session_id as the conflict key. Called on each field blur and on step navigation in multi-step forms. The session_id is a UUID generated on first form open and persisted in local storage so the same user resumes the same draft across app restarts.
Note: The critical detail: use upsert with onConflict: 'session_id' — not a plain insert. A plain insert creates a new draft row on every blur, causing duplicate rows and resume logic failures.
File attachment handler
UIThe file_picker package opens the native iOS/Android file browser for document and image selection. The selected file is uploaded to a Supabase Storage bucket via multipart upload. The returned public or signed URL is stored in the form_responses data JSON field alongside the other field values.
Note: Supabase Storage bucket RLS must include an INSERT policy for authenticated users — this is the most common missing piece. Without it, uploads fail silently with a 403 and the form appears to submit normally.
Validation layer
Dataflutter_form_builder's built-in validators handle required fields, email format, regex patterns, minimum length, and numeric ranges. Cross-field validators (end_date must be after start_date) run in a custom validate callback on the FormBuilderState before step advancement.
Note: flutter_form_builder validators run synchronously on the client. For server-side validation (e.g., checking a submitted value against a database), call a Supabase Edge Function in the onSubmit handler before marking the response as submitted.
Submission + confirmation
BackendA Supabase Edge Function receives the complete form payload, strips null values from hidden fields, runs server-side validation, marks the form_responses row status as 'submitted', and calls Resend API to send a confirmation email to the submitter.
Note: The Edge Function is where hidden field value cleanup happens authoritatively. Client-side cleanup is best-effort; the server must also filter null values before writing the final submitted record.
The data model
Two tables: form_schemas stores the field definitions and conditional rules for each form; form_responses stores user drafts and final submissions. RLS ensures users can only access their own responses. Run this in the Supabase SQL editor:
1create table public.form_schemas (2 id uuid primary key default gen_random_uuid(),3 name text not null,4 fields jsonb not null default '[]',5 conditional_rules jsonb not null default '[]',6 created_at timestamptz not null default now()7);89create table public.form_responses (10 id uuid primary key default gen_random_uuid(),11 form_id uuid references public.form_schemas(id) on delete cascade not null,12 user_id uuid references auth.users(id) on delete cascade,13 session_id text not null unique,14 data jsonb not null default '{}',15 status text not null default 'draft' check (status in ('draft', 'submitted')),16 submitted_at timestamptz,17 updated_at timestamptz not null default now()18);1920create index form_responses_user_idx21 on public.form_responses (user_id, updated_at desc);2223create index form_responses_session_idx24 on public.form_responses (session_id);2526alter table public.form_schemas enable row level security;27alter table public.form_responses enable row level security;2829create policy "Public can read form schemas"30 on public.form_schemas for select31 using (true);3233create policy "Users can insert own responses"34 on public.form_responses for insert35 with check (auth.uid() = user_id);3637create policy "Users can update own responses"38 on public.form_responses for update39 using (auth.uid() = user_id);4041create policy "Users can read own responses"42 on public.form_responses for select43 using (auth.uid() = user_id);4445create policy "Admins can read all responses"46 on public.form_responses for select47 using (auth.jwt() ->> 'role' = 'admin');Heads up: The session_id unique constraint is what makes the upsert pattern work — Supabase's onConflict: 'session_id' updates the existing draft instead of creating a new row. Store the session_id in Flutter's shared_preferences or secure_storage on first form open and retrieve it on each resume to maintain draft continuity.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Full control over the conditional rule engine JSON schema, offline drift storage for drafts, cross-field validation, and schema versioning. The only path for forms with 50+ fields, deep branching logic, or offline submission requirements.
Step by step
- 1Flutter app with flutter_form_builder; conditional rules stored as a JSON schema in Supabase form_schemas; a Dart RuleEvaluator class processes each rule against the current FormBuilderState on every onChange
- 2Offline draft storage using drift (SQLite) — form progress persists locally even without connectivity and syncs to Supabase when the network returns
- 3Cross-field validators run in the FormBuilderState.validate() callback before each step advancement; server-side validation in a Supabase Edge Function on final submit
- 4Schema versioning: form_schemas includes a version field; the app checks the schema version on resume and migrates any saved draft data if the form structure changed
Where this path bites
- Schema versioning adds complexity — migrating saved draft data when a form's field list changes requires careful mapping logic
- Testing all conditional branches in a 50-field form requires a dedicated QA pass; AI-generated forms typically only test the happy path
Third-party services you'll need
Supabase handles form storage and file uploads at no cost at launch. Resend covers confirmation emails within its free tier for most early-stage forms:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Form response storage (drafts and submissions) plus file attachments in Storage buckets with RLS-protected access | Free (500MB DB, 1GB Storage) | Pro $25/mo (8GB DB, 100GB Storage) |
| Resend | Confirmation and notification emails sent from the Supabase Edge Function on successful form submission | Free (3,000 emails/mo) | Pro $20/mo (50,000 emails/mo) |
| Cloudinary | Optional image optimization if the form collects photos that need resizing before storage (e.g., field inspection photo uploads) | Free (25 credits/mo) | Plus $89/mo (approx) |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier handles 100 users' form drafts and file uploads. Resend free tier covers 3,000 confirmation emails — more than enough for 100 submitters per month.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Hidden field values appear in the submitted payload, causing validation errors or bad data
Symptom: FlutterFlow's Conditional Visibility hides a field from the UI but does not remove its key from the flutter_form_builder state. When the form is submitted, the FormBuilderState.value map still contains the hidden field's value. The Edge Function receives unexpected data keys and either throws a validation error or inserts bad data into the response.
Fix: On each conditional visibility toggle, explicitly set the hidden field's value to null using FormBuilderState.fields['field_name']?.reset(). In the Edge Function, filter all null values from the incoming data payload before writing to Supabase: `const cleaned = Object.fromEntries(Object.entries(data).filter(([_, v]) => v !== null))`.
Auto-save upsert creates a new row on every field blur instead of updating the draft
Symptom: AI-generated Supabase calls default to insert, not upsert. Without the onConflict clause, each field blur creates a new form_responses row. When the user submits, the app may be pointing to the first row (which has only the first field saved) while 50 other partial rows accumulate in the database.
Fix: Change the Supabase call to: `supabase.from('form_responses').upsert(payload, { onConflict: 'session_id' })`. The session_id unique constraint on the table ensures all subsequent auto-saves update the same row. Verify in the Supabase Dashboard that the form_responses table has the unique constraint on session_id.
File upload fails silently — no error shown and the file never appears in Supabase Storage
Symptom: The Supabase Storage bucket's RLS policy is missing an INSERT permission for authenticated users, or the bucket is configured as private without a policy allowing uploads. The upload call returns a 403 error, but if the error is not explicitly caught in the FlutterFlow action chain, the UI moves forward as if the upload succeeded.
Fix: In the Supabase Dashboard, go to Storage → your bucket → Policies → New Policy → allow authenticated users to INSERT. Also verify the file size is within Supabase's default upload limit and that the Flutter app catches the upload error and displays an inline error message on the file picker field.
On Android, the active text field is hidden behind the soft keyboard
Symptom: When a user taps a text field near the bottom of a long conditional form, the Android soft keyboard slides up and covers the field. The user can't see what they're typing. This happens when FlutterFlow's scaffold property Resize to Avoid Bottom Inset is disabled, or when the form Column is not inside a scrollable container.
Fix: In FlutterFlow, open the page settings and enable the 'Resize to Avoid Bottom Inset' scaffold property. Ensure the form fields are inside a SingleChildScrollView or a scrollable Column. Test specifically on an Android device in portrait mode with the full soft keyboard visible.
Best practices
Generate the session_id on the first form mount and persist it in shared_preferences so resume works across app restarts, even before the user is authenticated
Clear hidden field values immediately on visibility toggle — never rely on the Edge Function alone to clean them up, since double-layer cleanup prevents bad data at every stage
Use flutter_form_builder's autovalidateMode: AutovalidateMode.onUserInteraction so validation runs on blur for each field independently rather than all at once on submit
Show a progress indicator that reflects only visible, required fields — a step counter that includes hidden fields misleads users about how much is left
Test the resume flow by filling half the form, closing the app, reopening it, and verifying all entered values are pre-populated before shipping
Run final validation in the Supabase Edge Function even if client-side validation passed — never trust the client as the sole validation gate for submitted form data
Log failed file uploads to a separate Supabase table (upload_failures with session_id, error_code, file_size) so you can diagnose storage policy issues without waiting for user reports
When You Need Custom Development
FlutterFlow handles straightforward conditional forms with single-level rules. These requirements push past what the visual editor can produce:
- More than 3 levels of nested conditional logic where field C only appears when field B equals a specific value, which itself only appeared when field A passed a condition — this kind of dependency tree requires a custom rule engine
- Offline form filling where responses must be stored locally on the device and synced to Supabase when connectivity returns, without duplicate submissions or data loss
- Multi-language form with per-locale field labels, validation error messages, and dropdown option text — requires internationalization architecture beyond FlutterFlow's default capabilities
- Real-time data push to an external CRM (Salesforce, HubSpot) on each field change or on final submission, with retry logic on CRM API failures
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Can FlutterFlow build forms with conditional fields?
Yes. FlutterFlow's Conditional Visibility feature lets you set a condition on any widget's parent container — typically the condition evaluates a page state variable or form field value. When the condition is false, the container and its field are hidden. For single-level conditions (show field B when field A equals 'yes'), the visual editor handles it without code.
How do I hide a field based on a previous answer in a mobile app?
In FlutterFlow, wrap the conditional field in a Container and set a Conditional Visibility rule on the container. The condition references the triggering field's value from page state. In custom Flutter code, use flutter_form_builder's onChange callback to update a visibility state variable, then wrap the field in a Visibility widget that reads that variable.
How do I auto-save form progress in Supabase?
Call `supabase.from('form_responses').upsert(payload, { onConflict: 'session_id' })` on each field blur. The session_id is a UUID generated on first form open and stored in shared_preferences. The unique constraint on session_id ensures every upsert updates the same draft row instead of creating a new one.
What happens to hidden field values when a user submits the form?
By default, nothing — flutter_form_builder keeps hidden field values in state unless you explicitly clear them. This is a bug in nearly every first build. Fix it by calling field.reset() when a field is hidden by a visibility change, and by filtering null values in your Supabase Edge Function before writing the final submission.
How do I add file upload to a mobile form?
Add the file_picker package to your Flutter project. On button tap, call FilePicker.platform.pickFiles() to open the native file browser. Upload the selected file bytes to a Supabase Storage bucket using the multipart upload method. Store the returned file URL in your form state as a string field value alongside the other form answers.
Can I resume a partially filled form?
Yes — this is what the session_id and auto-save upsert pattern enables. On form mount, read the session_id from shared_preferences. If it exists, query Supabase for the matching form_responses row and pre-populate each form field with the saved values using flutter_form_builder's initialValue or patchValue methods.
What's the difference between client-side and server-side validation for forms?
Client-side validation (flutter_form_builder validators) runs instantly on the device and gives users immediate inline feedback — it catches required fields, email format, and numeric ranges before a network call is made. Server-side validation (Supabase Edge Function) runs after submission and is the authoritative gate — it verifies data against your database, checks business rules, and cannot be bypassed by a modified app. Both layers are necessary: client-side for UX, server-side for security.
Need this feature production-ready?
RapidDev builds data collection forms with conditional logic into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.