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
0
points
1.13k
downloads

Publisher

verified publisherquickbirdstudios.com

Weekly Downloads

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

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

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

More

Packages that depend on survey_kit