formless 0.1.8
formless: ^0.1.8 copied to clipboard
A Flutter package that turns any list of fields into an AI-powered conversational form. Validates answers via Groq, OpenAI, Gemini, or DeepSeek and returns a clean key->value map when all fields are collected.
Formless #
A Flutter package that turns any list of fields into an AI-powered conversational form.
Instead of a traditional form, Formless walks the user through each question one at a time in a chat UI. The LLM validates answers in natural language, asks for corrections when needed, and returns a clean key → value map when all fields are collected.
Features #
- Chat-style UI — no traditional form widgets needed
- Supports Groq, OpenAI, Gemini, and DeepSeek
- Per-field validation with optional custom rules for the LLM, plus optional
onValidatefor your own API checks after the AI accepts an answer - Optional
optionsper question for multiple choice (chips or searchable sheet for long lists; bottom field disabled for that step) - Optional
optionsSheetPresenterto replace the default searchable bottom sheet with your own UI while still calling into Formless when the user picks a value - Users can edit any previous answer by long-pressing their bubble
- Fully themeable — bubble colors, input field, send button, and more
- Automatic JSON retry and rate-limit backoff
Free to use #
Formless works with providers that offer a free tier — you don't need a paid plan to get started:
| Provider | Free tier |
|---|---|
| Groq | Yes — generous free tier, very fast |
| Gemini | Yes — free tier available |
| OpenAI | No — pay per use |
| DeepSeek | No — pay per use |
Privacy & security #
User data is sent directly from the device to the AI provider — it never passes through any third-party server, including Formless itself. The package makes HTTP calls straight to the provider's API using the key you supply, so your users' answers stay between them and the provider you choose.
Installation #
Add to your pubspec.yaml:
dependencies:
formless: ^0.1.8
Then run:
flutter pub get
Getting started #
You need an API key from one of the supported providers:
| Provider | Where to get a key |
|---|---|
| Groq | https://console.groq.com |
| OpenAI | https://platform.openai.com |
| Gemini | https://aistudio.google.com |
| DeepSeek | https://platform.deepseek.com |
Pass the key at runtime using --dart-define — never hardcode it:
flutter run --dart-define=MY_API_KEY=your_key_here
Usage #
Minimal #
import 'package:formless/formless.dart';
Formless(
provider: AiProvider.openAi,
apiKey: const String.fromEnvironment('MY_API_KEY'),
onComplete: (data) {
// data = {'name': 'Alice', 'email': 'alice@example.com', ...}
print(data);
},
)
This uses the built-in default questions: name, age, email, and phone.
Custom questions #
Formless(
provider: AiProvider.groq,
apiKey: const String.fromEnvironment('MY_API_KEY'),
questions: [
QuestionsModel(
question: 'What is your full name?',
key: 'name',
type: QuestionFieldType.text,
),
QuestionsModel(
question: 'What is your email address?',
key: 'email',
type: QuestionFieldType.email,
),
QuestionsModel(
question: 'What is your monthly income?',
key: 'income',
type: QuestionFieldType.numeric,
validationMessage: 'Only accept values between 1000 and 100000',
),
],
onComplete: (data) => print(data),
)
Multiple choice (options) #
When options is a non-empty list, Formless shows tappable chips centered under the question for short lists. The bottom text field stays visible but is disabled with a “Pick an option above” hint; the user’s choice (trimmed, deduplicated labels) is sent to the LLM as their reply, and the system prompt tells the model to accept only those values.
QuestionsModel(
question: 'Which plan do you want?',
key: 'plan',
type: QuestionFieldType.text,
options: ['Free', 'Pro', 'Team'],
),
Long lists (e.g. countries): by default, if there are more than 12 options (after trim/dedupe), Formless shows a control that opens a searchable bottom sheet instead of chips. You can override with optionsPresentation and tune the threshold with optionsChipMaxCount, or set optionsSheetTitle for the sheet header.
QuestionsModel(
question: 'Which country are you in?',
key: 'country',
type: QuestionFieldType.text,
optionsSheetTitle: 'Select country',
options: myCountryNames, // any length — use QuestionOptionsPresentation.searchableSheet to force the sheet even for short lists
optionsPresentation: QuestionOptionsPresentation.searchableSheet, // optional: always use sheet
),
Custom options sheet (optionsSheetPresenter) #
When the step uses the searchable sheet path, you can replace the default modal with your own. Your presenter receives FormlessOptionsSheetArgs (context, options, title, questionKey, theme, select). Call args.select(value) when the user confirms a choice (then pop your route if needed). To keep the default sheet for most steps and customize one key:
import 'package:formless/formless.dart';
Formless(
provider: AiProvider.groq,
apiKey: const String.fromEnvironment('MY_API_KEY'),
optionsSheetPresenter: (args) async {
if (args.questionKey == 'country') {
await showModalBottomSheet<void>(
context: args.context,
builder: (ctx) => MyCountryPicker(
onPick: (v) {
Navigator.pop(ctx);
args.select(v);
},
),
);
return;
}
await showDefaultFormlessOptionPickerSheet(args);
},
questions: [...],
onComplete: (data) => print(data),
)
Lower-level access: showOptionsSearchSheet opens the package default sheet with explicit parameters.
Post-AI validation (onValidate) #
The LLM first decides whether the answer fits the question and any validationMessage / field type. After it accepts, you can run your own validation on the user’s text — for example calling your API to check that a nickname is not already taken.
onValidate is optional on each QuestionsModel. It runs only when the AI has already accepted the answer. Return null to keep the answer, or a non-empty String to reject it; that string is shown to the user so they can try again.
QuestionsModel(
question: 'What nickname would you like?',
key: 'nickname',
type: QuestionFieldType.text,
onValidate: (answer) async {
final taken = await myBackend.isNicknameTaken(answer);
if (taken) {
return 'That nickname is already taken — please choose another.';
}
return null;
},
),
Use this for uniqueness checks, database rules, or any logic you control on the device or via your own services — anything the model cannot reliably verify on its own.
Custom theme #
Formless(
provider: AiProvider.openAi,
apiKey: const String.fromEnvironment('MY_API_KEY'),
theme: FormlessTheme(
userBubbleColor: Colors.blue.shade700,
botBubbleColor: Colors.grey.shade100,
sendButtonColor: Colors.blue.shade700,
inputDecoration: InputDecoration(
hintText: 'Type your answer...',
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
onComplete: (data) => print(data),
)
Override the model #
Formless(
provider: AiProvider.openAi,
apiKey: const String.fromEnvironment('MY_API_KEY'),
model: 'gpt-4o',
onComplete: (data) => print(data),
)
API reference #
Formless #
| Parameter | Type | Required | Description |
|---|---|---|---|
provider |
AiProvider |
Yes | Which LLM API to use |
apiKey |
String |
Yes | API key for the chosen provider |
questions |
List<QuestionsModel>? |
No | Fields to collect; defaults to name/age/email/phone |
model |
String? |
No | Override the provider's default model |
theme |
FormlessTheme? |
No | Colors and input field styling |
backgroundColor |
Color? |
No | Background behind the chat padding |
sendIcon |
Widget? |
No | Custom icon inside the send button |
optionsSheetPresenter |
FormlessOptionsSheetPresenter? |
No | Custom UI for the searchable options sheet; default uses showDefaultFormlessOptionPickerSheet |
onComplete |
Function(Map<String, dynamic>)? |
No | Called with collected data when done |
onError |
Function(String)? |
No | Called on network or API errors |
QuestionsModel #
| Parameter | Type | Required | Description |
|---|---|---|---|
question |
String |
Yes | The question shown to the user |
key |
String |
Yes | Key used in the onComplete data map |
type |
QuestionFieldType? |
No | Drives validation rules (email, phone, numeric, etc.) |
validationMessage |
String? |
No | Custom rule the LLM must strictly follow |
options |
List<String>? |
No | Multiple choice; chips or searchable sheet; restricts valid answers for that field |
optionsPresentation |
QuestionOptionsPresentation? |
No | automatic (default): chips if ≤12 options, else sheet; or chips / searchableSheet |
optionsChipMaxCount |
int? |
No | When automatic, max count for chips before using the sheet (default 12) |
optionsSheetTitle |
String? |
No | Title on the searchable sheet and on the open button |
onValidate |
Future<String?> Function(String)? |
No | After the AI accepts, run custom checks (e.g. API); return null or an error message for the user |
FormlessTheme #
| Parameter | Description |
|---|---|
userBubbleColor |
User message bubble background |
botBubbleColor |
Bot message bubble background |
userTextColor |
Text color in user bubbles |
botTextColor |
Text color in bot bubbles |
sendButtonColor |
Send button background |
typingIndicatorColor |
Animated typing dots color |
inputDecoration |
Full InputDecoration override for the text field |
inputTextStyle |
Text style for what the user types |
inputBorderColor |
Border color (ignored when inputDecoration is set) |
inputHintText |
Hint text (ignored when inputDecoration is set) |
License #
MIT