[Flutter] 일반 Alert 대화 상자 클래스 만들기

138320 단어 FlutterDartRiverpodtech
이런 분들께 추천하는 기사입니다.
  • AlertDialog의 설치가 좀 번거로운 것 같다
  • 공통 바코드를 원합니다
  • 실제 업무에서 다양한 대화 상자를 만드는 과정에서 버튼 동작 등이 취합되는 부분이 있다고 생각해서 해봤어요.
    상태 관리에는 Hooks Riverpod이 사용됩니다.
    가능하면 사용하세요.

    목표 구성



    AlertDialog에 앞서 일반 지원 클래스(Custom AlertDialog, Custom Text FieldDialog)가 준비되어 있습니다.

    일반 Alert 대화 상자 클래스 만들기


    CustomAlertDialog


    대화 상자의 컨텐트를 자유롭게 구현할 수 있는 Custom AlertDialog를 만듭니다.
    custom_alert_dialog.dart
    import 'package:flutter/material.dart';
    
    class CustomAlertDialog extends StatelessWidget {
      const CustomAlertDialog({
        Key? key,
        required this.title,
        required this.contentWidget,
        this.cancelActionText,
        this.cancelAction,
        required this.defaultActionText,
        this.action,
      }) : super(key: key);
    
      final String title;
      final Widget contentWidget;
      final String? cancelActionText;
      final Function? cancelAction;
      final String defaultActionText;
      final Function? action;
    
      
      Widget build(BuildContext context) {
        return AlertDialog(
          title: Text(title),
          content: contentWidget,
          actions: [
            if (cancelActionText != null)
              TextButton(
                child: Text(cancelActionText!),
                onPressed: () {
                  if (cancelAction != null) cancelAction!();
                  Navigator.of(context).pop(false);
                },
              ),
            TextButton(
              child: Text(defaultActionText),
              onPressed: () {
                if (action != null) action!();
                Navigator.of(context).pop(true);
              },
            ),
          ],
        );
      }
    }
    
    요점:
  • 취소 버튼은 임의, 사전 구현content 속성
  • actions, action에서 동작 단추를 눌렀을 때의 처리
  • 이 샘플을 사용한 샘플이 표시됩니다.

    CustomTextFieldDialog


    텍스트 필드가 있는 대화 상자만 있는 CustomTextFieldDialog를 만들었습니다.
    솔직히 커스텀 얼러트다이얼로그를 사용해도 가능하지만, OK 버튼으로 검증할 수 있는 폼 위젯을 미리 준비했다.
    custom_text_field_dialog.dart
    import 'package:flutter/material.dart';
    
    class CustomTextFieldDialog extends StatelessWidget {
      const CustomTextFieldDialog({
        Key? key,
        required this.title,
        required this.contentWidget,
        this.cancelActionText,
        this.cancelAction,
        required this.defaultActionText,
        this.action,
      }) : super(key: key);
    
      final String title;
      final Widget contentWidget;
      final String? cancelActionText;
      final Function? cancelAction;
      final String defaultActionText;
      final Function? action;
    
      
      Widget build(BuildContext context) {
        const key = GlobalObjectKey<FormState>('FORM_KEY');
        return AlertDialog(
          title: Text(title),
          content: Form(
            key: key,
            child: contentWidget,
          ),
          actions: [
            if (cancelActionText != null)
              TextButton(
                child: Text(cancelActionText!),
                onPressed: () {
                  if (cancelAction != null) cancelAction!();
                  Navigator.of(context).pop(false);
                },
              ),
            TextButton(
              child: Text(defaultActionText),
              onPressed: () {
                if (key.currentState!.validate()) {
                  if (action != null) action!();
                  Navigator.of(context).pop(true);
                }
              },
            ),
          ],
        );
      }
    }
    

    견본집


    위의 공통 Alert 대화 상자 클래스를 사용하여 대화 상자를 구현합니다.
    우선 임용할 반을 준비해라.
    dialog_sample_page.dart

    dialog_sample_page.dart
    import 'package:flutter/material.dart';
    import 'widgets/alert_dialog_button_widget.dart';
    import 'widgets/dropdown_dialog_button_widget.dart';
    import 'widgets/text_field_dialog_button_widget.dart';
    import 'widgets/text_field_dialog_button_widget2.dart';
    
    class DialogSamplePage extends StatelessWidget {
      const DialogSamplePage({Key? key}) : super(key: key);
    
      
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Dialog sample'),
            actions: const [],
          ),
          body: _buildBody(context),
        );
      }
    
      Widget _buildBody(BuildContext context) {
        return Center(
          child: Column(
            children: const [
              AlertDialogButtonWidget(),
              DropdownDialogButtonWidget(),
              TextFieldDialogButtonWidget(),
              TextFieldDialogButtonWidget2(),
            ],
          ),
        );
      }
    }
    

    기본 대화상자



    버튼을 누르면 대화상자의 코드가 여기에 표시됩니다.
    기본 대화상자 견본
    alert_dialog_button_widget.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_dialog_sample/presentation/common_widgets/custom_alert_dialog.dart';
    
    class AlertDialogButtonWidget extends StatelessWidget {
      const AlertDialogButtonWidget({
        Key? key,
      }) : super(key: key);
    
      
      Widget build(BuildContext context) {
        return ElevatedButton(
          child: const Text('基本のAlertダイアログボタン'),
          onPressed: () => showDialog(
            context: context,
            builder: (context) => CustomAlertDialog(
              title: '基本のAlertダイアログ',
              contentWidget: const Text('This is an alert dialog.'),
              cancelActionText: 'Cancel',
              cancelAction: () {},
              defaultActionText: 'OK',
              action: () {
                // TODO: implement method
              },
            ),
          ),
        );
      }
    }
    
    OK, 취소 버튼 클릭 시 처리를 적절히 수행하십시오.

    드롭다운 목록이 있는 대화 상자



    드롭다운 목록이 있는 대화 상자가 생성되었습니다.
    드롭다운 대화 상자 견본
    dropdown_dialog_button_widget.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_dialog_sample/presentation/common_widgets/custom_alert_dialog.dart';
    import 'package:flutter_dialog_sample/presentation/common_widgets/custom_dropdown.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    enum CityType {
      tokyo,
      nagoya,
      osaka,
    }
    
    class City {
      static const Map<CityType, String> allItems = {
        CityType.tokyo: '東京',
        CityType.nagoya: '名古屋',
        CityType.osaka: '大阪',
      };
    }
    
    
    class DropdownDialogButtonWidget extends HookConsumerWidget {
      const DropdownDialogButtonWidget({Key? key}) : super(key: key);
    
      
      Widget build(BuildContext context, WidgetRef ref) {
        final cityType = useState<CityType>(CityType.tokyo);
        return ElevatedButton(
          child: const Text('ドロップダウンダイアログボタン'),
          onPressed: () => showDialog(
            context: context,
            builder: (context) => CustomAlertDialog(
              title: 'ドロップダウンダイアログ',
              contentWidget: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  CustomDropdown<CityType>(
                    labelText: '',
                    list: City.allItems.keys.toList(),
                    allTitles: City.allItems.entries
                        .map(
                          (e) => e.value,
                        )
                        .toList(),
                    selectedValue: cityType.value,
                    onChanged: (cityType) => cityType.value = cityType!,
                  ),
                ],
              ),
              cancelActionText: 'Cancel',
              cancelAction: () {},
              defaultActionText: 'OK',
              action: () {
                // TODO: implement method
              },
            ),
          ),
        );
      }
    }
    
    
    Dropdown MenuItem 목록 제작과 관련해서는 다른 학급의 커스텀 Dropdown 레벨을 준비해 구현했다.enum에서 정의한 클래스와 데이터 목록을 이 항목에 전달합니다.
    Dropdown MenuItem 목록을 구현하여 제작한 Custom Dropdown 레벨입니다.
    custom_dropdown.dart
    custom_dropdown.dart
    import 'package:flutter/material.dart';
    
    class CustomDropdown<T> extends StatefulWidget {
      const CustomDropdown({
        Key? key,
        required this.labelText,
        required this.list,
        required this.allTitles,
        required this.selectedValue,
        required this.onChanged,
      }) : super(key: key);
    
      final String labelText;
      final List<T> list;
      final List<String> allTitles;
      final T selectedValue;
      final Function(dynamic) onChanged;
    
      
      _CustomDropdownState<T> createState() => _CustomDropdownState<T>();
    }
    
    class _CustomDropdownState<T> extends State<CustomDropdown> {
      late T _selectedValue;
    
      
      void initState() {
        super.initState();
        _selectedValue = widget.selectedValue;
      }
    
      
      Widget build(BuildContext context) {
        final List<DropdownMenuItem<T>> _dropDownMenuModelNameItems = [];
    
        for (int i = 0; i < widget.list.length; i++) {
          _dropDownMenuModelNameItems.add(
            DropdownMenuItem(
              child: Text(
                widget.allTitles[i],
              ),
              value: widget.list[i],
            ),
          );
        }
    
        return InputDecorator(
          decoration: InputDecoration(
            labelText: widget.labelText,
          ),
          child: DropdownButtonHideUnderline(
            child: DropdownButton<T>(
              isExpanded: true,
              isDense: true,
              value: _selectedValue,
              items: _dropDownMenuModelNameItems,
              onChanged: (value) {
                setState(() => _selectedValue = value!);
                widget.onChanged(value);
              },
            ),
          ),
        );
      }
    }
    
    원래 OK 버튼을 클릭하면 선택한 항목이 저장됩니다.
    다음은 StateNotifier 클래스에서 save 방법을 준비한 샘플입니다.(영구화층 미실현)
    StateNotifier 클래스를 준비하여 샘플 저장
    dialog_sample_state.dart
    import 'package:flutter_dialog_sample/presentation/pages/widgets/dropdown_dialog_button_widget.dart';
    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'dialog_sample_state.freezed.dart';
    // part 'dialog_sample_state.g.dart';
    
    
    class DialogSampleState with _$DialogSampleState {
      factory DialogSampleState({
        (CityType.tokyo) CityType cityType,
        ('') String name,
        ('') String number,
      }) = _DialogSampleState;
    
      // factory DialogSampleState.fromJson(Map<String, dynamic> json) =>
      //     _$DialogSampleStateFromJson(json);
    }
    
    dialog_sample_notifier.dart
    import 'package:flutter_dialog_sample/presentation/pages/widgets/dropdown_dialog_button_widget.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    import 'dialog_sample_state.dart';
    
    final dialogSampleStateProvider =
        StateNotifierProvider.autoDispose<DialogSampleNotifier, DialogSampleState>(
      (ref) => DialogSampleNotifier(),
    );
    
    class DialogSampleNotifier extends StateNotifier<DialogSampleState> {
      DialogSampleNotifier() : super(DialogSampleState());
    
      void read() {}
    
      void save({
        CityType? cityType,
        String? name,
        String? number,
      }) {
        if (cityType != null) {
          state = state.copyWith(cityType: cityType);
        }
        if (name != null) {
          state = state.copyWith(name: name);
        }
        if (number != null) {
          state = state.copyWith(number: number);
        }
      }
    }
    
    dropdown_dialog_button_widget.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_dialog_sample/presentation/common_widgets/custom_alert_dialog.dart';
    import 'package:flutter_dialog_sample/presentation/common_widgets/custom_dropdown.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    + import '../dialog_sample_notifier.dart';
    
    enum CityType {
      tokyo,
      nagoya,
      osaka,
    }
    
    class City {
      static const Map<CityType, String> allItems = {
        CityType.tokyo: '東京',
        CityType.nagoya: '名古屋',
        CityType.osaka: '大阪',
      };
    }
    
    class DropdownDialogButtonWidget extends HookConsumerWidget {
      const DropdownDialogButtonWidget({Key? key}) : super(key: key);
    
      
      Widget build(BuildContext context, WidgetRef ref) {
    +    final state = ref.watch(dialogSampleStateProvider);
    +    final notifier = ref.watch(dialogSampleStateProvider.notifier);
    -    final cityType = useState<CityType>(CityType.tokyo);
    +    final cityType = useState<CityType>(state.cityType);
    
        return ElevatedButton(
          child: const Text('ドロップダウンダイアログボタン'),
          onPressed: () => showDialog(
            context: context,
            builder: (context) => CustomAlertDialog(
              title: 'ドロップダウンダイアログ',
              contentWidget: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  CustomDropdown<CityType>(
                    labelText: '',
                    list: City.allItems.keys.toList(),
                    allTitles: City.allItems.entries
                        .map(
                          (e) => e.value,
                        )
                        .toList(),
                    selectedValue: cityType.value,
                    onChanged: (value) => cityType.value = value!,
                  ),
                ],
              ),
              cancelActionText: 'Cancel',
              cancelAction: () {
    +            cityType.value = state.cityType;
              },
              defaultActionText: 'OK',
              action: () {
    +            notifier.save(
    +              cityType: cityType.value,
    +            );
              },
            ),
          ),
        );
      }
    }
    

    텍스트 필드가 있는 대화 상자



    텍스트 필드가 있는 위 그림 대화상자를 실현해 보십시오.
    텍스트 필드 대화상자 견본
    text_field_dialog_button_widget.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_dialog_sample/presentation/common_widgets/custom_text_field_dialog.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    class TextFieldDialogButtonWidget extends HookConsumerWidget {
      const TextFieldDialogButtonWidget({
        Key? key,
      }) : super(key: key);
    
      
      Widget build(BuildContext context, WidgetRef ref) {
        final nameController = useTextEditingController();
        final numberController = useTextEditingController();
        return ElevatedButton(
          child: const Text('テキストフィールドダイアログボタン'),
          onPressed: () => showDialog(
            context: context,
            builder: (context) => CustomTextFieldDialog(
              title: 'テキストフィールドダイアログ',
              contentWidget: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextFormField(
                    controller: nameController,
                    maxLength: 10,
                    autovalidateMode: AutovalidateMode.onUserInteraction,
                    keyboardType: TextInputType.text,
                    textInputAction: TextInputAction.next,
                    decoration: const InputDecoration(
                      labelText: '名前',
                      errorMaxLines: 2,
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        // return 'Name must not be null or empty.';
                        return '名前を入力してください。';
                      }
                      if (value.length > 10) {
                        return '';
                      }
                      return null;
                    },
                  ),
                  TextFormField(
                    controller: numberController,
                    maxLength: 10,
                    autovalidateMode: AutovalidateMode.onUserInteraction,
                    keyboardType: TextInputType.number,
                    textInputAction: TextInputAction.next,
                    decoration: const InputDecoration(
                      labelText: '番号',
                      errorMaxLines: 2,
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        // return 'Number must not be null or empty.';
                        return '番号を入力してください。';
                      }
                      if (value.length > 10) {
                        return '';
                      }
                      return null;
                    },
                  ),
                ],
              ),
              cancelActionText: 'Cancel',
              cancelAction: () {},
              defaultActionText: 'OK',
              action: () {
                // TODO: implement method
              },
            ),
          ),
        );
      }
    }
    
    입력한 경우 및 OK 버튼 헤더가 검증됩니다.

    여기서도 보존 처리를 적절하게 실시해야 하지만, 예와 같다
    샘플 저장 처리
    text_field_dialog_button_widget.dart
    
    + import '../dialog_sample_notifier.dart';
    
    class TextFieldDialogButtonWidget extends HookConsumerWidget {
      const TextFieldDialogButtonWidget({
        Key? key,
      }) : super(key: key);
    
      
      Widget build(BuildContext context, WidgetRef ref) {
    +    final state = ref.watch(dialogSampleStateProvider);
    +    final notifier = ref.watch(dialogSampleStateProvider.notifier);
    -    final nameController = useTextEditingController();
    +    final nameController = useTextEditingController(text: state.name);
    -    final numberController = useTextEditingController();
    +    final numberController = useTextEditingController(text: state.number);
        return ElevatedButton(
          child: const Text('テキストフィールドダイアログボタン'),
          onPressed: () => showDialog(
            context: context,
            builder: (context) => CustomTextFieldDialog(
              title: 'テキストフィールドダイアログ',
              contentWidget: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextFormField(
                    controller: nameController,
                    maxLength: 10,
                    autovalidateMode: AutovalidateMode.onUserInteraction,
                    keyboardType: TextInputType.text,
                    textInputAction: TextInputAction.next,
                    decoration: const InputDecoration(
                      labelText: '名前',
                      errorMaxLines: 2,
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        // return 'Name must not be null or empty.';
                        return '名前を入力してください。';
                      }
                      if (value.length > 10) {
                        return '';
                      }
                      return null;
                    },
                  ),
                  TextFormField(
                    controller: numberController,
                    maxLength: 10,
                    autovalidateMode: AutovalidateMode.onUserInteraction,
                    keyboardType: TextInputType.number,
                    textInputAction: TextInputAction.next,
                    decoration: const InputDecoration(
                      labelText: '番号',
                      errorMaxLines: 2,
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        // return 'Number must not be null or empty.';
                        return '番号を入力してください。';
                      }
                      if (int.tryParse(value) == null) {
                        return '番号を入力してください。';
                      }
                      if (value.length > 10) {
                        return '';
                      }
                      return null;
                    },
                  ),
                ],
              ),
              cancelActionText: 'Cancel',
              cancelAction: () {
                nameController.text = state.name;
                numberController.text = state.number;
              },
              defaultActionText: 'OK',
              action: () {
                notifier.save(
                  name: nameController.text,
                  number: numberController.text,
                );
              },
            ),
          ),
        );
      }
    }
    

    AlertDialog와CupertinoAlertDialog를 분리합니다.


    iOS 스타일의 대화상자인 Cupertino AlertDialog의 경우 TextFormField를 실행하려면 구축할 때widget 오류가 발생합니다.
    ※ 자세한 내용은 아래 내용을 참조하세요.
    https://www.choge-blog.com/programming/fluttercupertinoalertdialogtextfielduse/
    결론은 다음과 같습니다cancelAction의 카드 애플릿 패키지를 사용하면 됩니다.
    AlertDialog와CupertinoAlertDialog의 견본을 나누다
    text_field_dialog_button_widget.dart
    import 'dart:io';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_dialog_sample/presentation/common_widgets/custom_text_field_dialog.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    class TextFieldDialogButtonWidget2 extends HookConsumerWidget {
      const TextFieldDialogButtonWidget2({
        Key? key,
      }) : super(key: key);
    
      
      Widget build(BuildContext context, WidgetRef ref) {
        final nameController = useTextEditingController();
        final numberController = useTextEditingController();
    
        final builder = CustomTextFieldDialog(
          title: 'テキストフィールドダイアログ2',
          contentWidget: Card(
            color: Colors.transparent,
            elevation: 0.0,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                TextFormField(
                  controller: nameController,
                  maxLength: 10,
                  autovalidateMode: AutovalidateMode.onUserInteraction,
                  keyboardType: TextInputType.text,
                  textInputAction: TextInputAction.next,
                  decoration: const InputDecoration(
                    labelText: '名前',
                    errorMaxLines: 2,
                  ),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      // return 'Name must not be null or empty.';
                      return '名前を入力してください。';
                    }
                    if (value.length > 10) {
                      return '';
                    }
                    return null;
                  },
                ),
                TextFormField(
                  controller: numberController,
                  maxLength: 10,
                  autovalidateMode: AutovalidateMode.onUserInteraction,
                  keyboardType: TextInputType.number,
                  textInputAction: TextInputAction.next,
                  decoration: const InputDecoration(
                    labelText: '番号',
                    errorMaxLines: 2,
                  ),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      // return 'Number must not be null or empty.';
                      return '番号を入力してください。';
                    }
                    if (value.length > 10) {
                      return '';
                    }
                    return null;
                  },
                ),
              ],
            ),
          ),
          cancelActionText: 'Cancel',
          cancelAction: () {},
          defaultActionText: 'OK',
          action: () {
            // TODO: implement method
          },
        );
    
        return ElevatedButton(
          child: const Text('テキストフィールドダイアログボタン2'),
          onPressed: () {
            if (kIsWeb || Platform.isAndroid) {
              showDialog(
                context: context,
                builder: (context) => builder,
              );
            } else {
              showCupertinoDialog(
                context: context,
                builder: (context) => builder,
              );
            }
          },
        );
      }
    }
    
    evelation:0.0에서는 Platform.isAndroidshowDialog로 나뉜다.
    builder 부분은 이전에 정의된 카드 애플릿으로 싸인 부분을 공동으로 사용했다(showDialog와 카드 애플릿으로 싸도 잘 나타낼 수 있다).
    (2022.4.1 보충)
    ▶ CustomText Field Dialog도 Cupertino Alert Dialog에 대한 대응이 필요해서 덧붙였다🙇
    CustomTextFieldDialog
    custom_text_field_dialog.dart
    // ignore_for_file: avoid_print
    
    import 'dart:io';
    
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    
    class CustomTextFieldDialog extends StatelessWidget {
      const CustomTextFieldDialog({
        Key? key,
        required this.title,
        required this.contentWidget,
        this.cancelActionText,
        this.cancelAction,
        required this.defaultActionText,
        this.action,
      }) : super(key: key);
    
      final String title;
      final Widget contentWidget;
      final String? cancelActionText;
      final Function? cancelAction;
      final String defaultActionText;
      final Function? action;
    
      
      Widget build(BuildContext context) {
        const key = GlobalObjectKey<FormState>('FORM_KEY');
    
        if (kIsWeb || Platform.isAndroid) {
          return AlertDialog(
            title: Text(title),
            content: Form(
              key: key,
              child: contentWidget,
            ),
            actions: [
              if (cancelActionText != null)
                TextButton(
                  child: Text(cancelActionText!),
                  onPressed: () {
                    if (cancelAction != null) cancelAction!();
                    Navigator.of(context).pop(false);
                  },
                ),
              TextButton(
                child: Text(defaultActionText),
                onPressed: () {
                  if (key.currentState!.validate()) {
                    print('Validate OK');
                    if (action != null) action!();
                    Navigator.of(context).pop(true);
                  } else {
                    print('Validate NG');
                  }
                },
              ),
            ],
          );
        } else {
          return CupertinoAlertDialog(
            title: Text(title),
            content: Form(
              key: key,
              child: contentWidget,
            ),
            actions: [
              if (cancelActionText != null)
                CupertinoDialogAction(
                  child: Text(cancelActionText!),
                  onPressed: () {
                    if (cancelAction != null) cancelAction!();
                    Navigator.of(context).pop(false);
                  },
                ),
              CupertinoDialogAction(
                child: Text(defaultActionText),
                onPressed: () {
                  if (key.currentState!.validate()) {
                    print('Validate OK');
                    if (action != null) action!();
                    Navigator.of(context).pop(true);
                  } else {
                    print('Validate NG');
                  }
                },
              ),
            ],
          );
        }
      }
    }
    

    최후


    코드량이 상당히 많은 양으로 바뀌었네요.
    요청이 있으면github 링크를 공개하고 싶습니다.

    참고 자료


    https://github.com/bizz84/codewithandrea_flutter_packages
    https://qiita.com/hiesiea/items/807e3ca2b57ed37e4a9b

    좋은 웹페이지 즐겨찾기