Search

Dialog without Context

Created
2020/09/22
tag
Flutter
Dialog

1. 들어가기 전에

Flutter를 이용하여 사용하고 있는 플랫폼에 적합한 Dialog를 띄울 때, Flutter에서 지원해주는 showDialog 함수를 이용했었다. 원하는 형태의 Dialog를 생성할 능력이 없어서,  이와 같이 기본적으로 제공되는 형태로만 이용을 하거나 누군가 이쁘장하게 만들어 둔 Third Party Dialog를 이용하곤 했었다. 또한 context를 잘 이용하는 사람들에게는 showDialog에 인자로 줘야하는 context가 별 문제가 되지 않겠지만, 나와 같은 초보에게는 context가 한 번씩 발목을 잡곤 했었다.
따라서 이 글에서는 context를 주지 않고도 원하는 형태의 Dialog를 생성하는 것에 대해서 알아보고자 한다. (순수하게 Flutter에서 제공되는 것만 이용하는 것이 아닌, Third Party Package를 다수 이용한다.
정확히 말하면 context를 이용하긴 하지만 builder를 통해서 생성한 context를 이용하게 되면서, 실제로 사용할 때는 context를 신경쓰지 않고 이용할 수 있다.

2. 준비 사항

pub.dev에서 아래의 Package들을 최신 버전으로 pubspec.yaml에 넣도록 한다.

1) Packages

get

injectable

flutter_hooks

stacked

stacked_services

injectable_generator (버전 명시 없이 사용)

build_runner (버전 명시 없이 사용)

2) 부연 설명

우선 대략적인 Package들의 용도를 살펴본 후 pubspec.yaml에 어떤식으로 추가하면 되는지 살펴보겠다.
Dialog의 구현을 위해선 Dialog에 대한 전반적인 서비스가 제공되어야 손쉽게 구현을 할 수 있다. Dialog, SnackBar, Navigation과 같은 서비스를 제공해주는 Package가 stacked_services이다.
이런 각 서비스들은 프로젝트 내에서 1개만 유지되어야 하는 Singleton의 패턴을 가져야 하는데, Singleton 서비스를 쉽게 유지, 관리, 등록할 수 있게 해주는 Package가 get이다. (get은 생각보다 굉장히 강력한 기능들이 많으니 한 번 살펴보면 좋을 것 같다.)
stacked_services로 제공되는 서비스들을 get에 수동으로 넣을 수 있지만 (get Package 링크를 확인해보면 수동으로 등록하는 방법이 있다.), 이번 Dialog 구현 외에도 꽤나 많은 Third Party Services를 등록해야하는 경우 일일이 해당 작업을 하는 것이 귀찮기 때문에 injectable을 이용하고, 이에 대한 코드를 자동으로 생성하기 위해서 injectable_generator와 build_runner를 사용한다.
flutter_hooksstacked는 크게 필요한 Package는 아니지만, StatefulWidget의 사용을 피하고자 사용하는 것이 flutter_hooks이고 ViewModel의 값이 바뀌는 것을 setState함수가 아닌 notifier에 의해 builder로 다시 Rendering하여 확인하고자 stacked를 사용하는 것이다.
stackedViewModelBuilder를 이용하는 방식은 일종의 Provider를 이용하는 방식과 비슷하다. notifyListeners를 이용하게 된다.

3) pubspec.yaml

name: custom_dialog description: A new Flutter project. publish_to: 'none' version: 1.0.0+1 environment: sdk: '>=2.7.0 <3.0.0' dependencies: flutter: sdk: flutter get: ^3.11.1 injectable: ^1.0.4 stacked: ^1.7.6 stacked_services: ^0.5.4+3 flutter_hooks: ^0.14.0 cupertino_icons: ^0.1.3 dev_dependencies: flutter_test: sdk: flutter build_runner: injectable_generator: flutter: uses-material-design: true
YAML
위와 같이 injectable_generatorbuild_runnerdev_depenecies에 들어가고 별도의 버전 명시를 하지 않았다 (Dart SDK 버전과 자꾸 충돌이 생겨 버전 명시를 하지 않고 받았다.) 이외의 Package들은 dependencies에 넣어주면 되겠다.
우선 get에서 제공하는 GetIt Instance를 생성하고 거기에 DialogService를 등록해줘야 한다. PackageDocumentation에 명시된 것처럼 수동으로 직접 작성할 수도 있겠지만, injectableinjectable_generator, build_runner을 이용하여 GetIt InstanceDialogService를 넣어보겠다.

3. locator.config.dart 생성

import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; import 'locator.config.dart'; final GetIt locator = GetIt.instance; void setupLocator() => $initGetIt(locator);
Dart
위와 같은 코드를 $rootOfProject/lib/services/locator.dart 로 두어 생성한다.
import 'package:injectable/injectable.dart'; import 'package:stacked_services/stacked_services.dart'; abstract class ThirdPartyServicesModule { NavigationService get navigationService; DialogService get dialogService; }
Dart
위의 코드를 $rootOfProject/lib/services/third_party_services_module.dart 로 두어 생성한다.
VSCode 혹은 SystemTerminal을 열어 프로젝트가 있는 Directory로 이동한다.
flutter pub run build_runner build --delete-conflicting-outputs
를 입력하면 locator.dart에 입력한대로 locator.config.dartservices 내에 생성되는 것을 볼 수 있다. (이 때 locator.dart에서 미리 import해둔 locator.config.dart가 빨간 밑줄이 생기면서 인식이 되지 않는다면, 빨간 줄 구문을 지우고 다시 입력해보면 정상적으로 되는 것을 볼 수 있다.)
생성된 locator.config.dart는 아래와 같다.
// GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** // InjectableConfigGenerator // ************************************************************************** import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; import 'package:stacked_services/stacked_services.dart'; import 'third_party_services_module.dart'; /// adds generated dependencies /// to the provided [GetIt] instance GetIt $initGetIt( GetIt get, { String environment, EnvironmentFilter environmentFilter, }) { final gh = GetItHelper(get, environment, environmentFilter); final thirdPartyServicesModule = _$ThirdPartyServicesModule(); gh.lazySingleton<DialogService>(() => thirdPartyServicesModule.dialogService); gh.lazySingleton<NavigationService>( () => thirdPartyServicesModule.navigationService); return get; } class _$ThirdPartyServicesModule extends ThirdPartyServicesModule { DialogService get dialogService => DialogService(); NavigationService get navigationService => NavigationService(); }
Dart
이렇게 DialogServicelocator라는 GetIt Instance 내에 Singleton으로 유지하도록 등록하는 함수를 생성하였다. (setupLocator 함수이다.) 이 함수는 main 함수 내에 runApp의 호출로 MaterialApp이 생성되기 전에 호출해줘야 한다.
따라서 main.dart내에 setupLocator 함수를 호출하도록 한다. (추가로 MaterialApp을 사용할 수 있지만, get에서 제공해주는 편리한 GetMaterialApp을 이용할 것이며, 이 때 navigatorKey 옵션으로 GetIt InstancelocatorDialogService가 갖고 있는 navigatorKey를 줄 것이다. DialogService를 뜯어보면 navigatorKey를 갖고 있는 것을 확인할 수 있다.)
main.dart의 코드는 아래와 같다.
import 'package:flutter/material.dart'; import 'package:get/route_manager.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:custom_dialog/views/home/home_view.dart'; import 'package:custom_dialog/services/locator.dart'; void main() { setupLocator(); runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return GetMaterialApp( home: HomeView(), navigatorKey: locator<DialogService>().navigatorKey, ); } }
Dart

4. HomeView 생성

이 글에서 Test할 항목은 Dialog, ConfirmationDialog, CustomDialog (일반), CustomDIalog (입력)으로 총 4개이다. 이들을 Test할 화면은 HomeVIew이며 HomeView의 화면은 아래와 같다.
위의 HomeView의 코드를 첨부하기 이전에, 위 화면은 DialogViewModel이라는 ViewModel을 기반으로 생성된 화면이다. 즉, ViewModel의 상태가 바뀌어 Notify를 날릴 때마다 HomeView에 있는 ViewModelBuilderUI를 다시 Rendering하게 된다. 따라서 아래의 코드를 보면 Scaffold라는 화면이 ViewModelBuilderbuilder를 통해서 생성되는 것을 볼 수 있고, 이 때 사용되는 ViewModelViewModelBuilder<DialogViewModel>로 타입을 주고, viewModelBuilder를 통해 생성된 것을 사용하는 것을 알 수 있다. (ProviderConsumer와 마찬가지로 staticChild를 두어 다시 Rendering 시키지 않고 고정적으로 이용할 UI를 지정할 수 있다.)
import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:custom_dialog/views/dialog/dialog_view_model.dart'; class HomeView extends StatelessWidget { const HomeView({Key key}) : super(key: key); Widget build(BuildContext context) { return ViewModelBuilder<DialogViewModel>.reactive( builder: ( BuildContext context, DialogViewModel dialogViewModel, Widget child, ) { return Scaffold( body: SizedBox( width: MediaQuery.of(context).size.width, child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ OutlineButton( child: Text('Show Basic Dialog'), onPressed: () => dialogViewModel.showBasicDialog(), ), Text( 'Basic result: ${dialogViewModel.basicResult}', ), SizedBox(height: 25), OutlineButton( child: Text('Show Confirmation Dialog'), onPressed: () => dialogViewModel.showConfirmationDialog(), ), Text( 'Confirmation result: ${dialogViewModel.confirmationResult}', ), SizedBox(height: 25), OutlineButton( child: Text('Show Basic Custom Dialog'), onPressed: () => dialogViewModel.showBasicCustomDialog(), ), Text( 'Custom Dialog result: ${dialogViewModel.basicCustomResult}', ), SizedBox(height: 25), OutlineButton( child: Text('Show Form Custom Dialog'), onPressed: () => dialogViewModel.showFormCustomDialog(), ), Text( 'Custom Dialog result: ${dialogViewModel.formCustomResult}', ), Text( 'Custom Dialog string: ${dialogViewModel.formCustomString}', ), ], ), ), ); }, viewModelBuilder: () => DialogViewModel(), ); } }
Dart

5. DialogViewModel 생성

HomeView에 이용되는 DialogViewModel을 만들어보자. 여기서 생성하는 DialogViewModelHomeView의  ViewModelBuilder로 이용되고, ViewModel의 변화를 builder에게 Notify 시킬 수 있어야하므로 pubspec.yaml에서 등록한 stacked PackageBaseViewModel을 상속 받아 이용한다.
DialogViewModel 내에는 Dialog를 생성하는 함수들을 작성하기 때문에 DialogService를 이용할 수 있어야한다. 따라서 GetIt Instance를 이용하여 해당 Service를 뽑아서 사용한다.
final DialogService _dialogService = locator<DialogService>();
Dart
기본적으로 Dialog를 통해 Interaction을 만들게 되면, 선택 결과에 따라 반환되는 값은 DialogResponse 타입으로 받게 된다. 또한 Dialog를 생성할 때 인자로 주는 title 값, description 값, buttonTitle 값들은 DialogRequest 타입으로 들어가게 된다. 우리는 HomeView에서 사용하는 ViewModel의 특정 값이 변할 때마다 보여줄 계획이므로 VIewModel 내에 각 Dialog의 결과들을 볼 수 있도록 변수를 생성한다. (DialogResponse 내에는 confirmed라는 bool 타입의 값이 있다.)
bool _basicResult; bool get basicResult => _basicResult; bool _confirmationResult; bool get confirmationResult => _confirmationResult; bool _basicCustomResult; bool get basicCustomResult => _basicCustomResult; bool _formCustomResult; bool get formCustomResult => _formCustomResult; String _formCustomString; String get formCustomString => _formCustomString;
Dart
따라서 아래와 같이 ViewModel을 생성하면 되겠다.
import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:custom_dialog/services/locator.dart'; class DialogViewModel extends BaseViewModel { final DialogService _dialogService = locator<DialogService>(); bool _basicResult; bool get basicResult => _basicResult; bool _confirmationResult; bool get confirmationResult => _confirmationResult; bool _basicCustomResult; bool get basicCustomResult => _basicCustomResult; bool _formCustomResult; bool get formCustomResult => _formCustomResult; String _formCustomString; String get formCustomString => _formCustomString; // DialogResponse _dialogResponse; // DialogResponse get dialogResponse => _dialogResponse; Future showBasicDialog() async {} Future showConfirmationDialog() async {} Future showBasicCustomDialog() async {} Future showFormCustomDialog() async {} }
Dart
각 함수들을 채워나가 보자.
Flutter의 특성 상, iOS에서 Android UI를 사용할 수 있고 Android에서 iOSUI를 사용할 수 있다. 따라서 아래에서 사용할 DialogService의 함수에서 dialogPlatform으로 특정 Platform을 주게 되면, Android에서 iOSDialog 형태를 고수할 수도 있고 iOS에서 Android Dialog 형태를 고수할 수도 있다. 물론 인자를 주지 않으면 Platform에 맞는 Dialog를 이용하게 된다.

6. BasicDialog 함수 작성

showBasicDialog의 경우 우리가 아는 일반적인 형태의 Dialog가 되겠다. 프로젝트 내에 별도의 작업 없이도 단순히 DialogServiceshowDialog 함수를 이용하여 생성할 수 있다. DialogServiceshowDialog를 통해 받은 Response를 변수에 받고, 해당 변수의 값을 ViewModel 내부의 값에 할당한다. 또한 ViewModelBuilder에게 ViewModel의 값이 변했다고 notifyListeners 함수를 통해 Notify 해준다.
Future showBasicDialog() async { DialogResponse response = await _dialogService.showDialog( title: '기본형 Dialog', description: '기본형 Dialog의 Description', cancelTitle: '취소', buttonTitle: '확인', barrierDismissible: true, ); _basicResult = response?.confirmed; notifyListeners(); }
Dart
만일 HomeView에서 DialogViewModelshowBasicDialog를 호출할 때 title, description 등을 가변으로 쓰고 싶다면 이를 인자로 받아서 사용하면 되겠다.

7. ConfirmationDialog 함수 작성

사실 BasicDialogConfirmationDialog의 차이를 잘 모르겠다. 하지만 DialogService 내부를 살펴보면 2개의 Method가 분리되어 있는 것을 볼 수 있다. 따라서 ConfirmationDialog도 다루긴 했는데, 방법은 사실 BasicDialog와 같다.
Future showConfirmationDialog() async { DialogResponse response = await _dialogService.showConfirmationDialog( title: '확인형 Dialog', description: '확인형 Dialog의 Description', cancelTitle: '취소', confirmationTitle: '확인', barrierDismissible: true, ); _confirmationResult = response?.confirmed; notifyListeners(); }
Dart
ConfirmationDialogHomeView에서 호출할 때 Dialogtitle, description등을 다르게 하고 싶다면 인자로 받아서 사용하면 된다.

8. CustomDialog 생성

1) 생성 전 설정

입력을 받지 않는 일반적인 BasicCustomDialog와 입력을 받는 FormCustomDialog를 작성할 것이기 때문에 프로젝트 내에 몇 가지 작업을 할 것이다.
$rootOfProject/lib/enum/dialog_type.dart 를 두고 아래의 코드를 두어 Dialog Type을 구분하도록 만든다.
enum DialogType { Basic, Form, }
Dart
Custom Dialog를 이용하기 위해선 DialogService에서 제공해주는 registerCustomDialogBuilder를 이용하여 CustomDialog를 등록해야 한다. 또한 이 registerCustomDialogBuilder의 호출로 CustomDialog를 추가하는 작업을 main.dartrunApp 함수가 호출되기 이전에 해줘야한다. (setupLocator 함수와 비슷하게 말이다.) 만일 Custom Dialog에 대한 생성이 1개라면 registerCustomDialogBuilderrunApp 이전에 직접 호출하면 되겠지만, Custom Dialog가 2개 이상이라면 일일이 registerCustomDialogBuildermain.dartmain 함수에 기록하는 것이 상당히 보기 좋지 않을 것이다.
따라서 registerCustomDialogBuilider를 호출하는 함수를 별도로 빼내어 작성한다. $rootOfProject/lib/ui/setup_dialog_ui.dart 를 두어 이 안에 registerCustomDialogBuilder를 호출하도록 만든다.
이 함수 내에도 DialogService를 이용해야 하므로 아래 구문을 이용한다.
final DialogService dialogService = locator<DialogService>();
Dart
registerCustomDialogBuilder를 호출하면 되는데, 이 때 받는 인자는 variantbuilder이다. Documentation을 직접 찾아보면 알겠지만, variant는 이전에 enum으로 뺴둔 CustomDialogType이라고 보면 된다. (stacked_services의 예전 버전을 보면 이전에는 variant라는 옵션이 없었기 때문에 enum으로 빼둔 DialogType을 이용하기 위해선 별도로 Dialog 클래스의 인자로 주어, DialogRequest로 넘겨 처리했었다. 즉, 좀 더 작업이 많고 복잡했었다. 지금은 variant라는 옵션에 Type을 명시하여 사용하면 되므로 좀더 작업이 수훨하다. Type 마다 일일이 registerCustomDialogBuilder를 호출해도 되고, Type 수에 맞춰 forEach를 통해 작성해도 되겠다. 여기서는 BasicCustomDialog, FormCustomDialog 2개 밖에 없으므로 일일이 작성을 하는 방식으로 사용하겠다.)
또한 builder 옵션의 인자는 BuildContextDialogRequest를 받는 함수를 작성하면 된다. 그리고 이 함수의 Return TypeDialog Widget이 된다.
dialogService.registerCustomDialogBuilder( variant: $dialogType, builder: (BuildContext context, DialogRequest dialogRequest) { return Dialog( child: $someDialogWidget ); }, );
Dart
이와 같은 내용이 들어간 setupDialogUi 함수를 main 함수에 호출하도록 한다. main.dart의 변경된 코드는 아래와 같다.
import 'package:flutter/material.dart'; import 'package:get/route_manager.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:custom_dialog/views/home/home_view.dart'; import 'package:custom_dialog/services/locator.dart'; import 'package:custom_dialog/ui/setup_dialog_ui.dart'; void main() { setupLocator(); setupDialogUi(); runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return GetMaterialApp( home: HomeView(), navigatorKey: locator<DialogService>().navigatorKey, ); } }
Dart
이와 같은 준비가 되었다면, Dialog Widget의 인자로 주어지는 child 클래스를 작성해보자. 작성할 클래스는 BasicCustomDialog, FormCustomDialog 2개가 되겠다. $rootOfProject/lib/widgets의 경로 아래에 basic_custom_dialog.dartform_custom_dialog.dart를 추가하자.
함수의 prefixuse가 붙는 것들은 암시적으로 Hook을 이용했다는 것을 의미한다.
HookWidgetStatelessWidget, StatefulHookWidgetStatefulWidget과 생긴 형태가 비슷하다. 다만 차이가 있다면, HookWidget의 경우 StatelessWidget 처럼 생겼으나, Hook을 이용할 수 있기 때문에 useTextEditingController와 같은 Hook을 사용할 수 있다는 것이다. (HookWidget은 생긴 것만 StatelessWidget 처럼 생긴 것이지 StatelessWidget이 아니라는 소리이다.) 자세한 것은 flutter_hooksDocumentation을 살펴본다.

2) BasicCustomDialog 작성

import 'package:flutter/material.dart'; import 'package:stacked_services/stacked_services.dart'; class BasicCustomDialog extends StatelessWidget { final DialogRequest dialogRequest; final Function(DialogResponse) onDialogTap; const BasicCustomDialog(this.dialogRequest, this.onDialogTap, {Key key}) : super(key: key); Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10.0), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text( dialogRequest.title, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 23.0, ), ), SizedBox(height: 10.0), Text( dialogRequest.description, style: TextStyle( fontSize: 18.0, ), textAlign: TextAlign.center, ), SizedBox(height: 20.0), GestureDetector( onTap: () => onDialogTap(DialogResponse(confirmed: true)), child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 10.0), width: double.infinity, decoration: BoxDecoration( color: Colors.redAccent, borderRadius: BorderRadius.circular(5.0), ), child: dialogRequest.showIconInMainButton ? Icon(Icons.check_circle) : Text(dialogRequest.mainButtonTitle), ), ), ], ), ); } }
Dart
별 다르게 특별한 것은 없다. 우리가 원하는 형태의 Dialog UI를 작성하면 된다. 다만, 중요한 것은 DialogRequestFunction(DialogResponse) 2가지를 활용해야 하는 것이다. HomeView 혹은 ViewModel에서 Dialog의 어떤 내용을 띄울 것인지에 대한 것은 Dialog 호출 함수의 title, description, buttonTitle 등의 인자로 주는데, 이 값들은 DialogRequest에 담겨서 전달 되므로 DialogUI를 작성하는데 있어서 꼭 필요한 인자가 된다.
또한 Dialog에 특별히 Button이 없다면 상관 없지만, 대체적으로 Dialog에는 Button이 존재한다. 다른 BasicDialogConfirmationDialog를 살펴보면 알겠지만, Button을 누르게 되면 후처리는 DialogServicecompleteDialog를 통해서 하게 되며, completeDialog의 인자로 DialogResponse를 주면 이 값을 DialogReturn 값으로 사용하게 된다. 따라서 후처리를 위한 Function(DialogResponse)라는 Callback 함수를 인자로 줘야한다. (위의 GestureDetectoronTap을 보면 알 수 있듯이, 인자로 받은 onDialogTap이라는 Callback 함수를 DialogResponse를 주면서 호출하는 것을 볼 수 있다. 결과적으로 DialogServicecompleteDialogDialogResponse를 넣어 호출하게 되는 것이다.)

3) FormCustomDialog 생성

import 'package:flutter/material.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; class FormCustomDialog extends HookWidget { final DialogRequest dialogRequest; final Function(DialogResponse) onDialogTap; const FormCustomDialog(this.dialogRequest, this.onDialogTap, {Key key}) : super(key: key); Widget build(BuildContext context) { TextEditingController controller = useTextEditingController(); return Container( padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10.0), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text( dialogRequest.title, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 23.0, ), ), SizedBox(height: 10.0), TextField(controller: controller), SizedBox(height: 20.0), GestureDetector( onTap: () => onDialogTap( DialogResponse(confirmed: true, responseData: [controller.text]), ), child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 10.0), width: double.infinity, decoration: BoxDecoration( color: Colors.redAccent, borderRadius: BorderRadius.circular(5.0), ), child: dialogRequest.showIconInMainButton ? Icon(Icons.check_circle) : Text(dialogRequest.mainButtonTitle), ), ), ], ), ); } }
Dart
BasicCustomDialog와 큰 차이는 없다. 다만 TextEditingController를 사용하기 위해선 StatefulWidget을 이용해야 하는데, 코드가 길어지는 것과 initState, dispose 등의 작업을 일일이 해야하는 문제 때문에 StatefulWidget을 상속받지 않고 HookWidget을 상속 받아 이용하였다. (자세히 보면 TextEditingController에 대한 생성도 build 함수 내에서 하며, useTextEditingController라는 함수로 한 것을 볼 수 있다.)
또한 이 때 입력 받은 값을 DialogResponse로 내보내야 하는데, DialogResponsebool 타입인 confirmed 이외에도 dynamic 타입의 responseData라는 것도 갖고 있다. 따라서 TextField에 입력한 값은 Controller에 있기 때문에, Controller의 값을 DialogResponseresponseData로 넘겨서 completeDialog가 수행되도록 한다.

9. setup_dialog_ui.dart와 dialog_view_model.dart 마무리

위와 같이 작성한 클래스들은 setup_dialog_ui.dart내에 있는 registerCustomDialogBuilder 함수의 builder 옵션으로 준 함수의 Return TypeDialogChild로 준다. setup_dialog.ui.dart의 코드는 아래와 같다.
import 'package:custom_dialog/enum/dialog_type.dart'; import 'package:custom_dialog/widgets/basic_custom_dialog.dart'; import 'package:custom_dialog/widgets/form_custom_dialog.dart'; import 'package:flutter/material.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:custom_dialog/services/locator.dart'; void setupDialogUi() { final DialogService dialogService = locator<DialogService>(); dialogService.registerCustomDialogBuilder( variant: DialogType.Basic, builder: (BuildContext context, DialogRequest dialogRequest) { return Dialog( child: BasicCustomDialog( dialogRequest, (DialogResponse dialogResponse) => dialogService.completeDialog(dialogResponse), ), ); }, ); dialogService.registerCustomDialogBuilder( variant: DialogType.Form, builder: (BuildContext context, DialogRequest dialogRequest) { return Dialog( child: FormCustomDialog( dialogRequest, (DialogResponse dialogResponse) => dialogService.completeDialog(dialogResponse)), ); }, ); }
Dart
그리고 이렇게 생성한 setupDialogUi 함수는 main 함수에서 runApp 이전에 호출되어서 Custom Dialog에 대해서 등록하게 되므로, Custom Dialog에 대한 호출은 DialogViewModel 내애 있는 Method를 통해서 하게 된다. DialogViewModelshowBasicCustomDialogshowFormCustomDialog는 아래와 같다.
Future showBasicCustomDialog() async { DialogResponse response = await _dialogService.showCustomDialog( variant: DialogType.Basic, title: '기본형 Custom Dialog', description: '기본형 Custom Dialog Description', mainButtonTitle: '확인', takesInput: true, barrierDismissible: true, ); _basicCustomResult = response?.confirmed; notifyListeners(); } Future showFormCustomDialog() async { DialogResponse response = await _dialogService.showCustomDialog( variant: DialogType.Form, title: '작성형 Custom Dialog', description: '작성형 Custom Dialog Description', mainButtonTitle: '확인', takesInput: true, barrierDismissible: true, ); _formCustomResult = response?.confirmed; _formCustomString = response?.responseData[0]; notifyListeners(); }
Dart

10. 마무리

왔다 갔다 작업한 것들이 많아 전체적인 코드가 매우 헷갈릴 것 같다. 전체 코드는 아래 주소에서 확인할 수 있다.
위 작업의 결과 화면은 아래와 같다.