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

KangHo Lee's avatar
Jan 07, 2025
[플러터] 25. MVVM 패턴 - (6) 페이징

라이브러리

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

devleekangho