ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter - 코딩마스터하기|사진 업로드,게시하기2
    개발/Flutter 2021. 11. 16. 21:09
    반응형

    horizontal scroll tags

    여기서 태그들은 라이브러리를 사용할 것이다. pub.dev에서 flutter_tags를 사용해보자. 강의는 0.4.8+2이고 나는 최신 버전인 flutter_tags: ^0.4.9+1 를 사용하였다. 그리고 share_post_screen으로 가서 Tags()를 만들어 준다. 우리는 가로 스크롤이 되게 할 것이므로 horizontalScroll을 true로 해준다. 그리고 우선 여기에 사용할 List를 상단에 만들어 준다. List<String> _tagItems로 []에 아무 단어 30개 정도를 넣어준다. 그리고 Tags에서 itemCount에 _tagItems.length를 해준다. 그리고 이 Tags의 높이 부분인 heightHorizontalScroll은 30으로 해준다. itemBuilder에 (index)=> ItemTags()를 만들어 준다. title에는 _tagItems[index], index 에는 index를 해준다. color은 선택된 아이템 상태로 Colors.red를 해주고 activeColor은 기본 상태의 색인데 grey[200]정도로 해준다. 그외에도 color의 옵션들이 여러개 있는데 직접 색을 넣어보면서 해보면 된다. borderRadius는 circular(4)로 해주고 그림자가 너무 많이 있어서 elevation은 2정도로 해주면 된다.


      List<String> _tagItems = [
        'quam',
        'elementum',
        'pulvinar',
        'etiam',
        'non',
        'quam',
        'lacus',
        'suspendisse',
        'faucibus',
        'interdum',
        'posuere',
        'lorem',
        'ipsum',
        'dolor',
        'sit',
        'amet',
        'consectetur',
        'adipiscing',
        'elit',
        'duis',
        'tristique',
        'sollicitudin',
        'nibh',
        'sit',
        'amet',
        'commodo',
        'nulla',
        'facilisi',
        'nullam',
        'vehicula',
        'ipsum',
        'a',
        'arcu',
        'cursus',
        'vitae',
        'congue',
        'mauris',
        'rhoncus',
        'aenean',
        'vel',
      ];
              Tags(
                horizontalScroll: true,
                itemCount: _tagItems.length,
                heightHorizontalScroll: 30,
                itemBuilder: (index) => ItemTags(
                  title: _tagItems[index],
                  index: index,
                  splashColor: Colors.grey[800],
                  color: Colors.red,
                  activeColor: Colors.grey[200],
                  textActiveColor: Colors.black87,
                  borderRadius: BorderRadius.circular(4),
                  elevation: 2,//떠있는 높이. 그림자가 더 많이 나옴
    
                ),
              ),

    section switch layout

    태그 부분 밑에 각 sns에 같이 올리는 스위치 section부분을 만들어 보자. 전에 만들었던 _sectionButton 부분을 복사해서 _sectionSwitch를 만들어주고 trailing 부분의 아이콘을 삭제한뒤에 CupertionoSwitch를 넣어준다. 그냥 Swtich를 써도 되지만 IOS 모양의 스위치를 넣어주려면 CupertionoSwitch를 써주면 된다. value는 true로, onChanged는 (onValue){}를 해주면 정상적으로 나온다. 그리고 이부분은 스위치가 움직여야 하므로 stateful로 변경해줘야 한다. 여기서 전체를 변경 시킬 수 있지만 _sectionSwitch의 return 부분을 flutter widget로 빼준다. 그리고 stateful 로 변경 해주면 된다. 기본 클래스에 final String title를 만들어 주고 key 부분에 this.title를 {}밖에 넣어줘서 필수로 만들어 준다. 그리고 state 클래스 네에 bool checked = false;를 만들어 주고 Text()에 title를 widget.title로 해주면 된다. CupertinoSwitch에 value에는 checked를 넣어주고 onChanged에 setState를 생성해준뒤 checked = onValue;를 넣어주면 된다. 기존 _sectionSwitch는 삭제해주고  _tags()아래 _divider를 하나 생성해 준뒤 SectionSwtich를 3개 만들어 주고 각각 Facebook, Instagram, Tumblr을 넣어준뒤 실행하면 스위치도 잘 바뀌고 정상 작동하는 것을 볼 수 있다. 태그와 스위치 사이가 너무 좁아서 _tags()와 _divider사이에 SizedBox에 height를 common_s_gap만큼 주면 된다.


          body: ListView(
            children: [
              _captionWithImage(),
              _divider,
              _sectionButton('Tag People'),
              _divider,
              _sectionButton('Add Location'),
              _tags(),
              SizedBox(height: common_s_gap,),
              _divider,
              const SectionSwitch('Facebook'),
              const SectionSwitch('Instagram'),
              const SectionSwitch('Tumblr'),
              _divider,
            ],
          ),
    class SectionSwitch extends StatefulWidget {
      final String title;
      const SectionSwitch( this.title,{
        Key key,
      }) : super(key: key);
    
      @override
      State<SectionSwitch> createState() => _SectionSwitchState();
    }
    
    class _SectionSwitchState extends State<SectionSwitch> {
      bool checked = false;
      @override
      Widget build(BuildContext context) {
        return ListTile(
          title: Text(
            widget.title,
            style: TextStyle(fontWeight: FontWeight.w400),
          ),
          trailing: CupertinoSwitch( //그냥 switch 써도 되지만 IOS 모양 쓰려면 이걸로
            value: checked,
            onChanged: (onValue){
              setState(() {
                checked = onValue;
              });
            },
          ),
          dense: true,
          contentPadding: EdgeInsets.symmetric(
            horizontal: common_gap,
          ),
        );
      }
    }

    resize image

    스토리지에 이미지를 올리기 전 이미지를 리사이즈 해보자. repo - helper 폴더에 image_helper.dart 새파일을 만들어 준다. decodeImage를 사용하기 위해서 import 'package:image/image.dart';를 해주자. 그냥 사용하려면 잘 나오지 않아 미리 import 해주자. 우선 오리지날 파일을 byte로 읽어와서 Image object로 변경해준다. 그 뒤 300x300으로 사이즈를 줄여 준다.  줄여준 파일을 기존 오리지날 파일의 경로를 읽어 온다. 이때 0부터 length-3까지 읽어온다. 이러면 사진 찍었을때 png인데 png 전까지 읽어 와주고 거기에 jpg로 해줘서 파일을 jpg로 변경해준다. 그리고 encodeJpg로 퀄리티를 50으로 낮춰준 후 File 형식으로 다시 만들어주고 리턴해 주는 방식이다.(단 문제는 이 부분이 오래 걸릴수 있음, 이부분을 하면서 프로세싱이 안멈추게 할것인가를 다음시간에 배울 것임)


    File getResizedImage(File originImage){
      Image image = decodeImage(originImage.readAsBytesSync());  //오리지날 파일을 바이트로 읽어와서 Image object 로 읽음
      Image resizedImage = copyResizeCropSquare(image, 300); //사이즈 줄임 정사각형으로 만들어줌
    
      File resizedFile =  File(originImage.path.substring(0, originImage.path.length-3)+"jpg"); // 0 처음 부터 오른쪽에서 -3 곧 png부분 제외하고 가져와서 jpg로 변경
      resizedFile.writeAsBytesSync(encodeJpg(resizedImage, quality: 50)); //퀄리티 줄임
      return resizedFile;
    }

    use isolate to resize the image

     

    repo 폴더에 image_network_repository.dart 파일을 만들어 준다. firestore 와 연결해줄 부분이다. 그 부분에서 try catch를 이용해서 할 것이다. 우선 클래스 네에 Future<void> uploadImageNCreateNewPost(File originImage)를 만들어 주고 async 해준다. 그리고 그안에 try catch를 만들어 준다. 그리고 try안에 final File resize = await compute()를 만들어 준다. compute 안에는 메소드, 파일 순서로 넣어 주면 된다. 그리고 파일크기가 얼마나 변경 되었는지 확인하기 위해 print해본다. share_post_screen에 build 의 return위에 imageNetworkRepository.uploadImageNCreateNewPost(imageFile); 를 넣어줘서 리사이즈 되게 해준다. 실행해 보면 사이즈가 줄어든 것을 알 수 있다.


    class ImageNetworkRepository {
      Future<void> uploadImageNCreateNewPost(File originImage) async {
        try {
          final File resized = await compute(
              getResizedImage, originImage); // compute는 메소드명, 거기에 들어갈 파일? 로 전달해주면됨.
          originImage
              .length()
              .then((value) => print('original image size: $value'));
          resized.length().then((value) => print('resized image size: $value'));
        } catch (e) {}
      }
    }
    
    ImageNetworkRepository imageNetworkRepository = ImageNetworkRepository();

    show loading while processing(modal bottom sheet)

    사진 프로세스를 처리하는 동안 로딩바를 보여주자. 우선 위젯 build 안에서 업로드 하던 부분을 버튼의 onPressed 안에 집어 넣어주고, image_network_repository에서 try의 가장 하단에 await Future.delayed(Duration(second:3)) 으로 프로세스 진행 후 3초정도 기다리게 해준다. 로딩바를 보여주는 방법이 여러개인데 이번에는 showModalBottomSheet를 사용해서 해보자. onPressed 안에 showModalBottomSheet를 만들어 주고 context: context,를 해준다. builder은 (_) => MyProgerssIndicator(),를 해준다. 이때 ()안에 _를 해주는데 원래는 context를 넣지만 여기선 사용을 하지 않으므로 그냥 _를 넣어서 사용해준다. 그리고 isDismissible: false로 해준다. 이것은 해당 모달창이 올라왔을때 바깥부분을 누르면 꺼지게 하는 옵션인데 로딩동안 기다려야 하므로 false를 해준다. enableDrag도 같은 이유로 false로 해준다. 로딩창이 imageNetworkRepository.uploadImageNCreateNewPost(imageFile);를 기다리고 해야 하므로 앞에 await를 해주고 onPressed에 () async {}를 해주면 된다. 그리고 창을 닫아주기 위해 await 후에 Navigator.of(context).pop();를 해준다. 실행해보면 share시에 모달창으로 로딩이 뜨고 3초 후 정도에 꺼지는 것을 알 수있다.


              FlatButton(
                onPressed: () async {
                  showModalBottomSheet(
                    context: context,
                    builder: (_) => MyProgressIndicator(),
                    //원래 빌더에 ()안에 context가 와야하는데 context를 사용 안하므로 _ 로 해준다
                    isDismissible: false,
                    //이 창의 바깥 눌렀을때 끝나게 해줄것이냐
                    enableDrag: false, //드래그를 하게 해줄 것이냐.
                  );
                  await imageNetworkRepository.uploadImageNCreateNewPost(imageFile);
                  Navigator.of(context).pop(); //await 되고 나면 pop으로 꺼짐짐
                },
                child: Text(
                  "Share",
                  textScaleFactor: 1.4,
                  style: TextStyle(
                    color: Colors.blue,
                  ),
                ),
              ),
    class ImageNetworkRepository {
      Future<void> uploadImageNCreateNewPost(File originImage) async {
        try {
          final File resized = await compute(
              getResizedImage, originImage); // compute는 메소드명, 거기에 들어갈 파일? 로 전달해주면됨.
          originImage
              .length()
              .then((value) => print('original image size: $value'));
          resized.length().then((value) => print('resized image size: $value'));
          await Future.delayed(Duration(seconds: 3,));
        } catch (e) {}
      }
    }

    upload image to firebase storage

    firebase storage에 업로드 하기위해서 라이브러리를 사용하자. 해본결과 firebase_storage: ^3.1.6 에서 정상작동하므로 우선 이 버전을 사용해 준다. 기존 try문에서  resized를 해주는 부분을 빼고 지운다. 그리고 postKey를 넘겨 주는 부분과 putFile해주는 부분을 추가해준다. share_post_screen에서 postKey를 넘겨준다.


    class ImageNetworkRepository {
      Future<StorageTaskSnapshot> uploadImageNCreateNewPost(File originImage, {@required String postKey}) async {
        try {
          final File resized = await compute(
              getResizedImage, originImage); // compute는 메소드명, 거기에 들어갈 파일? 로 전달해주면됨.
          final StorageReference storageReference = FirebaseStorage().ref().child(_getImagePathByPostKey(postKey));
          final StorageUploadTask uploadTask = storageReference.putFile(resized);
          return uploadTask.onComplete;
        } catch (e) {
          print(e);
          return null;
        }
      }
    
      String _getImagePathByPostKey(String postKey) => 'post/$postKey/post.jpg';
    }
    
    ImageNetworkRepository imageNetworkRepository = ImageNetworkRepository();

    위코드처럼 작성후 uploadImageNCreateNewPost를 넘겨주는 부분에서 postKey:postKey까지 해주고 실행하면 로딩바가 나오고 정상적으로 스토리지에 폴더가 생성된것을 볼 수 있다. 안을 확인하면 업로드가 된것을 볼 수 있다.

     

    get image download link and show the image

    업로드 된 이미지를 앱으로 가져와서 보여줘 보자. image_network_repository.dart에서 하단에 메소드를 하나 만들어 주자. Future<dynamic>으로 getPostImageUrl(String postKey) 라는 메소드를 만들자. 다운로드 Url을 가져오는 메소드 이다. return에 FirebaseStorage().ref().child(_getImagePathByPostKey(postKey)).getDownloadURL();로 다운로드 경로를 가져오면 된다. 이제 이 부분을 post.dart에서 _postImage()에서 불러오면 된다. 우선 그것을 위해서는 CachedNetworkImage를 StreamBuilder로 감싸준다. 그리고 StreamBuilder을 FutureBuilder<dynamic>으로 변경해주고 stream 옵션을 future로 변경해준다. 그리고 CachedNetworkImage였던 위젯을 Widget로 변경해준다. future 부분에 imageNetworkRepository.getPostImageUrl('이미지가 저장된 postKey'),를 해줘서 Future<dynamic>의 값을 가져온다. 그리고 builder안에 if(snapshot.hasData)일때 기존 return 부분을 다 넣어주고, else 에 MyProgressIndicator를 return해준다. 여기서 로딩 부분이 겹치므로 위젯의 상단에 Widget progress =  MyProgressIndicator( containerSize: size.width, );를 만들어 주고 CachedNetworkImage의 placeholder안의 return에 progress와 방금 만들었던 if문의 else에 return에 progress를 넣어주면 된다. 그럼 정상적으로 피드화면에 방금찍은 사진이 보이는것이 보인다.


     

     

    post.dart
      Widget _postImage() {
        Widget progress = MyProgressIndicator(
          containerSize: size.width,
        );
        return FutureBuilder<dynamic>(
          future: imageNetworkRepository
              .getPostImageUrl('1637063370169_QMaZBYr2CwfIH3i8ykqLSBWG83l2'),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return CachedNetworkImage(
                //flutter의 이미지는 캐시를 안함 창이 바뀔때마다 새로 불러옴. 그래서 저장해놓은 이미지를 쓰기 위해서 이걸 씀.
                imageUrl: snapshot.data.toString(),
                placeholder: (BuildContext context, String url) {
                  return progress;
                },
                imageBuilder: (BuildContext context, ImageProvider imageProvider) {
                  return AspectRatio(
                    aspectRatio: 1,
                    child: Container(
                      decoration: BoxDecoration(
                        image: DecorationImage(
                          image: imageProvider,
                          fit: BoxFit.cover,
                        ),
                      ),
                    ),
                  );
                },
              );
            } else {
              return progress;
            }
          },
        );
      }​
    image_network_repository.dart
    class ImageNetworkRepository {
      Future<StorageTaskSnapshot> uploadImageNCreateNewPost(File originImage, {@required String postKey}) async {
        try {
          final File resized = await compute(
              getResizedImage, originImage); // compute는 메소드명, 거기에 들어갈 파일? 로 전달해주면됨.
          final StorageReference storageReference = FirebaseStorage().ref().child(_getImagePathByPostKey(postKey));
          final StorageUploadTask uploadTask = storageReference.putFile(resized);
          return uploadTask.onComplete;
        } catch (e) {
          print(e);
          return null;
        }
      }
    
      String _getImagePathByPostKey(String postKey) => 'post/$postKey/post.jpg';
    
    
      Future<dynamic> getPostImageUrl(String postKey){
        return FirebaseStorage().ref().child(_getImagePathByPostKey(postKey)).getDownloadURL();
      }
    }
    
    ImageNetworkRepository imageNetworkRepository = ImageNetworkRepository();

    다음시간엔 storage에 저장한 post 데이터를 데이터베이스에 저장하는법을 해보자.

    반응형

    댓글

Designed by Tistory.