본문 바로가기

Flutter/12 Clone 'Used Goods app'

[Flutter] Clone - 당근마켓33(InputScreen - image picker/Getx)

이번에는 InputScreen 의 image picker 부분을 getx 로 구현해보겠습니다.

이전에는 multi_image_select.dart 에서 구현했으나 상위 화면인 InputScreen 에서도 접근이 필요해서 getx 로 구현합니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1

 

 

 

오늘 구현한 화면은 아래와 같습니다. refactorying 한 기능이 정상적으로 동작한다.

 

 

 

 

./src/router/locations.dart - Controller 추가

 

GetPage(
  name: '/input',
  page: () => const InputScreen(),
  transition: Transition.fadeIn,
  binding: BindingsBuilder((){
    Get.put(CategoryController());
    Get.put(SelectImageController());
  }),
  middlewares: [CheckAuth()], // 미들웨어를 먼저 확인(로그인 여부 확인)하고 "page:" 로 이동함
),

 

 

 

./src/states/select_image_controller.dart - Controller 생성

 

import 'dart:typed_data';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';

class SelectImageController extends GetxController {
  static SelectImageController get to => Get.find();

  final RxList<Uint8List> _images = <Uint8List>[].obs;
  RxList<Uint8List> get images => _images;

  Future setNewImages(List<XFile>? newImages) async {
    if (newImages != null && newImages.isNotEmpty) {
      _images.clear();
      for (var xFile in newImages) {
        _images.add(await xFile.readAsBytes());
      }
    }
  }

  void removeImage(int index) {
    if (_images.length >= index) {
      _images.removeAt(index);
    }
  }
}

 

 

 

./src/screens/input/multi_image_select.dart

1. Obx 로 필요한 역역 wrapping

2. SelectImageController.to 통해서 접근

 

import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';

import '../../constants/common_size.dart';
import '../../states/select_image_controller.dart';

class MultiImageSelect extends StatefulWidget {
  const MultiImageSelect({Key? key}) : super(key: key);

  @override
  State<MultiImageSelect> createState() => _MultiImageSelectState();
}

class _MultiImageSelectState extends State<MultiImageSelect> {
  bool _isPickingImages = false;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        Size _size = MediaQuery.of(context).size;
        var imgSize = (_size.width / 3) - padding_16 * 2;

        // rendering 을 다시 할 영역을 Obx 로 wrapping, 
        return Obx( ()
          => SizedBox(
            height: _size.width / 3,
            // width: _size.width,
            child: ListView(
              // 기본 스크롤 방향을 위/아래 에서 좌/우로 변경해줌,
              // 사이즈를 강제로 설정해야 함. 그래서 SizedBox 로 wrapping,
              scrollDirection: Axis.horizontal,
              children: <Widget>[
                Padding(
                  // 패딩 - 여기서는 전체를 16으로 처리해서 아래는 위/아래/오른쪽만 16 처리할 것,
                  padding: const EdgeInsets.all(padding_16),
                  child: InkWell(
                    // onTap 에서 이미지를 List<XFile> 타입 배열에 추가,
                    onTap: () async {
                      _isPickingImages = true;
                      // setState(() {}); // Obx 처리해서 생략 가능

                      final ImagePicker _picker = ImagePicker();
                      // Pick multiple images
                      final List<XFile>? images = await _picker.pickMultiImage(imageQuality: 20);
                      if (images != null && images.isNotEmpty) {
                        // 이미지 컨버팅을 로딩할때 처리함  - XFile to Uint8List 로 변경(XFile.readAsBytes)
                        // readAsBytes is for convert XFile to Uint8List
                        await SelectImageController.to.setNewImages(images);
                      }
                      _isPickingImages = false;
                      // setState(() {}); // Obx 처리해서 생략 가능
                    },
                    child: Container(
                      width: imgSize,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(padding_16),
                        border: Border.all(color: Colors.grey, width: 1),
                      ),
// 첫번째 사진 아이콘 부분 구현
                      child: _isPickingImages
                          ? const Padding(
                              padding: EdgeInsets.all(padding_16 * 2),
                              child: CircularProgressIndicator(),
                            )
                          : Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: [
                                const Icon(Icons.camera_alt_rounded, color: Colors.grey),
                                Text('0/10', style: Theme.of(context).textTheme.subtitle2),
                              ],
                            ),
                    ),
                  ),
                ),
// 중복되는 사진 부분 구현
// ... 으로 시작하면 기존 리스트에 새로운 결과(List.generate 결과)를 추가할 수 있다.
                ...List.generate(
                  SelectImageController.to.images.length,
                  (index) => Stack(
                    children: [
                      Padding(
                        // 패딩 - 위에서 전체를 16으로 처리해서 여기서는 위/아래/오른쪽만 16 처리할 것,
                        padding: const EdgeInsets.only(
                            right: padding_16, top: padding_16, bottom: padding_16),
                        // child: ExtendedImage.network(
                        // 변수(메모리)에 있는 이미지를 보여주기 위해서 ExtendedImage.memory 사용하고,
                        // 이미지를 화면에 보여주려면 컨버팅이 필요해서 FutureBuilder 로 wrapping 필요함,
                        // 이미지 컨버팅을 로딩할때 처리해서 FutureBuilder 가 필요없어서 삭제함,
                        child: ExtendedImage.memory(
                          SelectImageController.to.images[index],
                          width: imgSize,
                          height: imgSize,
                          fit: BoxFit.cover,
                          // 이미지 로딩중 각 이미지별로 인디케이터 처리함,
                          loadStateChanged: (state) {
                            switch (state.extendedImageLoadState) {
                              case LoadState.loading:
                                return const Center(child: CircularProgressIndicator());
                              case LoadState.completed:
                                return null;
                              case LoadState.failed:
                                return const Icon(Icons.cancel);
                            }
                          },
                          // shape 을 지정해야만 borderRadius 설정이 정상 동작함,
                          borderRadius: BorderRadius.circular(padding_16),
                          shape: BoxShape.rectangle,
                        ),
                      ),
                      Positioned(
                        top: 0,
                        right: 0,
                        // IconButton 의 기본 사이즈는 24 이므로 패딩 8 을 더해서 40 으로 설정,
                        width: 40,
                        height: 40,
                        child: IconButton(
                          padding: const EdgeInsets.all(8),
                          onPressed: () {
                            debugPrint('remove picture $index');
                            SelectImageController.to.removeImage(index);
                            setState(() {});
                          },
                          icon: const Icon(Icons.remove_circle),
                          color: Colors.black54,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}