chips_choice 3.0.1 copy "chips_choice: ^3.0.1" to clipboard
chips_choice: ^3.0.1 copied to clipboard

Lite version of smart_select package, zero dependencies, an easy way to provide a single or multiple choice chips.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:chips_choice/chips_choice.dart';
import 'package:theme_patrol/theme_patrol.dart';
import 'package:animated_checkmark/animated_checkmark.dart';
import 'package:dio/dio.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ThemePatrol(
      light: ThemeData(
        brightness: Brightness.light,
        colorScheme: ColorScheme.fromSeed(
          brightness: Brightness.light,
          seedColor: Colors.red,
        ),
        // materialTapTargetSize: MaterialTapTargetSize.padded,
      ),
      dark: ThemeData(
        brightness: Brightness.dark,
        colorScheme: ColorScheme.fromSeed(
          brightness: Brightness.dark,
          seedColor: Colors.red,
        ),
        visualDensity: VisualDensity.adaptivePlatformDensity,
        // materialTapTargetSize: MaterialTapTargetSize.padded,
      ),
      builder: (context, theme) {
        return MaterialApp(
          title: 'Chips Choice',
          theme: theme.lightData,
          darkTheme: theme.darkData,
          themeMode: theme.mode,
          home: const MyHomePage(),
        );
      },
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {
  // single choice value
  int tag = 3;

  // multiple choice value
  List<String> tags = ['Education'];

  // list of string options
  List<String> options = [
    'News',
    'Entertainment',
    'Politics',
    'Automotive',
    'Sports',
    'Education',
    'Fashion',
    'Travel',
    'Food',
    'Tech',
    'Science',
  ];

  String? user;
  final usersMemoizer = C2ChoiceMemoizer<String>();

  // Create a global key that uniquely identifies the Form widget
  // and allows validation of the form.
  //
  // Note: This is a GlobalKey<FormState>,
  // not a GlobalKey<MyCustomFormState>.
  final formKey = GlobalKey<FormState>();
  List<String> formValue = [];

  Future<List<C2Choice<String>>> getUsers() async {
    try {
      String url =
          "https://randomuser.me/api/?inc=gender,name,nat,picture,email&results=25";
      Response res = await Dio().get(url);
      return C2Choice.listFrom<String, dynamic>(
        source: res.data['results'],
        value: (index, item) => item['email'],
        label: (index, item) =>
            item['name']['first'] + ' ' + item['name']['last'],
        avatarImage: (index, item) =>
            NetworkImage(item['picture']['thumbnail']),
        meta: (index, item) => item,
      )..insert(0, const C2Choice<String>(value: 'all', label: 'All'));
    } on DioError catch (e) {
      throw ErrorDescription(e.message);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter ChipsChoice'),
        actions: <Widget>[
          ThemeConsumer(builder: ((context, theme) {
            return IconButton(
              onPressed: () => theme.toggleMode(),
              icon: Icon(theme.modeIcon),
            );
          })),
          IconButton(
            icon: const Icon(Icons.help_outline),
            onPressed: () => _about(context),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(5.0),
        child: Row(
          children: [
            Expanded(
              child: ListView(
                addAutomaticKeepAlives: true,
                children: <Widget>[
                  Content(
                    title: 'Scrollable List Single Choice',
                    child: ChipsChoice<int>.single(
                      value: tag,
                      onChanged: (val) => setState(() => tag = val),
                      choiceItems: C2Choice.listFrom<int, String>(
                        source: options,
                        value: (i, v) => i,
                        label: (i, v) => v,
                        tooltip: (i, v) => v,
                      ),
                      choiceCheckmark: true,
                      choiceStyle: C2ChipStyle.filled(
                        selectedStyle: const C2ChipStyle(
                          borderRadius: BorderRadius.all(
                            Radius.circular(25),
                          ),
                        ),
                      ),
                    ),
                  ),
                  Content(
                    title: 'Scrollable List Multiple Choice',
                    child: ChipsChoice<String>.multiple(
                      value: tags,
                      onChanged: (val) => setState(() => tags = val),
                      choiceItems: C2Choice.listFrom<String, String>(
                        source: options,
                        value: (i, v) => v,
                        label: (i, v) => v,
                        tooltip: (i, v) => v,
                      ),
                      choiceCheckmark: true,
                      choiceStyle: C2ChipStyle.outlined(),
                    ),
                  ),
                  Content(
                    title:
                        'Wrapped List Single Choice, Custom Border Radius, Leading and Trailing Widget',
                    child: ChipsChoice<int>.single(
                      value: tag,
                      onChanged: (val) => setState(() => tag = val),
                      choiceItems: C2Choice.listFrom<int, String>(
                        source: options,
                        value: (i, v) => i,
                        label: (i, v) => v,
                        tooltip: (i, v) => v,
                        delete: (i, v) => () {
                          setState(() => options.removeAt(i));
                        },
                      ),
                      choiceStyle: C2ChipStyle.toned(
                        borderRadius: const BorderRadius.all(
                          Radius.circular(5),
                        ),
                      ),
                      leading: IconButton(
                        tooltip: 'Add Choice',
                        icon: const Icon(Icons.add_box_rounded),
                        onPressed: () => setState(
                          () => options.add('Opt #${options.length + 1}'),
                        ),
                      ),
                      trailing: IconButton(
                        tooltip: 'Remove Choice',
                        icon: const Icon(Icons.remove_circle),
                        onPressed: () => setState(() => options.removeLast()),
                      ),
                      wrapped: true,
                    ),
                  ),
                  Content(
                    title:
                        'Wrapped List Multiple Choice and Right to Left Text Direction',
                    child: ChipsChoice<String>.multiple(
                      value: tags,
                      onChanged: (val) => setState(() => tags = val),
                      choiceItems: C2Choice.listFrom<String, String>(
                        source: options,
                        value: (i, v) => v,
                        label: (i, v) => v,
                        tooltip: (i, v) => v,
                      ),
                      choiceCheckmark: true,
                      textDirection: TextDirection.rtl,
                      wrapped: true,
                    ),
                  ),
                  Content(
                    title: 'Disabled and Hidden Choice Item',
                    child: ChipsChoice<int>.single(
                      value: tag,
                      onChanged: (val) => setState(() => tag = val),
                      choiceItems: C2Choice.listFrom<int, String>(
                        source: options,
                        value: (i, v) => i,
                        label: (i, v) => v,
                        tooltip: (i, v) => v,
                        disabled: (i, v) => [0, 2, 5].contains(i),
                        hidden: (i, v) => i > 9,
                      ),
                      wrapped: true,
                    ),
                  ),
                  Content(
                    title: 'Individual Style Choice Item',
                    child: ChipsChoice<String>.multiple(
                      value: tags,
                      onChanged: (val) => setState(() => tags = val),
                      choiceItems: C2Choice.listFrom<String, String>(
                        source: options,
                        value: (i, v) => v,
                        label: (i, v) => v,
                        tooltip: (i, v) => v,
                        style: (i, v) {
                          if (['Science', 'Politics', 'News', 'Tech']
                              .contains(v)) {
                            return C2ChipStyle.toned(
                              borderRadius: const BorderRadius.all(
                                Radius.circular(5),
                              ),
                            );
                          }
                          return null;
                        },
                      ),
                      choiceStyle: C2ChipStyle.filled(),
                      wrapped: true,
                    ),
                  ),
                  Content(
                    title: 'Without Checkmark and Custom Border Shape',
                    child: ChipsChoice<int>.single(
                      value: tag,
                      onChanged: (val) => setState(() => tag = val),
                      choiceItems: C2Choice.listFrom<int, String>(
                        source: options,
                        value: (i, v) => i,
                        label: (i, v) => v,
                        tooltip: (i, v) => v,
                      )..insert(
                          0, const C2Choice<int>(value: -1, label: 'All')),
                      choiceStyle: C2ChipStyle.filled(
                        foregroundStyle: const TextStyle(fontSize: 20),
                        borderRadius:
                            const BorderRadius.all(Radius.circular(5)),
                        selectedStyle: C2ChipStyle.filled(),
                      ),
                    ),
                  ),
                  Content(
                    title: 'Async Choice Items and Brightness Dark',
                    child: FutureBuilder<List<C2Choice<String>>>(
                      initialData: const [],
                      future: usersMemoizer.runOnce(getUsers),
                      builder: (context, snapshot) {
                        if (snapshot.connectionState ==
                            ConnectionState.waiting) {
                          return const Padding(
                            padding: EdgeInsets.all(20),
                            child: Center(
                              child: SizedBox(
                                width: 20,
                                height: 20,
                                child: CircularProgressIndicator(
                                  strokeWidth: 2,
                                ),
                              ),
                            ),
                          );
                        } else {
                          if (!snapshot.hasError) {
                            return ChipsChoice<String>.single(
                              value: user,
                              onChanged: (val) => setState(() => user = val),
                              choiceItems: snapshot.data ?? [],
                              choiceStyle: C2ChipStyle.filled(
                                selectedStyle: const C2ChipStyle(
                                  backgroundColor: Colors.green,
                                ),
                              ),
                            );
                          } else {
                            return Container(
                              padding: const EdgeInsets.all(25),
                              child: Text(
                                snapshot.error.toString(),
                                style: const TextStyle(color: Colors.red),
                              ),
                            );
                          }
                        }
                      },
                    ),
                  ),
                  Content(
                    title: 'Async Choice Items Using choiceLoader',
                    child: ChipsChoice<String>.single(
                      value: user,
                      onChanged: (val) => setState(() => user = val),
                      choiceLoader: getUsers,
                      choiceStyle: C2ChipStyle.filled(
                        selectedStyle: const C2ChipStyle(
                          backgroundColor: Colors.green,
                        ),
                      ),
                      choiceLeadingBuilder: (data, i) {
                        if (data.meta == null) return null;
                        return CircleAvatar(
                          maxRadius: 12,
                          backgroundImage: data.avatarImage,
                        );
                      },
                    ),
                  ),
                  Content(
                    title: 'Works with FormField and Validator',
                    child: Form(
                      key: formKey,
                      child: Column(
                        children: [
                          FormField<List<String>>(
                            autovalidateMode: AutovalidateMode.always,
                            initialValue: formValue,
                            onSaved: (val) =>
                                setState(() => formValue = val ?? []),
                            validator: (value) {
                              if (value?.isEmpty ?? value == null) {
                                return 'Please select some categories';
                              }
                              if (value!.length > 5) {
                                return "Can't select more than 5 categories";
                              }
                              return null;
                            },
                            builder: (state) {
                              return Column(
                                children: <Widget>[
                                  Container(
                                    alignment: Alignment.centerLeft,
                                    child: ChipsChoice<String>.multiple(
                                      value: state.value ?? [],
                                      onChanged: (val) => state.didChange(val),
                                      choiceItems:
                                          C2Choice.listFrom<String, String>(
                                        source: options,
                                        value: (i, v) => v.toLowerCase(),
                                        label: (i, v) => v,
                                        tooltip: (i, v) => v,
                                      ),
                                      choiceStyle: C2ChipStyle.outlined(
                                        borderWidth: 2,
                                        selectedStyle: const C2ChipStyle(
                                          borderColor: Colors.green,
                                          foregroundColor: Colors.green,
                                        ),
                                      ),
                                      wrapped: true,
                                    ),
                                  ),
                                  Container(
                                    padding: const EdgeInsets.fromLTRB(
                                        15, 0, 15, 10),
                                    alignment: Alignment.centerLeft,
                                    child: Text(
                                      state.errorText ??
                                          '${state.value!.length}/5 selected',
                                      style: TextStyle(
                                          color: state.hasError
                                              ? Colors.redAccent
                                              : Colors.green),
                                    ),
                                  )
                                ],
                              );
                            },
                          ),
                          const Divider(),
                          Padding(
                            padding: const EdgeInsets.fromLTRB(15, 0, 15, 10),
                            child: Row(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                ElevatedButton(
                                  onPressed: () {
                                    // Validate returns true if the form is valid, or false otherwise.
                                    if (formKey.currentState!.validate()) {
                                      // If the form is valid, save the value.
                                      formKey.currentState!.save();
                                    }
                                  },
                                  child: const Text('Submit'),
                                ),
                                const SizedBox(
                                  width: 15,
                                ),
                                Expanded(
                                  child: Column(
                                    crossAxisAlignment:
                                        CrossAxisAlignment.start,
                                    children: <Widget>[
                                      const Text('Submitted Value:'),
                                      const SizedBox(height: 5),
                                      Text(formValue.toString())
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                  Content(
                    title: 'Custom Choice Widget',
                    child: ChipsChoice<String>.multiple(
                      value: tags,
                      onChanged: (val) => setState(() => tags = val),
                      choiceItems: C2Choice.listFrom<String, String>(
                        source: options,
                        value: (i, v) => v,
                        label: (i, v) => v,
                      ),
                      choiceBuilder: (item, i) {
                        return CustomChip(
                          label: item.label,
                          width: 70,
                          height: 100,
                          selected: item.selected,
                          onSelect: item.select!,
                        );
                      },
                    ),
                  ),
                ],
              ),
            ),
            SizedBox(
              width: 100,
              child: Content(
                title: 'Vertical Direction',
                child: ChipsChoice<int>.single(
                  value: tag,
                  onChanged: (val) => setState(() => tag = val),
                  choiceItems: C2Choice.listFrom<int, String>(
                    source: options,
                    value: (i, v) => i,
                    label: (i, v) => v,
                  ),
                  padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
                  choiceBuilder: (item, i) {
                    return CustomChip(
                      label: item.label,
                      width: double.infinity,
                      height: 90,
                      color: Colors.redAccent,
                      margin: const EdgeInsets.fromLTRB(0, 5, 0, 5),
                      selected: item.selected,
                      onSelect: item.select!,
                    );
                  },
                  direction: Axis.vertical,
                ),
              ),
            ),
          ],
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class CustomChip extends StatelessWidget {
  final String label;
  final Color? color;
  final double? width;
  final double? height;
  final EdgeInsetsGeometry? margin;
  final bool selected;
  final Function(bool selected) onSelect;

  const CustomChip({
    Key? key,
    required this.label,
    this.color,
    this.width,
    this.height,
    this.margin,
    this.selected = false,
    required this.onSelect,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return AnimatedContainer(
      width: width,
      height: height,
      margin: margin ?? const EdgeInsets.symmetric(vertical: 15, horizontal: 5),
      duration: const Duration(milliseconds: 300),
      clipBehavior: Clip.antiAlias,
      decoration: BoxDecoration(
        color: selected
            ? (color ?? Colors.green)
            : theme.unselectedWidgetColor.withOpacity(.12),
        borderRadius: BorderRadius.all(Radius.circular(selected ? 25 : 10)),
        border: Border.all(
          color: selected
              ? (color ?? Colors.green)
              : theme.colorScheme.onSurface.withOpacity(.38),
          width: 1,
        ),
      ),
      child: InkWell(
        borderRadius: BorderRadius.all(Radius.circular(selected ? 25 : 10)),
        onTap: () => onSelect(!selected),
        child: Stack(
          alignment: Alignment.center,
          children: <Widget>[
            AnimatedCheckmark(
              active: selected,
              color: Colors.white,
              size: const Size.square(32),
              weight: 5,
              duration: const Duration(milliseconds: 400),
            ),
            Positioned(
              left: 9,
              right: 9,
              bottom: 7,
              child: Text(
                label,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: TextStyle(
                  color: selected ? Colors.white : theme.colorScheme.onSurface,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class Content extends StatefulWidget {
  final String title;
  final Widget child;

  const Content({
    Key? key,
    required this.title,
    required this.child,
  }) : super(key: key);

  @override
  ContentState createState() => ContentState();
}

class ContentState extends State<Content> {
  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      margin: const EdgeInsets.all(5),
      clipBehavior: Clip.antiAliasWithSaveLayer,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(15),
            // color: Colors.blueGrey[50],
            child: Text(
              widget.title,
              style: const TextStyle(
                // color: Colors.blueGrey,
                fontWeight: FontWeight.w500,
              ),
            ),
          ),
          Flexible(fit: FlexFit.loose, child: widget.child),
        ],
      ),
    );
  }
}

void _about(BuildContext context) {
  showDialog(
    context: context,
    builder: (_) => Dialog(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          ListTile(
            title: Text(
              'chips_choice',
              style: Theme.of(context)
                  .textTheme
                  .headlineSmall!
                  .copyWith(color: Colors.black87),
            ),
            subtitle: const Text('by davigmacode'),
            trailing: IconButton(
              icon: const Icon(Icons.close),
              onPressed: () => Navigator.pop(context),
            ),
          ),
          Flexible(
            fit: FlexFit.loose,
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 15),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text(
                    'Easy way to provide a single or multiple choice chips.',
                    style: Theme.of(context)
                        .textTheme
                        .bodyMedium!
                        .copyWith(color: Colors.black54),
                  ),
                  Container(height: 15),
                ],
              ),
            ),
          ),
        ],
      ),
    ),
  );
}
326
likes
150
points
9.55k
downloads

Publisher

verified publisherwidgetarian.com

Weekly Downloads

Lite version of smart_select package, zero dependencies, an easy way to provide a single or multiple choice chips.

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

async, flexi_chip, flutter

More

Packages that depend on chips_choice