사전캠프 7일차
공휴일은 사전 캠프가 없어 날짜로는 8일이 아닌 7일차다.
3주차 강의에서는 상태 관리에 대해 학습한 걸 활용해 저번 시간에 실습한 알람 앱에 기능을 더했다.
📌 알람앱 기능명세에 따른 할일 목록
- 알람 등록 화면 Get route 설정
- 알람 등록 화면 구성(위젯 구성)
- 알람 등록 기능 개발
- 홈 화면 등록된 알람 리스트
- 홈 화면 알람 편집 이벤트로 편집화면 구성
- 알람 삭제 기능 개발
- 알람 선택 시 기존 알람 등록 화면 > 알람 편집 화면으로 변경
- 개별 알람에 활성화/비활성화 기능 적용
flutter pub add get
- Terminal에 입력해 GetX라이브러리를 설치한다.
Widget build(BuildContext context) { return GetMaterialApp( title: 'Flutter Demo', theme: ThemeData( scaffoldBackgroundColor: Colors.black, textTheme: TextTheme( bodyMedium: TextStyle(color: Colors.white), ), useMaterial3: true, ), home: Home(), ); }
MaterialApp에서GetMaterialApp으로 변경한다.
📌 appbar를 통한 header 구성
appBar: AppBar( leading: GestureDetector( onTap: Get.back, behavior: HitTestBehavior.translucent, child: Center( child: Text( '취소', style: TextStyle( color: Color(0xffff9f0a), fontSize: 20, ), ), ), ), backgroundColor: Colors.transparent, title: Text('알람 추가', style: TextStyle(color: Colors.white)), actions: [ GestureDetector( onTap: () {}, child: Padding( padding: const EdgeInsets.only(right: 15), child: Text( '저장', style: TextStyle( color: Color(0xffff9f0a), fontSize: 20, ), ), ), ) ], ),
- 취소 버튼의 이벤트에
Get.back은 GetX에서 제공되는 Route를 설정하도록 설정했기 때문에 페이지 전환에 따른 페이지 history가 관리된다. 그렇기 때문에 단순히 뒤로 가기 위해서는 Get.back이라는 함수를 호출하면 된다.
📌 body 구성
body: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '오전', style: TextStyle( fontWeight: FontWeight.w100, fontSize: 28, color: Color(0xffababac), ), ), SizedBox(width: 15), SizedBox( width: 87, height: 87, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Color(0xff2c2c2e), ), child: Center( child: TextField( maxLength: 2, textAlign: TextAlign.center, keyboardType: TextInputType.number, inputFormatters: <TextInputFormatter>[ FilteringTextInputFormatter.digitsOnly, RangeTextInputFormatter(1, 23), ], style: TextStyle( color: Colors.white, fontSize: 40, fontWeight: FontWeight.w100, ), decoration: InputDecoration( border: InputBorder.none, counterText: '', ), ), ), ), ), SizedBox(width: 15), SizedBox( width: 87, height: 87, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Color(0xff2c2c2e), ), child: Center( child: TextField( maxLength: 2, textAlign: TextAlign.center, keyboardType: TextInputType.number, inputFormatters: <TextInputFormatter>[ FilteringTextInputFormatter.digitsOnly, RangeTextInputFormatter(0, 59), ], style: TextStyle( color: Colors.white, fontSize: 40, fontWeight: FontWeight.w100, ), decoration: InputDecoration( border: InputBorder.none, counterText: '', ), ), ), ), ), ], ), ),
keyboardType: TextInputType.number이렇게 설정하면 키보드가 숫자만 노출되는 것을 확인할 수 있다.
또한, 사용자가 복사 붙여넣기 등을 통해 숫자가 아닌 값을 넣을 수 있기 때문에 그것을 차단하기 위해서 inputFormatter라는 옵션이 있다.
이 옵션을 통해FilteringTextInputFormatter.digitsOnly를 사용하면 숫자만 입력을 받을 수 있게 된다.
RangeTextinputFormatter
class RangeTextInputFormatter extends TextInputFormatter { final int min; final int max; // RangeTextInputFormatter(this.min, this.max); // TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { if (newValue.text.isEmpty) { return newValue; } // final int? value = int.tryParse(newValue.text); if (value == null || value < min || value > max) { return oldValue; } return newValue; } }
- 추가적으로 시간과 분은 설정 범위가 있다.
시간은 1~23시간, 분의 경우 0~59까지 설정할 수 있어야 한다.
이것을 해결하기 위해서 별도의 custom Formatter를 이용해야 한다.
이것을 위해 RangeTextinputFormatter라는 이름으로 클래스를 만들어준다.
📌 AlarmController 생성
import 'package:get/get.dart'; // class AlarmController extends GetxController {}
📌 AlarmController 의존성 주입
Get.put(AlarmController());
Get.find통해 해당 컨트롤러에 접근이 가능하게 된다.
가능하게 되었습니다.
💡 원래 알람 앱을 만들기 위해 등록/수정 관련 controller, home 화면에 필요한 controller를 각각 만드는 등 controller를 기능별로 쪼개서 관리한다.
📌 AlarmController 등록에 필요한 상태 및 이벤트 생성
import 'package:get/get.dart'; // class AlarmController extends GetxController { int hour = 0; int minute = 0; // void setHour(int hour) { this.hour = hour; update(); } // void setMinute(int minute) { this.minute = minute; update(); } }
📌 알람 추가 페이지 컨트롤러 연결
body: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ GetBuilder<AlarmController>( builder: (controller) { return Text( controller.hour < 12 ? '오전' : '오후', style: TextStyle( fontWeight: FontWeight.w100, fontSize: 28, color: Color(0xffababac), ), ); }, ),
- 오전/오후를 표기하기 위해서 GetBuilder를 사용해서 상태가 변경될 때마다 이 문자만 다시 그려지도록 설정한다.
시간의 값이 12시보다 낮으면 오전 그 이후는 오후로 표기되도록 삼항 연산자를 사용해서 처리한다.SizedBox(width: 15), SizedBox( width: 87, height: 87, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Color(0xff2c2c2e), ), child: Center( child: TextField( maxLength: 2, textAlign: TextAlign.center, keyboardType: TextInputType.number, inputFormatters: <TextInputFormatter>[ FilteringTextInputFormatter.digitsOnly, RangeTextInputFormatter(1, 23), ], style: TextStyle( color: Colors.white, fontSize: 40, fontWeight: FontWeight.w100, ), decoration: InputDecoration( border: InputBorder.none, counterText: '', ), onChanged: (String hour) { if (hour == '') return; Get.find<AlarmController>().setHour(int.parse(hour)); }, ), ), ), ), SizedBox(width: 15), SizedBox( width: 87, height: 87, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Color(0xff2c2c2e), ), child: Center( child: TextField( maxLength: 2, textAlign: TextAlign.center, keyboardType: TextInputType.number, inputFormatters: <TextInputFormatter>[ FilteringTextInputFormatter.digitsOnly, RangeTextInputFormatter(0, 59), ], style: TextStyle( color: Colors.white, fontSize: 40, fontWeight: FontWeight.w100, ), decoration: InputDecoration( border: InputBorder.none, counterText: '', ), onChanged: (String minute) { if (minute == '') return; Get.find<AlarmController>().setMinute(int.parse(minute)); }, ), ), ), ), ], ), ),
Get.find<AlarmController>().setHour(int.parse(hour));
- 시간 입력 필드와 분 입력 필드 동일하게 onChanged 함수를 통해 값을 update 될 수 있도록 controller에 각각 setHour 과 setMinute로 연결해준다.
이때 TextField에서 전달되는 입력 값은 String이기 때문에 int 타입으로 형 변환을 시켜준다.
📌 홈화면에서 사용되는 알람 목록 상태 추가
class AlarmController extends GetxController { int hour = 0; int minute = 0; // List alarmList = [];
- 여기서 List에 어떤 타입을 지정해 줄지 정의를 해줘야 한다.
📌 alarmModel 설계
class AlarmModel { int hour; int minute; bool isOn; // AlarmModel({ required this.hour, required this.minute, this.isOn = true, }); }
📌 홈화면에서 사용되는 알람 목록 상태 타입 정의
class AlarmController extends GetxController { int hour = 0; int minute = 0; // List<AlarmModel> alarmList = [];
- 이제 사용자가 저장을 누르면 이벤트를 발생시켜 alarmList에 데이터를 넣어주면 된다.
📌 저장 이벤트 등록
void saveAlarm(){ alarmList.add(AlarmModel(hour: hour, minute: minute)); update(); }
📌 저장 이벤트 연결
GestureDetector( onTap: () { Get.find<AlarmController>().saveAlarm(); Get.back(); }, child: Padding( padding: const EdgeInsets.only(right: 15), child: Text( '저장', style: TextStyle( color: Color(0xffff9f0a), fontSize: 20, ), ), ), )💡 Controller 삭제되는 이슈 대응
의도한 것과 다르게 저장을 통해 페이지에서 빠져나왔을 때 debug console에
[GETX] "AlarmController" deleted from memory와 같은 로그가 찍혀 있을 수도 있다.
GetX는 성능 향상을 위해 항상 최적화를 자동으로 진행하다 보니, 사용하지 않을 경우 자동으로 삭제된다.
✔ 의존성 영구적 유지 설정Get.put(AlarmController(), permanent: true);이럴 경우, 앱을 종료하기 전까지 controller를 삭제되지 않고, 항상 사용해야 하는 controller는 자동으로 삭제되지 않도록 설정을 해줘야 한다.
편집모드 상태 및 이벤트 추가
📌 alarmController에 모드 상태 추가 및 편집모드 전환 이벤트
class AlarmController extends GetxController { int hour = 0; int minute = 0; // bool isEditMode = false; // List<AlarmModel> alarmList = []; // void setHour(int hour) { this.hour = hour; update(); } // void setMinute(int minute) { this.minute = minute; update(); } // void saveAlarm() { alarmList.add(AlarmModel(hour: hour, minute: minute)); update(); } // void toggleEditMode() { isEditMode = !isEditMode; update(); } }
- 이제 화면에서 편집 버튼을 누르면
toggleEditMode이벤트를 호출해주면 된다.
📌 편집 버튼 이벤트 연결
leading: GestureDetector( onTap: Get.find<AlarmController>().toggleEditMode, child: Center( child: GetBuilder<AlarmController>( builder: (controller) { return Text( controller.isEditMode ? '완료' : '편집', style: TextStyle( color: controller.isEditMode ? Colors.red : Color(0xffff9f0a), fontSize: 20, ), ); }, ), ), ),
isEditMode에 따라서 버튼도 변경이 필요하다.
편집을 누르면 완료를 해줘서 다시 원래 상태로 돌아오기 위함으로, 기본적으로 색상 값과 문구만 변경해 주도록 했다.
기타 알람 목록 좌측에 알람 삭제 버튼을 위치
📌 _etcAlarm함수 파라미터 editmode 전달
return Column( children: controller.alarmList.map((alarm) { return _etcAlarm(alarm, controller.isEditMode); }).toList(), );
📌 _etcAlarm함수 editMode일때 화면 처리
Widget _etcAlarm(AlarmModel alarm, bool isEditMode) { return Row( children: [ if (isEditMode) Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: GestureDetector( onTap: () {}, child: Icon(Icons.remove_circle, color: Colors.red), ), ), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: Color(0xff262629)))), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(alarm.hour < 12 ? '오전' : '오후', style: TextStyle( fontSize: 25, color: Color(0xff8d8d93))), SizedBox(width: 10), Text( '${alarm.hour.toString().padLeft(2, '0')}:${alarm.minute.toString().padLeft(2, '0')}', style: TextStyle( fontSize: 60, color: Color(0xff8d8d93), height: 1, letterSpacing: -3)) ], ), Switch( onChanged: (value) { print(value); }, value: alarm.isOn, ), ], ), Text('알람', style: TextStyle(fontSize: 18, color: Color(0xff8d8d93))), ], ), ), ), ], ); }
- 기존의 위젯은 그대로 두고, 좌측에 아이콘을 넣고 빼기만 하면 되니, 전체를 Row로 감싸주고 isEditMode에 따라서 icon를 넣어준다.
이제 실습 중에서 6-5 알람 삭제 기능 개발과 6-6 알람 선택시 기존 알람 등록 화면 > 알람 편집 화면으로 변경이 남았다.
그리고 다음 실습은 스레드 앱인데 알람 앱보다 더 내용이 많고, 할 것이 많아 보인다.
오늘과 내일 강의는 실습 위주라 코드를 수정하고, 왜 쓰였는지 정도를 알려주는 정도라 TIL 작성할 것은 많지 않다.
하지만 내용이 생각보다 많은데, 참고용 코드를 백업해두는 것으로 해야겠다.