Flutter

[Flutter] 스크롤 위치 파악 & 무한 스크롤

hminor 2023. 8. 10. 15:04

스크롤 위치 파악하는 법과 더보기 요청

  • 문자 중간에 변수를 넣는 방법
    • React와 같이 ${} 사이에 변수를 넣어주게 되면 된다.
    • Text('좋아요 ${homeData[idx]['likes'].toString()}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.*w700*),),
  • 무한 스크롤 기능 구현
    • 인스타나 페이스북과 같이 스크롤이 끝까지 내려가게 되면 서버에서 다시 데이터를 받아와서 끝없는 사용 경험을 줄 수 있도록 하기.
    • 순서
      • 스크롤바 높이 측정하기
        • import 'package:flutter/rendering.dart'; ← 스크롤 관련 유용한 함수가 있음
        • 현재 LiswView 위젯에 서버로부터 받아온 데이터를 뿌리고 있기에 LiswView의 높이를 측정하려 하는데
          • 스크롤바 높이를 측정하기 위해선 StatefulWidget이어야 한다.
          • 그래서 변경하게 되는데 부모에서 보낸 state의 등록은 첫 클래스에서 하고 사용은 두 번째 클래스에서 해야한다.
          • 다만 두 번째 클래스에서 그냥 사용할 수는 없기에 widget을 앞에 붙여줘야 한다.
          • ex) widget.homeData
          class Home extends StatefulWidget { // <-- 첫 번째 클래스에서 등록
            Home({super.key, this.homeData});
            final homeData;
          
            @override
            State<Home> createState() => _HomeState();
          }
          
          class _HomeState extends State<Home> { // <-- 두 번째 클래스에서 사용
            @override
            Widget build(BuildContext context) {
              if (widget.homeData.isNotEmpty){ // <-- widget.homeData
                return ListView.builder(
                  itemCount: widget.homeData.length,
                  itemBuilder: (context, idx){
                    return Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Image.network(widget.homeData[idx]['image']),
                        Container(
                          margin: EdgeInsets.fromLTRB(10, 20, 10, 20),
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Row(
                                children: [
                                  Text('좋아요 ${widget.homeData[idx]['likes'].toString()}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),),
                                ],
                              ),
                              Text(widget.homeData[idx]['date'].toString()),
                              Text(widget.homeData[idx]['content'].toString()),
                            ],
                          ),
                        )
                      ],
                    );
                  },
                );
              } else {
                return Text('빈');
              }
            }
          }
          
      • 그리고 변수에 ScrollController() 를 담아주기
        • ScrollController는 자료를 저장해주는 클래스로 스크롤 정보를 저장해준다.
      • 이후 스크롤을 사용하는 위젯의 controller에 변수를 넣어주기!
      class Home extends StatefulWidget {
        Home({super.key, this.homeData});
        final homeData;
      
        @override
        State<Home> createState() => _HomeState();
      }
      
      class _HomeState extends State<Home> {
      
        // ScrollController() 는 
        // 자료를 저장해주는 클래스로 스크롤 정보를 저장해준다.
        var scroll = ScrollController();
      
        @override
        Widget build(BuildContext context) {
          if (widget.homeData.isNotEmpty){
            return ListView.builder(
              controller: scroll,
              itemCount: widget.homeData.length,
              itemBuilder: (context, idx){
                return Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Image.network(widget.homeData[idx]['image']),
                    Container(
                      margin: EdgeInsets.fromLTRB(10, 20, 10, 20),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            children: [
                              Text('좋아요 ${widget.homeData[idx]['likes'].toString()}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),),
                            ],
                          ),
                          Text(widget.homeData[idx]['date'].toString()),
                          Text(widget.homeData[idx]['content'].toString()),
                        ],
                      ),
                    )
                  ],
                );
              },
            );
          } else {
            return Text('빈');
          }
        }
      }
      
  • 이제 무한 스크롤을 위해 스크롤 위치를 측정하면서 스크롤의 끝인지에 대해 판단을 할 수 있도록 하기
    • 해당 기능을 하기 위해선 첫 로딩시부터 변경될 마다 계속 해줘야 하기에
    • initstate를 생성 후 만들어 둔 변수 scroll에 리스너를 달아서 변수가 변경될 때 마다 실행되도록 하기
    • 주의점으로 리스너는 더 이상 필요가 없다면 제거하는 것이 성능에 좋다고 한다.
    class Home extends StatefulWidget {
      Home({super.key, this.homeData});
      final homeData;
    
      @override
      State<Home> createState() => _HomeState();
    }
    
    class _HomeState extends State<Home> {
    
      // ScrollController() 는 자료를 저장해주는 것으로, 클래스가 된다. 그래서 스크롤 정보를 저장해주는 것을 하는데 도움을 준다
      var scroll = ScrollController();
    
      @override
      void initState() {
        super.initState();
        scroll.addListener(() {
    
        });
      }
    
      @override
      Widget build(BuildContext context) {
        if (widget.homeData.isNotEmpty){
          return ListView.builder(
            controller: scroll,
            itemCount: widget.homeData.length,
            itemBuilder: (context, idx){
              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Image.network(widget.homeData[idx]['image']),
                  Container(
                    margin: EdgeInsets.fromLTRB(10, 20, 10, 20),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            Text('좋아요 ${widget.homeData[idx]['likes'].toString()}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),),
                          ],
                        ),
                        Text(widget.homeData[idx]['date'].toString()),
                        Text(widget.homeData[idx]['content'].toString()),
                      ],
                    ),
                  )
                ],
              );
            },
          );
        } else {
          return Text('빈');
        }
      }
    }
    
    • 그래서 우선 출력문으로 현재 위치를 알고자 한다면
      • print(scroll.position.pixels)
    • 스크롤 방향
      • scroll.position.userScrollDirection
    • 최대 스크롤 가능 높이는 아래와 같이 작성하면 알 수 있다.
      • scroll.position.maxScrollExtent
  • 이제 조건문을 사용해서 끝에 도달했다면이라는 조건을 준다면 아래와 같다
  • @override void initState() { super.initState(); scroll.addListener(() { if (scroll.position.pixels == scroll.position.maxScrollExtent) { print('같당'); } }); }

숙제

무한 스크롤을 구현하기

그리고 응용 과제

  • 처음으로 끝에 도달했을 땐 more1.json
  • 두 번째로 끝에 도달했을 땐 more2.json 에 요청해서 게시글 가져오기

응용 과제로는 그냥 변수 하나를 more${변수} 에 담아서 요청하고 성공시에만 게시글 변수에 추가하는 함수를 실행시키도록 했음

import 'dart:js_interop';

import 'package:flutter/material.dart';
import './style.dart' as style;
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter/rendering.dart';

void main() {
  runApp(
    MaterialApp(
      theme: style.theme,
      home: MyApp()
    )
  );
}

class MyApp extends StatefulWidget {
  MyApp({super.key});
  
  @override
  State createState() => _MyAppState();
}

class _MyAppState extends State {

  var homeData = [];

  appendData(value){
    setState(() {
      homeData.add(value);
    });
  }
  
  getData() async{
    var result = await http.get(Uri.parse('<주소>'));
    if (result.statusCode == 200) {
      setState(() {
        homeData = jsonDecode(result.body);
      });
    } else {
      print('실패');
    }
  }

  @override
  void initState() {
    super.initState();
    getData();
  }

  // 0: home, 1: shop
  var tab = 0;

  changeTab(tabNumber){
    setState(() {
      tab = tabNumber;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Instagram'),
          actions: [
            IconButton(
              onPressed: (){},
              icon: Icon(Icons.add_box_outlined)
            )
          ],
        ),
      body: [Home(homeData:homeData, appendData:appendData),Text('shop')][tab],
      bottomNavigationBar: BottomNavigationBar(
        onTap: (i){
          changeTab(i);
        },
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home_outlined),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.shopping_bag_outlined),
            label: 'Shopping',
          ),
        ],
        showSelectedLabels: false, // 선택된 아이템의 레이블 숨김
        showUnselectedLabels: false, // 선택되지 않은 아이템의 레이블 숨김
      ),
    );
  }
}

class Home extends StatefulWidget {
  Home({super.key, this.homeData, this.appendData});
  final homeData;
  final appendData;

  @override
  State createState() => _HomeState();
}

class _HomeState extends State {

  // ScrollController() 는 자료를 저장해주는 것으로, 클래스가 된다. 그래서 스크롤 정보를 저장해주는 것을 하는데 도움을 준다
  var scroll = ScrollController();
  var cnt = 0;
  getAddData() async{
    cnt ++;
    print(cnt);
    var data = await http.get(Uri.parse('<주소/more${cnt}.json>'));
    if (data.statusCode == 200){
      widget.appendData(jsonDecode(data.body));
    } else {
      print('더 이상 없어');
    }
  }
  
  @override
  void initState() {
    super.initState();
    scroll.addListener(() {
      if (scroll.position.pixels == scroll.position.maxScrollExtent) {
        getAddData();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    if (widget.homeData.isNotEmpty){
      return ListView.builder(
        controller: scroll,
        itemCount: widget.homeData.length,
        itemBuilder: (context, idx){
          return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Image.network(widget.homeData[idx]['image']),
              Container(
                margin: EdgeInsets.fromLTRB(10, 20, 10, 20),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Text('좋아요 ${widget.homeData[idx]['likes'].toString()}', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),),
                      ],
                    ),
                    Text(widget.homeData[idx]['date'].toString()),
                    Text(widget.homeData[idx]['content'].toString()),
                  ],
                ),
              )
            ],
          );
        },
      );
    } else {
      return Text('빈');
    }
  }
}