form_phoenix 1.0.0 copy "form_phoenix: ^1.0.0" to clipboard
form_phoenix: ^1.0.0 copied to clipboard

Advanced Flutter form handler with undo, redo, and recovery

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:form_phoenix/form_phoenix.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Form Phoenix Wizard',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
          brightness: Brightness.light,
        ),
      ),
      home: const MultiStepWizardPage(),
    );
  }
}

class MultiStepWizardPage extends StatefulWidget {
  const MultiStepWizardPage({super.key});

  @override
  State<MultiStepWizardPage> createState() => _MultiStepWizardPageState();
}

class _MultiStepWizardPageState extends State<MultiStepWizardPage> {
  late final FormPhoenixController _formController;
  int _currentStep = 0;

  @override
  void initState() {
    super.initState();
    _formController = FormPhoenixController(
      maxHistory: 50,
      debounceDuration: const Duration(milliseconds: 300),
      persistKey: 'wizard_v1',
      syncOnRestart: true,
      enableDataEncryption: true,
    );
    _formController.init();
  }

  @override
  void dispose() {
    _formController.dispose();
    super.dispose();
  }

  void _nextStep() {
    setState(() => _currentStep++);
  }

  void _prevStep() {
    setState(() => _currentStep--);
  }

  void _reset() {
    _formController.clearHistory();
    setState(() => _currentStep = 0);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Step ${_currentStep + 1} of 3'),
        actions: [
          IconButton(
            icon: const Icon(Icons.history),
            onPressed: () {
              showModalBottomSheet(
                context: context,
                builder: (_) => HistoryViewer(controller: _formController),
              );
            },
            tooltip: 'View History',
          ),
          // Global Undo/Redo
          ListenableBuilder(
            listenable: _formController,
            builder: (context, _) {
              return Row(
                children: [
                  IconButton(
                    icon: const Icon(Icons.undo),
                    onPressed: _formController.canUndo
                        ? _formController.undo
                        : null,
                  ),
                  IconButton(
                    icon: const Icon(Icons.redo),
                    onPressed: _formController.canRedo
                        ? _formController.redo
                        : null,
                  ),
                ],
              );
            },
          ),
        ],
      ),
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: _buildStepContent(),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildStepContent() {
    if (_currentStep == 0) {
      return Step1Form(formController: _formController, onNext: _nextStep);
    } else if (_currentStep == 1) {
      return Step2Form(
        formController: _formController,
        onPrev: _prevStep,
        onNext: _nextStep,
      );
    } else if (_currentStep == 2) {
      return Step3Form(
        formController: _formController,
        onPrev: _prevStep,
        onSubmit: _nextStep, // Move to summary (index 3)
      );
    } else {
      return SummaryScreen(formController: _formController, onRestart: _reset);
    }
  }
}

// --- Step 1 ---

class Step1Form extends StatefulWidget {
  final FormPhoenixController formController;
  final VoidCallback onNext;

  const Step1Form({
    required this.formController,
    required this.onNext,
    super.key,
  });

  @override
  State<Step1Form> createState() => _Step1FormState();
}

class _Step1FormState extends State<Step1Form> {
  late final TextEditingController _firstNameCtrl;
  late final TextEditingController _lastNameCtrl;

  @override
  void initState() {
    super.initState();
    _firstNameCtrl = TextEditingController();
    _lastNameCtrl = TextEditingController();

    widget.formController.bindTextController(
      field: 'first_name',
      controller: _firstNameCtrl,
    );
    widget.formController.bindTextController(
      field: 'last_name',
      controller: _lastNameCtrl,
    );
  }

  @override
  void dispose() {
    _firstNameCtrl.dispose();
    _lastNameCtrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text(
          'Personal Information',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
        const SizedBox(height: 20),
        TextField(
          controller: _firstNameCtrl,
          decoration: const InputDecoration(
            labelText: 'First Name',
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 16),
        TextField(
          controller: _lastNameCtrl,
          decoration: const InputDecoration(
            labelText: 'Last Name',
            border: OutlineInputBorder(),
          ),
        ),
        const Spacer(),
        ElevatedButton(onPressed: widget.onNext, child: const Text('Next')),
      ],
    );
  }
}

// --- Step 2 ---

class Step2Form extends StatefulWidget {
  final FormPhoenixController formController;
  final VoidCallback onPrev;
  final VoidCallback onNext;

  const Step2Form({
    required this.formController,
    required this.onPrev,
    required this.onNext,
    super.key,
  });

  @override
  State<Step2Form> createState() => _Step2FormState();
}

class _Step2FormState extends State<Step2Form> {
  late final TextEditingController _emailCtrl;
  late final TextEditingController _phoneCtrl;

  @override
  void initState() {
    super.initState();
    _emailCtrl = TextEditingController();
    _phoneCtrl = TextEditingController();

    widget.formController.bindTextController(
      field: 'email',
      controller: _emailCtrl,
    );
    widget.formController.bindTextController(
      field: 'phone',
      controller: _phoneCtrl,
    );
  }

  @override
  void dispose() {
    _emailCtrl.dispose();
    _phoneCtrl.dispose();
    super.dispose();
  }

  Future<void> _pickImage() async {
    final picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);
    if (image != null) {
      widget.formController.updateFieldImmediate('profile_image', image.path);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: widget.formController,
      builder: (context, _) {
        final imagePath = widget.formController.getValue<String>(
          'profile_image',
        );
        return Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              'Contact Details',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 20),
            TextField(
              controller: _emailCtrl,
              decoration: const InputDecoration(
                labelText: 'Email Address',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.email),
              ),
              keyboardType: TextInputType.emailAddress,
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _phoneCtrl,
              decoration: const InputDecoration(
                labelText: 'Phone Number',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.phone),
              ),
              keyboardType: TextInputType.phone,
            ),
            const SizedBox(height: 24),
            Text(
              'Profile Photo',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            Center(
              child: GestureDetector(
                onTap: _pickImage,
                child: Container(
                  width: 120,
                  height: 120,
                  decoration: BoxDecoration(
                    color: Theme.of(
                      context,
                    ).colorScheme.surfaceContainerHighest,
                    borderRadius: BorderRadius.circular(16),
                    border: Border.all(
                      color: Theme.of(
                        context,
                      ).colorScheme.outline.withValues(alpha: 0.3),
                    ),
                    image: imagePath != null
                        ? DecorationImage(
                            image: FileImage(File(imagePath)),
                            fit: BoxFit.cover,
                          )
                        : null,
                  ),
                  child: imagePath == null
                      ? const Icon(Icons.add_a_photo, size: 40)
                      : null,
                ),
              ),
            ),
            if (imagePath != null)
              TextButton.icon(
                onPressed: _pickImage,
                icon: const Icon(Icons.edit),
                label: const Text('Change Photo'),
              ),
            const Spacer(),
            Row(
              children: [
                Expanded(
                  child: OutlinedButton(
                    onPressed: widget.onPrev,
                    child: const Text('Previous'),
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: ElevatedButton(
                    onPressed: widget.onNext,
                    child: const Text('Next'),
                  ),
                ),
              ],
            ),
          ],
        );
      },
    );
  }
}

// --- Step 3 ---

class Step3Form extends StatefulWidget {
  final FormPhoenixController formController;
  final VoidCallback onPrev;
  final VoidCallback onSubmit;

  const Step3Form({
    required this.formController,
    required this.onPrev,
    required this.onSubmit,
    super.key,
  });

  @override
  State<Step3Form> createState() => _Step3FormState();
}

class _Step3FormState extends State<Step3Form> {
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: widget.formController,
      builder: (context, _) {
        final jobRole = widget.formController.getValue<String>('job_role');
        final experience =
            widget.formController.getValue<double>('experience') ?? 0;

        return Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              'Job Preferences',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 20),
            DropdownButtonFormField<String>(
              key: ValueKey(jobRole),
              initialValue: jobRole,
              decoration: const InputDecoration(
                labelText: 'Job Role',
                border: OutlineInputBorder(),
              ),
              items: const [
                DropdownMenuItem(
                  value: 'Frontend',
                  child: Text('Frontend Developer'),
                ),
                DropdownMenuItem(
                  value: 'Backend',
                  child: Text('Backend Developer'),
                ),
                DropdownMenuItem(
                  value: 'Fullstack',
                  child: Text('Fullstack Developer'),
                ),
                DropdownMenuItem(
                  value: 'Mobile',
                  child: Text('Mobile Developer'),
                ),
              ],
              onChanged: (v) {
                if (v != null) {
                  widget.formController.updateField('job_role', v);
                }
              },
            ),
            const SizedBox(height: 24),
            Text(
              'Years of Experience: ${experience.toInt()}',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            Slider(
              value: experience,
              min: 0,
              max: 20,
              divisions: 20,
              label: experience.toInt().toString(),
              onChanged: (v) =>
                  widget.formController.updateField('experience', v),
            ),
            const Spacer(),
            Row(
              children: [
                Expanded(
                  child: OutlinedButton(
                    onPressed: widget.onPrev,
                    child: const Text('Previous'),
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Theme.of(context).colorScheme.primary,
                      foregroundColor: Theme.of(context).colorScheme.onPrimary,
                    ),
                    onPressed: widget.onSubmit,
                    child: const Text('Submit'),
                  ),
                ),
              ],
            ),
          ],
        );
      },
    );
  }
}

// --- Summary Screen ---

class SummaryScreen extends StatelessWidget {
  final FormPhoenixController formController;
  final VoidCallback onRestart;

  const SummaryScreen({
    required this.formController,
    required this.onRestart,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final data = formController.currentData;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text(
          'Submission Summary',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
        const SizedBox(height: 8),
        const Text('All data collected across steps:'),
        const SizedBox(height: 16),
        Expanded(
          child: Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Theme.of(
                context,
              ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
              borderRadius: BorderRadius.circular(12),
              border: Border.all(
                color: Theme.of(context).colorScheme.outlineVariant,
              ),
            ),
            child: SingleChildScrollView(
              child: Text(
                _prettyPrint(data),
                style: const TextStyle(fontFamily: 'Courier', fontSize: 14),
              ),
            ),
          ),
        ),
        if (data['profile_image'] != null) ...[
          const SizedBox(height: 16),
          const Center(
            child: Text(
              'Restored Profile Photo',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
          ),
          const SizedBox(height: 8),
          Center(
            child: ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: Image.file(
                File(data['profile_image']),
                width: 120,
                height: 120,
                fit: BoxFit.cover,
                errorBuilder: (context, error, stackTrace) =>
                    const Icon(Icons.broken_image, size: 60),
              ),
            ),
          ),
        ],
        const SizedBox(height: 16),
        ElevatedButton.icon(
          onPressed: onRestart,
          icon: const Icon(Icons.refresh),
          label: const Text('Start Over'),
        ),
      ],
    );
  }

  String _prettyPrint(Map<String, dynamic> data) {
    if (data.isEmpty) return 'No data';
    final buffer = StringBuffer();
    data.forEach((key, value) {
      buffer.writeln('$key: $value');
    });
    return buffer.toString();
  }
}

// --- Helper: History Viewer ---

class HistoryViewer extends StatelessWidget {
  final FormPhoenixController controller;
  const HistoryViewer({required this.controller, super.key});

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: controller,
      builder: (context, _) {
        final history = controller.exportHistory();
        return Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(
                'Time Travel History',
                style: Theme.of(context).textTheme.titleLarge,
              ),
            ),
            Expanded(
              child: ListView.builder(
                itemCount: history.length,
                itemBuilder: (context, index) {
                  final isCurrent = index == controller.currentIndex;
                  final item = history[index];
                  final timestamp = DateTime.parse(item['timestamp']);

                  return ListTile(
                    selected: isCurrent,
                    selectedTileColor: Theme.of(
                      context,
                    ).colorScheme.secondaryContainer,
                    leading: CircleAvatar(
                      backgroundColor: isCurrent
                          ? Theme.of(context).colorScheme.primary
                          : null,
                      foregroundColor: isCurrent
                          ? Theme.of(context).colorScheme.onPrimary
                          : null,
                      child: Text('$index'),
                    ),
                    title: Text(
                      'Snapshot at ${timestamp.hour}:${timestamp.minute}:${timestamp.second}',
                    ),
                    subtitle: Text(
                      (item['data'] as Map).keys.join(', '),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    onTap: () {
                      controller.jumpTo(index);
                      Navigator.pop(context);
                    },
                  );
                },
              ),
            ),
          ],
        );
      },
    );
  }
}
0
likes
160
points
27
downloads

Documentation

API reference

Publisher

verified publisherniteshmandal1.com.np

Weekly Downloads

Advanced Flutter form handler with undo, redo, and recovery

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

encrypt, flutter, flutter_secure_storage

More

Packages that depend on form_phoenix