formless 0.1.8 copy "formless: ^0.1.8" to clipboard
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.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:formless/formless.dart';

/// Pass your API key at run time, e.g.:
/// `flutter run --dart-define=FORMLESS_API_KEY=your_key_here`
const String _apiKey = String.fromEnvironment('FORMLESS_API_KEY');

void main() {
  runApp(const FormlessExampleApp());
}

/// Uses the default searchable sheet for most option steps; for `country` only,
/// shows a custom bottom sheet.
Future<void> _presentFormlessOptionSheet(FormlessOptionsSheetArgs args) async {
  if (args.questionKey != 'country') {
    return showDefaultFormlessOptionPickerSheet(args);
  }

  await showModalBottomSheet<void>(
    context: args.context,
    isScrollControlled: true,
    useSafeArea: true,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (sheetContext) {
      return _CountryPickerSheetBody(
        title: args.title,
        options: args.options,
        theme: args.theme,
        onPick: (value) {
          Navigator.of(sheetContext).pop();
          args.select(value);
        },
      );
    },
  );
}

class FormlessExampleApp extends StatelessWidget {
  const FormlessExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Formless example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const FormlessDemoPage(),
    );
  }
}

class FormlessDemoPage extends StatelessWidget {
  const FormlessDemoPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Formless')),
      body: _apiKey.isEmpty
          ? const Center(
              child: Text(
                'Provide your API key at run time:\n\n'
                'flutter run --dart-define=FORMLESS_API_KEY=your_key_here',
                textAlign: TextAlign.center,
              ),
            )
          : Formless(
              provider: AiProvider.groq,
              apiKey: _apiKey,
              optionsSheetPresenter: _presentFormlessOptionSheet,
              questions: [
                QuestionsModel(
                  question: 'What is your first and last name?',
                  key: 'name',
                  type: QuestionFieldType.text,
                ),
                QuestionsModel(
                  question: 'Which plan do you want?',
                  key: 'plan',
                  type: QuestionFieldType.text,
                  options: ['Free', 'Pro', 'Team'],
                ),
                QuestionsModel(
                  question: 'What is your email address?',
                  key: 'email',
                  type: QuestionFieldType.email,
                ),
                QuestionsModel(
                  question: 'What is your age?',
                  key: 'age',
                  type: QuestionFieldType.numeric,
                  validationMessage: 'Only accept ages between 18 and 120',
                ),
                QuestionsModel(
                  question: 'What is your phone number?',
                  key: 'phone',
                  type: QuestionFieldType.phone,
                ),
                QuestionsModel(
                  question: 'What company do you work for?',
                  key: 'company',
                  type: QuestionFieldType.text,
                ),
                QuestionsModel(
                  question: 'What is your job title?',
                  key: 'jobTitle',
                  type: QuestionFieldType.text,
                ),
                QuestionsModel(
                  question: 'How did you hear about us?',
                  key: 'referralSource',
                  type: QuestionFieldType.text,
                  options: ['Friend', 'Search', 'Social media', 'Conference'],
                ),
                QuestionsModel(
                  question: 'Which country are you in?',
                  key: 'country',
                  type: QuestionFieldType.text,
                  optionsSheetTitle: 'Select country',
                  options: [
                    'United States',
                    'United Kingdom',
                    'Canada',
                    'Germany',
                    'France',
                    'Italy',
                    'Spain',
                    'Netherlands',
                    'Belgium',
                    'Switzerland',
                    'Austria',
                    'Sweden',
                    'Norway',
                    'Denmark',
                    'Finland',
                    'Poland',
                    'Portugal',
                    'Ireland',
                    'Australia',
                    'Japan',
                    'Brazil',
                    'Mexico',
                    'India',
                    'South Korea',
                    'Singapore',
                  ],
                ),
                QuestionsModel(
                  question: 'Do you want product updates by email?',
                  key: 'marketingOptIn',
                  type: QuestionFieldType.text,
                  options: ['Yes', 'No'],
                ),
              ],
              onComplete: (data) {
                debugPrint('Formless complete: $data');
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Done: $data')),
                );
              },
              onError: (error) {
                debugPrint('Formless error: $error');
              },
            ),
    );
  }
}

class _CountryPickerSheetBody extends StatefulWidget {
  const _CountryPickerSheetBody({
    required this.title,
    required this.options,
    required this.onPick,
    this.theme,
  });

  final String title;
  final List<String> options;
  final FormlessTheme? theme;
  final ValueChanged<String> onPick;

  @override
  State<_CountryPickerSheetBody> createState() => _CountryPickerSheetBodyState();
}

class _CountryPickerSheetBodyState extends State<_CountryPickerSheetBody> {
  final TextEditingController _query = TextEditingController();
  late List<String> _filtered;

  @override
  void initState() {
    super.initState();
    _filtered = List<String>.from(widget.options);
    _query.addListener(_applyFilter);
  }

  void _applyFilter() {
    final q = _query.text.trim().toLowerCase();
    setState(() {
      if (q.isEmpty) {
        _filtered = List<String>.from(widget.options);
      } else {
        _filtered = widget.options
            .where((o) => o.toLowerCase().contains(q))
            .toList();
      }
    });
  }

  @override
  void dispose() {
    _query.removeListener(_applyFilter);
    _query.dispose();
    super.dispose();
  }

  String _initials(String name) {
    final parts = name.split(RegExp(r'\s+')).where((p) => p.isNotEmpty).toList();
    if (parts.isEmpty) return '?';
    if (parts.length == 1) {
      return parts[0].length >= 2
          ? parts[0].substring(0, 2).toUpperCase()
          : parts[0].toUpperCase();
    }
    return (parts[0][0] + parts[1][0]).toUpperCase();
  }

  @override
  Widget build(BuildContext context) {
    final t = widget.theme;
    final accent = t?.sendButtonColor ?? Theme.of(context).colorScheme.primary;

    return Padding(
      padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
      child: SizedBox(
        height: MediaQuery.sizeOf(context).height * 0.78,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const SizedBox(height: 8),
            Center(
              child: Container(
                width: 40,
                height: 4,
                decoration: BoxDecoration(
                  color: Colors.grey.shade400,
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(20, 16, 12, 8),
              child: Row(
                children: [
                  Expanded(
                    child: Text(
                      widget.title,
                      style: Theme.of(context).textTheme.titleLarge?.copyWith(
                            fontWeight: FontWeight.w600,
                          ),
                    ),
                  ),
                  IconButton(
                    onPressed: () => Navigator.of(context).pop(),
                    icon: const Icon(Icons.close),
                  ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
              child: TextField(
                controller: _query,
                style: t?.inputTextStyle,
                decoration: InputDecoration(
                  hintText: 'Search countries',
                  prefixIcon: const Icon(Icons.public),
                  filled: true,
                  fillColor: t?.botBubbleColor ?? Colors.grey.shade100,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(14),
                  ),
                ),
              ),
            ),
            Expanded(
              child: _filtered.isEmpty
                  ? Center(
                      child: Text(
                        'No matches',
                        style: TextStyle(color: Colors.grey.shade600),
                      ),
                    )
                  : ListView.separated(
                      padding: const EdgeInsets.fromLTRB(12, 0, 12, 24),
                      itemCount: _filtered.length,
                      separatorBuilder: (context, index) =>
                          const SizedBox(height: 4),
                      itemBuilder: (context, i) {
                        final country = _filtered[i];
                        return Material(
                          color: Colors.transparent,
                          child: ListTile(
                            shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(12),
                            ),
                            tileColor: Colors.grey.shade100,
                            leading: CircleAvatar(
                              backgroundColor: accent.withValues(alpha: 0.15),
                              foregroundColor: accent,
                              child: Text(
                                _initials(country),
                                style: const TextStyle(
                                  fontWeight: FontWeight.bold,
                                  fontSize: 12,
                                ),
                              ),
                            ),
                            title: Text(
                              country,
                              style: t?.inputTextStyle,
                            ),
                            trailing: Icon(Icons.chevron_right, color: accent),
                            onTap: () => widget.onPick(country),
                          ),
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }
}
4
likes
150
points
237
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#form #ai #llm #chat #validation

License

MIT (license)

Dependencies

flutter, http

More

Packages that depend on formless