
pull_to_refresh | Flutter package
a widget provided to the flutter scroll component drop-down refresh and pull up load.
/// 1. 창고 관리자
final joinProvider = NotifierProvider<JoinFM, JoinModel>(() {
return JoinFM();
});
/// 2. 창고
class JoinFM extends Notifier<JoinModel> {
@override
JoinModel build() {
return JoinModel("", "", "", "", "", "");
}
void username(String username) {
final error = validateUsername(username);
print("error : ${error}");
state = state.copyWith(username: username, usernameError: error);
}
void email(String email) {
final error = validateEmail(email);
state = state.copyWith(email: email, emailError: error);
}
void password(String password) {
final error = validatePassword(password);
state = state.copyWith(password: password, passwordError: error);
}
bool validate() {
final usernameError = validateUsername(state.username);
final emailError = validateEmail(state.email);
final passwordError = validatePassword(state.password);
return usernameError.isEmpty && emailError.isEmpty && passwordError.isEmpty;
}
}
/// 3. 창고 데이터 타입
class JoinModel {
final String username;
final String email;
final String password;
final String usernameError;
final String emailError;
final String passwordError;
JoinModel(
this.username,
this.email,
this.password,
this.usernameError,
this.emailError,
this.passwordError,
);
JoinModel copyWith({
String? username,
String? email,
String? password,
String? usernameError,
String? emailError,
String? passwordError,
}) {
return JoinModel(
username ?? this.username,
email ?? this.email,
password ?? this.password,
usernameError ?? this.usernameError,
emailError ?? this.emailError,
passwordError ?? this.passwordError,
);
}
@override
String toString() {
return 'JoinModel{username: $username, email: $email, password: $password, usernameError: $usernameError, emailError: $emailError, passwordError: $passwordError}';
}
}
class JoinForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
JoinFM fm = ref.read(joinProvider.notifier);
JoinModel model = ref.watch(joinProvider);
print("창고 state : ${model}");
return Form(
child: Column(
children: [
CustomAuthTextFormField(
title: "Username",
errorText: model.usernameError,
onChanged: (value) {
fm.username(value);
},
),
const SizedBox(height: mediumGap),
CustomAuthTextFormField(
title: "Email",
errorText: model.emailError,
onChanged: (value) {
fm.email(value);
},
),
const SizedBox(height: mediumGap),
CustomAuthTextFormField(
title: "Password",
errorText: model.passwordError,
obscureText: true,
onChanged: (value) {
fm.password(value);
},
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "회원가입",
click: () {
if (fm.validate()) {
Navigator.pushNamed(context, "/login");
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("유효성 검사 실패입니다")),
);
}
},
),
CustomTextButton(
text: "로그인 페이지로 이동",
click: () {
Navigator.pushNamed(context, "/login");
},
),
],
),
);
}
}
class CustomAuthTextFormField extends StatelessWidget {
final String title;
final String errorText;
final Function(String)? onChanged;
final bool obscureText;
const CustomAuthTextFormField({
required this.title,
this.errorText = "",
this.onChanged,
this.obscureText = false,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title),
const SizedBox(height: smallGap),
TextFormField(
obscureText: obscureText,
onChanged: onChanged,
decoration: InputDecoration(
hintText: "Enter $title",
errorText: errorText.isEmpty ? null : errorText,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
],
);
}
}
/// global view model
/// 1. 창고 관리자
final sessionProvider = NotifierProvider<SessionGVM, SessionModel>(() {
return SessionGVM();
});
/// 2. 창고 (상태가 변경되어도 화면에 알려주지 않음) watch x
class SessionGVM extends Notifier<SessionModel> {
@override
SessionModel build() {
return SessionModel();
}
/// 트랜잭션
Future<void> join(String username, String email, String password) async {
}
Future<void> login(String username, String password) async {
}
Future<void> logout() async {}
}
/// 3. 창고 데이터 타입
class SessionModel {
int? id;
String? username;
String? imgUrl;
String? accessToken;
bool? isLogin;
SessionModel({this.id, this.username, this.imgUrl, this.accessToken, this.isLogin = false});
}
/// 책임 -> 통신 & 파싱(body data)
class UserRepository {
Future<Map<String, dynamic>> join(String username, String email, String password) async {
}
Future<Map<String, dynamic>> login(String username, String password) async {
}
}
/// 책임 -> 통신 & 파싱(body data)
class UserRepository {
Future<Map<String, dynamic>> join(String username, String email, String password) async {
// 1. Map 변환
final requestBody = {
"username": username,
"email": email,
"password": password,
};
// 2. 통신
Response response = await dio.post("/join", data: requestBody);
Map<String, dynamic> responseBody = response.data;
Logger().d(responseBody);
return responseBody;
}
...
}
class SessionGVM extends Notifier<SessionModel> {
...
/// 트랜잭션
Future<void> join(String username, String email, String password) async {
Logger().d("username : ${username}, email : ${email}, password : ${password}");
// 검증
bool isValid = ref.read(joinProvider.notifier).validate();
if (!isValid) {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("유효성 검사 실패입니다")),
);
return;
}
// 통신
Map<String, dynamic> body = await UserRepository().join(username, email, password);
if (!body["success"]) {
// 토스트 띄움
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("${body["errorMessage"]}")),
);
return;
}
Navigator.pushNamed(mContext, "/login");
}
...
}
/// 2. 창고 (상태가 변경되어도, 화면 갱신 안함 - watch 하지마)
class SessionGVM extends Notifier<SessionModel> {
final mContext = navigatorKey.currentContext!;
@override
SessionModel build() {
return SessionModel();
}
...
}
// TODO: 1. Stack의 가장 위 context를 알고 있다. [지금 몰라도 됨] 맨 위에 있는 화면의 context를 찾을 수 있다. -> 뒤로가기 알림창 띄울때 사용
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(const ProviderScope(child: MyApp()));
}
...
class JoinForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
JoinFM fm = ref.read(joinProvider.notifier);
JoinModel model = ref.watch(joinProvider);
return Form(
child: Column(
children: [
...
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "회원가입",
click: () {
ref.read(sessionProvider.notifier).join(model.username, model.email, model.password);
},
),
CustomTextButton(
text: "로그인 페이지로 이동",
click: () {
Navigator.pushNamed(context, "/login");
},
),
],
),
);
}
}
/// 1. 창고 관리자
final loginProvider = NotifierProvider<LoginFM, LoginModel>(() {
return LoginFM();
});
/// 2. 창고
class LoginFM extends Notifier<LoginModel> {
@override
LoginModel build() {
return LoginModel("", "", "", "");
}
void username(String username) {
final error = validateUsername(username);
state = state.copyWith(username: username, usernameError: error);
}
void password(String password) {
final error = validatePassword(password);
state = state.copyWith(password: password, passwordError: error);
}
bool validate() {
final usernameError = validateUsername(state.username);
final passwordError = validatePassword(state.password);
return usernameError.isEmpty && passwordError.isEmpty;
}
}
/// 3. 창고 데이터 타입
class LoginModel {
final String username;
final String password;
final String usernameError;
final String passwordError;
LoginModel(
this.username,
this.password,
this.usernameError,
this.passwordError,
);
LoginModel copyWith({
String? username,
String? password,
String? usernameError,
String? passwordError,
}) {
return LoginModel(
username ?? this.username,
password ?? this.password,
usernameError ?? this.usernameError,
passwordError ?? this.passwordError,
);
}
@override
String toString() {
return 'LoginModel{username: $username, password: $password, usernameError: $usernameError, passwordError: $passwordError}';
}
}
class LoginForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
LoginFM fm = ref.read(loginProvider.notifier);
LoginModel model = ref.watch(loginProvider);
return Form(
child: Column(
children: [
CustomAuthTextFormField(
title: "Username",
errorText: model.usernameError,
onChanged: (value) {
fm.username(value);
},
),
const SizedBox(height: mediumGap),
CustomAuthTextFormField(
title: "Password",
errorText: model.passwordError,
obscureText: true,
onChanged: (value) {
fm.password(value);
},
),
...
],
));
}
}
/// 책임 -> 통신 & 파싱(body data)
class UserRepository {
...
Future<Map<String, dynamic>> login(String username, String password) async {
// 1. Map 변환
final requestBody = {
"username": username,
"password": password,
};
// 2. 통신
Response response = await dio.post("/login", data: requestBody);
Map<String, dynamic> responseBody = response.data;
// 3. 헤더에서 토큰을 꺼내야 함. 헤더에 토큰이 들어있음
// 이제 body 안에 토큰을 넣어주기 때문에 헤더에 꺼낼 필요 없어짐
// String accessToken = "";
// try {
// accessToken = response.headers["Authorization"]![0];
// responseBody["response"]["accessToken"] = accessToken;
// } catch (e) {}
Logger().d(responseBody);
return responseBody;
}
}
class SessionGVM extends Notifier<SessionModel> {
...
Future<void> login(String username, String password) async {
// 1. 유효성 검사
Logger().d("username : ${username}, password : ${password}");
bool isValid = ref.read(loginProvider.notifier).validate();
if (!isValid) {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("유효성 검사 실패입니다")),
);
return;
}
// 2. 통신
Map<String, dynamic> body = await UserRepository().login(username, password);
if (!body["success"]) {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("${body["errorMessage"]}")),
);
return;
}
// 3. 파싱
User user = User.fromMap(body["response"]);
// 4. 토큰을 디바이스 저장
await secureStorage.write(key: "accessToken", value: user.accessToken);
// 5. 세션모델 갱신
state = SessionModel(user: user, isLogin: true);
// 5. dio의 header에 토큰 세팅
dio.options.headers["Authorization"] = user.accessToken;
// 6. 게시글 목록 페이지 이동
Navigator.pushNamed(mContext, "/post/list");
}
...
}
/// 2. 창고 (상태가 변경되어도, 화면 갱신 안함 - watch 하지마)
class SessionGVM extends Notifier<SessionModel> {
final mContext = navigatorKey.currentContext!;
@override
SessionModel build() {
return SessionModel();
}
...
}
// TODO: 1. Stack의 가장 위 context를 알고 있다. [지금 몰라도 됨] 맨 위에 있는 화면의 context를 찾을 수 있다. -> 뒤로가기 알림창 띄울때 사용
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(const ProviderScope(child: MyApp()));
}
...
class LoginForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
LoginFM fm = ref.read(loginProvider.notifier);
LoginModel model = ref.watch(loginProvider);
return Form(
child: Column(
children: [
...
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "로그인",
click: () {
ref.read(sessionProvider.notifier).login(model.username, model.password);
},
),
CustomTextButton(
text: "회원가입 페이지로 이동",
click: () {
Navigator.pushNamed(context, "/join");
},
),
],
));
}
}
class User {
int id;
String username;
String imgUrl;
String? accessToken;
User({
required this.id,
required this.username,
required this.imgUrl,
this.accessToken,
});
User.fromMap(Map<String, dynamic> data)
: id = data['id'],
username = data['username'],
imgUrl = data['imgUrl'],
accessToken = data['accessToken'];
@override
String toString() {
return 'User(id: $id, username: $username, imgUrl: $imgUrl, accessToken: $accessToken)';
}
}
/// 3. 창고 데이터 타입 (불변 아님)
class SessionModel {
User? user;
bool? isLogin;
SessionModel({this.user, this.isLogin = false});
@override
String toString() {
return 'SessionModel{user: $user, isLogin: $isLogin}';
}
}
/// 2. 창고 (상태가 변경되어도, 화면 갱신 안함 - watch 하지마)
class SessionGVM extends Notifier<SessionModel> {
final mContext = navigatorKey.currentContext!;
@override
SessionModel build() {
return SessionModel();
}
...
Future<void> logout() async {
// 1. 토큰 디바이스 제거
await secureStorage.delete(key: "accessToken");
// 2. 세션모델 초기화
state = SessionModel();
// 3. dio 세팅 제거
dio.options.headers.remove("Authorization");
// 4. login 페이지 이동
scaffoldKey.currentState!.openEndDrawer();
Navigator.pushNamed(mContext, "/login");
}
}
final scaffoldKey = GlobalKey<ScaffoldState>();
class PostListPage extends ConsumerWidget {
final refreshKey = GlobalKey<RefreshIndicatorState>();
PostListPage();
@override
Widget build(BuildContext context, WidgetRef ref) {
SessionModel model = ref.read(sessionProvider);
return Scaffold(
key: scaffoldKey,
drawer: CustomNavigation(scaffoldKey),
appBar: AppBar(
title: Text("Blog ${model.isLogin} ${model.user!.username}"),
),
body: RefreshIndicator(
key: refreshKey,
onRefresh: () async {},
child: PostListBody(),
),
);
}
}
class CustomNavigation extends ConsumerWidget {
final scaffoldKey;
const CustomNavigation(this.scaffoldKey, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
SessionGVM gvm = ref.read(sessionProvider.notifier);
return Container(
width: getDrawerWidth(context),
height: double.infinity,
color: Colors.white,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton(
onPressed: () {
scaffoldKey.currentState!.openEndDrawer();
Navigator.pushNamed(context, "/post/write");
},
child: const Text(
"글쓰기",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black54,
),
),
),
const Divider(),
TextButton(
onPressed: () {
gvm.logout();
},
child: const Text(
"로그아웃",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black54,
),
),
),
const Divider(),
],
),
),
),
);
}
}
import 'package:dio/dio.dart';
import 'package:flutter_blog/_core/utils/my_http.dart';
import 'package:logger/logger.dart';
class PostRepository {
Future<Map<String, dynamic>> getList({int page = 0}) async {
Response response = await dio.get("/api/post", queryParameters: {"page": page});
final responseBody = response.data;
Logger().d(responseBody);
return responseBody;
}
}
import 'package:flutter_blog/_core/utils/my_http.dart';
import 'package:flutter_blog/data/repository/post_repository.dart';
void main() async {
dio.options.headers["Authorization"] =
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpbWdVcmwiOiIvaW1hZ2VzLzEucG5nIiwic3ViIjoibWV0YWNvZGluZyIsImlkIjoxLCJleHAiOjE3NDk2MDU1MTEsInVzZXJuYW1lIjoic3NhciJ9.ZK50ecQD8LEuS1jrucCw3sf-ETATjopDu3L7VPNfr42heoRC5T8g7vWpWX60ijItBPgy_1zNMj6U5dsVkZSt8Q";
PostRepository repo = PostRepository();
await repo.getList();
}
import 'package:flutter_blog/data/model/user.dart';
class Post {
int id;
String title;
String content;
DateTime createdAt;
DateTime updatedAt;
User user;
Post({
required this.id,
required this.title,
required this.content,
required this.createdAt,
required this.updatedAt,
required this.user,
});
Post.fromMap(Map<String, dynamic> data)
: id = data['id'],
title = data['title'],
content = data['content'],
createdAt = DateTime.parse(data['createdAt']),
updatedAt = DateTime.parse(data['updatedAt']),
user = User.fromMap(data['user']);
}
import 'package:flutter_blog/data/model/post.dart';
import 'package:flutter_blog/main.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// 1. 창고 관리자
final postListProvider = NotifierProvider<PostListVM, PostListModel?>(() {
return PostListVM();
});
/// 2. 창고
class PostListVM extends Notifier<PostListModel?> {
final mContext = navigatorKey.currentContext!;
@override
PostListModel? build() {
init();
return null;
}
Future<void> init({int page = 0}) async {}
}
/// 3. 창고 데이터 타입
class PostListModel {
final bool isFirst;
final bool isLast;
final int pageNumber;
final int size;
final int totalPage;
final List<Post> posts;
PostListModel({
required this.isFirst,
required this.isLast,
required this.pageNumber,
required this.size,
required this.totalPage,
required this.posts,
});
@override
String toString() {
return 'PostListModel{isFirst: $isFirst, isLast: $isLast, pageNumber: $pageNumber, size: $size, totalPage: $totalPage, posts: $posts}';
}
}
/// 1. 창고 관리자
final postListProvider = NotifierProvider<PostListVM, PostListModel?>(() {
return PostListVM();
});
/// 2. 창고 (상태가 변경되어도, 화면 갱신 안함 - watch 하지마)
class PostListVM extends Notifier<PostListModel?> {
final mContext = navigatorKey.currentContext!;
@override
PostListModel? build() {
init(); // 통신 해놓고
return null; // 일단 null
}
Future<void> init({int page = 0}) async {
Map<String, dynamic> body = await PostRepository().getList(page: page);
state = PostListModel.fromMap(body["response"]);
}
}
/// 3. 창고 데이터 타입
class PostListModel {
final bool isFirst;
final bool isLast;
final int pageNumber;
final int size;
final int totalPage;
final List<Post> posts;
PostListModel({
required this.isFirst,
required this.isLast,
required this.pageNumber,
required this.size,
required this.totalPage,
required this.posts,
});
PostListModel.fromMap(Map<String, dynamic> data)
: isFirst = data['isFirst'],
isLast = data['isLast'],
pageNumber = data['pageNumber'],
size = data['size'],
totalPage = data['totalPage'],
posts = (data['posts'] as List).map((e) => Post.fromMap(e)).toList();
PostListModel copyWith({
bool? isFirst,
bool? isLast,
int? pageNumber,
int? size,
int? totalPage,
List<Post>? posts,
}) {
return PostListModel(
isFirst: isFirst ?? this.isFirst,
isLast: isLast ?? this.isLast,
pageNumber: pageNumber ?? this.pageNumber,
size: size ?? this.size,
totalPage: totalPage ?? this.totalPage,
posts: posts ?? this.posts,
);
}
@override
String toString() {
return 'PostListModel{isFirst: $isFirst, isLast: $isLast, pageNumber: $pageNumber, size: $size, totalPage: $totalPage, posts: $posts}';
}
}
class PostListBody extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
PostListModel? model = ref.watch(postListProvider);
if (model == null) {
return Center(child: CircularProgressIndicator());
} else {
return ListView.separated(
itemCount: model.posts.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => PostDetailPage()));
},
child: PostListItem(model.posts[index]),
);
},
separatorBuilder: (context, index) {
return const Divider();
},
);
}
}
}
class PostListItem extends StatelessWidget {
Post post;
PostListItem(this.post);
@override
Widget build(BuildContext context) {
return ListTile(
title: Text("${post.title}", style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(
"${post.content}",
style: TextStyle(color: Colors.black45),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
trailing: SizedBox(
width: 50,
height: 50,
child: ClipOval(
child: CachedNetworkImage(
imageUrl: "${baseUrl}${post.user.imgUrl}",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
fit: BoxFit.cover,
),
),
),
);
}
}
dependencies:
cached_network_image: ^3.4.1
class PostListBody extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
PostListModel? model = ref.watch(postListProvider);
if (model == null) {
return Center(child: CircularProgressIndicator());
} else {
return ListView.separated(
itemCount: model.posts.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => PostDetailPage(model.posts[index].id)));
},
child: PostListItem(model.posts[index]),
);
},
separatorBuilder: (context, index) {
return const Divider();
},
);
}
}
}
import 'package:flutter/material.dart';
import 'package:flutter_blog/ui/pages/post/detail_page/widgets/post_detail_body.dart';
class PostDetailPage extends StatelessWidget {
int postId;
PostDetailPage(this.postId);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: PostDetailBody(postId),
);
}
}
class PostDetailBody extends StatelessWidget {
int postId;
PostDetailBody(this.postId);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
PostDetailTitle("제목"),
const SizedBox(height: largeGap),
PostDetailProfile(),
PostDetailButtons(),
const Divider(),
const SizedBox(height: largeGap),
PostDetailContent("내용"),
],
),
);
}
}
import 'package:flutter_blog/main.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// 1. 창고 관리자
/// NotifierProvider.family 의 세번째 값으로 매개변수 타입을 작성
final postDetailProvider = NotifierProvider.family<PostDetailVM, PostDetailModel?, int>(() {
return PostDetailVM();
});
/// 2. 창고
/// FamilyNotifier 를 사용해서 매개변수를 받을 수 있다. 제네릭에 2번째 값으로 매개변수 타입을 작성
/// TODO 3 : init 완성하기
class PostDetailVM extends FamilyNotifier<PostDetailModel?, int> {
final mContext = navigatorKey.currentContext!;
// 빌드시 매개변수를 받을 수 있는 방법
@override
PostDetailModel? build(int postId) {
init(postId);
return null;
}
Future<void> init(int postId) async {}
}
/// 3. 창고 데이터 타입
/// TODO 2 : replies 빼고 상태 관리
class PostDetailModel {}
NotifierProvider.family
가 만들어내는 내부 구현체에서 사용됨 (개념적으로 이해하면 충분)NotifierProvider
에 파라미터를 전달할 수 있게 해주는 기능import 'package:dio/dio.dart';
import 'package:flutter_blog/_core/utils/my_http.dart';
import 'package:logger/logger.dart';
class PostRepository {
...
/// TODO 1 : getOne 만들기
Future<Map<String, dynamic>> getOne(int postId) async {
Response response = await dio.get('/api/post/${postId}');
final responseBody = response.data;
Logger().d(responseBody);
return responseBody;
}
}
void main() async {
dio.options.headers["Authorization"] =
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpbWdVcmwiOiIvaW1hZ2VzLzEucG5nIiwic3ViIjoibWV0YWNvZGluZyIsImlkIjoxLCJleHAiOjE3NDk2MDU1MTEsInVzZXJuYW1lIjoic3NhciJ9.ZK50ecQD8LEuS1jrucCw3sf-ETATjopDu3L7VPNfr42heoRC5T8g7vWpWX60ijItBPgy_1zNMj6U5dsVkZSt8Q";
PostRepository repo = PostRepository();
await repo.getOne(1);
}
/// 1. 창고 관리자
/// NotifierProvider.family 의 세번째 값으로 매개변수 타입을 작성
final postDetailProvider = NotifierProvider.family<PostDetailVM, PostDetailModel?, int>(() {
return PostDetailVM();
});
/// 2. 창고
/// FamilyNotifier 를 사용해서 매개변수를 받을 수 있다. 제네릭에 2번째 값으로 매개변수 타입을 작성
/// TODO 3 : init 완성하기
class PostDetailVM extends FamilyNotifier<PostDetailModel?, int> {
final mContext = navigatorKey.currentContext!;
// 빌드시 매개변수를 받을 수 있는 방법
@override
PostDetailModel? build(int postId) {
init(postId);
return null;
}
Future<void> init(int postId) async {
Map<String, dynamic> body = await PostRepository().getOne(postId);
if (!body["success"]) {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("게시글 상세보기 실패 : ${body["errorMessage"]}")),
);
return;
}
state = PostDetailModel.fromMap(body['response']);
}
}
/// 3. 창고 데이터 타입
/// TODO 2 : replies 빼고 상태 관리
class PostDetailModel {
final Post post;
PostDetailModel({required this.post});
PostDetailModel.fromMap(Map<String, dynamic> data) : post = Post.fromMap(data);
PostDetailModel copyWith({Post? post}) {
return PostDetailModel(post: post ?? this.post);
}
@override
String toString() {
return 'PostDetailModel{post: $post}';
}
}
import 'package:flutter_blog/data/model/user.dart';
class Post {
int id;
String title;
String content;
DateTime createdAt;
DateTime updatedAt;
User user;
int bookmarkCount;
bool? isBookmark;
Post({
required this.id,
required this.title,
required this.content,
required this.createdAt,
required this.updatedAt,
required this.user,
required this.bookmarkCount,
this.isBookmark,
});
Post.fromMap(Map<String, dynamic> data)
: id = data['id'],
title = data['title'],
content = data['content'],
createdAt = DateTime.parse(data['createdAt']),
updatedAt = DateTime.parse(data['updatedAt']),
user = User.fromMap(data['user']),
bookmarkCount = data["bookmarkCount"],
isBookmark = data["isBookmark"];
@override
String toString() {
return 'Post{id: $id, title: $title, content: $content, createdAt: $createdAt, updatedAt: $updatedAt, user: $user, bookmarkCount: $bookmarkCount, isBookmark: $isBookmark}';
}
}
class PostDetailBody extends ConsumerWidget {
int postId;
PostDetailBody(this.postId);
@override
Widget build(BuildContext context, WidgetRef ref) {
// family 를 사용하면 매개변수를 넘길 수 있다
PostDetailModel? model = ref.watch(postDetailProvider(postId));
/// TODO 4 : model 데이터 렌더링하기
if (model == null) {
return Center(child: CircularProgressIndicator());
} else {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
PostDetailTitle("${model.post.title}"),
const SizedBox(height: largeGap),
PostDetailProfile(model.post), // 유저 넘기고 email 생략
PostDetailButtons(model.post),
const Divider(),
const SizedBox(height: largeGap),
PostDetailContent("${model.post.content}"),
],
),
);
}
}
}
class PostDetailProfile extends StatelessWidget {
Post post;
PostDetailProfile(this.post);
@override
Widget build(BuildContext context) {
return ListTile(
title: Text("${post.user.username}"),
leading: SizedBox(
height: 50,
width: 50,
child: ClipOval(
child: CachedNetworkImage(
imageUrl: "${baseUrl}${post.user.imgUrl}",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
fit: BoxFit.cover,
),
),
),
subtitle: Row(
children: [
Text("ssar@nate.com"),
const SizedBox(width: mediumGap),
const Text("·"),
const SizedBox(width: mediumGap),
const Text("Written on "),
Text("${post.createdAt}"),
],
));
}
}
class PostDetailButtons extends ConsumerWidget {
Post post;
PostDetailButtons(this.post);
@override
Widget build(BuildContext context, WidgetRef ref) {
SessionModel model = ref.read(sessionProvider);
return Visibility(
visible: model.user!.id == post.user.id,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () async {},
icon: const Icon(CupertinoIcons.delete),
),
IconButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => PostUpdatePage()));
},
icon: const Icon(CupertinoIcons.pen),
),
],
),
);
}
}
import 'package:dio/dio.dart';
import 'package:flutter_blog/_core/utils/my_http.dart';
import 'package:logger/logger.dart';
class PostRepository {
...
Future<Map<String, dynamic>> deleteOne(int postId) async {
Response response = await dio.delete('/api/post/${postId}');
final responseBody = response.data;
Logger().d(responseBody);
return responseBody;
}
}
void main() async {
dio.options.headers["Authorization"] =
"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpbWdVcmwiOiIvaW1hZ2VzLzEucG5nIiwic3ViIjoibWV0YWNvZGluZyIsImlkIjoxLCJleHAiOjE3NDk2MDU1MTEsInVzZXJuYW1lIjoic3NhciJ9.ZK50ecQD8LEuS1jrucCw3sf-ETATjopDu3L7VPNfr42heoRC5T8g7vWpWX60ijItBPgy_1zNMj6U5dsVkZSt8Q";
PostRepository repo = PostRepository();
await repo.deleteOne(1);
}
final postDetailProvider = AutoDisposeNotifierProvider.family<PostDetailVM, PostDetailModel?, int>(() {
return PostDetailVM();
});
// TODO 3 : init 완성하기 (state 갱신)
class PostDetailVM extends AutoDisposeFamilyNotifier<PostDetailModel?, int> {
final mContext = navigatorKey.currentContext!;
@override
PostDetailModel? build(int postId) {
// 1. 상태 초기화
init(postId);
// 2. VM 파괴되는지 확인하는 이벤트
ref.onDispose(() {
Logger().d("PostDetailModel 파괴됨");
});
// 3. 상태 값 세팅
return null;
}
Future<void> deleteOne(int postId) async {
Map<String, dynamic> body = await PostRepository().deleteOne(postId);
if (!body["success"]) {
ScaffoldMessenger.of(mContext!).showSnackBar(
SnackBar(content: Text("게시글 삭제하기 실패 : ${body["errorMessage"]}")),
);
return;
}
//init(postId);
ref.read(postListProvider.notifier).notifyDeleteOne(postId);
Navigator.pop(mContext);
}
Future<void> init(int postId) async {
Map<String, dynamic> body = await PostRepository().getOne(postId);
if (!body["success"]) {
ScaffoldMessenger.of(mContext!).showSnackBar(
SnackBar(content: Text("게시글 상세보기 실패 : ${body["errorMessage"]}")),
);
return;
}
state = PostDetailModel.fromMap(body["response"]);
}
}
// TODO 2 : replies 빼고 상태로 관리하기
class PostDetailModel {
Post post;
PostDetailModel({required this.post});
PostDetailModel.fromMap(Map<String, dynamic> data) : post = Post.fromMap(data);
PostDetailModel copyWith({Post? post}) {
return PostDetailModel(post: post ?? this.post);
}
@override
String toString() {
return 'PostDetailModel(post: $post)';
}
}
/// 1. 창고 관리자
final postListProvider = NotifierProvider<PostListVM, PostListModel?>(() {
return PostListVM();
});
/// 2. 창고
class PostListVM extends Notifier<PostListModel?> {
final mContext = navigatorKey.currentContext!;
@override
PostListModel? build() {
init();
return null;
}
Future<void> init({int page = 0}) async {
Map<String, dynamic> body = await PostRepository().getList(page: page);
if (!body["success"]) {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("게시글 목록보기 실패 : ${body["errorMessage"]}")),
);
return;
}
state = PostListModel.fromMap(body["response"]);
}
void notifyDeleteOne(int postId) {
PostListModel model = state!;
model.posts = model.posts.where((p) => p.id != postId).toList();
state = state!.copyWith(posts: model.posts);
}
}
/// 3. 창고 데이터 타입
class PostListModel {
bool isFirst;
bool isLast;
int pageNumber;
int size;
int totalPage;
List<Post> posts;
PostListModel({
required this.isFirst,
required this.isLast,
required this.pageNumber,
required this.size,
required this.totalPage,
required this.posts,
});
PostListModel.fromMap(Map<String, dynamic> data)
: isFirst = data['isFirst'],
isLast = data['isLast'],
pageNumber = data['pageNumber'],
size = data['size'],
totalPage = data['totalPage'],
posts = (data['posts'] as List).map((e) => Post.fromMap(e)).toList();
PostListModel copyWith({
bool? isFirst,
bool? isLast,
int? pageNumber,
int? size,
int? totalPage,
List<Post>? posts,
}) {
return PostListModel(
isFirst: isFirst ?? this.isFirst,
isLast: isLast ?? this.isLast,
pageNumber: pageNumber ?? this.pageNumber,
size: size ?? this.size,
totalPage: totalPage ?? this.totalPage,
posts: posts ?? this.posts,
);
}
@override
String toString() {
return 'PostListModel{isFirst: $isFirst, isLast: $isLast, pageNumber: $pageNumber, size: $size, totalPage: $totalPage, posts: $posts}';
}
}
class PostDetailButtons extends ConsumerWidget {
Post post;
PostDetailButtons(this.post);
@override
Widget build(BuildContext context, WidgetRef ref) {
SessionModel sessionModel = ref.read(sessionProvider);
PostDetailVM vm = ref.read(postDetailProvider(post.id).notifier);
return Visibility(
visible: sessionModel.user!.id == post.user.id,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () async {
vm.deleteOne(post.id);
},
icon: const Icon(CupertinoIcons.delete),
),
IconButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => PostUpdatePage()));
},
icon: const Icon(CupertinoIcons.pen),
),
],
),
);
}
}
final postWriteProvider = NotifierProvider<PostWriteFM, PostWriteModel>(() {
return PostWriteFM();
});
class PostWriteFM extends Notifier<PostWriteModel> {
@override
PostWriteModel build() {
return PostWriteModel("", "");
}
void title(String title) {
state = state.copyWith(title: title);
}
void content(String content) {
state = state.copyWith(content: content);
}
}
class PostWriteModel {
final String title;
final String content;
PostWriteModel(this.title, this.content);
PostWriteModel copyWith({
String? title,
String? content,
}) {
return PostWriteModel(
title ?? this.title,
content ?? this.content,
);
}
@override
String toString() {
return 'PostWriteModel{title: $title, content: $content}';
}
}
class PostWriteForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
PostWriteFM fm = ref.read(postWriteProvider.notifier);
return Form(
child: ListView(
shrinkWrap: true,
children: [
CustomTextFormField(
hint: "Title",
onChanged: (value) {
fm.title(value);
},
),
const SizedBox(height: smallGap),
CustomTextArea(
hint: "Content",
onChanged: (value) {
fm.content(value);
},
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "글쓰기",
click: () {
vm.write(model.title, model.content);
},
),
],
),
);
}
}
class PostRepository {
...
Future<Map<String, dynamic>> write(String title, String content) async {
Response response = await dio.post(
'/api/post',
data: {
"title": title,
"content": content,
},
);
final responseBody = response.data;
Logger().d(responseBody);
return responseBody;
}
}
/// 1. 창고 관리자
final postListProvider = NotifierProvider<PostListVM, PostListModel?>(() {
return PostListVM();
});
/// 2. 창고
class PostListVM extends Notifier<PostListModel?> {
final mContext = navigatorKey.currentContext!;
@override
PostListModel? build() {
init();
return null;
}
...
Future<void> write(String title, String content) async {
// 1. 레포지토리 함수 호출
Map<String, dynamic> body = await PostRepository().write(title, content);
// 2. 성공 여부 확인
if (!body["success"]) {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("게시글 쓰기 실패 : ${body["errorMessage"]}")),
);
return;
}
// 3. 상태 갱신
// 3-1. 파싱
Post post = Post.fromMap(body["response"]);
// 3-2. 상태 변경
List<Post> nextPosts = [post, ...state!.posts];
// 3-3. 상태 갱신
state = state!.copyWith(posts: nextPosts);
// 4. 글쓰기 화면 pop
Navigator.pop(mContext);
}
}
/// 3. 창고 데이터 타입
class PostListModel {
...
}
class PostWriteForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
PostWriteFM fm = ref.read(postWriteProvider.notifier);
PostWriteModel model = ref.watch(postWriteProvider);
PostListVM vm = ref.read(postListProvider.notifier);
return Form(
child: ListView(
shrinkWrap: true,
children: [
CustomTextFormField(
hint: "Title",
onChanged: (value) {
fm.title(value);
},
),
const SizedBox(height: smallGap),
CustomTextArea(
hint: "Content",
onChanged: (value) {
fm.content(value);
},
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "글쓰기",
click: () {
vm.write(model.title, model.content);
},
),
],
),
);
}
}
class PostDetailButtons extends ConsumerWidget {
Post post;
PostDetailButtons(this.post);
@override
Widget build(BuildContext context, WidgetRef ref) {
SessionModel sessionModel = ref.read(sessionProvider);
PostDetailVM vm = ref.read(postDetailProvider(post.id).notifier);
return Visibility(
visible: sessionModel.user!.id == post.user.id,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () async {
vm.deleteOne(post.id);
},
icon: const Icon(CupertinoIcons.delete),
),
IconButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => PostUpdatePage(post)));
},
icon: const Icon(CupertinoIcons.pen),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_blog/data/model/post.dart';
import 'package:flutter_blog/ui/pages/post/update_page/widgets/post_update_body.dart';
class PostUpdatePage extends StatelessWidget {
Post post;
PostUpdatePage(this.post);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: PostUpdateBody(post),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_blog/data/model/post.dart';
import 'package:flutter_blog/ui/pages/post/update_page/widgets/post_update_form.dart';
class PostUpdateBody extends StatelessWidget {
Post post;
PostUpdateBody(this.post);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: PostUpdateForm(post),
);
}
}
class PostUpdateForm extends StatelessWidget {
Post post;
PostUpdateForm(this.post);
@override
Widget build(BuildContext context) {
return Form(
child: ListView(
children: [
CustomTextFormField(
hint: "Title",
initialValue: post.title,
onChanged: (value) {},
),
const SizedBox(height: smallGap),
CustomTextArea(
hint: "Content",
initialValue: post.content,
onChanged: (value) {},
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "글 수정하기",
click: () {},
),
],
),
);
}
}
class PostUpdateForm extends StatelessWidget {
Post post;
PostUpdateForm(this.post);
@override
Widget build(BuildContext context) {
return Form(
child: ListView(
children: [
CustomTextFormField(
hint: "Title",
initialValue: post.title,
onChanged: (value) {
post.title = value;
},
),
const SizedBox(height: smallGap),
CustomTextArea(
hint: "Content",
initialValue: post.content,
onChanged: (value) {
post.content = value;
},
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "글 수정하기",
click: () {},
),
],
),
);
}
}
class PostRepository {
...
Future<Map<String, dynamic>> updateOne(Post post) async {
Response response = await dio.put(
'/api/post/${post.id}',
data: {
"title": post.title,
"content": post.content,
},
);
final responseBody = response.data;
Logger().d(responseBody);
return responseBody;
}
}
final postDetailProvider = AutoDisposeNotifierProvider.family<PostDetailVM, PostDetailModel?, int>(() {
return PostDetailVM();
});
// TODO 3 : init 완성하기 (state 갱신)
class PostDetailVM extends AutoDisposeFamilyNotifier<PostDetailModel?, int> {
final mContext = navigatorKey.currentContext!;
@override
PostDetailModel? build(int postId) {
// 1. 상태 초기화
init(postId);
// 2. VM 파괴되는지 확인하는 이벤트
ref.onDispose(() {
Logger().d("PostDetailModel 파괴됨");
});
// 3. 상태 값 세팅
return null;
}
Future<void> updateOne(Post post) async {
// 1. 통신
Map<String, dynamic> body = await PostRepository().updateOne(post);
if (!body["success"]) {
ScaffoldMessenger.of(mContext!).showSnackBar(
SnackBar(content: Text("게시글 수정 실패 : ${body["errorMessage"]}")),
);
return;
}
// 2. 파싱
Post nextPost = Post.fromMap(body["response"]);
// 3. 상태갱신 (detail)
state = state!.copyWith(post: nextPost);
// 4. 상태갱신 (list -> notify)
ref.read(postListProvider.notifier).notifyUpdateOne(nextPost);
// 5. pop
Navigator.pop(mContext);
}
...
}
// TODO 2 : replies 빼고 상태로 관리하기
class PostDetailModel {
...
}
/// 1. 창고 관리자
final postListProvider = NotifierProvider<PostListVM, PostListModel?>(() {
return PostListVM();
});
/// 2. 창고
class PostListVM extends Notifier<PostListModel?> {
final mContext = navigatorKey.currentContext!;
@override
PostListModel? build() {
init();
return null;
}
...
void notifyUpdateOne(Post post) {
List<Post> nextPosts = state!.posts.map((p) => p.id == post.id ? post : p).toList();
state = state!.copyWith(posts: nextPosts);
}
...
}
/// 3. 창고 데이터 타입
class PostListModel {
...
}
class PostUpdateForm extends ConsumerWidget {
Post post;
PostUpdateForm(this.post);
@override
Widget build(BuildContext context, WidgetRef ref) {
PostDetailVM vm = ref.read(postDetailProvider(post.id).notifier);
return Form(
child: ListView(
children: [
CustomTextFormField(
hint: "Title",
initialValue: post.title,
onChanged: (value) {
post.title = value;
},
),
const SizedBox(height: smallGap),
CustomTextArea(
hint: "Content",
initialValue: post.content,
onChanged: (value) {
post.content = value;
},
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "글 수정하기",
click: () {
vm.updateOne(post);
},
),
],
),
);
}
}
// TODO: 1. Stack의 가장 위 context를 알고 있다. [지금 몰라도 됨] 맨 위에 있는 화면의 context를 찾을 수 있다. -> 뒤로가기 알림창 띄울때 사용
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
// context가 없는 곳에서 context를 사용할 수 있는 방법
debugShowCheckedModeBanner: false,
home: SplashPage(),
// 스플레시 페이지는 준비 화면인데 통신을 하지 않으면 쓸모 없다
routes: {
"/login": (context) => const LoginPage(),
"/join": (context) => const JoinPage(),
"/post/list": (context) => PostListPage(),
"/post/write": (context) => const PostWritePage(),
},
theme: theme(),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_blog/data/gvm/session_gvm.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SplashPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.read(sessionProvider.notifier).autoLogin();
return Scaffold(
body: Center(
child: Image.asset(
'assets/splash.gif',
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
);
}
}
import 'package:dio/dio.dart';
import 'package:flutter_blog/_core/utils/my_http.dart';
import 'package:logger/logger.dart';
/// 책임 -> 통신 & 파싱(body data)
class UserRepository {
...
Future<Map<String, dynamic>> autoLogin(String accessToken) async {
Response response = await dio.post("/auto/login", options: Options(headers: {"Authorization": accessToken}));
Map<String, dynamic> responseBody = response.data;
Logger().d(responseBody);
return responseBody;
}
}
/// 1. 창고 관리자
final sessionProvider = NotifierProvider<SessionGVM, SessionModel>(() {
// 의존하는 VM
return SessionGVM();
});
/// 2. 창고 (상태가 변경되어도, 화면 갱신 안함 - watch 하지마)
class SessionGVM extends Notifier<SessionModel> {
final mContext = navigatorKey.currentContext!;
@override
SessionModel build() {
return SessionModel();
}
...
Future<void> autoLogin() async {
// 디바이스에서 토큰 값 가져오기
String? accessToken = await secureStorage.read(key: "accessToken");
if (accessToken == null) {
Navigator.pushReplacement(mContext, MaterialPageRoute(builder: (_) => LoginPage()));
return;
}
// 1. 통신
Map<String, dynamic> body = await UserRepository().autoLogin(accessToken);
if (!body["success"]) {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("${body["errorMessage"]}")),
);
Navigator.pushReplacement(mContext, MaterialPageRoute(builder: (_) => LoginPage()));
return;
}
User user = User.fromMap(body["response"]);
user.accessToken = accessToken;
// 5. 세션모델 갱신
state = SessionModel(user: user, isLogin: true);
// 5. dio의 header에 토큰 세팅 [Bearer <- 이거 들어가 있음]
dio.options.headers["Authorization"] = user.accessToken;
// 6. 게시글 목록 페이지 이동
Navigator.pushReplacement(mContext, MaterialPageRoute(builder: (_) => PostListPage()));
}
}
/// 3. 창고 데이터 타입 (불변 아님)
class SessionModel {
...
}
pushReplacement
→ 현재 페이지를 pop하고, 새 페이지를 push함. 즉, 대체하는 방식pull_to_refresh | Flutter package
a widget provided to the flutter scroll component drop-down refresh and pull up load.
dependencies:
pull_to_refresh: ^2.0.0
class PostListBody extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
PostListModel? model = ref.watch(postListProvider);
PostListVM vm = ref.read(postListProvider.notifier);
if (model == null) {
return Center(child: CircularProgressIndicator());
} else {
return SmartRefresher(
controller: vm.refreshCtrl,
enablePullDown: true,
enablePullUp: true,
onRefresh: () {
vm.init();
},
onLoading: () {
vm.nextList();
},
child: ListView.separated(
itemCount: model.posts.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => PostDetailPage(model.posts[index].id)));
},
child: PostListItem(model.posts[index]),
);
},
separatorBuilder: (context, index) {
return const Divider();
},
),
);
}
}
}
SmartRefresher
→ 컬렉션 위젯에 적용 가능한 새로고침 및 추가요청 가능한 위젯import 'package:flutter/material.dart';
import 'package:flutter_blog/data/model/post.dart';
import 'package:flutter_blog/data/repository/post_repository.dart';
import 'package:flutter_blog/main.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
/// 1. 창고 관리자
final postListProvider = NotifierProvider<PostListVM, PostListModel?>(() {
return PostListVM();
});
/// 2. 창고
class PostListVM extends Notifier<PostListModel?> {
final mContext = navigatorKey.currentContext!;
final refreshCtrl = RefreshController();
@override
PostListModel? build() {
init();
return null;
}
Future<void> init({int page = 0}) async {
Map<String, dynamic> body = await PostRepository().getList(page: page);
if (!body["success"]) {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("게시글 목록보기 실패 : ${body["errorMessage"]}")),
);
return;
}
state = PostListModel.fromMap(body["response"]);
refreshCtrl.refreshCompleted();
}
...
Future<void> nextList() async {
PostListModel prevModel = state!;
if (prevModel.isLast) {
await Future.delayed(Duration(milliseconds: 500));
refreshCtrl.loadComplete();
return;
}
Map<String, dynamic> body = await PostRepository().getList(page: prevModel.pageNumber + 1);
if (!body["success"]) {
ScaffoldMessenger.of(mContext).showSnackBar(
SnackBar(content: Text("게시글 로딩 실패 : ${body["errorMessage"]}")),
);
refreshCtrl.loadComplete();
return;
}
PostListModel nextModel = PostListModel.fromMap(body["response"]);
state = nextModel.copyWith(posts: [...prevModel.posts, ...nextModel.posts]);
refreshCtrl.loadComplete();
}
}
/// 3. 창고 데이터 타입
class PostListModel {
...
}
import 'package:flutter/material.dart';
import 'package:flutter_blog/data/model/post.dart';
import 'package:flutter_blog/data/repository/post_repository.dart';
import 'package:flutter_blog/main.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logger/logger.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
/// 1. 창고 관리자
final postListProvider = AutoDisposeNotifierProvider<PostListVM, PostListModel?>(() {
return PostListVM();
});
/// 2. 창고
class PostListVM extends AutoDisposeNotifier<PostListModel?> {
final mContext = navigatorKey.currentContext!;
final refreshCtrl = RefreshController();
@override
PostListModel? build() {
init();
ref.onDispose(() {
refreshCtrl.dispose();
Logger().d("PostListVM 파괴됨");
});
return null;
}
...
}
/// 3. 창고 데이터 타입
class PostListModel {
...
}
AutoDispose
→ 를 사용하면 모든 구독자가 사라질 때 모델이 같이 사라진다ref
→ 현재 Provider의 생명주기를 추적하는 객체onDispose
→ Provider가 dispose될 때 실행할 작업을 등록jjack1