[flutter] 9주 완성 프로젝트 캠프 과제일지 (유데미x스나이퍼팩토리): day8 GridView, connectivity_plus, pull_to_refresh, Shimmer

KoEunseo·2023년 9월 28일
0

flutter

목록 보기
16/45

요구사항

  • 다음의 URL에 데이터를 요청하여 문제를 해결합니다.
  • 디바이스가 인터넷에 연결돼있는지 확인하는 패키지를 사용합니다.
    • 패키지명 : connectivity_plus
    • FAB을 누르면 인터넷이 연결되어있는지 확인합니다.
    • 인터넷 연결을 확인중일 때 “인터넷 확인 중 입니다”와 로딩 위젯을 보여줍니다.
      • 이 때, 로딩 위젯은 어떠한 것이든 상관없습니다.
  • 화면을 아래로 당기면 데이터를 새로 요청할 수 있도록 패키지를 사용합니다.
    • 패키지명 : pull_to_refresh
  • 데이터를 가져올 때 사용자에게 데이터가 로딩중이라는 것을 알려줄 수 있도록 패키지를 활용합니다.
    • 패키지명 : Shimmer
  • 위 기능을 우선적으로 구현하며, 최대한 자연스러운 UX를 구현할 수 있도록 합니다.
    그 외 과제를 위한 기능 및 디자인은 자유입니다.

역대급 오래걸린 과제.
당일과제는 당일 제출하겠다는 나의 포부를 부셔버린 과제였다...
"역시 통신은 쉽지않군" 이라고 생각하기 쉽지만 사실 통신이 문제가 아니라 다른게 문제였다. 데이터는 금방 받았다...

문제1: 그리드뷰가 뷰를 못그림

그리드뷰를 SizedBox로 감싸서 해결

문제2: connectivity_plus

어케쓰는고
로딩중이 안뜨는디요?

  bool isConnected = false;
  bool connectCheck = false;
  
  ..생략
  connectCheck
              ? const Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text('인터넷 확인 중 입니다.'),
                    CircularProgressIndicator(),
                  ],
                )
              : CardList(
                  future: futureData,
                  url: url,
                  refreshData: refreshData,
                ),
...생략
floatingActionButton: FloatingActionButton(
          onPressed: () async {
            connectCheck = true;
            setState(() {});
            await Future.delayed(const Duration(seconds: 2));
            var res = await Connectivity().checkConnectivity();
            if (res == ConnectivityResult.mobile ||
                res == ConnectivityResult.wifi) {
              isConnected = true;
              refreshData(url);
              setState(() {});
            }
            connectCheck = false;
          },
          child: const Icon(Icons.wifi_find),
        ),

비교적 쉬웠다.
Connectivity().checkConnectivity(); 이렇게 하면 인터넷 연결 상태를 리턴한다.
그럼 그 상태를 ConnectivityResult랑 비교하면 됨.

isConnected는 지금은 별로 쓸모없는 값이지만 뭔가 의도에 따르면 필요한 값일 것 같아서 그냥 뒀다.
딜레이를 준 이유는 체크상태일때 스피너가 화면에 노출되도록 하기 위해서...
너무 금방 사라져서 딜레이하도록 수정했다.

문제3: Shimmer

로딩중일때 나오게 했는데 왜 안나오냐
이거 원리가 뭐냐..

그냥 body에 바로 Shimmer를 박아버리니 스켈레톤이 적용되는 것을 볼 수 있었다.
그래서 그냥 컴포넌트에 상태에 따라서 Shimmer만 주면 되나 했다.
근데 그게 맘처럼 안되더라... 각 카드가 Shimmer를 입어야하는데 그냥 통으로 반짝거림.

결국 ShimmerCard를 따로 만들었다.
그리고 CardListBuilder에서 카드를 빌드할때 데이터가 들어가면 제대로 된 카드가, 아니면 ShimmerCard가 나오도록 했다.
res가 들어오면 res.length, 아니면 ShimmerCard 6개를 화면에 그린다.

GridView.builder(
        physics: const AlwaysScrollableScrollPhysics(),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          mainAxisSpacing: 4,
          crossAxisSpacing: 4,
          childAspectRatio: 0.6,
        ),
        itemCount: widget.res?.length ?? 6,
        itemBuilder: (context, idx) {
          if (widget.res != null) {
            return widget.buildWidget(widget.res[idx]);
          }
          return widget.buildWidget(null);
        },

여기서 보면 빌드할 위젯을 부모로부터 받는 걸 알 수 있는데, CardList에서 로딩 상태에 따라 Shimmer를 바로 리턴하도록 하기 위함이기도 하고, 확장성을 위함이기도 함.

타입을 어떻게해야 생각한대로 동작할까

FutureBuilder(
        future: widget.future,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            var res = snapshot.data?.data["body"];
            return CardListBuilder(
              buildWidget: (item) => CustomCard(item: item),
              res: res,
              url: widget.url,
              refreshData: widget.refreshData,
            );
          }
          return Shimmer.fromColors(
            baseColor: Colors.grey,
            highlightColor: Colors.grey.shade300,
            child: CardListBuilder(
              //여기서 shimmerCard가 받는 인자가 없는데도 item이라는 키워드가 들어가야 에러가 안나는 이유는 뭘까?
              buildWidget: (item) => const ShimmerCard(),
              url: widget.url,
              refreshData: widget.refreshData,
            ),
          );
        });

여튼간 이렇게 위젯을 넘기기 위해서 수많은 시도가 있었다.
위젯 타입을 어떻게 해야 하고, 어떻게 넘겨야하느냐가 문제였다.

class CardListBuilder extends StatefulWidget {
  const CardListBuilder({
    super.key,
    this.res,
    required this.buildWidget,
    required this.refreshData,
    required this.url,
  });

  final res;
  final Function buildWidget;
  final Future<void> Function(String) refreshData;
  final String url;

  
  State<CardListBuilder> createState() => _CardListBuilderState();
}

여기서 보면 결국 Function으로 전달하고 있다.
첨엔 Widget으로 했는데, 자꾸 위젯이 Type이고 Widget이 아니라고 하는게 아닌가...;
Type이 뭔데;;;
다른 위젯을 받는 위젯들은 어떻게 타입이 설정되어있나 봤다.
CustomCard() 이런식으로 빌드하니까 Function이 맞는 것 같기도 해서 Function을 줘서 해결했다.
호출할때도 buildWidget: (item) => CustomCard(item: item), 이런식으로 해야함.

풀리지 않은 의문

여기서 shimmerCard가 받는 인자가 없는데도 item이라는 키워드가 들어가야 에러가 안나는 이유는 뭘까?
item이 ShimmerCard를 생성하는데 아무런 역할을 하지 않는데도 저 키워드가 없으면 에러가 발생한다.

Shimmer.fromColors(
            baseColor: Colors.grey,
            highlightColor: Colors.grey.shade300,
            child: CardListBuilder(
              buildWidget: (item) => const ShimmerCard(),
              url: widget.url,
              refreshData: widget.refreshData,
            ),
          );

문제4: Refresh_to_pull

refresh가 안됨

그냥 리프레시가 안됨.
ListView 바로 "상위"에 두었더니 작동했다.
공식문서에는 스크롤 가능한 위젯을 '포함'한 위젯을 감싸면 된다했는데 아닌듯.
기능별로 나누고싶어서 막 위젯을 나누었는데,
Card가 있고 CardListBuilder가 있고 CardList가 있다.
1. main에서 데이터를 받아 CardList에 전달한다.
2. CardList에서 FutureBuilder를 사용해서 데이터를 CardListBuilder에게 넘긴다.
3. 그럼 CardListBuilder는 GridView를 이용해서 각 Card에 알맞은 데이터를 넘기고 레이아웃을 잡는다.
4. 카드는 데이터를 받아서 적절하게 화면을 그린다.

여기서 refresh를 cardList에 주고싶었다.
처음엔 CardListBuilder를 따로 분리할 생각이 없었기도 하고 props drilling하기도 싫고... 그런데 분리를 하면서 작동이 안됐다. 🤔
GridView가 있는 CardListBuilder에 SmartRefresher를 심으니 제대로 기능이 됐다.

    return SmartRefresher(
      controller: refreshController,
      header: const MaterialClassicHeader(),
      physics: const AlwaysScrollableScrollPhysics(),
      onRefresh: () async {
        await widget.refreshData(widget.url);
        refreshController.refreshCompleted();
      },
      child: GridView.builder( ...

타입에러

Future 이랑 Response은 subtype이 아님(수고)

위 에러가 계속 날 괴롭힘... ㅠㅠ

  SmartRefresher(
      controller: refreshController,
      header: const MaterialClassicHeader(),
      physics: const AlwaysScrollableScrollPhysics(),
      onRefresh: () async {
        await widget.refreshData(widget.url);
        refreshController.refreshCompleted();
      },

위 코드에서 보면 onRefresh에서 refreshData를 하는 걸 볼 수 있다.
근데 이 refreshData가 자꾸 Response<dynamic>라고 하는거임.

여기서 refresh하는 데이터는 main에서 처음 렌더링할때 초기화되는 데이터다.

    
  void initState() {
    super.initState();
    futureData = getData(url);
  }
    Future<Response> getData(url) async {
    var res = await dio.get(url);
    print(futureData.runtimeType);
    await Future.delayed(const Duration(seconds: 2));
    return res;
  }

  Future<void> refreshData(url) async {
    futureData = getData(url);
    setState(() {});
  }

근데 여기서 futureData는 Future<dynamic> 이다.
그래서 자꾸 에러가 난 것...
refreshData에서 원래는 getData를 newData라는 변수에 초기화해 넘겼다.
이런식.

  Future<void> refreshData(url) async {
    var newData = getData(url);
    futureData = newData;
    setState(() {});
  }

getData를 실행해서 바로 futureData로 넘기도록 수정하고,
getData도 타입을 Future로 바꿨더니 제대로 동작했다.
그랬더니 이번엔 futureData가 'Future<Response>'라고 함.


본 후기는 유데미-스나이퍼팩토리 9주 완성 프로젝트캠프 학습 일지 후기로 작성 되었습니다.

profile
주니어 플러터 개발자의 고군분투기

0개의 댓글