[Flutter] Flutter 무한스크롤 cached query flutter을 이용한..

soonmuu·2023년 1월 20일
0

flutter

목록 보기
7/11
post-thumbnail

cached_query_flutter

  • react query에서 영감을 받은 cached_query_flutter

react-query를 사용해보니 편하고 빠르게 작업할 수 있어서 다른 프레임워크 사용시에도 유사한 라이브러리를 찾아 쓰게된다.

특히 sveltekit 사용시에 쓸 수 있는 svelte-query는 출력방식을 제외하고는 거의 90%가 유사해서 추가 공부 시간이 필요없었다

flutter에도 유사한 라이브러리가 있나 레딧에 검색하다 cached uery 라는 라이브러리를 발견했다

공식문서를 보니 실제로 SWR, React Query, RTKQuery에서 영감을 받았다고 써있기도 하고 친숙한 방식의 라이브러리라 선택했다

일반 dart 언어에서는 cached_query를 설치하고 flutter 에서는 cached_query_flutter를 설치하면된다


0-0. 설치

flutter pub add cached_query_flutter

cached_query를 설치하지 않도록 주의하자
(cached_query_flutter 검색해도 cached_query가 먼저나옴)

0-1. 파일 및 폴더 구성

/lib
	/models		// 불러올 json 객체 모델
    /service	// query 생성 및 query key 관리
    /page		// 출력 페이지

0-2. 테스트 api

"https://jsonplaceholder.typicode.com/posts?_limit=10&_page=$arg"

다음과 같은 데이터를 리턴해준다

[
 {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  ...
]

1. 최상단에서 호출

  • 최상단 main.dart 파일에서 CachedQuery를 호출한다
    공식문서에서는 CachedStorage도 사용했는데 에뮬레이터에서는 문제가 없으나 크롬 브라우저 실행시 오류가나서 사용을 보류했다
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  CachedQuery.instance.configFlutter(
    config: QueryConfigFlutter(
      refetchOnResume: true,
      refetchOnConnection: true,
    ),
    // web에서 오류
    // storage: await CachedStorage.ensureInitialized(),
  );

  runApp(const MyApp());
}

2. json 파싱하기

0-2의 테스트 api 데이터를 참고하여 아래와 같은 모델을 만든다

class PostModel {
  final String title;
  final int id;
  final String body;
  final int userId;

  PostModel({
    required this.title,
    required this.id,
    required this.body,
    required this.userId,
  });

  factory PostModel.fromJson(Map<String, dynamic> json) => PostModel(
        title: json["title"],
        body: json["body"],
        id: json["id"],
        userId: json["userId"],
      );
  static List<PostModel> listFromJson(List<dynamic> json) => json
      .map((dynamic e) => PostModel.fromJson(e as Map<String, dynamic>))
      .toList();
}

3. query 작성

1에서 생성한 모델로 InfiniteQuery를 생성한다

  • getNextArg 에서 페이지 증가가 발생하며 데이터가 더이상 없는 빈배열일시 state.lastPage?.isEmpty가 true를 반환한다
InfiniteQuery<List<PostModel>, int> getPosts() {
  return InfiniteQuery<List<PostModel>, int>(
    key: 'posts',
    config: QueryConfig(
      refetchDuration: const Duration(seconds: 2),
      // use a serializer for cached storage
      serializer: (dynamic postJson) {
        return (postJson as List<dynamic>)
            .map(
              (dynamic page) => PostModel.listFromJson(page as List<dynamic>),
            )
            .toList();
      },
    ),
    getNextArg: (state) {
      if (state.lastPage?.isEmpty ?? false) return null;
      return state.length + 1;
    },
    queryFn: (arg) async {
      final uri = Uri.parse(
        'https://jsonplaceholder.typicode.com/posts?_limit=10&_page=$arg',
      );
      final res = await http.get(uri);
      return PostModel.listFromJson(
        List<Map<String, dynamic>>.from(
          jsonDecode(res.body) as List<dynamic>,
        ),
      );
    },
  );
}

4. InfiniteQueryBuilder로 출력

  • 데이터 패치 상태에 대한 출력은 childrens 하위 배열에서 가능하다
if (state.status == QueryStatus.error)
	Container(...)

if문 아래 한칸 들여쓰기로 조건에 맞는 위젯을 넣을 수 있다

전체코드

class PostListScreen extends StatefulWidget {
  static const routeName = '/';

  const PostListScreen({Key? key}) : super(key: key);

  
  State<PostListScreen> createState() => _PostListScreenState();
}

class _PostListScreenState extends State<PostListScreen> {
  final _scrollController = ScrollController();

  
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const HeaderMain(
        title: '무한스크롤',
      ),
      body: InfiniteQueryBuilder<List<PostModel>, int>(
          query: getPosts(),
          builder: (context, state, query) {
            final allPosts = state.data?.expand((e) => e).toList();
            return CustomScrollView(
              controller: _scrollController,
              slivers: [
                if (state.status == QueryStatus.error)
                  SliverToBoxAdapter(
                    child: DecoratedBox(
                      decoration:
                          BoxDecoration(color: Theme.of(context).errorColor),
                      child: Text(
                        state.error is SocketException
                            ? "No internet connection"
                            : state.error.toString(),
                        style: const TextStyle(color: Colors.white),
                        textAlign: TextAlign.center,
                      ),
                    ),
                  ),
                if (allPosts != null)
                  SliverList(
                    delegate: SliverChildBuilderDelegate(
                      (context, i) => PostList(
                        post: allPosts[i],
                        index: i,
                      ),
                      childCount: allPosts.length,
                    ),
                  ),
                if (state.status == QueryStatus.loading)
                  const SliverToBoxAdapter(
                    child: Center(
                      child: SizedBox(
                        height: 40,
                        width: 40,
                        child: CircularProgressIndicator(),
                      ),
                    ),
                  ),
                SliverPadding(
                  padding: EdgeInsets.only(
                    bottom: MediaQuery.of(context).padding.bottom,
                  ),
                )
              ],
            );
          }),
    );
  }

// 최하단 도달시 다음페이지 패치
  void _onScroll() {
    final query = getPosts();
    if (_isBottom && query.state.status != QueryStatus.loading) {
      query.getNextPage();
    }
  }

// 최하단 판별
  bool get _isBottom {
    if (!_scrollController.hasClients) return false;
    final maxScroll = _scrollController.position.maxScrollExtent;
    final currentScroll = _scrollController.offset;
    return currentScroll >= (maxScroll * 0.9);
  }

// 메모리 해제
  
  void dispose() {
    _scrollController
      ..removeListener(_onScroll)
      ..dispose();
    super.dispose();
  }
}
profile
프론트엔드

0개의 댓글

관련 채용 정보