개발/Flutter

Flutter - 코딩마스터하기|사진 업로드,게시하기2

ffuny 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 데이터를 데이터베이스에 저장하는법을 해보자.

반응형