์ด๋ฒ์ ์๊ฐํ ํจํค์ง๋ pull_to_refresh๋ผ๋ ๋ก๋ฉ ์ธ๋์ผ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ ์ ์๋ ํจํค์ง์ด๋ค.
https://pub.dev/packages/pull_to_refresh
Pub.dev์ 2533 LIKES์ธ๊ฒ์ ๋ณด๋, ๊ฝค ๋ง์ ์ฌ๋๋ค์ด ์ฌ์ฉํ๊ณ ์๊ณ ์ด๋์ ๋ ๊ฒ์ฆ๋ ํจํค์ง๋ผ๋ ์๋ฏธ์ด๋ค.
์ด์ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์ฐจ๊ทผํ ์์๋ณด์.
๊ทธ์ ์ ํน์ ๋ฌด์จ ํจํค์ง์ธ์ง ๊ฐ์ ์์ค๋ ๋ถ๋ค์ ์ํด์ ๋์ ์ด๋ฏธ์ง๋ฅผ ๊ฐ๋จํ ๋ณด์ฌ์ฃผ์๋ฉด
์ฐ๋ฆฌ๊ฐ Flutter๋ด์ฅ ํจ์์ธ RefreshIndicatorํจ์๋ผ๊ณ ์๊ฐํ๋ฉด ๋ ๊ฒ ๊ฐ๋ค.
์ฆ ์์์ ๋ฐ์ผ๋ก(pull up)์คํฌ๋กค์ ํ์๊ฒฝ์ฐ์๋ ์๋ก๊ณ ์นจ!
๋ฐ์์ ์๋ก(pull down)ํ์๊ฒฝ์ฐ์๋ ๋ฐ์ดํฐ๋ฅผ ๊ณ์ํด์ ๋ฐ์์ค๋ ๊ธฐ๋ฅ์
๋ก๋ฉ Indicator๋ฅผ ์ปค์คํฐ๋ง์ด์ง์ ์ฝ๊ฒ ๋์์ฃผ๋ ํจํค์ง๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค.
์ผ๋จ ๊ธฐ๋ณธ์ ์ธ ํ๋ฌํฐ ๋ก๋ฉ ํ๋ฉด์(ํจํค์ง ์ฌ์ฉ์ํ์ ์)
๋์ถฉ ์ด๋ฐ ๋๋์ด๋ค. ํ๋์์ ๊ฐ์ด๋ฐ ๋๊ทธ๋๊ฒ ๋์๊ฐ๊ณ ๋์ธ ํํ์ด๋ค.
ํ์ง๋ง ์ด ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ค๋ฉด
์ด ํจํค์ง์ ๊ธฐ๋ฅ๋ค์ ๋ณด๋ฉด
GridView, ListView๋ฑ ์ฌ๋ฌ ํจํค์ง์ ์ฌ์ฉํ์ง ์์ฃผ ์ ํฉํ๋ค๊ณ ํ๋ค.
๊ทธ๋ฅ ๋ถ๋ชจ๋ก SmartRefresher()๋ฅผ ๊ฐ์ธ์ฃผ๋ฉด ๋์ด๋ผ๊ณ ํ๋ค.
์ด์ ๋์ถฉ ๊ตฌํ๋ฐฉ์์ ์๊ฒ ๋์์ผ๋ ๋ฐ๋ก ์ฝ๋๋ก ์์๋ณด์.
์ผ๋จ ๋๋ ๋ค ์ฌ์ฉํ์ง๋ ์๊ณ ํ์ํ ๋ถ๋ถ๋ง ์ค๋ช ํ ์์ ์ด๋ค.
onRefresh: onRefresh,
onLoading: onLoading,
enablePullDown: true,
enablePullUp: true,
controller: refreshController,
footer : footer,
enablePullDown: true,
enablePullUp: true,
์ ๋๊ฐ๋ ๋ณ์๋ช ์์๋ ์ ์ ์๋ฏ์ด ๋๋๊ทธ ๊ธฐ๋ฅ์ ON/OFF ํ ๊ฒ์ธ์ง ์ ๋ฌด์ด๋ค.
๊ทธ๋ค์์ผ๋ก๋ ์ปจํธ๋กค๋ฌ๋ฅผ ์์ฑํด์ฃผ์ด์ผ ํ๋ค.
controller: refreshController,
final refreshController = useState(RefreshController(initialRefresh: false)).value;
์ปจํธ๋กค๋ฌ๋ ์ด๋ ๊ฒ ์์ฑํด ์ฃผ๋ฉด ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ง์ง๋ง์ผ๋ก onRefresh์ onLoading๋ถ๋ถ์ ์ค์ ํด์ผ ํ๋ค.
onRefresh: onRefresh,
onLoading: onLoading,
// ์์์ ์คํฌ๋กค
void onRefresh() async {
// ๋คํธ์ํฌ ๋ชจ๋ฐฉ ๋๋ ์ด
await Future.delayed(const Duration(milliseconds: 1000));
refreshController.refreshCompleted();
}
// ์๋์์ ์คํฌ๋กค (์๋ก์ด ๋ฐ์ดํฐ ๋ก๋ฉ)
void onLoading() async {
await Future.delayed(const Duration(milliseconds: 1000));
// items.value.add({items.value.length + 1}.toString());
final newItems = List<String>.from(items.value)..add((items.value.length + 1).toString());
items.value = newItems;
refreshController.loadComplete();
}
Future.delayed๋ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ๋ ๋๋ ์ด๊ฐ ์๊ธฐ๋ฏ๋ก ๋ชจ๋ฐฉํ๊ธฐ ์ํด ์ฃผ์๋ค.
neewItems์ ์ญํ์ ์๋๋ก ์คํฌ๋กค์ํด์ ๋ก๋ฉ๋ฐ๊ฐ ์๊ธด ํ ์๋ก์ด ์์ดํ ์ ์์ฑํด์ฃผ๊ธฐ ์ํด
์๋ก์ด ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด์ ๊ฐ์ ์ถ๊ฐํด์ฃผ์๋ค. ์ฌ๊ธฐ์ ์๋ก์ด ๋ฆฌ์คํธ๋ฅผ ๋ฃ์ด์ ๋ง๋ ์ด์ ๋ก๋
๋ด๊ฐ ์ฌ์ฉํ๋ hook์ ๋์์๋ฆฌ์ ์ฐ๊ด์ด ์๋ค.
์ ์ฝ๋์ ๋ณด๋ฉด ์ฃผ์์ฒ๋ฆฌํ
items.value.add({items.value.length + 1}.toString()); ๊ฐ ๋ณด์ผ ๊ฒ์ด๋ค.
์ฒ์์ ์๋ฌด์๊ฐ์์ด items๋ผ๋ List[]์๋ค๊ฐ addํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋๋ฐ,
๊ฐ์ ์ ์์ ์ผ๋ก ๋ณํ๋๋ฐ ๋ด๊ฐ ์๋์ผ๋ก hotReload๋ฅผ ํด์ฃผ์ด์ผ๋ง ํ๋ฉด์ด ์ฌ๊ฐฑ์ ๋๋ ๊ฒ์ด๋ค.
์ฆ, ๊ฐ์ด ๋ณํ๋ฉด hooks์ useState()๊ฐ ๋ณํ๋ฅผ ์๋์ผ๋ก ๊ฐ์งํด์ Rebuild๋ฅผ ํ์ง ๋ชปํ์๋ค.
๊ทธ๋์ ๊ทธ ์ด์ ๋ฅผ ์ฐพ์๋ณด๋
์ฆ, List[]์ add๋ฉ์๋๋ฅผ ํตํด์ ๊ฐ์ ์ถ๊ฐํด์ useState๋ ์ด๋ฅผ ์ํ๋ณํ๋ก ๊ฐ์ง ํ ์ ์๋ค๋ ์๋ฏธ์๋ค.
์ฆ ์ ๋ด์ฉ์ ๋ด๊ฐ ์์ ์ ๋ฐฐ์ด ๊น์๋ณต์ฌ(Deep copy)์ ์์ ๋ณต์ฌ(Shallow Copy)์ ๋ด์ฉ์ ์ดํดํ๋ฉด ์ ๋ณํ๊ฐ ์๋๋์ง
์ฝ๊ฒ ์ดํด ํ ์ ์์ ๊ฒ์ด๋ค.
์ฆ, List[]์ ๋จ์์ด add๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋๊ฒ์ ์์๋ณต์ฌ์ด๊ธฐ์ ์ฐธ์กฐ๊ฐ ๋ณ๊ฒฝ๋์ง ์์์ ์ํ๋ฅผ ๊ฐ์งํ์ง ๋ชปํด์ ์ฌ๋น๋๊ฐ
๋์ง ์๋ ๊ฒ์ด๋ค.
๊น์ ๋ณต์ฌ์ ์์๋ณต์ฌ์ ๊ด๋ จ๋ ๋ด์ฉ์ ์ฌ๊ธฐ์ ์ ๊ธฐ์๋ ๊ธธ์ด์ง ๊ฒ ๊ฐ์์ ์ถํ์ ๋ค๋ฃฐ ์์ ์ด๋ค.
๋ฌดํผ ์ด๋ ๊ฒ onRefresh()์ onLoading() ๋๊ฐ์ ํจ์ ์ค๋ช ์ ๊ฐ๋จํ๊ฒ ๋ง์ณค๋๋ฐ,
๋ง์ง๋ง์ผ๋ก
refreshController.refreshCompleted();
๋ ๋ฌด์์ธ์ง ์์๋ณด์.
์ ์ฝ๋๋ ๋ก๋ฉํ์ ๋ง์ง๋ง์ผ๋ก ๋ณด์ฌ์ค ๋ก๋ฉ๋ฐ๊ฐ ๋ฌด์์ธ์ง ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ์ด๋ค.
๋ง์ฝ Failed์ ํ์ํ๊ณ ์ถ๋ค๋ฉด ์๋ ์ฝ๋์ฒ๋ผ ๋ฐ๊พธ๋ฉด ๋๋ค.
refreshController.refreshFailed();
๋ ๋ง์ ๊ธฐ๋ฅ๋ค์ ์๋๋ฅผ ์ฐธ๊ณ ํ๊ธฐ ๋ฐ๋๋ค.
/// request complete,the header will enter complete state,
///
/// resetFooterState : it will set the footer state from noData to idle
void refreshCompleted({bool resetFooterState: false}) {
headerMode?.value = RefreshStatus.completed;
if (resetFooterState) {
resetNoData();
}
}
/// end twoLeveling,will return back first floor
Future<void>? twoLevelComplete(
{Duration duration: const Duration(milliseconds: 500),
Curve curve: Curves.linear}) {
headerMode?.value = RefreshStatus.twoLevelClosing;
WidgetsBinding.instance!.addPostFrameCallback((_) {
position!
.animateTo(0.0, duration: duration, curve: curve)
.whenComplete(() {
headerMode!.value = RefreshStatus.idle;
});
});
return null;
}
/// request failed,the header display failed state
void refreshFailed() {
headerMode?.value = RefreshStatus.failed;
}
/// not show success or failed, it will set header state to idle and spring back at once
void refreshToIdle() {
headerMode?.value = RefreshStatus.idle;
}
/// after data returned,set the footer state to idle
void loadComplete() {
// change state after ui update,else it will have a bug:twice loading
WidgetsBinding.instance!.addPostFrameCallback((_) {
footerMode?.value = LoadStatus.idle;
});
}
/// If catchError happen,you may call loadFailed indicate fetch data from network failed
void loadFailed() {
// change state after ui update,else it will have a bug:twice loading
WidgetsBinding.instance!.addPostFrameCallback((_) {
footerMode?.value = LoadStatus.failed;
});
}
/// load more success without error,but no data returned
void loadNoData() {
WidgetsBinding.instance!.addPostFrameCallback((_) {
footerMode?.value = LoadStatus.noMore;
});
}
/// reset footer noData state to idle
void resetNoData() {
if (footerMode?.value == LoadStatus.noMore) {
footerMode!.value = LoadStatus.idle;
}
}
๊ทธ๋ฆฌ๊ณ ์ถ๊ฐ์ ์ผ๋ก
header: const WaterDropMaterialHeader(),
header๋ฅผ ์ค์ ํ ์ ์๋ค.
์์ ๊ฐ์ด Pull-down์ ์ฌ๋ฏธ์๋ ์ ๋๋ฉ์ด์ ํจ๊ณผ๋ฅผ ์ถ๊ฐํ ์ ์๋ค.
์์ธํ ๋ด์ฉ์ pub.dev์ ์ ๋์์์ผ๋ ์ฐธ๊ณ ํ์
๊ทธ๋ฆฌ๊ณ ๋ง์ง๋ง์ผ๋ก footer๋ฅผ ์ค์ ํ ์ ๊ฐ ์๋ค.
๋ก๋ฉ์ ๋ณด์ฌ์ค ํ ์คํธ ๋ถ๋ถ์ ์ค์ ํ๋ ๋ถ๋ถ์ด๋ผ ์๊ฐํ๋ฉด ๋๋ค.
footer๋ฅผ ์ค์ ํ์ง ์์์ (๊ธฐ๋ณธ ํ ์คํธ๋ก ์ค์ ๋จ)
footer ์ ์ฉ์
footer: CustomFooter(
builder: (context, mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = const Text("์๋ก ๋๊ธฐ");
} else if (mode == LoadStatus.loading) {
body = const CupertinoActivityIndicator();
} else if (mode == LoadStatus.failed) {
body = const Text("๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ ์คํจ!");
} else if (mode == LoadStatus.canLoading) {
body = const Text("๋ ๋ง์ ๋ด์ฉ ๋ถ๋ฌ์ค๊ธฐ");
} else {
body = const Text("์ถ๊ฐํ ๋ฐ์ดํฐ ์์");
}
return SizedBox(
height: 55.0,
child: Center(child: body),
);
},
),
์ผ๋จ ์ด์ ๋๋ง ์๊ฒ ๋๋ค๋ฉด pull_to_refresh์ ๊ธฐ๋ณธ์ ์ธ ๊ธฐ๋ฅ๋ค์ ๋ค ์ฌ์ฉํ๋ค๊ณ ๋ณผ ์ ์์ ๊ฒ ๊ฐ๋ค.
๋ ์ ๋ฌธ์ ์ธ ๋ถ๋ถ์ (์๋ฅผ๋ค์ด ๋ค๊ตญ์ด ์ง์๋ถ๋ถ)์ ๊ฐ๋ฐ์ ํ์ด์ง๋ฅผ ์ฐธ๊ณ ํด์ ๊ฐ๋ฐํด๋ณด์.
์ถ๊ฐ์ ์ธ ์ง๋ฌธ์ ๋๊ธ์ ํตํด ๋ฌ์์ฃผ๊ธฐ ๋ฐ๋๋ค.
๊นํ๋ธ ํ์ฝ๋
https://github.com/baka9131/dartPackage-pullToRefresh-usage
์ ์ฒด ์ฝ๋
์ฝ๋ ๋ณด๊ธฐ
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
void main() {
runApp(
const MyApp(),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
);
}
}
class HomeScreen extends HookWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final items = useState(["1", "2", "3", "4", "5", "6", "7", "8"]);
// refresh ์ปจํธ๋กค๋ฌ
final refreshController = useState(RefreshController(initialRefresh: false)).value;
// ์์์ ์คํฌ๋กค
void onRefresh() async {
// ๋คํธ์ํฌ ๋ชจ๋ฐฉ ๋๋ ์ด
await Future.delayed(const Duration(milliseconds: 1000));
refreshController.refreshCompleted();
}
// ์๋์์ ์คํฌ๋กค (์๋ก์ด ๋ฐ์ดํฐ ๋ก๋ฉ)
void onLoading() async {
await Future.delayed(const Duration(milliseconds: 1000));
// items.value.add({items.value.length + 1}.toString());
final newItems = List<String>.from(items.value)..add((items.value.length + 1).toString());
items.value = newItems;
refreshController.loadNoData();
}
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Pull to refresh'),
titleTextStyle: const TextStyle(color: Colors.black),
),
body: SmartRefresher(
onRefresh: onRefresh,
onLoading: onLoading,
enablePullDown: true,
enablePullUp: true,
controller: refreshController,
header: const WaterDropMaterialHeader(),
footer: CustomFooter(
builder: (context, mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = const Text("์๋ก ๋๊ธฐ");
} else if (mode == LoadStatus.loading) {
body = const CupertinoActivityIndicator();
} else if (mode == LoadStatus.failed) {
body = const Text("๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ ์คํจ!");
} else if (mode == LoadStatus.canLoading) {
body = const Text("๋ ๋ง์ ๋ด์ฉ ๋ถ๋ฌ์ค๊ธฐ");
} else {
body = const Text("์ถ๊ฐํ ๋ฐ์ดํฐ ์์");
}
return SizedBox(
height: 55.0,
child: Center(child: body),
);
},
),
child: ListView.builder(
itemExtent: 50.0,
itemCount: items.value.length,
itemBuilder: (context, index) => Card(
child: Center(
child: Text(
items.value[index],
),
),
),
),
),
);
}
}