라이브러리
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. 페이징 후 발생하는 에러


- 로그아웃 버튼을 누르고 다시 로그인하면서 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