inblog logo
|
devleekangho
    플러터

    [플러터] 25. MVVM 패턴 - (6) 페이징

    KangHo Lee's avatar
    KangHo Lee
    Jan 07, 2025
    [플러터] 25. MVVM 패턴 - (6) 페이징
    Contents
    1. PostListBody 2. PostListVM 3. 페이징 후 발생하는 에러4. 수정

    라이브러리

    dependencies: pull_to_refresh: ^2.0.0

    1. PostListBody

    class PostListBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { PostListModel? model = ref.watch(postListProvider); // SmartRefresher 의 컨트롤러 하나 때문에 적은 것 PostListVM vm = ref.read(postListProvider.notifier); if (model == null) { return Center(child: CircularProgressIndicator()); } else { print("body 로드 완료"); return SmartRefresher( controller: vm.refreshCtrl, enablePullUp: true, onRefresh: () async => await vm.init(), // enablePullDown, onLoading은 쌍 enablePullDown: true, onLoading: () async => await vm.nextList(), child: ListView.separated( itemCount: model.posts.length, itemBuilder: (context, index) { // 클릭 가능하도록 InkWell return InkWell( onTap: () { // push Navigator.push( context, MaterialPageRoute( builder: (_) => PostDetailPage(model.posts[index].id!))); }, child: PostListItem(post: model.posts[index]), ); }, separatorBuilder: (context, index) { return const Divider(); }, ), ); } } }
    • PostListVM vm = ref.read(postListProvider.notifier);
      • SmartRefresher 위젯에 필요한 컨트롤러을 위해 추가

    SmartRefresher

    controller: vm.refreshCtrl, enablePullUp: true, onRefresh: () async => await vm.init(), // enablePullDown, onLoading은 같이 적어야 한다. enablePullDown: true, onLoading: () async => await vm.nextList(),
    • enablePullUp
      • 화면을 아래로 당기면 새로고침이 일어납니다.
      • onRefresh로 새로고침 동작을 정의합니다.
    • enablePullDown
      • 화면을 아래로 내리면 추가적인 게시글 목록을 불러옵니다.
      • onLoading로 추가 데이터를 불러오는 동작을 정의합니다.

    2. PostListVM

    // PostListModel class 생략 final postListProvider = NotifierProvider<PostListVM, PostListModel?>(() { return PostListVM(); }); class PostListVM extends Notifier<PostListModel?> { // 페이징용 컨트롤러 final refreshCtrl = RefreshController(); final mContext = navigatorKey.currentContext!; PostRepository postRepository = const PostRepository(); @override PostListModel? build() { init(); return null; // watch로 볼 예정이라 반환값 필요 x } // 1. 페이지 초기화 (0페이지 리스트 로딩) Future<void> init() async { Map<String, dynamic> responseBody = await postRepository.findAll(); // 리스트 로딩 실패 시 if (!responseBody["success"]) { ScaffoldMessenger.of(mContext!).showSnackBar( SnackBar( content: Text("게시글 목록 보기 실패 : ${responseBody["errorMessage"]}")), ); return; } // 0 페이지 일 때 state = PostListModel.fromMap(responseBody["response"]); // init 메서드가 종료되면, 로딩 중 위젯 종료 refreshCtrl.refreshCompleted(); } // 2. 페이징 로드 Future<void> nextList() async { PostListModel model = state!; // isLast -> 마지막 페이지 인가요? if (model.isLast) { refreshCtrl.loadComplete(); return; } Map<String, dynamic> responseBody = await postRepository.findAll(page: state!.pageNumber + 1); if (!responseBody["success"]) { ScaffoldMessenger.of(mContext!).showSnackBar( SnackBar( content: Text("게시글 추가 로드 보기 실패 : ${responseBody["errorMessage"]}")), ); return; } PostListModel prevModel = state!; PostListModel nextModel = PostListModel.fromMap(responseBody["response"]); state = nextModel.copyWith(posts: [...prevModel.posts, ...nextModel.posts]); refreshCtrl.loadComplete(); } }
    • final refreshCtrl = RefreshController();
      • 페이징에 쓸 컨트롤러입니다.
      • 화면에서 SmartRefresher의 컨트롤러에 설정하면 됩니다.
    • init()
      • 초기 화면을 로드 → 새로고침
      • refreshCompleted()
      • 새로고침 동작이 완료되었다는 것을 SmartRefresher에게 알려주고, 새로고침 인디케이터를 숨깁니다.
    • nextList()
      • postRepository.findAll(page: state!.pageNumber + 1)
        • 다음 페이지 게시글 목록을 불러 옵니다.
      • copyWith를 통해 0페이지 게시글 목록에 1페이지 게시글 목록을 더합니다.
      • loadComplete()
        • 추가 데이터 로딩이 완료되었다는 것을 SmartRefresher에게 알려주고, 로딩 인디케이터를 숨깁니다.

    3. 페이징 후 발생하는 에러

    notion image
    notion image
    • 로그아웃 버튼을 누르고 다시 로그인하면서 PostList 페이지로 오면 발생하는 에러입니다.
    Blog Assertion failed: file:///C:/Users/GGG/ AppData/Local/Pub/Cache/hosted/ pub.dev/pull_to_refresh-2.0.0/lib/src/ smart_refresher.dart:608:12 _refresherState == null "Don't use one refreshController to multiple SmartRefresher,It will cause some unexpected bugs mostly in TabBarView" See also: https://docs.flutter.dev/testing/ errors
    • PostListVM 이 autopose가 되지 않아 남아있는 refreshCtrl 과 충돌이 일어나서 발생하는 문제입니다.

    4. 수정

    PostListVM

    // autoDispose -> ui 파괴 -> vm도 파괴 -> refreshCtrl도 파괴 final postListProvider = NotifierProvider.autoDispose<PostListVM, PostListModel?>(() { return PostListVM(); }); // extends AutoDisposeNotifier 로 번경 class PostListVM extends AutoDisposeNotifier<PostListModel?> { final refreshCtrl = RefreshController(); final mContext = navigatorKey.currentContext!; PostRepository postRepository = const PostRepository(); @override PostListModel? build() { // VM 파괴를 실행 Logger().d("PostListVM build 실행"); ref.onDispose( () { Logger().d("PostListVM 파괴 실행"); refreshCtrl.dispose(); }, ); init(); return null; } }
    • NotifierProvider → NotifierProvider.autoDispose
    • extends Notifier → AutoDisposeNotifier
    • init() 전에 명시적으로 PostListVM과 RefreshController 파괴를 시킵니다.

    SessionGVM

    Future<void> logout() async { await secureStorage.delete(key: "accessToken"); // 2. 상태 갱신 state = SessionUser( ); // 3. dio 갱신 dio.options.headers["Authorization"] = ""; // 화면 다 파괴하고, LoginPage 가기 Navigator.pushNamedAndRemoveUntil( mContext, "/login", (route) => false, ); }
    • Navigator.pushNamedAndRemoveUntil( mContext, "/login", (route) => false, );
      • mContext
        • 현재 위젯의 빌드 컨텍스트입니다.
        • 네비게이션 작업을 수행할 때 필요한 컨텍스트를 제공합니다.
      • "/login"
        • 이동하고자 하는 새로운 경로의 이름입니다.
      • (route) => false
        • 경로를 제거할 조건을 나타내는 콜백 함수입니다.
        • 콜백 함수는 각 경로에 대해 호출되며, false를 반환하면 해당 경로를 제거합니다.
        • 여기서는 모든 경로를 제거하고 새로운 경로로 이동하려고 합니다.
      • 동작 원리
        • 모든 기존 경로가 제거합니다.
        • "/login" 가 네비게이션 스택의 최상단에 추가됩니다.
     
    Share article
    Contents
    1. PostListBody 2. PostListVM 3. 페이징 후 발생하는 에러4. 수정

    devleekangho

    RSS·Powered by Inblog