예전에도 이와 비슷한 글을 올린 적이 있다.
이 SmartRefresher를 이용해서 구현했었는데,
물론 위에 방법이 더 쉽지만,
이번에는 provider와 같이 쓰면서 이해하기로 해서
이번에도 코딩 팩토리님의 강의를 듣고,
한번 더 infinite scroll 배운 내용을 정리해 보고자 한다.
코틀린이나 자바를 사용할 때도 ViewPager 때문에
많이 애를 먹었는데,
역시나 몇번을 만들어도 어렵다..
그래도 예전보다는 많이 이해한 느낌이다. 기분이 좋다.
코딩 팩토리님의 코드를 그대로 가져왔고, 코드 하나하나 정리해보고자 한다.
우선, 아래 링크에서 provider 패키지를 다운로드하자.
https://pub.dev/packages/provider/example
provider를 이용해 만들어 보려고 한다.
우선 앱 자체에 프로바이더를 설정해주자.
나 같은 경우 연습을 위해 MultiProvider를 사용해 만들었다.
추후에 앱을 만들 때 여러 provider를 쓸 것을 우려해 적용하였다.
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => InfiniteProvider())
],
child: MyApp()
));
}
그리고 Provider 클래스를 만들어 주고, 다음 코드를 넣어주었다.
import 'package:flutter/cupertino.dart';
class InfiniteProvider extends ChangeNotifier {
List<int> cache = [];
// 로딩
bool loading = false;
// 아이템이 더 있는지
bool hasMore = true;
_makeRequest({
@required int nextId
}) async{
assert(nextId != null);
await Future.delayed(Duration(seconds: 1));
//Item은 99까지 있다.
if(nextId == 100){
return [];
}
return List.generate(20, (index) {
return nextId + index;});
}
fetchItems({
int nextId
}) async{
loading = true;
notifyListeners();
final items = await _makeRequest(nextId: nextId ?? 0);
this.cache = [
...this.cache,
...items
];
if(items.length == 0){
hasMore = false;
}
loading = false;
notifyListeners();
}
}
코드를 하나하나 보면서 설명해 나가며 풀어보려 한다.
우선 ChangeNotifier를 상속하여,
notifyListeners()를 쓸 준비를 해두었다.
모르는 분들을 위해 설명하자면, notifyListeners()는 build에서 setState()와
마찬가지라고 할 수 있다.
Provider에 대한 자세한 내용은 예전에 글을 쓴 것이 있어
먼저 익히고 오면 더 이해하기 쉬울 것 같다.
위에서부터 하나하나 살펴보겠다.
List<int> cache = [];
// 로딩
bool loading = false;
// 아이템이 더 있는지
bool hasMore = true;
provider의 특징은 변수에 넣어둔 값을 어느 곳에서나
사용할 수 있다는 점이다.
List <int> cache는 리스트에 아이템 index값을 담아주기 위해 만들어졌다.
아래 loading은 로딩 중인지 아닌지 체크용이고,
hasMore은 더 가져올 아이템이 있는지에 대한 체크이다.
_makeRequest({
@required int nextId
}) async{
assert(nextId != null);
await Future.delayed(Duration(seconds: 1));
//Item은 99까지 있다.
if(nextId == 100){
return [];
}
return List.generate(20, (index) => nextId + index);
}
fetchItems 메서드에서 호출되는 _makeRequest 메서드이다.
멤버 변수로 nextId(리스트 개수)를 받는다.
assert(nextId!= null)을 선언해
nextId는 날이면 안된다고 조건을 준다.
assert는 생소하겠지만,
이 함수는 debug(개발중일 때)에만 조건이 성립되면,
조건에 맞춰 실행되지만,
실제 배포 버전에서는 적용되지 않는다.
await Future.delayed(Duration(seconds: 1))로
1초를 기다린 후에
return List.generate(20, (index) => nextId + index);
함수를 실행하여, 처음에는 nextId값이 0으로 넘어와
index값에 개수만큼 리스트를 반환한다.
index는 현재 있는 List에 값이다.(초기 값은 20)
그리고
if(nextId == 100){
retrunt [];
}
조건문이 있는데,
이는 임의로 nextId가 100일 때,
아이템이 없는 상황을 보기 위해 빈 리스트 값을 반환하도록 하였다.
fetchItems({
int nextId
}) async{
loading = true;
notifyListeners();
final items = await _makeRequest(nextId: nextId ?? 0);
this.cache = [
...this.cache,
...items
];
if(items.length == 0){
hasMore = false;
}
loading = false;
notifyListeners();
}
그다음은 fetchItems 메서드이다.
infinite_page에서 첫 호출되며,
_makeRequest를 통해 리스트 값을 전달받고,
infinite_page로 넘겨준다.
이 메서드가 실행되면 제일 처음,
loading 불리언을 true로 만들어주고 notifyListeners 메서드를 호출하여,
infinite_page에 ui를 업데이트시켜준다.(로딩 중으로)
그 후 _makeRequest에 멤버 변수로 받은 nextId값을 넘겨주어
items 변수에 넣어준다(처음 nextId의 값은 넘겨받지 않는다. 고로 null값이 와 기본값으로 0이 적용된다.).
그리고 chche 전역 변수 리스트에
현재 가지고 있는 리스트 => this.cache와
방금 담아온 items를 리스트를 차례대로 넣는다.
여기서...(three dot) 점 세 개 또한 생소할 수 있다.
리스트에 보통 이어서 값을 추가해 넣을 때,
이처럼 점 세 개를 앞에 붙여주며 넣는다.
그 후에
loading 불리언을 false로 바꾸어주고, 똑같이
notifyListeners 메서드를 호출해 로딩을 멈추어 준다.
만약 if(items.length == 0)
받아온 아이템이 0이면
아이템이 더 이상 없다는 의미이므로(코드 상에는 아이템 개수가 100이 넘으면)
hasMore을 false로 만들어, 더 이상 값이 없음을 전달해준다.
이제 infinite_page를 살펴보자.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:test2/provider/Infinite_provider.dart';
import 'package:provider/provider.dart';
class InfinitePage extends StatefulWidget {
@override
_InfinitePageState createState() => _InfinitePageState();
}
class _InfinitePageState extends State<InfinitePage> {
@override
initState() {
super.initState();
Future.microtask(() =>Provider.of<InfiniteProvider>(context, listen: false).fetchItems());
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
child: _renderListView()
),
),
);
}
_renderListView() {
final provider = Provider.of<InfiniteProvider>(context);
final cache = provider.cache;
final loading = provider.loading;
//로딩중이면서 캐시에 아무것도 없음
if(loading && cache.length == 0){
return Center(
child: CircularProgressIndicator()
);
}
//로딩중이 아닌데 캐시에 아무것도 없음
//아무것도 가져올 아이템이 없을때
if(!loading && cache.length == 0){
return Center(
child: Text('아이템이 없습니다.')
);
}
return ListView.builder(
itemCount: cache.length + 1,
itemBuilder: (context, index){
print('넘어온 인덱스');
print(index);
print(cache);
if (index < cache.length){
return ListTile(
title: Text(
cache[index].toString()
)
);
}
if(!provider.loading && provider.hasMore){
Future.microtask(() => provider.fetchItems(nextId: index));
}
if(provider.hasMore) {
return Center(
child: CircularProgressIndicator()
);
}else{
return Center(
child: Text('더 이상 아이템이 없습니다.')
);
}
}
);
}
}
로딩 페이지가 실행되는 곳이다.
제일 먼저 앱이 먼저 실행되었을 때,
initState()가 호출이 된다.
여기서 InfiniteProvider에 있는 fetchItems 메서드를 실행해준다.
초깃값을 가져와야 하기 때문이다.
@override
initState() {
super.initState();
Future.microtask(() =>Provider.of<InfiniteProvider>(context, listen: false).fetchItems());
}
여기서 살펴볼 것이 있다.
먼저 Future.microtask 함수를 붙여서 fetchItems 메서드를 호출한 이유는
앱이 실행되면 initState -> build 순으로 호출하게 되는데,
fetchItems안에 notifyListeners는 build안에 setState와 같은 함수라고 볼 수 있다.
(setState는 빌드 안에 또 setState 된 부분만 다시 빌드가 된다.)
한 가지 플러터 앱을 만들면서 지켜야 할 것이 있는데,
build가 되기 전에는 setState가 호출이 되면 안 된다.
오류가 뜨기 때문이다.
고로, initState에서 처음 데이터를 가져오되
build보다 늦게 실행이 되어야 하기 때문에
Future.microtask로 잠깐의 딜레이를 주고, build가 호출된
다음에 호출이 되게 해준 것이다.
이밖에도
Future.delayed(Duration.zero, () =>...) 함수를 사용해도 된다.
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
child: _renderListView()
),
),
);
}
앱이 사용자에게 잘 보이게 영역을 설정해주는 SafeArea위젯 안에
Container 안에 _renderListView를 호출해주었다.
그러면 아래 _renderListView가 호출된다.
_renderListView() {
final provider = Provider.of<InfiniteProvider>(context);
final cache = provider.cache;
final loading = provider.loading;
//로딩중이면서 캐시에 아무것도 없음
if(loading && cache.length == 0){
return Center(
child: CircularProgressIndicator()
);
}
//로딩중이 아닌데 캐시에 아무것도 없음
//아무것도 가져올 아이템이 없을때
if(!loading && cache.length == 0){
return Center(
child: Text('아이템이 없습니다.')
);
}
return ListView.builder(
itemCount: cache.length + 1,
itemBuilder: (context, index){
if (index < cache.length){
return ListTile(
title: Text(
cache[index].toString()
)
);
}
if(!provider.loading && provider.hasMore){
Future.microtask(() => provider.fetchItems(nextId: index));
}
if(provider.hasMore) {
return Center(
child: CircularProgressIndicator()
);
}else{
return Center(
child: Text('더 이상 아이템이 없습니다.')
);
}
}
);
}
위에서부터 차근차근 봐보자.
provider를 초기화해주고,
그 안에 cache와 loading을 참조해 변수에 넣어준다.
provider 사용 시 이런 식으로 변수를 넣었다가
빼서 사용할 수 있다.
로딩 중이면서 캐시에 아무것도 없을 때는
로딩 창을 호출해준다.
로딩 중이 아닌데 캐시에 아무것도 없을 때는
아이템이 없다는 것이므로,
앱 가운데에다가 '아이템이 없습니다'
텍스트를 넣어준다.
return ListView.builder(
itemCount: cache.length + 1,
itemBuilder: (context, index){
if (index < cache.length){
return ListTile(
title: Text(
cache[index].toString()
)
);
}
if(!provider.loading && provider.hasMore){
Future.microtask(() => provider.fetchItems(nextId: index));
}
if(provider.hasMore) {
return Center(
child: CircularProgressIndicator()
);
}else{
return Center(
child: Text('더 이상 아이템이 없습니다.')
);
}
}
);
이 부분이 중요하니 집중해서 봐보자
ListView.builder를 통해 리스트를 구현해준다.
itemCount(리스트의 개수)를 넣어준다.
cache의 개수를 넣어주고, + 1을 해준다.
+1을 해주는 이유는
provider에서 fetchItems 메서드를 호출하게 되면
index: 18
cache: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
index와 cache값이 이런 식으로 찍히게 되는데,
index값이 cache값보다 작을 때, (provider에 담긴 cache 리스트보다 현재 리스트 아이템들이 작을 때)
만 리스트를 추가해준다.
만약 아이템들을 더 가져오게 되어 index값이 cache값의 개수와 같아지면
데이터를 더 가져와준다.
그 사이가 1 차이가 나서 넣어준 것이다.
로딩 중이 아니고, 데이터가 더 있다면,
데이터를 더 가져오는 메서드가 실행되고,
동시에 값이 있으면 로딩 중 없으면,
더 이상 아이템이 없다를 반환해준다.
뒤죽박죽 정리지만, 도움이 되길 바란다.