- 전체
- 명
- 오늘 찾아주신 분
- 명
각 OS 에서는 다른 형태의 다이얼로그 레이아웃을 가지고 있는데 안드로이드에서는 Material 디자인에 맞는 레이아웃을, iOS 에서는 Cupertino 디자인에 맞는 레이아웃을 가지고 있고 그 모습은 우리가 아는 그 모습이 맞다.
Flutter 의 다이얼로그는 어떻게 만들까 확인하고자 하나를 만들어 봤는데, Builder 하는 부분부터 Android 나 iOS 에서 만드는 방식과는 너무 달라서 복잡해보이는 부분이 조금 있었다. 게다가 다이얼로그를 통합으로 관리하는 기능이 있겠지 하며 찾아봤는데 웬걸.. Android 용 iOS 용 서로 다른 메소드를 통해서 개발을 해야 하는 것 같았다.
물론 Android 나 iOS 디자인 중 하나를 선택해서 개발하는 것이 편할 수도 있겠지만 요구사항은 항상 그렇지가 않기 때문에 하나의 메소드에서 플랫폼 별 UI를 대응하는 방식으로 짜보았다.
1. Dialog 관리 클래스 만들기
몫 좋은 위치에 dialog.dart 파일을 만들었다. 이 파일 안에서 alert 와 confirm 형태의 다이얼로그를 관리할 예정이다. 개발에 들어가기 이전에 공통 메소드로 호출을 할 것이기 때문에 머릿속에서 다이얼로그에서 제일 필요로 하는 것들이 어떤 것들이 있는가에 대해 생각해보도록한다. 그렇게 나온 속성들은,
1. 다이얼로그 타이틀
2. 다이얼로그 메세지
3. Positive Event
4. (Optional) Negative Event
이 4가지 였다. 여기서 4번째 항목을 Optional 이라고 정한 것은, confirm 형은 보통 2개(확인/취소) 의 버튼을 가지고 있지만 alert 형은 1개의 버튼 만을 다뤄서 있을 수도, 없을 수도 있는 Nullable 한 값이기 때문에 Optional 으로 지정을 한 것이다.
그럼 Dart 파일에 해당되는 속성에 맞도록 클래스와 메소드를 작성해보자.
// dialog.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class UDialog {
static void confirm(BuildContext context, {
required String title,
required String content,
required String positiveText,
required void Function() onPositive,
String negativeText,
void Function() onNegative,
}){
// Not Implemented.
}
}
코드블럭을 Swift로 했더니 required 단어까지 인식해서 좋네
일단 대표 메소드를 작성하긴 했는데, Positive 와 Negative 를 받는 부분이 너무 지저분한 것 같다. 굳이 같은 파트를 다루는데 하나는 텍스트, 하나는 이벤트처리 파라미터로 각각 받아야 되나 싶다. 그래서 이쁜 코딩을 위해 조금 바꿔보도록 한다.
// dialog.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class UDialog {
static void confirm(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
// Not Implemented.
}
}
class DialogAction {
String text;
OnPressed? event;
DialogAction(this.text, this.event);
void _onDismiss(BuildContext context) { Navigator.pop(context); }
VoidFunction _invokeOnFunction(BuildContext context) => (){
if(event != null && !event!()) return;
_onDismiss(context);
}
}
typedef OnPressed = bool Function();
typedef VoidFunction = void Function();
아까보다 파라미터 관리하기도 쉽고, 추가적으로 이벤트를 선택할 때에 Alert 가 Dismiss 될 수 있도록 로직을 추가하였다. Navigator 의 pop() 메소드를 사용하면 Alert 까지 삭제 해주는 것 같다. 그리고 typedef 를 통해서 보기 힘든 Function 리턴 타입을 재정의 해보았다. Flutter 에서는 Function 을 리턴 타입으로 넘겨줄 때에 Function 을 초기화 하는 모습처럼 넘겨주는 거 같은데 그리 친숙하지 않아보여서typedef 을 사용해 보았다. 이렇게 하나 써 보는거지..
DialogAction 의 역할은 대략 이런 것이다. 버튼의 텍스트와 이벤트는 받는데, 그 중 이벤트는 bool 값을 던져주어야 하며 T 이면 해당 팝업을 이벤트를 발생하고서 Dismiss 를 시켜주고, F 이면 Dismiss 를 시켜주지 않는 그런 역할을 하도록 만들었다. 여기서 더 발전하게 된다면 옵션값을 통해서 리턴값에 상관없이 Dismiss 를 시켜주도록 만들 수 있을 것이다.
그럼 이제 다이얼로그 UI 를 그려보도록 하자.
2. 다이얼로그 UI 를 만들기
Android 에서는 AlertDialog.Builder 로, iOS 는 오래전에 했지만 UIAlertController 와 UIAlertAction 의 조합으로 만들었었는데 Flutter 에서는 showDialog 와 showCupertinoDialog 를 통해 각각 Android 와 iOS 의 디자인을 가진 Alert UI 를 만들 수 있었다.
각각 Dialog 의 사용법을 확인해보면,
// Material UI
showDialog(context: BuildContext,
builder: (BuildContext context) =>
AlertDialog(
title: Text(String),
content: Text(String),
actions: [
...
],
)
);
// Cupertino UI
showCupertinoDialog(context: BuildContext,
builder: (BuildContext context) =>
CupertinoAlertDialog(
title: Text(String),
content: Text(String),
actions: [
...
],
)
);
Android 와 iOS 에서 사용하는 정형화된 Alert 과는 다르게 title 과 content 가 String 만을 받는 것이 아니라 Widget 단위로 데이터를 받는 점을 알 수 있다. 이렇게 메소드를 뚫어 UI 를 커스터마이즈하기 쉽게 만들어 놓았다. 그리고 저 3개의 파라미터 이외에 다른 파라미터도 각각이 가지고 있지만 제일 중요한 것은 다음의 파라미터들이 공통적으로 들어간다는 것이다.
1. title
2. content
3. actions
그래서 다시 클래스에 메소드를 각각 정의해보면,
// dialog.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class UDialog {
static void confirm(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
// Not Implemented.
}
// Material UI
static void _showMaterialStyle(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
showDialog(context: BuildContext,
builder: (BuildContext context) =>
AlertDialog(
title: Text(String),
content: Text(String),
actions: [ ... ],
)
);
}
// Cupertino UI
static void _showCupertinoStyle(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
showCupertinoDialog(context: BuildContext,
builder: (BuildContext context) =>
CupertinoAlertDialog(
title: Text(String),
content: Text(String),
actions: [
...
],
)
);
}
}
으로 일단 정리 해놓을 수 있다. 하지만 'actions' 에 대한 값은 아직 정의하지 않았다.
Action 은 Android 와 iOS 의 정의가 조금 다른데, 여기는 iOS 쪽에서 UIAlertAction 넣는 부분과 비슷한 느낌을 가지고 있는 것 같다. iOS 에서는 UIAlertController 에 UIAlertAction 이라는 객체를 하나씩 정의한 다음 Array 형식으로 추가를 시키는데, Flutter 에서는 Android 는 TextButton, iOS 는 CupertinoDialogAction 이라는 객체를 통해서 Alert 에 들어가는 버튼을 추가시켜주는 것 같다.
그래서 각각을 만들어 본다면,
// Material UI
TextButton(onPressed: Function, child: Text(String));
// Cupertino UI
CupertinoDialogAction(child: Text(String),
onPressed: Function,
isDefaultAction: Boolean,
isDestructiveAction: Boolean);
이런 방식으로 이루어진다. 둘의 공통점은 버튼의 텍스트를 받는 곳도 Widget 형식으로 넘겨받을 수 있고, 눌렀을 때의 이벤트를 호출하는 onPressed 를 받고 있다는 것이다. CupertinoDialogAction 은 isDefaultAction, isDestructiveAction 이란 부가적인 옵션도 가지고 있다.
이것들을 positive 와 negative 를 표현하기 위한 소스로 구성해 본다면,
// dialog.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class UDialog {
static void confirm(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
// Not Implemented.
}
// Material UI
static void _showMaterialStyle(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
showDialog(context: BuildContext,
builder: (BuildContext context) =>
AlertDialog(
title: Text(String),
content: Text(String),
actions: [
TextButton(child: Text(positive.text), onPressed: positive.event),
if(negative != null) TextButton(child: Text(negative.text), onPressed: negative.event),
],
)
);
}
// Cupertino UI
static void _showCupertinoStyle(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
showCupertinoDialog(context: BuildContext,
builder: (BuildContext context) =>
CupertinoAlertDialog(
title: Text(String),
content: Text(String),
actions: [
CupertinoDialogAction(child: Text(positive.text), onPressed: positive.event, isDefaultAction: true),
if(negative != null) CupertinoDialogAction(child: Text(negative.text), onPressed: negative.event, isDestructiveAction: true),
],
)
);
}
}
Action 에 이런식으로 작성을 할 수 있을 것이다. 그런데 여기서 2가지를 고민해 보아야 할 것이 있다.
1. 굳이 TextButton/CupertinoDialogAction 을 하나씩 써줘야 하는가.
2. DialogAction 에서 정의한 onDismiss 는 어디로???
text 와 event가 속성에 있지만, 점(.)만 찍고 접근하는 거면 Data 클래스의 역할 밖에 되지 않지 괜히 클래스를 만든거기 때문에 DialogAction 클래스의 의미가 사라지게 될 것이다. 그래서 DialogAction 에서 각 OS 에 맞는 Action 을 만들어주는 메소드를 따로 만들어주도록 한다.
class DialogAction {
String text;
OnPressed? event;
DialogAction(this.text, this.event);
void _onDismiss(BuildContext context) { Navigator.pop(context); }
VoidFunction _invokeOnFunction(BuildContext context) => (){
if(event != null && !event!()) return;
_onDismiss(context);
}
TextButton createActionMaterial(BuildContext context) =>
TextButton(onPressed: _invokeOnFunction(context), child: Text(text));
CupertinoDialogAction createActionCupertino(BuildContext context, {
bool isDefaultAction = false,
bool isDestructiveAction = false
}) => CupertinoDialogAction(child: Text(text),
onPressed: _invokeOnFunction(context),
isDefaultAction: isDefaultAction,
isDestructiveAction: isDestructiveAction,);
}
typedef OnPressed = bool Function();
typedef VoidFunction = void Function();
이제 외부에서는 createActionMaterial 과 createActionCupertino 만 호출하게되면 해당되는 Action 객체와 Dismiss 이벤트가 자동으로 탑재된다. Dialog 클래스의 내용도 바꿔보자.
// dialog.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class UDialog {
static void confirm(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
// Not Implemented.
}
// Material UI
static void _showMaterialStyle(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
showDialog(context: BuildContext,
builder: (BuildContext context) =>
AlertDialog(
title: Text(String),
content: Text(String),
actions: [
positive.createActionMaterial(context),
if(negative != null) negative.createActionMaterial(context),
],
)
);
}
// Cupertino UI
static void _showCupertinoStyle(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
showCupertinoDialog(context: BuildContext,
builder: (BuildContext context) =>
CupertinoAlertDialog(
title: Text(String),
content: Text(String),
actions: [
positive.createActionCupertino(context, isDefaultAction: true),
if(negative != null) negative.createActionCupertino(context, isDestructiveAction: true),
],
)
);
}
}
Actions 의 부분이 참 깔끔해졌다. 여기서 Cupertino 의 isDefaultAction 과 isDestructiveAction 을 간단하게 설명해본다면,
1. isDefaultAction : true 로 넘겨주면 버튼이 파란색 스타일을 가지게 된다. Postive 형식일 때 사용한다.
2. isDestructiveAction : true 로 넘겨주면 버튼이 빨간색 스타일을 가지게 된다. Negative 형식일 때 사용한다.
둘의 기능적인 차이는 딱히 없고, 스타일을 바꿔주는 상징적인 플래그 라고 생각하면 접근하기가 조금 쉬울 거라 생각한다.
이렇게 UI 는 그려보았고, 다음에는 분기처리하여 보여주는 것을 만들어보자.
3. 분기 처리
이제 confirm 에 들어갈 내용을 다듬어야 할 차례이다. 지금까지 각각의 UI 를 만들었지만, 정작 호출해야될 메소드인 confirm 에는 아무것도 작성하지 않았기 때문에 작동을 할 수 없는 상태이다.
그래서 confirm 에서는 간단하게 분기처리하여 보여줄 코드만 작성하도록 한다.
// dialog.dart
class UDialog {
static void confirm(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
switch(Theme.of(context).platform){
case TargetPlatform.iOS:
_showCupertinoStyle(context,
title: title,
content: content,
positive: positive,
negative: negative);
break;
default:
_showMaterialStyle(context,
title: title,
content: content,
positive: positive,
negative: negative);
break;
}
}
...Codes
}
Theme 의 platform 을 통해 해당 디바이스의 OS 를 가지고 올 수 있다. 그래서 OS 별로 알맞는 Alert 를 뿌려줄 수 있도록 메소드를 호출하면,
완성!!!
전체 소스 복사 할 생각말고 열심히 원리를 공부해 보자.
// dialog.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class UDialog {
static void confirm(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
switch(Theme.of(context).platform){
case TargetPlatform.iOS: _showCupertinoStyle(context, title: title, content: content, positive: positive, negative: negative); break;
default: _showMaterialStyle(context, title: title, content: content, positive: positive, negative: negative); break;
}
}
static void _showMaterialStyle(BuildContext context, {
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
showDialog(context: context,
builder: (BuildContext context) =>
AlertDialog(
title: Text(title),
content: Text(content),
actions: [
positive.createActionMaterial(context),
if(negative != null) negative.createActionMaterial(context)
],
)
);
}
static void _showCupertinoStyle(BuildContext context,{
required String title,
required String content,
required DialogAction positive,
DialogAction? negative,
}){
showCupertinoDialog(context: context,
builder: (BuildContext context) =>
CupertinoAlertDialog(
title: Text(title),
content: Text(content),
actions: [
positive.createActionCupertino(context, isDefaultAction: true),
if(negative != null) negative.createActionCupertino(context, isDestructiveAction: true)
],
)
);
}
}
class DialogAction {
String text;
OnPressed? event;
DialogAction(this.text, this.event);
void _onDismiss(BuildContext context){ Navigator.pop(context); }
VoidFunction _invokeOnFunction(BuildContext context) => (){
if(event != null && !event!()) return;
_onDismiss(context);
};
TextButton createActionMaterial(BuildContext context) => TextButton(onPressed: _invokeOnFunction(context), child: Text(text));
CupertinoDialogAction createActionCupertino(BuildContext context, {
bool isDefaultAction = false,
bool isDestructiveAction = false
}) => CupertinoDialogAction(child: Text(text), onPressed: _invokeOnFunction(context), isDefaultAction: isDefaultAction, isDestructiveAction: isDestructiveAction,);
}
typedef OnPressed = bool Function();
typedef VoidFunction = void Function();
[Flutter/적응기] Multi-platform 개발 접근하기 (Desktop/Mobile) (0) | 2022.02.09 |
---|