getX 패턴, 그리고 getX 라이브러리를 사용하면서 flutter 앱을 만들면 여러가지 장점이 있다.
기존보다 편리한 navigation.
Get 클래스로 사전 정의된 UI 위젯들(SnackBar, Dialog 등등).
그 밖에도 언어 전환이나, 테마 설정 등의 기능들...
하지만 대다수의 getX 라이브러리를 사용하는 유저들이 가장 학습하기 어려워하고, 또 동시에 만족스러워하면서 가장 필요로 하는 기능은 상태관리 기능이다.
앱의 성능을 향상시키기 위해서 꼭 알아두어야 할 개념 상태관리.
나 역시도 이번 프로젝트 앱의 모든 코드를 getX 패턴을 사용하며 작성하는 중으로, 이번 게시글은 getX 패턴으로 애니메이션의 state를 조작해야 하는 위젯들을 구현해보면서 얻은 경험들을 기술해본다.
해당 게시글의 내용은 기능 구현을 위한 정석적인 방법이 아닐수 있으며, 구현 과정이 미숙하거나 정리되지 않은 코드가 포함될수도 있다.
그럼에도 플러터를 학습하는 누군가와, 얼마 못가 이 내용을 까먹을 미래의 나를 위해 이 게시글을 작성해본다.
우선 당연하게도 getX 라이브러리가 설치되어있어야 한다.
그리고 필자는 UI 구현을 위해 추가로 flutter_speed_dial 패키지를 설치해주었다.
speed dial 패키지는 위와 같이 기존의 floating action button에 해당 UI를 애니메이션 아이콘을 동반한 메뉴 버튼으로 쓸수 있게 만들어주는 패키지다.
첫번째로 구현해볼 UI는 tabBar.
tabBar는 기존의 navigation bar와 비슷하다고 생각하면 쉽다.
다만 필자는 getX 패턴을 적용하여 앱을 작성하는 중으로, 탭바의 state가 변경될때마다 이를 감지하고 UI에 표시해주는 컨트롤러를 따로 만들어주어야 한다.
//탭바를 감싸고있는 friedns view의 UI.
class FriendsView extends GetView<FriendsController> {
const FriendsView({Key? key}) : super(key: key);
Widget build(BuildContext context) {
Get.put(FriendsController());
return Scaffold(
backgroundColor: bgColor,
appBar: AppBar(
bottom: TabBar(
indicatorColor: accentBrown,
dividerColor: accentBrown,
controller: controller.tabController,
tabs: controller.myTabs,
),
title: Text(
'Walk',
style: TextStyle(
fontFamily: 'LS',
fontSize: 30,
fontWeight: FontWeight.w700,
color: accentYellow),
),
centerTitle: false,
elevation: 0,
backgroundColor: bgColor,
),
body: TabBarView(
controller: controller.tabController,
children: [
FriendsRankingView(),
FriendsInviteView(),
FriendsAcceptView(),
],
),
);
}
}
위의 코드는 탭바를 감싸고있는 friedns view의 UI로, 앱바의 bottom 부분에 tabBar의 디자인적 요소와 할당할 컨트롤러를, Scaffold의 body 부분에 TabBarView를 넣어 구현되었다.
TabBarView의 자식들로 들어가있는 View들은 위의 gif에서 보이는 페이지들이다.
class FriendsController extends GetxController
with GetSingleTickerProviderStateMixin {
//탭바.
//탭 클릭 감지시 표시될 탭바 UI.
final List<Tab> myTabs = <Tab>[
Tab(
child: Text(
'랭킹',
style: TextStyle(
color: textDark,
fontFamily: 'IBMKR',
fontSize: 15,
fontWeight: FontWeight.w700),
),
),
Tab(
child: Text(
'친구 초대',
style: TextStyle(
color: textDark,
fontFamily: 'IBMKR',
fontSize: 15,
fontWeight: FontWeight.w700),
),
),
Tab(
child: Text(
'받은 요청',
style: TextStyle(
color: textDark,
fontFamily: 'IBMKR',
fontSize: 15,
fontWeight: FontWeight.w700),
),
),
];
//탭 컨트롤러 선언
late TabController tabController;
Friends View에 연결되어있는 Friends Controller의 코드.
FriendsView Appbar의 bottom 부분을 차지하고 있는 TabBar의 controller와 tabs를 이 부분이 담당한다.
List로 선언된 myTabs가 tabBar의 UI 객체가 되고, 탭 컨트롤러를 선언하여 외부에서 사용할수 있도록 한다.
다만 이 컨트롤러는 주의사항이 있는데, GetXController를 상속하면서 동시에 GetSingleTickerProviderStateMixin를 같이 추가해야만 애니메이션의 동작을 제어할수 있다.
GetSingleTickerProviderStateMixin은 기존의 SingleTickerProviderStateMixin와 비슷한 역할을 한다.
이제 맨 위에서 보았던 speed dial을 구현해볼 차례다.
class FriendsView extends GetView<FriendsController> {
const FriendsView({Key? key}) : super(key: key);
Widget build(BuildContext context) {
Get.put(FriendsController());
Get.lazyPut(() => FriendsDialController());
FriendsDialController friendsDialController = FriendsDialController();
return Scaffold(
backgroundColor: bgColor,
floatingActionButton: SpeedDial(
openCloseDial: friendsDialController.isDialOpen,
onOpen: () {},
onClose: () {
friendsDialController.isDialBtnClosed();
},
onPress: () {
friendsDialController.isDialBtnClicked();
},
child: AnimatedIcon(
icon: AnimatedIcons.menu_close,
progress: friendsDialController.animationController,
),
backgroundColor: accentYellow,
//dial item 들.
//클릭시 해당 함수에 해당하는 디알로그 출력.
children: [
SpeedDialChild(
child: Icon(
Icons.add,
color: accentBrown,
),
label: '친구 추가하기',
onTap: () {
friendsDialController.isAddBtnClicked();
}),
SpeedDialChild(
child: Icon(
Icons.search_rounded,
color: accentBrown,
),
label: '친구 검색',
onTap: () {
friendsDialController.isSearchBtnClicked();
}),
SpeedDialChild(
child: Icon(
Icons.people_rounded,
color: accentBrown,
),
label: '친구 관리',
onTap: () {
friendsDialController.isControlBtnClicked();
}),
SpeedDialChild(
child: Icon(
Icons.share,
color: accentBrown,
),
label: 'SNS 연동',
onTap: () {
friendsDialController.isSNSBtnClicked();
}),
],
),
appBar: AppBar(
bottom: TabBar(
indicatorColor: accentBrown,
dividerColor: accentBrown,
controller: controller.tabController,
tabs: controller.myTabs,
),
title: Text(
'Walk',
style: TextStyle(
fontFamily: 'LS',
fontSize: 30,
fontWeight: FontWeight.w700,
color: accentYellow),
),
centerTitle: false,
elevation: 0,
backgroundColor: bgColor,
),
body: TabBarView(
controller: controller.tabController,
children: [
FriendsRankingView(),
FriendsInviteView(),
FriendsAcceptView(),
],
),
);
}
}
이 FriendsView는 위의 TabBar 구현 방법을 설명할때 보았던 것과 같은 소스다.
다만 다른 점은 Scaffold 의 floating action button이 있어야 할 자리에 Speed dial이 사용되었다.
Get.lazyPut으로 speed dial을 조작할 FriendsDialController를 추가로 삽입했으며, 컨트롤러를 사용하기 쉽도록 friendsDialController라는 이름으로 다시 선언해주었다.
즉, FriendsView는 두개의 컨트롤러를 사용한다.
후술할 friendsDialController는 다이얼이 오픈되었을 때, 닫혔을 때, 클릭되었을때의 기능들, 그리고 애니메이션의 progress를 담고있다.
speed dial의 자식들로 들어가있는 SpeedDialChild는 다이얼을 눌렀을때 위에 표시되는 메뉴 버튼들이며, UI 정보와 onTap 실행할 메소드를 담고있다.
onTap시 실행될 메소드들은 friendsDialController에 선언되어있다.
class FriendsDialController extends GetxController
with GetSingleTickerProviderStateMixin {
//초기값이 없어 발생하는 빌드 오류를 막기 위해 초기값을 미리 선언.
//왜인지 onInit 에 넣었을때는 작동하지 않았음. 확인 필요.
late AnimationController animationController =
AnimationController(vsync: this, duration: Duration(seconds: 1));
//speed dial 버튼 클릭시 감지 노티파이어.
//애니메이션과 무관. 다이알 올라오는 기능 감지용.
ValueNotifier<bool> isDialOpen = ValueNotifier(false);
var isPlaying = false.obs;
//다이알 버튼 클릭시 선언된 애니메이션 컨트롤러 작동. 동시에 다이알 올라옴.
isDialBtnClicked() {
isDialOpen.value = true;
animationController.forward();
}
//다이알 버튼 종료시 애니메이션 닫힘. 동시에 다이알 내려옴.
isDialBtnClosed() {
isDialOpen.value = false;
animationController.reverse();
}
isAddBtnClicked() {
Get.dialog(
CustomDialog(
titleText: ' 친구 추가하기',
dialogContent: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 8,
child: TextField(
keyboardType: TextInputType.number,
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide(color: accentYellow)),
focusColor: accentYellow,
fillColor: Colors.grey,
hintText: '추천코드로 친구를 찾아보세요',
hintStyle: TextStyle(
fontSize: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
),
),
),
),
SizedBox(
width: 5,
),
Expanded(
flex: 2,
child: SizedBox(
height: 55,
width: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: accentYellow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
),
onPressed: () {},
child: Icon(Icons.search),
),
),
),
],
),
SizedBox(
height: 50,
),
Text(
'친구하고 싶은 사용자를 추천코드로 검색합니다.\n 이미 친구인 사용자를 검색하고\n 싶다면 아래의 버튼을 선택해주세요',
style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
),
SizedBox(
height: 50,
),
CustomButtonYellow(
btnText: '친구 검색하러 가기',
onPressed: () {},
),
SizedBox(
height: 250,
),
SizedBox(
width: 250,
child: CustomButtonBrown(
btnText: '확인',
onPressed: () {
Get.back();
},
),
),
],
),
),
);
}
isSearchBtnClicked() {
Get.dialog(
CustomDialog(
titleText: '친구 검색하기',
dialogContent: Column(
children: [],
),
),
);
}
isControlBtnClicked() {
Get.dialog(
CustomDialog(
titleText: '친구 관리하기',
dialogContent: Column(
children: [],
),
),
);
}
isSNSBtnClicked() {
Get.dialog(
CustomDialog(
titleText: 'SNS 연동하기',
dialogContent: Column(
children: [],
),
),
);
}
}
다이얼의 메뉴 버튼들을 눌렀을때 나타나는 UI는 Get.dialog를 통해 만들었다.
CustomDialog는 필자가 AlertDialog를 리턴하여 만든 커스텀 UI이며, 이는 추후에 따로 서술하겠다.
지금은 그냥 미리 UI가 만들어져있는 AlertDialog로 생각해도 좋다.
중요하게 봐야할 포인트는 AnimationController인데, 코드의 최상단에 animationController의 기본값들을 미리 선언해둔다.
나는 애니메이션의 듀레이션을 1초로 설정해두었다.
다이얼이 오픈되거나 닫힐때 호출되는 메소드에 animation 컨트롤러를 호출하면서, 오픈되었을때는 forward로 애니메이션을 재생하며, 닫힐때는 reverse로 애니메이션을 역재생한다.
speed dial의 애니메이션 아이콘이 잘 작동하는 모습.