-
Flutter + Spring Boot 가족, 모임 서비스 구현하기 3.1 (Feat. UI 구현)Flutter 2023. 10. 24. 11:16728x90반응형SMALL
가족, 모임 서비스 구현 중 Flutter로 UI 구성 시 있었던 시행착오들을 글로 작성해보려고 합니다.
구현 중 알게 된 점들이나, 트러블슈팅 과정들이 섞여 있어 글이 매끄럽지 않을 수 있다는 점 양해 부탁드립니다.🥲
1. Flutter 통신
Flutter에서 통신을 위해서는 http 패키지를 추가해줘야 합니다.
pubspec.yaml
dependencies: ... http: ^1.1.0
http 패키지를 pubspec.yaml에 추가해줍니다.
만약 Api 통신을 하게 된다면 반환받을 데이터가 있을 경우 json을 다시 객체 형태로 바꿔주는 작업이 필요할 수 있습니다. (Map 자료구조를 사용한다면 이 작업이 필요없을 수 있습니다.)
저와 같은 경우 Api 요청 시 반환 받을 값들을 모두 dto로 선언해서 json을 바로 객체로 변환해 사용할 수 있도록 해주었습니다.
dto 클래스 예시)
class GroupInfoDto { final String group_name; final List<UserInfoInGroupDto> userInfos; static GroupInfoDto emptyGroupInfo = GroupInfoDto(group_name: '', userInfos: []); GroupInfoDto({ required this.group_name, required this.userInfos, }); GroupInfoDto.fromJson(Map<String, dynamic> json) : group_name = json['group_name'], userInfos = (json['user_infos'] as List) .map((e) => UserInfoInGroupDto.fromJson(e)) .toList(); bool isEmpty() => group_name == ''; }
위의 클래스가 json->객체 형태로의 자동 변환을 가지고 있는 dto입니다.
모든 멤버변수들을 필요로 하는 생성자를 통해 GroupInfoDto 객체를 생성해줄 수도 있습니다.
<클래스명>.fromJson 메서드를 통해 API 통신을 통해 얻어진 json 형태의 데이터를 인자로 갖는 생성자를 가질 수 있습니다.
class CurrentGroupInfo { final String groupName, uuid, usernameInGroup; CurrentGroupInfo.fromJson(Map<String, dynamic> json) : groupName = json['groupName'], uuid = json['uuid'], usernameInGroup = json['usernameInGroup']; CurrentGroupInfo({ required this.groupName, required this.uuid, required this.usernameInGroup, }); Map<String, dynamic> toJson() => { 'groupName': groupName, 'uuid': uuid, 'usernameInGroup': usernameInGroup, }; bool isEmpty() => groupName == ''; }
toJson 메서드를 통해 객체->json으로 변환해주는 메서드를 정의해 request body에 데이터를 넣어줄 수 있습니다.
http 통신 예시)
final response = await http.get(url, headers: headers); if (response.statusCode == 404) { return GroupInfoDto.emptyGroupInfo; } final json = jsonDecode(utf8.decode(response.bodyBytes)); GroupInfoDto groupInfo = GroupInfoDto.fromJson(json);
http 패키지를 통해 GET 방식의 요청을 보낼 수 있습니다.
반환 받을 객체에 한글이 포함되어 있다면 response.bodyBytes를 통해 디코딩하여 깨지지 않도록 해줍니다. 이 후 json 형식으로 디코딩 해줍니다.
디코딩된 json형태를 GroupInfoDto.fromJson에 넘겨준다면 해당 json을 통해 GroupInfoDto 객체가 생성되게 됩니다.
Spring과 같은 경우 dto 클래스에 getter 혹은 setter 롬복을 추가해준다면 response body에 있는 객체와 request body에 있는 json 데이터를 직렬화, 역직렬화 과정을 자동으로 해주지만, flutter와 같은 경우 직접 json을 객체로 바꿔주기 위한 메서드인 fromJson과 toJson 메서드를 정의해주어야 합니다!
2. model 시작과 동시에 초기화해주기
앱을 시작할 때, 바로 비동기 작업들을 해주어야 하는 경우가 있습니다. 만약 상태값인 model을 따로 정의하고 사용한다면 앱 시작 시 바로 초기화해주는 작업을 어떻게 해야할지 고민 하게 되었습니다.
model을 모든 컴포넌트, 스크린 상에서 사용하기 위해서는 ChangeNotifier를 상속받은 상태 클래스를 runApp시 NotifiyProvider로 추가해주어야 합니다.
Future main() async { ... runApp( MultiProvider( providers: [ ChangeNotifierProvider( create: (context) => LoginModel(KakaoLogin()), ), ChangeNotifierProvider( create: (context) => CurrentGroupInfoModel(), ), ], child: const JibbapApp(), ), ); }
위와 같이 ChangeNotifierProvider로 상태값을 정의해주는 과정이 앱 시작 초기에 발생하게 됩니다. 이 때 각 Model들의 생성자가 불리게 되는 것을 알 수 있습니다.
그러므로, 생성자가 호출되는 시점에 비동기 메서드를 실행하고 .then을 통해 상태값이 변경되었음을 알려준다면, 앱이 시작하자마자 비동기 메서드를 실행할 수 있게 됩니다.
class LoginModel extends ChangeNotifier { ... LoginModel(this._kakaoLogin) { init().then((_) { notifyListeners(); }); } Future init() async { ... } }
저와 같은 경우 상태를 초기화해주는 비동기 메서드인 init을 선언해주고, 생성자 안에서 호출해준 뒤, then을 통해 비동기 메서드가 끝나는 시점에 notifyListeners() 메서드를 호출해서 상태값이 변경되었음을 알려줄 수 있도록 해주었습니다.
그 다음, progress indicator 등을 통해 비동기 메서드들이 실행되고 있다는 것을 잠시 동안만 알려준다면 비교적 깔끔한 UI를 제공할 수 있다고 생각했습니다.
위와 같은 방법은 stateful widget에서도 사용될 수 있는 방법인 것 같습니다. initState 안에서 초기화 비동기 메서드를 불러준 뒤 .then 을 통해 비동기 메서드 실행이 끝나는 시점에 setState를 통해 상태값이 변경되었음을 알려준다면 위의 model 초기화와 동일하게 동작할 것으로 예상됩니다.
3. FutureBuilder
Flutter에서는 비동기 함수에서 리턴되는 Future 변수들을 비동기 실행 중, 끝난 시점에 자동으로 처리해주기 위해 FutureBuilder를 제공해줍니다.
late Future<GroupInfoDto> groupInfo; @override void initState() { super.initState(); groupInfo = GroupApiService.getGroupInfo(uuid: widget.uuid); } @override Widget build(BuildContext context) { ... return Scaffold( ..., body: Column( children: [ ..., FutureBuilder( future: groupInfo, builder: (context, snapshot) { if (snapshot.hasData) { return Column( children:[ ... ] ) } return const CircularProgressIndicator(); ...
Future 변수를 stateful widget에 선언해줍니다. late 키워드를 통해 해당 변수가 나중에 초기화 됨을 알려줍니다.
initState 안에서 Future 변수에 비동기 함수의 리턴값을 넣어줍니다.
build 메서드 내에서 FutureBuilder로 UI를 구성해줍니다. snapshot안에 Future 변수에 해당하는 값이 들어가있게 됩니다.
snapshot.hasData는 비동기 실행이 끝났다면 true를 반환해줍니다. 그러므로 비동기 실행이 끝나지 않았다면 ProgressIndicator를 반환해주고, 비동기 실행이 끝났다면 해당 UI를 반환해줄 수 있도록 해줍니다.
FutureBuilder가 없었다면, 비동기 실행이 끝났음을 알려주는 상태값을 정의해주고 함께 사용해야 하고, build 메서드 내에서도 해당 상태값과 함께 사용하여, 만약 한 위젯 상에 Future 변수가 여러 개일 경우 조금 복잡해질 수 있다고 생각합니다.
하지만, FutureBuilder를 통해 Future 변수의 비동기 작업의 진행 여부에 따라 자동으로 UI를 다르게 리턴해줄 수 있다는 것이 장점인 것 같습니다.4. GestureDetector 빈 공간 클릭
GestureDetector를 통해 클릭에 반응할 수 있는 버튼을 만들 경우, Container 위젯 내부에 빈 영역을 클릭하면 동작하지 않는 경우가 있습니다. 이럴 경우 다음과 같은 속성을 추가해주어야 합니다.
return GestureDetector( onTap: () { ... }, behavior: HitTestBehavior.translucent, child: ... )
아래는 translucent의 정의입니다.
HitTestBehavior translucent Type: HitTestBehavior package:flutter/src/rendering/proxy_box.dart Translucent targets both receive events within their bounds and permit targets visually behind them to also receive events.
영역 내의 반투명 영역과 그 뒤의 모든 영역들이 이벤트를 받을 수 있도록 허용해줍니다.
5. SingleChildScrollView height 조절
SingleChildScrollView를 통해 스크롤이 가능한 위젯을 만들 경우, 만약 SingleChildScrollView 외부에 다른 위젯과 함께 있다면 SingleChildScrollView가 이를 인식하지 못하고 height를 원래 스크린 크기만큼 확장하여 바깥의 위젯이 화면 밖으로 밀려나는 경우가 발생했습니다.
이를 해결하기 위해 SingleChildScrollView의 height를 직접 조정해줄 수 있도록 했습니다.
SingleChildScrollView는 내부 위젯만큼 height가 지정되게 되는데, 이는 실제 스크린 height를 넘지 않을 정도로 되어 있습니다. 그러므로 SingleChildScrollView에 Container를 덮어주고 max height를 지정해줌으로써 스크린 크기보다 더 이상 커지지 않도록 해주어야 합니다.
Column( children: [ Container( constraints: BoxConstraints(maxHeight: maxHeight), child: SingleChildScrollView( child: Column() ) ) ] Container( ... ) )
위젯의 크기를 스크린의 크기에 맞게 조정하고 싶은 경우 MediaQuery를 사용하면 됩니다.
double maxHeight = MediaQuery.of(context).size.height
MediaQuery.of에 현재 context를 넣어준다면 현재 스크린의 width와 heigh를 알 수 있습니다. 이는 Scaffold의 AppBar의 height를 전부 합친 것으로 이를 빼주어야지만 body의 전체 크기를 알 수 있습니다.
AppBar().preferredSize.height
AppBar의 크기를 변경하지 않았다면 default AppBar의 prefferedSize를 통해 AppBar의 height를 구할 수 있습니다.
이제 Body의 전체 height를 알았으므로 이를 통해 SingleChildScrollView의 높이를 지정해주면 됩니다.
한 스크린 내에 SingleChildScrollView 위젯만 있을 경우 위젯이 스크린 밖으로 밀려나는 경우는 생기지 않습니다. 위와 같은 상황은 SingleChildScrollView와 다른 위젯이 동일한 위치에 함께 있을 경우에만 발생하는 문제입니다.
써야할 양이 많아 여러 번 나누어서 포스팅해보도록 하겠습니다.🙃
반응형LIST'Flutter' 카테고리의 다른 글
Flutter + Spring Boot 가족, 모임 서비스 구현하기 3.2 (Feat. UI 구현) (3) 2023.10.26 Flutter + Spring Boot 가족, 모임 서비스 구현하기 1 (Feat. 카카오 로그인) (2) 2023.08.11 Google Codelabs Flutter 앱 만들기 실습 2 (0) 2023.08.03 Google Codelabs Flutter 앱 만들기 실습 1 (0) 2023.08.02 Dart 문법 정리 (0) 2023.07.28