[개발하는남자 핸즈온 플러터] 챕터15 상품 등록 페이지 개발 1 리뷰
개요
안녕하세요, 개발하는남자 개남입니다.. 지난 포스트에 이어서 챕터 15 상품등록 페이지 관련 리뷰를 진행하고자 합니다. 493페이지부터 ~ 588페이지까지 상품 등록 페이지 개발 1차를 다루고 있는 만큼 많은 정보를 담고 있어 해당 과정의 최신화및 추가적으로 설명및 소개드릴 부분을 다루고자 합니다.
소스코드 : https://github.com/sudar-life/bamtol_market_clone_coding
브런치 정보 : chapter15
플러터 버전 : Flutter 3.32.5
Dart 버전 : Dart 3.8.1
최신화
가장먼저 pubspec.yaml 파일을 최신버전에 맞춰 버전을 올려주도록 하겠습니다.
photo_manager: ^3.7.1 flutter_map: ^8.2.1 geolocator: ^14.0.2 latlong2: ^0.9.0 uuid: ^4.5.1
책 집필당시 사용했던 버전에 비해 flutter_map이 버전차이가 많이 발생하고 있어서 flutter clean후
flutter pub get을 통해 신규 버전으로 업그레이드를 하게 되면 MapOptions 의 사용중인 필드에서 오류가 발생하고 있는 것을 확인하실 수 있습니다.
center와 interactiveFlags 필드를 MapOptions에서 더이상 사용하지 않게 되었습니다. 다음 소스코드로 변경해주면 됩니다.
options: MapOptions( initialZoom: 15.0, initialCenter: myLocation, // 변경 interactionOptions: const InteractionOptions( // 변경 flags: InteractiveFlag.pinchZoom | InteractiveFlag.drag, ), // 이하 생략
기존 center필드는 initialCenter로 변경되었고 interactiveFlags필드는 interactionOptions로 변경되면서 사용방법이 변경되었습니다.
또한 기존 TileLayer 사용을 urlTemplate만 넣어서 사용했는데 userAgentPackageName을 추가해주지 않으면 사용하지 못하는 문제도 발생했습니다.
TileLayer( urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", userAgentPackageName: 'com.bamtol.market', // 추가 ),
마지막으로 _mapController에서 center로 바로 접근해서 위치를 불러왔던 방식이 변경되었습니다.
Get.back(result: { 'label': result, 'location': _mapController.camera.center });
camera라는 객채로부터 center를 받아와야 합니다.
갤러리 선택 리스트에서 선택할때마다 모든 이미지들이 깜빡이는 문제 개선
문제 확인
해당 문제는 multiful_image_view.dart파일의 _selectedImage함수에서 선택할때
setState를 통해 전체적으로 화면을 다시 그리기 때문에 발생된 문제 입니다.
이것을 해결하려면 이미지 선택에 대한 데이터를 상태관리를 통해 선택된 이미지만 화면을 다시 그리게 처리 해야 합니다. GetxController를 통해 해결 할 수 있겠지만 ValueNotifier를 통해 간단한 방식으로 처리 하도록 하겠습니다.
ValueNotifier이란?
Flutter의 ValueNotifier에 대해 간단하게 설명드리겠습니다! 🎯
📋 ValueNotifier란?
ValueNotifier는 Flutter에서 제공하는 간단한 상태 관리 클래스입니다. 하나의 값을 저장하고, 그 값이 변경될 때 자동으로 리스너들에게 알림을 보내는 역할을 합니다.
기본 사용법
1. ValueNotifier 생성
ValueNotifier<int> counter = ValueNotifier<int>(0); ValueNotifier<String> userName = ValueNotifier<String>('김성덕'); ValueNotifier<bool> isLoading = ValueNotifier<bool>(false);
2. 값 변경
counter.value = 5; // 값 변경 시 자동으로 리스너들에게 알림 userName.value = '개발하는남자'; isLoading.value = true;
3. UI에서 사용 - ValueListenableBuilder
ValueListenableBuilder<int>( valueListenable: counter, builder: (context, value, child) { return Text('카운터: $value'); // 값이 변경되면 자동으로 UI 업데이트 }, )
주요 특징
장점
- 간단함: 복잡한 설정 없이 바로 사용 가능
- 효율적: 값이 변경될 때만 UI 업데이트
- 가벼움: 작은 메모리 사용량
- 반응형: 값 변경 시 자동으로 리스너들에게 알림
주의사항
- dispose() 필수: 메모리 누수 방지를 위해 반드시 해제
- 하나의 값만: 여러 값을 관리하려면 여러 개의 ValueNotifier 필요
- 복잡한 상태: 복잡한 상태 관리에는 Getx, Bloc 등이 더 적합
multiful_image_view.dart 소스코드 수정
1. 변수 선언 수정
var selectedImages = <AssetEntity>[]; // 변경 전 final selectedImages = ValueNotifier<List<AssetEntity>>([]); // 변경 후
2. 초깃값 업데이트 수정
// 변경 전 if (widget.initImages != null) { selectedImages.addAll([...widget.initImages!]); } // 변경 후 if (widget.initImages != null) { selectedImages.value = [...widget.initImages!]; }
3. containValue함수 수정
//변경 전 bool containValue(AssetEntity value) { return selectedImages.where((element) => element.id == value.id).isNotEmpty; } //변경 후 bool containValue(AssetEntity value) { return selectedImages.value .where((element) => element.id == value.id) .isNotEmpty; }
4. returnIndexValue함수의 asMap 사용 수정
//변경 전 var find = selectedImages.asMap().entries.where((element) { return element.value.id == value.id; }); //변경 후 var find = selectedImages.value.asMap().entries.where((element) { return element.value.id == value.id; });
5. _selectedImage 함수 수정
//변경 전 void _selectedImage(AssetEntity imageList) async { setState(() { if (containValue(imageList)) { selectedImages.remove(imageList); } else { if (10 > selectedImages.length) { selectedImages.add(imageList); } } }); } //변경 후 void _selectedImage(AssetEntity imageList) async { final currentSelected = List<AssetEntity>.from(selectedImages.value); if (containValue(imageList)) { currentSelected.remove(imageList); } else { if (10 > currentSelected.length) { currentSelected.add(imageList); } } selectedImages.value = currentSelected; }
6. _photoWidget 위젯 ValueListenableBuilder사용
//변경 전 if (snapshot.hasData) { return Stack( // 이하 생략 //변경 후 if (snapshot.hasData) { return ValueListenableBuilder<List<AssetEntity>>( valueListenable: selectedImages, builder: (context, selectedList, child) { return Stack( //이하 생략
7. Get.back 반환값 수정
//변경 전 if (selectedImages.isNotEmpty) { Get.back(result: selectedImages); } //변경 후 if (selectedImages.value.isNotEmpty) { Get.back(result: selectedImages.value); }
8. dispose 추가
void dispose() { super.dispose(); scrollController.dispose(); selectedImages.dispose(); }
결과 확인

이미지 개수 표시 + 키보드 내리기 버튼 영역이 키보드 올라올때 따라 올라오지 않는 문제 개선
책 집필 당시에 이미지 선택기능과 거래 희망 장소에 초점을 맞추다보니 현재 키보드가 올라올때 하단영역이 따라 올라와야 하는데 올라오지 않고있습니다. 이부분을 놓쳤네요 해당 문제를 수정해보겠습니다.
CommonLayout 수정
이미 로딩화면을 공통으로 사용하기 위해서 Scaffold 위젯을 wrapping한 CommonLayout을 만들어 사용하고 있었습니다. Scaffold의 옵션중에 resizeToAvoidBootomInset이라는 옵션이 있는데 해당 옵션은 키보드가 올라올 때 화면의 크기를 조정할지 여부를 결정하는 속성입니다
현제는 false로 고정되어있어서 키보드가 올라와도 화면 크기 조절을 하지 않아 하단영역이 올라오지 않았던 것입니다.
단순히 CommonLayout의 resizeToAvoidBootomInset값을 true로 변경하면 문제가 바로 해결되겠지만 기존에 false로 한 이유가 있었을 것이라 default를 false로 설정하고
product_write_page위젯에서만 CommonLayout에 속성값으로 true 받아 적용하는 방식으로 개선하겠습니다.
CommonLayout 수정
class CommonLayout extends GetView<CommonLayoutController> { final Widget body; final PreferredSizeWidget? appBar; final Widget? bottomNavBar; final Widget? floatingActionButton; final bool? resizeToAvoidBottomInset; final bool useSafeArea; const CommonLayout({ super.key, required this.body, this.appBar, this.floatingActionButton, this.useSafeArea = false, this.bottomNavBar, this.resizeToAvoidBottomInset = false, }); Widget build(BuildContext context) { return Material( child: Obx( () => Stack( fit: StackFit.expand, children: [ Scaffold( resizeToAvoidBottomInset: resizeToAvoidBottomInset, // 이하 생략
ProductWritePage 수정
class ProductWritePage extends GetView<ProductWriteController> { const ProductWritePage({super.key}); Widget get _divder => const Divider( color: Color(0xff3C3C3E), indent: 25, endIndent: 25, ); Widget build(BuildContext context) { return CommonLayout( resizeToAvoidBottomInset: true, //추가
Write by :
개발하는남자
Developer
💬댓글 (0)
댓글을 작성하려면 로그인이 필요합니다.
댓글을 불러오는 중...