survey_kit 2.0.0-beta2 copy "survey_kit: ^2.0.0-beta2" to clipboard
survey_kit: ^2.0.0-beta2 copied to clipboard

Create beautiful surveys with Flutter (inspired by iOS ResearchKit Surveys)

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart' hide Step;
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:survey_kit/survey_kit.dart';
import 'package:survey_kit_audio/survey_kit_audio.dart' as ska;
import 'package:survey_kit_lottie/survey_kit_lottie.dart';
import 'package:survey_kit_video/survey_kit_video.dart';

void main() => runApp(const MyApp());

// ---------------------------------------------------------------------------
// Survey definition
// ---------------------------------------------------------------------------
final _survey = SurveyFlow(
  id: 'health-survey',
  navigationRules: {
    // Branching: yes → age question, no → skip straight to media section
    'medication': ConditionalNavigationRule(
      resultToStepIdentifierMapper: (_, input) {
        final answer = input?.result as BooleanResult?;
        if (answer == BooleanResult.positive) {
          return const NavigateToStep('age');
        }
        return const NavigateToStep('media-intro');
      },
    ),
  },
  steps: [
    Step(
      content: const [
        TextContent(
          text: 'Welcome to the\nHealth Survey',
          fontSize: 32,
        ),
        TextContent(
          text:
              'Take a few minutes to complete our survey and help us understand your health profile.',
          fontSize: 18,
        ),
      ],
      buttonText: "Let's begin",
    ),
    Step(
      id: 'medication',
      content: const [
        TextContent(text: 'Medication?', fontSize: 22),
        TextContent(text: 'Are you using any medication?'),
      ],
      answerFormat: const BooleanAnswerFormat(
        positiveAnswer: 'Yes',
        negativeAnswer: 'No',
      ),
      buttonText: 'Continue',
    ),
    Step(
      id: 'age',
      content: const [TextContent(text: 'How old are you?')],
      answerFormat: const IntegerAnswerFormat(
        defaultValue: 25,
        hint: 'Please enter your age',
      ),
      buttonText: 'Continue',
    ),
    Step(
      content: const [
        TextContent(text: 'Tell us about you', fontSize: 22),
        TextContent(
          text:
              'Tell us about yourself and why you want to improve your health.',
        ),
      ],
      answerFormat: const TextAnswerFormat(maxLines: 5, hint: 'Your answer'),
      buttonText: 'Continue',
    ),
    Step(
      content: const [TextContent(text: 'Select your body type')],
      answerFormat: const ScaleAnswerFormat(
        maximumValue: 5.0,
        minimumValue: 1.0,
        defaultValue: 3.0,
        step: 1.0,
        minimumValueDescription: '1',
        maximumValueDescription: '5',
      ),
      buttonText: 'Continue',
    ),
    Step(
      content: const [TextContent(text: 'Known allergies')],
      answerFormat: MultipleChoiceAnswerFormat(
        choices: [
          TextChoice(id: 'penicillin', value: 'Penicillin', text: 'Penicillin'),
          TextChoice(id: 'latex', value: 'Latex', text: 'Latex'),
          TextChoice(id: 'pet', value: 'Pet', text: 'Pet'),
          TextChoice(id: 'pollen', value: 'Pollen', text: 'Pollen'),
        ],
        otherField: true,
      ),
      buttonText: 'Continue',
    ),
    Step(
      content: const [TextContent(text: 'When did you wake up?')],
      answerFormat: const TimeAnswerFormat(
        defaultValue: TimeOfDay(hour: 8, minute: 0),
      ),
      buttonText: 'Continue',
    ),
    Step(
      content: const [TextContent(text: 'When was your last holiday?')],
      answerFormat: DateAnswerFormat(
        minDate: DateTime(2015),
        maxDate: DateTime(2028, 12, 31),
        defaultDate: DateTime(2021, 6, 25),
      ),
      buttonText: 'Continue',
    ),
    Step(
      id: 'media-intro',
      content: const [
        TextContent(text: 'Thanks! One more section with media examples.'),
      ],
      buttonText: 'Continue',
    ),
    Step(
      content: const [
        TextContent(text: 'Video'),
        VideoContent(
          title: 'Big Buck Bunny',
          url:
              'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
        ),
      ],
      buttonText: 'Next',
    ),
    Step(
      content: const [
        TextContent(text: 'Audio'),
        ska.AudioContent(
          title: 'Gong',
          subtitle: 'Sample audio',
          url:
              'https://github.com/QuickBirdEng/survey_kit/raw/main/assets/gong.mp3',
        ),
      ],
      buttonText: 'Next',
    ),
    Step(
      content: const [
        TextContent(text: 'Lottie'),
        LottieContent(
          asset: 'assets/fancy_checkmark.json',
          repeat: true,
          width: 180,
          height: 180,
        ),
      ],
      buttonText: 'Next',
    ),
    Step(
      content: const [TextContent(text: 'Did you enjoy this survey?')],
      answerFormat: SingleChoiceAnswerFormat(
        choices: [
          TextChoice(id: 'yes', value: 'yes', text: 'Yes'),
          TextChoice(id: 'no', value: 'no', text: 'No'),
        ],
      ),
      buttonText: 'Finish',
    ),
    Step(
      content: const [
        TextContent(text: 'Thank You!', fontSize: 32),
        TextContent(
          text:
              'Your feedback is incredibly valuable to us. We will be in touch soon.',
          fontSize: 18,
        ),
      ],
      buttonText: 'Submit Responses',
    ),
  ],
);

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF00C4CC), // Vibrant cyan
          brightness: Brightness.light,
        ),
        useMaterial3: true,
        scaffoldBackgroundColor: Colors.white,
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.white,
          elevation: 0,
          centerTitle: true,
          iconTheme: IconThemeData(color: Color(0xFF00C4CC)),
          titleTextStyle: TextStyle(
            color: Color(0xFF00C4CC),
            fontSize: 18,
            fontWeight: FontWeight.w600,
          ),
        ),
        inputDecorationTheme: InputDecorationTheme(
          fillColor: const Color(0xFFF7F7F9),
          filled: true,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(16),
            borderSide: BorderSide.none,
          ),
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(16),
            borderSide: BorderSide.none,
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(16),
            borderSide: const BorderSide(color: Color(0xFF00C4CC), width: 2),
          ),
          contentPadding:
              const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
        ),
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(16),
            ),
            textStyle:
                const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            backgroundColor: const Color(0xFF00C4CC),
            foregroundColor: Colors.white,
            elevation: 0,
          ),
        ),
        outlinedButtonTheme: OutlinedButtonThemeData(
          style: OutlinedButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 18),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(24),
            ),
            side: const BorderSide(color: Color(0xFF00C4CC), width: 1.5),
            textStyle:
                const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
            foregroundColor: const Color(0xFF00C4CC),
          ),
        ),
        textButtonTheme: TextButtonThemeData(
          style: TextButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            textStyle:
                const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
            foregroundColor: const Color(0xFF00C4CC),
          ),
        ),
        textTheme: const TextTheme(
          headlineMedium:
              TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF1E1E2C)),
          bodyLarge: TextStyle(fontSize: 18, color: Color(0xFF4A4A5A)),
        ),
      ),
      localizationsDelegates: const [
        SurveyKitLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: SurveyKitLocalizations.supportedLocales,
      home: const SurveyPage(),
    );
  }
}

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

  @override
  State<SurveyPage> createState() => _SurveyPageState();
}

class _SurveyPageState extends State<SurveyPage> {
  int _session = 0;
  late final SurveyController _controller = SurveyController(
    onNextStep: _onNextStep,
  );

  void _onNextStep(
    BuildContext context,
    StepResult? stepResult,
    void Function() proceed,
  ) {
    final state = SurveyStateProvider.of(context).state;
    if (state is PresentingSurveyState &&
        state.currentStep.id == 'medication' &&
        stepResult?.result == BooleanResult.positive) {
      Navigator.of(context, rootNavigator: true)
          .push<void>(
            MaterialPageRoute<void>(
              builder: (_) => const IntermissionPage(),
            ),
          )
          .then((_) => proceed());
      return;
    }
    proceed();
  }

  @override
  Widget build(BuildContext context) {
    return SurveyKit(
      key: ValueKey<int>(_session),
      task: _survey,
      surveyController: _controller,
      decoration: const BoxDecoration(
        color: Colors.white,
      ),
      surveyProgressbarConfiguration: SurveyProgressConfiguration(
        backgroundColor: const Color(0xFFF1F1F5),
        progressbarColor: const Color(0xFF00C4CC),
        height: 8,
      ),
      registries: [
        ska.SurveyKitAudio(),
        SurveyKitVideo(),
        SurveyKitLottie(),
      ],
      onResult: (result) => _showResult(context, result),
    );
  }

  Future<void> _showResult(BuildContext context, SurveyResult result) {
    Object? toEncodable(Object? obj) {
      if (obj is Enum) return obj.name;
      final dynamic d = obj;
      try {
        // ignore: avoid_dynamic_calls
        return d.toJson();
      } catch (_) {
        return obj.toString();
      }
    }

    final json = JsonEncoder.withIndent('  ', toEncodable).convert(
      result.toJson(),
    );
    return showDialog<void>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text('Result (${result.finishReason.name})'),
        content: SizedBox(
          width: 520,
          child: SingleChildScrollView(
            child: Text(json, style: const TextStyle(fontFamily: 'monospace')),
          ),
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(ctx).pop();
              if (mounted) setState(() => _session++);
            },
            child: const Text('Restart'),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Intermission page (demonstrates async navigation intercept)
// ---------------------------------------------------------------------------
class IntermissionPage extends StatelessWidget {
  const IntermissionPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Intermission')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Medication answer was "Yes". Continue to age question.',
            ),
            const Spacer(),
            Align(
              alignment: Alignment.bottomRight,
              child: FilledButton(
                onPressed: () => Navigator.of(context).pop(),
                child: const Text('Continue Survey'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
126
likes
60
points
1.79k
downloads

Documentation

API reference

Publisher

verified publisherquickbirdstudios.com

Weekly Downloads

Create beautiful surveys with Flutter (inspired by iOS ResearchKit Surveys)

License

MIT (license)

Dependencies

collection, flutter, flutter_localizations, flutter_markdown_plus, intl, json_annotation, meta, url_launcher, uuid

More

Packages that depend on survey_kit