ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter - 코딩마스터하기|피드스크린 만들기2
    개발/Flutter 2021. 10. 26. 17:19
    반응형

    온라인 이미지 보여주기

    asset 를 이용하는게 아닌 온라인 이미지를 보여주기

    post의 Container를 CachedNetworkImage를 사용하는데 내장함수가 아니라 라이브러리를 가져와야 한다. pub.dev 에서 cached_network_image 를 찾아서 installing에서 dependencies 하단의 부분을 복사해서 pubspec.yaml에 dependencies 부분에 붙여넣어준다. 강의에선 nullsafety전이라 2.2.0+1을 사용하지기 때문에 이 버전을 사용해보자(만일 null safety가 나온다면 pubspec.yaml의 상단에 sdk의 2.12.0을 2.7.0으로 바꿔본다. 2.70으로 낮추면 다른 dart 파일들의 Key? key에서 ?들을 제거해줘야 한다.). 붙여넣은 후 pub get을 하고 Process finished with exit code 0 가 나오면 정상적으로 설치된것을 확인 할 수 있다. CachedNetworkImage의 imageUrl에 https://picsum.photos/id/237/200/300 의 랜덤 이미지를 사용한다. id뒤의 숫자는 $index를 줘서 사진이 다 다르게 표현 해주면 된다. 만약 핫 리로드로 이상하게 나온다면 정지 시킨후 재실행 하자. imageBuilder을 이용해 이미지가 차지하는 부분을 꽉차게 해보자. (BuildContext context, ImageProvider imageProvier){}를 주면 된다. return으론 AspectRatio를 주면 된다. AspectRaio는 비율로 이미지를 준다. aspectRatio에 1을 준다. (가로)/(세로)비율이다. child에 Container을 주고 그 안에 BoxDecoration, 그 안의 image 옵션에 DecorationImage를 줘서 image에 imageProvider를 준다. 그리고 DecorationImage 의 fit은 BoxFit.cover을 준다. 다른 옵션을 줘서 확인해보는것도 좋다. 각각의 비율이 있다.


    class Post extends StatelessWidget {
      final int index;
    
      const Post(
        this.index, {
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return CachedNetworkImage(
          //flutter의 이미지는 캐시를 안함 창이 바뀔때마다 새로 불러옴. 그래서 저장해놓은 이미지를 쓰기 위해서 이걸 씀.
          imageUrl: "https://picsum.photos/id/$index/200/300",
          imageBuilder: (BuildContext context, ImageProvider imageProvider) {
            return AspectRatio(
              aspectRatio: 1,
              child: Container(
                decoration: BoxDecoration(
                  image: DecorationImage(
                    image: imageProvider,
                    fit: BoxFit.cover,
                  ),
                ),
              ),
            );
          },
        );
        // return Container(
        //   color: Colors.accents[index % Colors.accents.length],
        //   //Colors.accents 이게 리스트로 되어있음
        //   height: 100,
        // );
      }
    }

    로딩위젯 만들기

    placeholder를 이용해서 이미지 로딩 보여주기

    placeholder 은 (BuildContext context, String url){}을 받는다. CircularProgressIndicator를 return에 주고 height, width를 60씩 주고 color도 black로 준다. 그 걸 SizedBox로 감싸고 또 이걸 Center로 감싸고 또 Container로 감싼후 Container에 width와 height를 준다. 가장 상단에 Size size;를 준 후 위젯 안에 size에 MediaQuery.of(context).size를 줘서 스마트폰의 사이즈를 구한 뒤 height와 width에 size.width로 1대1 비율의 로딩화면을 만들어 주면 된다. 이 때 const Post에 빨간줄이 뜨는데 const를 제거해 준다. constant로 변하지 않는다는 뜻인데 size가 값이 변하므로 없애준다.

    (그런데 노란줄이 뜨는데 Container을 SizedBox로 변경해주고, Center에 const를 붙여주면 없어진다. 이부분 참조)


    class Post extends StatelessWidget {
      final int index;
      Size size;
    
      Post(
        this.index, {
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        // size ??= MediaQuery.of(context).size; 밑의 식과 동일
        if (size == null) {
          size = MediaQuery.of(context).size;
        }
        return CachedNetworkImage(
          //flutter의 이미지는 캐시를 안함 창이 바뀔때마다 새로 불러옴. 그래서 저장해놓은 이미지를 쓰기 위해서 이걸 씀.
          imageUrl: "https://picsum.photos/id/$index/5000/5000",
          placeholder: (BuildContext context, String url) {
            return Container(
              width: size.width,
              height: size.width,
              child: Center(
                child: SizedBox(
                  child: CircularProgressIndicator(
                    backgroundColor: Colors.black87,
                  ),
                  height: 60,
                  width: 60,
                ),
              ),
            );
          },
          imageBuilder: (BuildContext context, ImageProvider imageProvider) {
            return AspectRatio(
              aspectRatio: 1,
              child: Container(
                decoration: BoxDecoration(
                  image: DecorationImage(
                    image: imageProvider,
                    fit: BoxFit.cover,
                  ),
                ),
              ),
            );
          },
        );
        // return Container(
        //   color: Colors.accents[index % Colors.accents.length],
        //   //Colors.accents 이게 리스트로 되어있음
        //   height: 100,
        // );
      }
    }

    custom progress indicator 

    커스텀 로딩 위젯 만들기

    widgets폴더에 my_progress_indicator.dart를 만들어 주고 클래스 명을 MyProgressIndicator로 해준다. 그리고 size들을 final double로 containerSize와 progressSize를 만들어 준다. containerSize는 Container에 height와 width에, progressSize는 SizedBox에 넣어주고 CircularProgrssIndicator를 없애주고 gif 로딩이미지를 받아서 넣어준다. 그리고 post에서 MyProgressIndicator의 containerSize = size.width로 해주면 된다. 정상적으로 커스텀된걸 알 수 있다.


    class MyProgressIndicator extends StatelessWidget {
    
      final double containerSize;
      final double progressSize;
    
      const MyProgressIndicator({Key key, this.containerSize, this.progressSize=60}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          width: containerSize,
          height: containerSize,
          child: Center(
            child: SizedBox(
              child: Image.asset("assets/images/loading_img.gif"),
              height: progressSize,
              width: progressSize,
            ),
          ),
        );
      }
    }

    post header

    기존 CachedNetworkImage 는 Column으로 감싸준후 method로 refactor해주고 _postImage로 만들어 준다. 그리고 children의 _postImage() 위해 _postHeader()을 만들어 준다. 그 안에 CachedNetworkImage, Text, IconButton을 만들어 준다. 이미지는 프사로 width, height 30을 주고 text는 username, icon은 more_horiz로 준다. 이때 이미지가 네모낳게 나오는데 ClipOval로 감싸주면 동그랗게 된다. CircleAvatar로 해도 된다. 이 부분이 너무 붙어 있으므로 Padding로 감싸고 8로 준다. 그리고 아이콘이 가장 오른쪽으로 보내도 되지만 Text부분을 공간을 다 차지하면 아이콘이 가장 오른쪽으로 가므로 이 방법으로 한다. 그리고 사이즈나 갭이 다 따로 있는데 이 부분을 한번에 변경하고 관리하기 위해서 위젯으로 만들어 줘도 된다. lib에 constants 폴더를 만들어 주고 common_size.dart를 만들어 준후 const double로 common_gap = 14.0; common_xxs_gap = 8.0; avatar_size = 30.0;의 사이즈를 준다. 다른 사이즈들도 추가 해준뒤 Padding 나 다른 사이즈가 들어가는 부분을 바꿔주면 된다.


    const double common_gap = 14.0;
    const double common_xxs_gap = 8.0;
    
    const double avatar_size = 30.0;
    class Post extends StatelessWidget {
      final int index;
      Size size;
    
      Post(
        this.index, {
        Key key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        // size ??= MediaQuery.of(context).size; 밑의 식과 동일
        if (size == null) {
          size = MediaQuery.of(context).size;
        }
        return Column(
          children: [
            _postHeader(),
            _postImage(),
          ],
        );
        // return Container(
        //   color: Colors.accents[index % Colors.accents.length],
        //   //Colors.accents 이게 리스트로 되어있음
        //   height: 100,
        // );
      }
    
      Widget _postHeader() {
        return Row(
          children: [
            Padding(
              padding: const EdgeInsets.all(common_xxs_gap),
              child: ClipOval(
                child: CachedNetworkImage(
                  imageUrl: "https://picsum.photos/id/$index/500/500",
                  width: avatar_size,
                  height: avatar_size,
                ),
              ),
            ),
            Expanded(
              child: Text('Username'),
            ),
            IconButton(
              onPressed: () {},
              icon: Icon(
                Icons.more_horiz,
                color: Colors.black87,
              ),
            ),
          ],
        );
      }
    
      CachedNetworkImage _postImage() {
        return CachedNetworkImage(
          //flutter의 이미지는 캐시를 안함 창이 바뀔때마다 새로 불러옴. 그래서 저장해놓은 이미지를 쓰기 위해서 이걸 씀.
          imageUrl: "https://picsum.photos/id/$index/1000/1000",
          placeholder: (BuildContext context, String url) {
            return MyProgressIndicator(
              containerSize: size.width,
            );
          },
          imageBuilder: (BuildContext context, ImageProvider imageProvider) {
            return AspectRatio(
              aspectRatio: 1,
              child: Container(
                decoration: BoxDecoration(
                  image: DecorationImage(
                    image: imageProvider,
                    fit: BoxFit.cover,
                  ),
                ),
              ),
            );
          },
        );
      }
    }

    extract 위젯

    ClipOval 부분을 refactor 해서 RoundedAvatar의 Widget로 만들어준다. 그리고 widgets 폴더에 rounded_avatar.dart 파일을 생성한 뒤 붙여넣어 주고 빨간줄 부분을 모두 import 등으로 해결해주면 된다.


      Widget _postHeader() {
        return Row(
          children: [
            Padding(
              padding: const EdgeInsets.all(common_xxs_gap),
              child: RoundedAvatar(index: index),
            ),
            Expanded(
              child: Text('Username'),
            ),
            IconButton(
              onPressed: () {},
              icon: Icon(
                Icons.more_horiz,
                color: Colors.black87,
              ),
            ),
          ],
        );
      }
    class RoundedAvatar extends StatelessWidget {
      const RoundedAvatar({
        Key key,
        @required this.index,
      }) : super(key: key);
    
      final int index;
    
      @override
      Widget build(BuildContext context) {
        return ClipOval(
          child: CachedNetworkImage(
            imageUrl: "https://picsum.photos/id/$index/500/500",
            width: avatar_size,
            height: avatar_size,
          ),
        );
      }
    }

    post actions button

    post의 _postImage() 하단에 Row를 만들어 준다. 그리고 그안에 IconButton을 만들어 준다 icon에 ImageIcon으로 만들어 주고 그 안에 AssetImage로 만들어서 복사로 4개를 만들어 준다. 인스타그램은 3개가 왼쪽, 1개가 오른쪽으로 나뉘어져있는데 그 사이에 Spacer()을 해주면 사이의 공간을 차지해서 한개가 오른쪽으로 간다. 그리고 AssetImage에는 각각 bookmark, comment, direct_message, heart_selected로 채워주면 된다. 색은 전부 black87로 해준다. 이걸 refactor로 method로 _postActions로 만들어 주면 된다.


      Row _postActions() {
        return Row(
            children: [
              IconButton(
                onPressed: () {},
                icon: ImageIcon(
                  AssetImage('assets/images/bookmark.png'),
                ),
                color: Colors.black87,
              ),
              IconButton(
                onPressed: () {},
                icon: ImageIcon(
                  AssetImage('assets/images/comment.png'),
                ),
                color: Colors.black87,
              ),
              IconButton(
                onPressed: () {},
                icon: ImageIcon(
                  AssetImage('assets/images/direct_message.png'),
                ),
                color: Colors.black87,
              ),
              Spacer(),
              IconButton(
                onPressed: () {},
                icon: ImageIcon(
                  AssetImage('assets/images/heart_selected.png'),
                ),
                color: Colors.black87,
              ),
            ],
          );
      }

    num of likes for post

    좋아요 숫자 표시

    우선 _postActions() 하단에 Text를 만들어서 확인을 해보자. 10000 likes라 해주고 style을 bold로 지정해주면 가운데 정렬인것을 알 수 있다. 이를 수정하기 위해서 Column에 crossAxisAlignment 옵션에서 CrossAxisAlignment.start 를 주게 되면 왼쪽 정렬이 되는 것을 알 수 있다. 그리고 Padding로 감싸주고 all이 아닌 only에서 left 옵션을 줘서 common_gap을 주면 된다.


            Padding(
              padding: const EdgeInsets.only(left: common_gap),
              child: Text(
                '12300 likes',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),

    caption/comment widget 생성

    인스타를 잘 보면 ID 부분과 Comment 부분의 스타일이 다르다. flutter에서 다른 스타일을 적용하는 Text가 있는데 바로 RichText이다. RichText의 text에 TextSpan을 넣어주고 그 안에 children에 다시 TextSpan 여러개를 써서 다르게 표현하는 방식으로 할 것이다. children안에 2개의 TextSpan을 해주고 첫번째는 username111, 두번째는 comment를 달아준다. 그리고 첫번째 TextSpan에 style을 bold로 준다. 그리고 실행해보면 글씨가 보이지 않는데 그 이유는 TextSpan은 main에 primarySwatch 의 색을 따라가기 때문이다. 그래서 각각의 스타일에 색을 주면 된다. 그리고 아이디와 코멘트 사이에 공간이 없는데 패딩을 줘도 되지만 TextSpan(' ')으로 빈 공간으로 주는 방식으로 해도 된다. 그리고 RichText가 너무 붙어있는데 Padding으로 감싼 뒤, 이번에는 symmetric으로 준 후 그 안에 horizontal은 common_gap으로 vertical은 common_xxs_gap으로 주면 된다. 그리고 commet를 누르면 코멘트만 있는 상세 페이지로 들어가는데 이 부분이 같이 쓰이므로 위젯으로 만들어 줄것이다. 우선 상세부분에 대한 것까지 만들어야 한다. 코멘트 밑에 작성시간이 얼마나 됐는지가 나오는데 그러기 위해선 Column으로 감싸준다. 그리고 RichText 하단에 Text로 만들고 아무 글자나 넣은뒤 style에 grey[500], fontsize를 10으로 주면 된다. 그리고 중앙 정렬로 되어있기 때문에, crossAxisAlignment를 start로 준다. 그리고 프사 부분이 좌측에 있기 때문에 Column을 다시 한번 Row로 감싸준다. 그리고 전에 만들었던 RpimdedAvatar를 Column 위에 해준다. 사이에 공간을 주기 위해 SizedBox를 사이에다가 넣어주고 width를 common_xxs_gap만큼 준다. 그런데 프사가 너무 크므로 이부분을 조절 할 수 있게 해줘야 한다. rounded_avatar.dart로 가서 상단에 final double size; 를 만들어 준 후 기본 값으로 avatar_size를 만들어준다.(여기 강의에선 프사를 똑같은 이미지로 해서 index가 없음. 그러나 나는 index를 줬고 required로까지 지정했음. 없으면 이미지가 작동 안하므로. 이 부분 참조해서 하면 될듯.) 그리고 post의 RoundedAvatar에 size를 24로 입력해주면 정상 작동 하는 것을 알 수 있다. 그리고 이제 상세 페이지가 아닐 시 안보여주는 부분을 만들어 보자. comment 파일에서 final bool로 showImage를 만들어 주고 기본값으로 true를 넣는다. 그리고 RoundedAvatar를 if문으로 제어해주면 된다. 그런데 Row안에서 if문에 {}를 쓰면 빨간줄이 뜬다. 여기서는 들여쓰기 부분을 맞춰주면 된다. if문과 Column을 같은 위치에 놓고, if문 안에서 작동해야할 RoundedAvatar과 SizedBox를 한칸 들여써주면 된다. post에서 _postCaption에서 Comment에 showImage를 false로 하면 이미지만 안보이는 것을 알 수 있다. 그리고 comment에서 String username, String text, DateTiem dateTime 를 추가해준다. 그리고 필수인 username과 text에 @required를 달아준 후, post에 필수 옵션을 채워주면 된다. 그리고 datetime도 if문으로 not null 일때 보여준다. 'timestamp' 부분을  dateTime.toIso8601String()로 변경해준다.


    class Comment extends StatelessWidget {
      final int index;
      final bool showImage;
      final String username;
      final String text;
      final DateTime dateTime;
    
      const Comment({
        Key key,
        @required this.index,
        this.showImage = true,
        @required this.username,
        @required this.text,
        this.dateTime,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Row(
          children: [
            if (showImage)
              RoundedAvatar(
                index: index,
                size: 24,
              ),
            SizedBox(
              width: common_xxs_gap,
            ),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                RichText(
                  text: TextSpan(
                    children: [
                      TextSpan(
                        text: username,
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          color: Colors.black87,
                        ),
                      ),
                      TextSpan(
                        text: '  ',
                      ),
                      TextSpan(
                        text: text,
                        style: TextStyle(
                          color: Colors.black87,
                        ),
                      ),
                    ],
                  ),
                ),
                if (dateTime != null)
                  Text(
                    dateTime.toIso8601String(),
                    style: TextStyle(color: Colors.grey[500], fontSize: 10),
                  ),
              ],
            ),
          ],
        );
      }
    }
      Widget _postCaption() {
        return Padding(
          padding: const EdgeInsets.symmetric(
              horizontal: common_gap, vertical: common_xxs_gap),
          child: Comment(
            index: index,
            showImage: false,
            text: 'I love myself!!!',
            username: 'testing user',
          ),
        );
      }
    class RoundedAvatar extends StatelessWidget {
    
      final double size;
    
      const RoundedAvatar({
        Key key,this.size = avatar_size,
        @required this.index,
      }) : super(key: key);
    
      final int index;
    
      @override
      Widget build(BuildContext context) {
        return ClipOval(
          child: CachedNetworkImage(
            imageUrl: "https://picsum.photos/id/$index/500/500",
            width: size,
            height: size,
          ),
        );
      }
    }

     

    반응형

    댓글

Designed by Tistory.