ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter - 인별 클론 코딩 V1.0, 피드화면
    개발/Flutter 2021. 10. 1. 21:40
    반응형

    1. 일단 각 네비게이션 버튼을 누르면 앱바은 이름이 달라짐. 우선 이걸 먼저 할 것임.

    기존 AppBar를 삭제하면 삭제하고도 정상 작동하는것을 알 수 있다. 이제 화면에 따른 각각의 Appbar를 넣어주는데 각 Container에 Appbar를 넣어줄 것이다. lib에 새 Package를 Screens로 추가 해 준다. feed_page.dart를 새로 만들어 준다. Feed Page는 stateless로 만들어 줄 것이다.

    FeedPage라는 stl class를 만들어 준후 return을 Scaffold로 변경하고 Appbar를 추가한 후, AppBar에 title와 actions로 이름와 이미지를 추가해 준다. 정상적으로 작동을 확인.

    body에 List를 줄것이다. ListView를 사용 할 것이다. ListView.builder로 만든다. itemBuilder 옵션에서 context와  index를 이용하여 container의 색을다르게 나타 낼 수 있다. itemcount로 갯수 제한을 할 수 있다.

    Tip. Ctrl + Alt + L을 누르면 코드 정리가 된다.


          body: ListView.builder(
              itemCount: 15,
              itemBuilder: (BuildContext context, int index) {
                return Container(
                  height: 300,
                  color: Colors.primaries[index % Colors.primaries.length],
                );
              }),

    AppBar를 완전히 정리 하고 가보자. 일단 Icon을 IconButton으로 감싸준다.

    onPressed기능은 아직은 null로 두고 ImageIcon에서 color을 변경 해준다.


              IconButton(
                icon: ImageIcon(
                  AssetImage('assets/actionbar_camera.png'),
                  color: Colors.black,
                ),
                onPressed: null,
              ),
              IconButton(
                icon: ImageIcon(
                  AssetImage('assets/direct_message.png'),
                  color: Colors.black,
                ),
                onPressed: null,
              ),

    instagram 글씨 앞에 아이콘은 AppBar에 leading라는 옵션에서 추가해 줄 수 있다. 일단 IconButton을 복사해서 붙여 넣고 title도 logo를 넣으면 된다. Image.assets로 추가해 주면 된다. 그러나 크기가 너무 크거나 작으면 조절 해주면된다. Image.asset의 height 옵션을 주면 된다.


            leading: IconButton(
              icon: ImageIcon(
                AssetImage('assets/actionbar_camera.png'),
                color: Colors.black,
              ),
              onPressed: null,
            ),
            title: Image.asset('assets/insta_text_logo.png', height: 30,),

    그리고 AppBar의 배경색을 변경 해보자. MaterialApp에는 theme 옵션을 줄 수 있다. ThemeData에는 primarySwatch로 색을 줄 수 있다. 기존 처럼 Colors.white로 컬러를 주면 에러가 난다. primarySwatch는 MaterialColor이기 때문이다. red는 MaterialColor이다. 근데 하얀색으로 주고 싶다면 Colors에 들어가서 red 처럼 만들어 주면 된다.


    const MaterialColor white = MaterialColor(
      0xFFFFFFFF,
      const <int, Color>{
        50: Color(0x0FFFFFFF),
        100: Color(0x1FFFFFFF),
        200: Color(0x2FFFFFFF),
        300: Color(0x3FFFFFFF),
        400: Color(0x4FFFFFFF),
        500: Color(0x5FFFFFFF),
        600: Color(0x6FFFFFFF),
        700: Color(0x7FFFFFFF),
        800: Color(0x8FFFFFFF),
        900: Color(0x9FFFFFFF),
      },
    );

    전체 프로젝트 내에서 사용해야 하기 때문에 lib 에 constants 패키지를 만들어 주고  material_white_color.dart를 만들고 그 안에 위 코드를 넣어서 import 해주면 사용 할 수 있다.


        return MaterialApp(
          home: MainPage(),
          theme: ThemeData(
            primaryColor: white,
          ),
        );

    레이아웃 디자인 하기

    flutter는 column과 row로 다 만들 수 있음. 만들고자 하는걸 나눠서 생각 하면됨. 보통 모바일은 세로로 많이 보므로 column으로 먼저 나누고 거기서 row 로 세부적으로 나눠서 하면 됨.

    캐시네트워크이미지 - 인터넷 이미지 사용

    우선 column 으로 묶어 볼 수 있음. childeren안에 Image.network로 이미지를 사용할 수 있지만 이것의 단점은 페이지를 옮겨서 다시 돌아오면 이미지를 다시 다운받음. 데이터나 앱에서 좋지 않음. 캐싱해서 쓰는게 좋은데 그러면 다른 걸로 써야함. 그 라이브러리가 cached_network_image를 쓰면됨. 2.5.1이 null safety 하지 않은 최신버전이라 이것을 사용 할 것임. pubspec.yaml cached_network_image: ^2.5.1을 넣어 줌. 만약 안되면 1.1.1 강의 버전으로 하면됨. children 안에 CachedNetworkImage(imageUrl: 'https://picsum.photos/id/$index/200/200'), 를 넣어주면 됨.

    2.1.0+1 버전으로 정상작동 확인함.

    근데 이미지가 꽉 안차고 좌우 공간이 남음. imageBuilder에서 할 수 있음. Url은 어딨는지 알려줌. Builder을 안주면 기본 값으로 줌. 우리가 필요한대로 Builder을 만들어서 사용할 수 있음. 이미지는 imageProvider를 통해서 받아옴

    decoration을 통해서 할 수 있음. BoxDecoration에서 image옵션에 DecorationImage에 imageProvider를 해서 나타내면 됨.fit을 통해서 어떻게 채워서 보여줄 수 있음. BoxFit.cover로 가득 채워서 보여줌. 이렇게만 하면 이미지가 안보임. Container로 해서 나타나지 않음. AspectRatio로 Container을 감싸줘야함. 그리고 옵션으로 aspectRatio: (가로)/(세로) 비율로 이미지를 보여줄 수 있음. 코드가 너무 길어지니 메소드로 리팩터해줌. column도 메소드로 리팩터해주면 보기가 수월해짐.


    class FeedPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            leading: IconButton(
              icon: ImageIcon(
                AssetImage('assets/actionbar_camera.png'),
                color: Colors.black,
              ),
              onPressed: null,
            ),
            title: Image.asset(
              'assets/insta_text_logo.png',
              height: 30,
            ),
            //text대신 이미지로 할 수 있음 높이 조절로 크기 조절 가능
            actions: [
              IconButton(
                //여기서 색깔 바꿔도 안바뀜, ImageIcon에서 바꿔야함
                icon: ImageIcon(
                  AssetImage('assets/actionbar_camera.png'),
                  color: Colors.black,
                ),
                onPressed: null,
              ),
              IconButton(
                icon: ImageIcon(
                  AssetImage('assets/direct_message.png'),
                  color: Colors.black,
                ),
                onPressed: null,
              ),
            ],
          ),
          body: ListView.builder(
              itemCount: 15,
              itemBuilder: (BuildContext context, int index) {
                return _feedItem(index);
              }),
        );
      }
    
      Column _feedItem(int index) {
        return Column(
                children: [
                  //Image.network('https://picsum.photos/id/237/200/200'), //이걸로 이미지를 받아와서 사용할 수 있음.그러나 다른페이지 이동후 다시 페이지 돌아오면 다시 이미지를 받음 캐싱이 안됨. 매우 단점
                  _feedImage(index),
                  //이미지 캐싱해서 사용
                ],
              );
      }
    
      CachedNetworkImage _feedImage(int index) {
        return CachedNetworkImage(
                    imageUrl: 'https://picsum.photos/id/$index/200/200',
                    imageBuilder:
                        (BuildContext context, ImageProvider imageProvider) =>
                            AspectRatio(
                              aspectRatio: 1/1,// 1/1로 비율 지정해줌. 가로/세로 AspectRatio없이 Container만 하면 화면이 안나타남
                              child: Container(
                      decoration: BoxDecoration(
                        image: DecorationImage(
                              image: imageProvider,// 이미지가 imageProvider를 통해서 받아옴.
                              fit: BoxFit.cover),
                      ),
                    ),
                            ),
                  );
      }
    }

    feed_page 현재까지 코드


    Feed Header 레이아웃 만들기

    Row로 만들 것임. 프사, 아이디, 글옵션 순서임. 위에서 메소드로 나눈 부분에서 이미지 위에 헤더를 만들면 된다. CircleAvatar로 프로필 사진을 만든다. backgroundImage 옵션을 사용하는데 imageProvider를 받으므로 cachedNetworkImageProvider를 사용해서 만들어보면 된다. 이름은 그냥 Text를 사용하고 글 옵션은 버튼이어야 아이콘에 버튼이므로 IconButton을 사용하면 다음과 같은 코드가 나온다.


            Row(
              children: [
                CircleAvatar(
                  backgroundImage: CachedNetworkImageProvider(
                      'https://picsum.photos/id/$index/50/50'),
                ),
                Text('username'),
                IconButton(
                  onPressed: null,
                  icon: Icon(
                    Icons.more_horiz,
                    color: Colors.black87,
                  ),
                )
              ],
            )

    서클아바타랑 너무 붙어있어서 패딩으로 감싸주고 padding 옵션에 EdgeInsets.all()로 패딩을 준다. 서클아바타의 크기도 radius옵션으로 크기를 정해준다. 그리고 유저 네임이 공간을 차지하게 하기 위해서 Expanded로 감싸준다. 이를 확인하는 방법은 안드로이드 스튜드오 우측에 Flutter Inspector을 누른후 마우스 모양을 누르고 원하는 위치를 에뮬레이터에서 클릭하면 범위를 볼 수 있다. 이제 헤더부분은 완성이므로 메소드로 리팩터 해주면 된다. 해당부분은 username으로 받아오는데 index를 안받아와서 이미지를 가져올수 없다.그래서 추가로 파일을 만들어 주는데 lib에 utils를 만들고 그 안에 profile_img_path.dart를 만들어 준후 다음 코드를 넣는다.


    import 'dart:convert';
    //이미지를 username에따라 받아 오는것
    String getProfileImgPath(String username){
      final encoder = AsciiEncoder();
      List<int> codes = encoder.convert(username);
      int sum = 0;
      codes.forEach((code) => sum += code);
    
      final imgNum = sum%1000;
    
      return "https://picsum.photos/id/$imgNum/30/30";
    }

    유저 네임을 아스키코드로 변환시켜서 각 글자별 숫자를 더해준후 1000의 나머지값을 이미지 주소에 넣어서 가져오는 파일이다. feed_page의 이미지 가져오는 부분도 바꿔주면 된다.


              Padding(
                padding: EdgeInsets.all(14),
                child: CircleAvatar(
                  backgroundImage: CachedNetworkImageProvider(
                      getProfileImgPath(username),),
                  radius: 16,
                ),
              ),
              Expanded(
                child: Text(username),
              ),
              IconButton(
                onPressed: null,
                icon: Icon(
                  Icons.more_horiz,
                  color: Colors.black87,
                ),
              ),

    Feed의 사진 아래 버튼 action 부분

    피드에서 이미지 밑에 버튼들 부분. IconButton 안에 ImageIcon으로 해줌. 그리고 Expanded대신 Space를 통해서 공간 차지 가능, 각 위치마다 넣으면 비율적으로 공간 차지. flex 옵션으로 비율을 얼마나 할지도 가능, 메소드로 빼줌


      Row _postActions() {
        return Row(
            children: [
              IconButton(
                onPressed: null,
                icon: ImageIcon(
                  AssetImage('assets/heart.png'),
                  color: Colors.black87,
                ),
              ),
              IconButton(
                onPressed: null,
                icon: ImageIcon(
                  AssetImage('assets/comment.png'),
                  color: Colors.black87,
                ),
              ),
              IconButton(
                onPressed: null,
                icon: ImageIcon(
                  AssetImage('assets/direct_message.png'),
                  color: Colors.black87,
                ),
              ),
              Spacer(
                //flex: 2, 비율 조절 가능
              ),//남은 공간을 차지함, 자동으로 비율을 해줌
              IconButton(
                onPressed: null,
                icon: ImageIcon(
                  AssetImage('assets/bookmark.png'),
                  color: Colors.black87,
                ),
              ),
            ],
          );
      }

    버튼밑에 몇명이 좋아요 눌렀나 부분

    바로 Text를 넣으면 가운데 정렬이됨. Column에서 corssAxisAlignment 옵션으로 좌측 정렬 가능. CrossAxisAlignment.start로 왼쪽 end로 오른쪽 가능. 그러나 너무 좌측에 붙어있으면 text를 padding로 감싸서 패딩을 주면 됨. EdgeInsets.only에서 옵션으로 원하는 위치만 줄 수 있음. 그리고 이부분이 볼드체인데 style 옵션에서 TextStyle에서 fontweight에 FontWeight.bold로 해줄수 있음.

    그리고 페이지가 여러개인데 패딩이나 radius를 다 바꿀때 힘듦. 그래서 이를 위해 상수로 세팅. constants 폴더에 size.dart를 만든 후 다음을 추가.


    const double common_xs_gap = 10.0;
    const double common_s_gap = 12.0;
    const double common_gap = 14.0;
    const double common_l_gap = 16.0;
    const double profile_radius = 16.0;

    그리고 해당하는 부분을 코드로 바꿔주면 됨. 앱에서 전체적인 변경이 가능해짐.

    다음은 캡션 부분. 댓글같은게 달린 부분.

    아이디 부분과 댓글 내용 부분이 다른데 이 부분을 나누면 댓글이 길어지면 이상해짐. 이를 해결 할 수 있는 것이 RichText임. text 옵션 (InlineSpan을 씀)은 그냥 text를 못씀. textSpan으로 쓸 수 가 있음. children이 있는데 이것도 리스트로 InlineSpan을 받음. 그래서 거기에 TextSpan두개를 해서 나누면 됨. style도 지정해줌. 그리고 현재 앱상태를 알기 위해 context를 받아온다. 기본 스타일을 그대로 유지 하기 위해 DefaultTextStyle를 사용하고 context로 현재 앱상태것을 그대로 사용한다. 아이디와 댓글 사이에 빈공간은 children에서는 TextSapn밖에 못써서 TextSpan에 text: ' '로 해주면 된다. RichText에는 패딩이 없어서 패딩으로 RichText를 감싸주고 common_gap만큼 패딩을 준다. 


            child: RichText(
              text: TextSpan(
                style: DefaultTextStyle.of(context).style,
                children: <TextSpan>[
                  TextSpan(
                    text: 'username $index',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  TextSpan(
                    text: ' ', //여기는 TextSpan 밖에 못넣어서 빈공간을 이렇게 해줘야함
                  ),
                  TextSpan(
                    text:
                        'I am Missing summer soooooooooooooooooooooooooooooooooooooo much~~!',
                  ),
                ],
              ),
            ),

    Comment 부분 위젯 생성.

    기존 만들었던거에서 댓글이 많으면 들어가는 부분 생성. 기존 피드에서는 그냥 보여주고 댓글 더보기 누르면 댓글로 가서 프사랑 좋아요 누르고 다 가능하게. widgets 패키지 만들고 comment 파일 추가. stl로 만들어줌. CircleAvatar와 Column 안에 RichText에 TextSpan으로 기존 코멘트 만든부분을 가져와서 쓰면 된다. 그리고 날짜 부분은 Text로 추가 해주면 된다. 위젯으로 만들어서 쓰는 것이기 때문에 기존 코멘트 쓰는 부분을 수정 해주면 된다. 그리고 circleavatar을 데이터가 있으면 나타내고 없으면 안보여주기 위해서 상수로 만들어주고 기본값을 false로 해준다. username도 필요하기 때문에 상수로 해주고 필수 값으로 required 해준다. 그리고 DateTime dateTime값을 가져와주고 null이 아닐때만 보이게 해준다. 그리고 text안에 dateTime을 toIso8601String()해준다. 그런데 여기서 null이면 에러가 나기 때문에 여기도 if문으로 null이면 sizedbox를 null이 아니면 시간을 보여주는 코드를 작성한다. 그러나 여기서 댓글이 길어지면 짤리는 에러가 발생한다. Column 부분이 댓글을 차지하는 부분인데 이부분이 보이는 공간을 차지하는 Expanded로 감싸줘서 보이는곳에서만 내용이 조절되서 나오게 해준다. 기존 피드에서 보여지듯이 이름하고 댓글만 보이면 잘 나오지만 dateTime: DateTime.now(),showProfile: true,를 줘서 프사와 날자가 나오면 이상하게 나오는것을 알 수 있다. 프사랑 댓글이 너무 붙어서 sizedbox로 width를 xs gap만큼 주면 된다. 그러나 showProfile이 false면 SizedBox만 남아서 이상하기 때문에 Visibility로 똑같이 보이고 안보이고를 해주면 된다. 그리고 시간이 중앙으로 되있는데 시간을 왼쪽으로 놓기 보다 Column이 왼쪽정렬이 다 되야 하기 때문에 column의 crossAxisAlignment 옵션을 start로 줘서 좌측 정렬을 해준다. 그리고 댓글과 시간을 좀 띄어놓기 위해 sizedBox를 넣어 주면 된다. 그리고 없을때 갭을 안보여주기 위해서 visibility를 넣었다. 그리고 프사도 중앙정렬 되어있어서 위쪽으로 붙여주기 위해서 column과 마찬가지로 crossAxisAlignment 을 start로 주면 된다. 그리고 날짜의 텍스트 색과 크기를 조절한다.


      Padding _postCaption(BuildContext context, int index) {
        return Padding(
          padding: const EdgeInsets.symmetric(
            horizontal: common_gap, //horizontal 양옆, vertical 위아래
            vertical: common_gap,
          ),
          child: Comment(
            username: 'username $index',
            caption: 'I love summer soooooooooooooooooooooooooooooo much!!!!!!!!!!',
            //dateTime: DateTime.now(),
            //showProfile: true,
          ),
        );
      }

    feed_page 부분 변경

    class Comment extends StatelessWidget {
      final String username;
      final bool showProfile; // 프로필 이미지 보여주기 안보여주기
      final DateTime dateTime;
      final String caption;
    
      const Comment(
          {Key key,
          @required this.username,
          this.showProfile = false,
          this.dateTime,
          @required this.caption})
          : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Visibility(
              visible: showProfile,
              child: CircleAvatar(
                backgroundImage: NetworkImage(
                  getProfileImgPath(username),
                ),
                radius: profile_radius,
              ),
            ),
            Visibility(
              visible: showProfile,
              child: SizedBox(
                width: common_xs_gap,
              ),
            ),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  RichText(
                    text: TextSpan(
                      style: DefaultTextStyle.of(context).style,
                      children: <TextSpan>[
                        TextSpan(
                          text: username,
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                        TextSpan(
                          text: ' ', //여기는 TextSpan 밖에 못넣어서 빈공간을 이렇게 해줘야함
                        ),
                        TextSpan(
                          text: caption,
                        ),
                      ],
                    ),
                  ),
                  Visibility(
                    child: SizedBox(
                      height: common_xxxs_gap,
                    ),
                    visible: dateTime != null,
                  ),
                  Visibility(
                    visible: dateTime != null,
                    child: dateTime == null
                        ? SizedBox()
                        : Text(dateTime.toIso8601String(), style: TextStyle(color: Colors.grey[600],fontSize: 10),),
                  ),
                ],
              ),
            ),
          ],
        );
      }
    }

    comment.dart 추가

    sizedp xxxs 4.0으로 추가해주면됨.


    댓글 더보기 추가.

    Text로 style을 조절해서 넣으면 된다. 거기에 클릭이 가능하게 해야한다. 방법은 2가지 인데 하나는 GestureDetector로 감쌀 수 있다. 어떤 widget든 클릭이 가능해진다. 다른 하나는 FlatButton을 사용하면 된다.(flutter 2.0가면서 다른 버튼으로 바뀜)

    피드화면은 끝!

    반응형

    댓글

Designed by Tistory.