widget_toolkit 0.2.0 copy "widget_toolkit: ^0.2.0" to clipboard
widget_toolkit: ^0.2.0 copied to clipboard

The Widget Toolkit package consists of several lightweight and customisable UI components that boost productivity and reduce the development time.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_rx_bloc/rx_form.dart';
import 'package:widget_toolkit/widget_toolkit.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Widget Toolkit Demo',
      theme: ThemeData.light()
          .copyWith(colorScheme: ColorScheme.fromSwatch(), extensions: [
        WidgetToolkitTheme.light(),
        ItemPickerTheme.light(),
        SearchPickerTheme.light(),
        TextFieldDialogTheme.light(),
        EditAddressTheme.light(),
      ]),
      darkTheme: ThemeData.dark()
          .copyWith(colorScheme: ColorScheme.fromSwatch(), extensions: [
        WidgetToolkitTheme.dark(),
        ItemPickerTheme.dark(),
        SearchPickerTheme.dark(),
        TextFieldDialogTheme.dark(),
        EditAddressTheme.dark(),
      ]),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late PageController pageController;
  String title = 'Common Components';
  int nextPageIndex = 1;

  @override
  void initState() {
    pageController = PageController();
    maintainAppBar();
    super.initState();
  }

  void maintainAppBar() {
    pageController.addListener(() {
      if (pageController.page == 0) {
        setState(() {
          title = 'Common Components';
          nextPageIndex = 1;
        });
      }
      if (pageController.page == 1) {
        setState(() {
          title = 'Pickers';
          nextPageIndex = 2;
        });
      }
      if (pageController.page == 2) {
        setState(() {
          title = 'Edit Fields';
          nextPageIndex = 0;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(title), actions: [
          IconButton(
              onPressed: () {
                pageController.animateToPage(nextPageIndex,
                    duration: const Duration(milliseconds: 800),
                    curve: Curves.ease);
              },
              icon: const Icon(Icons.arrow_forward))
        ]),
        body: PageView(
          controller: pageController,
          children: <Widget>[
            CommonComponentsPage(
              pageController: pageController,
            ),
            PickersPage(
              pageController: pageController,
            ),
            EditFieldsPage(
              pageController: pageController,
            ),
          ],
        ),
      );

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

//# region Showcase Pages
class CommonComponentsPage extends StatelessWidget {
  const CommonComponentsPage({required this.pageController, super.key});

  final PageController pageController;

  @override
  Widget build(BuildContext context) => SingleChildScrollView(
        child: Column(
          children: [
            const WidgetSection(
              description: 'OpenUrlWidget - launch URL link',
              child: OpenUrlWidgetWithDependencies(
                url: 'https://www.primeholding.com/',
                translateError: translateError,
                child: Text(
                  'https://www.primeholding.com/',
                  style: TextStyle(
                    color: Colors.blue,
                    decoration: TextDecoration.underline,
                  ),
                  textAlign: TextAlign.center,
                ),
              ),
            ),
            const WidgetSection(
              description: 'OpenUrlWidget - call a phone number',
              child: OpenUrlWidgetWithDependencies(
                url: '+123456789012',
                uriType: UriType.telephone,
                translateError: translateError,
                child: Text(
                  '+123456789012',
                  style: TextStyle(
                    color: Colors.blue,
                    decoration: TextDecoration.underline,
                  ),
                  textAlign: TextAlign.center,
                ),
              ),
            ),
            LoadingStateSwitcher(
              builder: (isLoading, simulateLoading) => WidgetSection(
                description: 'Shimmer Wrapper',
                childSize: const Size(180, 120),
                onRefresh: () => simulateLoading.call(true),
                child: ShimmerWrapper(
                  showShimmer: isLoading,
                  fadeTransition: true,
                  alignment: Alignment.center,
                  child: Image.network(
                      'https://www.btsbg.org/sites/default/files/obekti/stobski-piramidi-selo-stob.jpg'),
                ),
              ),
            ),
            LoadingStateSwitcher(
              builder: (isLoading, simulateLoading) => WidgetSection(
                description: 'Text Shimmer',
                childSize: const Size(320, 32),
                onRefresh: () => simulateLoading.call(true),
                child: ShimmerText(
                  isLoading ? null : 'Displays Text after loaded',
                  alignment: Alignment.center,
                  type: ShimmerType.random(),
                ),
              ),
            ),
            WidgetSection(
              description: 'Modal Sheet with message',
              child: OutlineFillButton(
                text: 'Open modal sheet',
                onPressed: () => showBlurredBottomSheet(
                  context: context,
                  builder: (BuildContext context) => const MessagePanelWidget(
                      message: 'This is an informative message',
                      messageState: MessagePanelState.informative),
                ),
              ),
            ),
            WidgetSection(
              description: 'Error Modal Sheet',
              child: GradientFillButton(
                text: 'Present error in modal',
                onPressed: () => showErrorBlurredBottomSheet(
                  error: 'This is an error message',
                  context: context,
                  retryCallback: (context) =>
                      Future.delayed(const Duration(seconds: 2)),
                ),
              ),
            ),
            WidgetSection(
              description: 'Buttons',
              child: Column(
                children: [
                  Padding(
                    padding: const EdgeInsets.symmetric(
                        vertical: 10, horizontal: 15),
                    child: OutlineFillButton(
                      text: 'OutlineFillButton',
                      onPressed: () {},
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.symmetric(
                        vertical: 10, horizontal: 15),
                    child: GradientFillButton(
                      text: 'GradientFillButton',
                      onPressed: () {},
                    ),
                  ),
                  const Padding(
                    padding: EdgeInsets.symmetric(vertical: 10, horizontal: 15),
                    child: GradientFillButton(
                      text: 'GradientFillButton - disabled',
                      state: ButtonStateModel.disabled,
                      onPressed: null,
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.symmetric(
                        vertical: 10, horizontal: 15),
                    child: IconTextButton(
                      text: 'IconTextButton',
                      icon: Icons.send_time_extension_outlined,
                      onPressed: () {},
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.symmetric(
                        vertical: 10, horizontal: 15),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        SmallButton(
                          onPressed: () {},
                          type: SmallButtonType.icon,
                          icon: Icons.home_work_outlined,
                        ),
                        SmallButton(
                          onPressed: () {},
                          type: SmallButtonType.outline,
                          icon: Icons.home_work_outlined,
                        ),
                        SmallButton(
                          onPressed: () {},
                          type: SmallButtonType.filled,
                          icon: Icons.home_work_outlined,
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      );
}

class PickersPage extends StatelessWidget {
  const PickersPage({required this.pageController, super.key});

  final PageController pageController;

  @override
  Widget build(BuildContext context) => SingleChildScrollView(
        child: Column(
          children: [
            WidgetSection(
              description: 'ItemPicker - single select',
              child: UpdateStateOnSelection<DataModel>(
                builder: (updatedData, updateFunction) => OutlineFillButton(
                  text: 'Select one item',
                  onPressed: () => showItemPickerBottomSheet<DataModel>(
                    context: context,
                    title: 'Select a single item',
                    selectedItems: updatedData,
                    callback: (data) => updateFunction.call(data),
                    service: DataService(),
                    configuration:
                        const ItemPickerConfiguration(isMultiSelect: false),
                  ),
                ),
              ),
            ),
            WidgetSection(
              description: 'ItemPicker - multi select',
              child: UpdateStateOnSelection<DataModel>(
                builder: (updatedData, updateFunction) => OutlineFillButton(
                  text: 'Select a few items',
                  onPressed: () => showItemPickerBottomSheet<DataModel>(
                      context: context,
                      title: 'Select a few items',
                      selectedItems: updatedData,
                      callback: (data) => updateFunction.call(data),
                      service: DataService(),
                      configuration:
                          const ItemPickerConfiguration(isMultiSelect: true)),
                ),
              ),
            ),
            WidgetSection(
              description: 'Search Picker',
              child: UpdateStateOnSelection<CountryModel>(
                getString: (CountryModel element) => element.itemDisplayName,
                builder: (updatedData, updateFunction) => OutlineFillButton(
                  text: 'Select an item from a long list',
                  onPressed: () => showSearchPickerBottomSheet<CountryModel>(
                      context: context,
                      title: 'Select country',
                      hintText: 'Type substring',
                      retryText: 'Retry',
                      selectedItem:
                          updatedData.isNotEmpty ? updatedData[0] : null,
                      onItemTap: (item) =>
                          updateFunction.call(item != null ? [item] : []),
                      service: SearchService(SearchCountryRepository()),
                      emptyBuilder: () => const MessagePanelWidget(
                            message:
                                'There is no results for the searched query!',
                            messageState: MessagePanelState.neutral,
                          ),
                      modalConfiguration: const SearchPickerModalConfiguration(
                          safeAreaBottom: true)),
                ),
              ),
            ),
          ],
        ),
      );
}

class EditFieldsPage extends StatelessWidget {
  const EditFieldsPage({required this.pageController, super.key});

  final PageController pageController;

  @override
  Widget build(BuildContext context) => SingleChildScrollView(
        child: Column(
          children: [
            WidgetSection(
              description: 'TextFieldDialog',
              child: TextFieldDialog<String>(
                translateError: (error) =>
                    TranslateErrorUtil.translateError<String>(error, context),
                label: 'First Name',
                value: 'John',
                validator: LocalAddressFieldService(),
                header: 'Enter your data',
              ),
            ),
            WidgetSection(
              description: 'EditAddress',
              child: EditAddressWidget<CountryModel>(
                translateError: (error) =>
                    TranslateErrorUtil.translateError<String>(error, context),
                service: CustomEditAddressService<CountryModel>(
                  searchRepository: SearchCountryRepository<CountryModel>(),
                ),
                onSaved: (address) => showBlurredBottomSheet(
                  context: context,
                  builder: (context) => MessagePanelWidget(
                    message: address.fullAddress,
                    messageState: MessagePanelState.informative,
                  ),
                ),
              ),
            ),
          ],
        ),
      );
}
//# endregion

//# region Helper Methods and Classes
class WidgetSection extends StatelessWidget {
  const WidgetSection({
    required this.child,
    this.onRefresh,
    required this.description,
    this.childSize,
    super.key,
  });

  final Widget child;
  final String description;
  final Size? childSize;
  final void Function()? onRefresh;

  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
        child: Card(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 10),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(
                      description,
                      style: const TextStyle(fontSize: 14),
                    ),
                    if (onRefresh != null)
                      IconButton(
                          onPressed: onRefresh,
                          icon: const Icon(Icons.refresh)),
                  ],
                ),
                SizedBox(
                  height: 10,
                  width: MediaQuery.of(context).size.width,
                ),
                childSize != null
                    ? SizedBox(
                        width: childSize!.width,
                        height: childSize!.height,
                        child: child,
                      )
                    : child,
              ],
            ),
          ),
        ),
      );
}

class LoadingStateSwitcher extends StatefulWidget {
  const LoadingStateSwitcher({required this.builder, super.key});

  final Widget Function(bool isDataLoading, void Function(bool value)) builder;

  @override
  State<LoadingStateSwitcher> createState() => _LoadingStateSwitcherState();
}

class _LoadingStateSwitcherState extends State<LoadingStateSwitcher> {
  late bool isLoading;

  @override
  void initState() {
    _slapAfterThreeSeconds(true);
    super.initState();
  }

  Future<void> _slapAfterThreeSeconds(bool initial) async {
    if (mounted) {
      setState(() {
        isLoading = initial;
      });
    }
    await Future.delayed(const Duration(seconds: 3));
    if (mounted) {
      setState(() {
        isLoading = !initial;
      });
    }
  }

  @override
  Widget build(BuildContext context) =>
      widget.builder.call(isLoading, _slapAfterThreeSeconds);
}

class UpdateStateOnSelection<T> extends StatefulWidget {
  const UpdateStateOnSelection({
    required this.builder,
    this.getString,
    super.key,
  });

  final Widget Function(
      List<T> data, void Function(List<T> newData) updateState) builder;
  final String Function(T element)? getString;

  @override
  State<UpdateStateOnSelection<T>> createState() =>
      _UpdateStateOnSelectionState<T>();
}

class _UpdateStateOnSelectionState<T> extends State<UpdateStateOnSelection<T>> {
  late List<T> localData;

  @override
  void initState() {
    localData = <T>[];
    super.initState();
  }

  void updateState(List<T> newData) {
    setState(() {
      localData = newData;
    });
  }

  @override
  Widget build(BuildContext context) => Column(
        children: [
          widget.builder
              .call(localData, (List<T> newData) => updateState(newData)),
          if (localData.isNotEmpty)
            Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const SizedBox(height: 12),
                Text((localData.length >= 2)
                    ? 'Selected Items:'
                    : 'Selected Item:'),
                const SizedBox(height: 12),
                SingleChildScrollView(
                  scrollDirection: Axis.horizontal,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      ...localData.map(
                        (e) => Text(
                          '${widget.getString?.call(e) ?? e.toString()} ',
                          style: const TextStyle(fontSize: 10),
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            )
        ],
      );
}

class TranslateErrorUtil {
  static void translateError<T>(Object error, BuildContext context) {
    if (error is ErrorFormFieldModel) {
      throw RxFieldException<T>(
        error: error.error,
        fieldValue: error.fieldValue,
      );
    }

    if (error is EditAddressErrorModel) {
      throw RxFieldException<T>(
        error: error.translate(context),
        fieldValue: error.fieldValue,
      );
    }

    throw error;
  }
}

String translateError(BuildContext context, Exception exception) {
  if (exception is ErrorAccessDeniedModel) {
    return 'Unable to open link on this device. Perhaps you are missing the'
        ' right application to open the link.';
  }
  return exception.toString();
}

//# endregion

//# region Services

class DataService extends ItemPickerService<DataModel> {
  DataService();

  @override
  Future<List<DataModel>> getItems() => Future.delayed(
        const Duration(seconds: 3),
        () => List.generate(
          20,
          (index) => DataModel(
            name: 'Person $index',
            description:
                'This may be very long description for user named Person $index',
          ),
        ),
      );
}

class SearchService<T> extends SearchPickerService<T> {
  SearchService(this._searchRepository);

  final SearchCountryRepository<T> _searchRepository;

  @override
  Future<List<T>> getItems() => _searchRepository.searchList;

  @override
  List<T> getPlaceholderList() =>
      List.generate(5, (index) => CountryModel.empty() as T);
}

class CustomEditAddressService<T> extends EditAddressService<T> {
  CustomEditAddressService({
    required this.searchRepository,
  });

  final SearchCountryRepository<T> searchRepository;

  @override
  Future<AddressModel> saveAddress(AddressModel addressModel) async {
    await Future.delayed(const Duration(seconds: 1));
    return addressModel;
  }

  @override
  Future<List<T>> getCountries() async => await searchRepository.searchList;

  @override
  List<T> getCountryPlaceholderList() =>
      List.generate(3, (index) => CountryModel.empty() as T);

  @override
  Future<String> validateCityOnSubmit(String text) async {
    await Future.delayed(const Duration(seconds: 1));
    return super.validateCityOnSubmit(text);
  }
}

class LocalAddressFieldService extends TextFieldValidator<String> {
  static const int minLengthRequired = 2;
  static const int maxLengthRequired = 7;

  @override
  Future<String> validateOnSubmit(String text) async {
    await Future.delayed(const Duration(seconds: 1));

    if (text.length >= maxLengthRequired) {
      throw ErrorFormFieldModel<String>(
        error: 'Please enter at max $maxLengthRequired symbols',
        fieldValue: text,
      );
    }

    return text;
  }

  @override
  void validateOnType(String text) {
    if (text.length < minLengthRequired) {
      throw ErrorFormFieldModel<String>(
        error: 'Please enter at least $minLengthRequired symbols',
        fieldValue: text,
      );
    }
  }
}
//# endregion

//# region Repositories
class SearchCountryRepository<T> {
  Future<List<T>> get searchList => Future.delayed(
        const Duration(seconds: 1),
        () => _countriesList
            .map((country) =>
                CountryModel(countryCode: 'US', countryName: country) as T)
            .toList(),
      );

  final _countriesList = ['Angola', 'Bulgaria', 'Cuba', 'Egypt', 'Italy'];
}
//# endregion

//# region Models
class DataModel extends PickerItemModel {
  DataModel({
    required this.name,
    required this.description,
  });

  final String name;
  final String description;

  @override
  String get itemDisplayName => name;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is DataModel &&
          runtimeType == other.runtimeType &&
          other.name == name &&
          other.description == description;

  @override
  int get hashCode => name.hashCode ^ description.hashCode;

  @override
  String toString() => name;
}

class ErrorFormFieldModel<T> extends ErrorModel {
  ErrorFormFieldModel({
    required this.error,
    required this.fieldValue,
  });

  final String error;
  final T fieldValue;

  @override
  String toString() =>
      'ErrorRequiredField. Translation Key: $error. Value: $fieldValue.';
}

extension EditAddressErrorModelTranslation on EditAddressErrorModel {
  String translate(BuildContext context) {
    switch (translationKey) {
      case EditAddressErrorType.editAddressEmptyCity:
        return 'Provide a value for the city';
      case EditAddressErrorType.editAddressEmptyStreet:
        return 'Provide a value for the street';
      case EditAddressErrorType.editAddressSaveError:
        return 'Could not save the address';
    }
  }
}

//# endregion
58
likes
140
points
917
downloads
screenshot

Publisher

verified publisherprimeholding.com

Weekly Downloads

The Widget Toolkit package consists of several lightweight and customisable UI components that boost productivity and reduce the development time.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

copy_with_extension, equatable, flutter, flutter_localizations, flutter_rx_bloc, flutter_svg, json_annotation, provider, rx_bloc, rxdart, shimmer, theme_tailor_annotation, url_launcher

More

Packages that depend on widget_toolkit