# How to Add Conditional Logic Forms to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

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.

## 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** (ui): flutter_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.
- **Dynamic field renderer** (ui): A 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.
- **Auto-save to Supabase** (backend): Supabase 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.
- **File attachment handler** (ui): The 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.
- **Validation layer** (data): flutter_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.
- **Submission + confirmation** (backend): A 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.

## 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:

```sql
create table public.form_schemas (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  fields jsonb not null default '[]',
  conditional_rules jsonb not null default '[]',
  created_at timestamptz not null default now()
);

create table public.form_responses (
  id uuid primary key default gen_random_uuid(),
  form_id uuid references public.form_schemas(id) on delete cascade not null,
  user_id uuid references auth.users(id) on delete cascade,
  session_id text not null unique,
  data jsonb not null default '{}',
  status text not null default 'draft' check (status in ('draft', 'submitted')),
  submitted_at timestamptz,
  updated_at timestamptz not null default now()
);

create index form_responses_user_idx
  on public.form_responses (user_id, updated_at desc);

create index form_responses_session_idx
  on public.form_responses (session_id);

alter table public.form_schemas enable row level security;
alter table public.form_responses enable row level security;

create policy "Public can read form schemas"
  on public.form_schemas for select
  using (true);

create policy "Users can insert own responses"
  on public.form_responses for insert
  with check (auth.uid() = user_id);

create policy "Users can update own responses"
  on public.form_responses for update
  using (auth.uid() = user_id);

create policy "Users can read own responses"
  on public.form_responses for select
  using (auth.uid() = user_id);

create policy "Admins can read all responses"
  on public.form_responses for select
  using (auth.jwt() ->> 'role' = 'admin');
```

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 paths

### Lovable — fit 2/10, 4–8 hours

Lovable targets web (Vite/React) and can build conditional forms in React. Use only if 'mobile' means a mobile-responsive web form — native keyboard handling, file picker behavior, and scroll avoidance won't match a true Flutter mobile app.

1. Create a Lovable project and connect Lovable Cloud for Supabase auto-provisioning
2. Paste the prompt below; Agent Mode will generate the conditional form logic with React state and a Supabase upsert on field change
3. Test on a real phone browser via the published URL — preview iframe will not reflect mobile keyboard behavior accurately
4. Verify the upsert is using onConflict: 'session_id' and not a plain insert by submitting the form twice and checking for duplicate rows in the Supabase Dashboard

Starter prompt:

```
Build a mobile-responsive conditional logic form. The form collects: full_name (text), email (email, required), employment_status (dropdown: employed, self-employed, unemployed), annual_income (number, only show when employment_status is 'employed' or 'self-employed'), company_name (text, only show when employment_status is 'employed'), date_of_birth (date picker), has_dependents (yes/no toggle), number_of_dependents (number, only show when has_dependents is 'yes'), document_upload (file upload for PDF or image, max 10MB). Conditional logic: evaluate visibility rules on every onChange event; when a field is hidden, clear its value from the form state. Auto-save: on every field blur, upsert into Supabase 'form_responses' table (session_id, form_id, data jsonb, status, updated_at) using onConflict: 'session_id'. Resume: on form mount, check localStorage for an existing session_id and pre-populate fields from the saved draft. Inline validation: show validation errors on blur, not on submit; required fields show 'This field is required', email shows 'Enter a valid email address'. On submit, call a Supabase Edge Function to validate and mark status as 'submitted'. Show a confirmation screen with a summary of submitted values. Handle the case where a file upload fails — show an inline error 'Upload failed, please try again' without blocking form progress.
```

Limitations:

- No native file picker — the web file input works but lacks the native iOS/Android Files app experience
- Keyboard avoidance is handled by the browser; it is less reliable than Flutter's native ResizeToAvoidBottomInset
- Cannot deploy to the App Store or Google Play; mobile-responsive web only

### Flutterflow — fit 4/10, 1–2 days

FlutterFlow's Conditional Visibility actions and flutter_form_builder are the natural fit for mobile conditional forms. Drag fields into the form, add Conditional Visibility conditions on each field's parent container, and wire auto-save to Supabase. Native keyboard behavior works automatically.

1. Add a Column with flutter_form_builder fields to your form page; set the parent Column to have a SingleChildScrollView so the form is scrollable
2. For each conditional field, wrap it in a Container and add a Conditional Visibility condition in the container's properties: set the condition to match your rule (e.g., 'Show when employment_status equals employed')
3. Wire auto-save: add an On Field Blur action to each field that calls a Supabase Upsert action on form_responses with the current form state as a JSON payload and the session_id as the conflict key
4. Add an On Page Load action that queries form_responses for the current session_id and pre-populates each field using Set Form Field actions if a draft exists
5. Enable Resize to Avoid Bottom Inset in the page's scaffold settings to prevent the keyboard from covering the active field
6. Add the file_picker package in FlutterFlow's custom packages section; wire it to a button action that uploads to Supabase Storage and stores the URL in the form state

Limitations:

- Complex nested conditional rules (if A then show B, if B equals X then show C) require a custom Dart action to evaluate; the visual condition editor handles only single-field comparisons
- Offline form filling with local draft storage requires adding the drift package via custom code — not achievable in the visual editor alone
- Schema-driven dynamic forms (rendering fields from a form_schemas table at runtime) require significant custom Dart code

### Custom — fit 5/10, 3–7 days

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.

1. Flutter 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
2. Offline draft storage using drift (SQLite) — form progress persists locally even without connectivity and syncs to Supabase when the network returns
3. Cross-field validators run in the FormBuilderState.validate() callback before each step advancement; server-side validation in a Supabase Edge Function on final submit
4. Schema 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

Limitations:

- 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

## Gotchas

- **Hidden field values appear in the submitted payload, causing validation errors or bad data** — 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** — 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** — 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** — 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/data-collection-forms-with-conditional-logic
© RapidDev — https://www.rapidevelopers.com/app-features/data-collection-forms-with-conditional-logic
