Never give up

Flutter - list with paging 본문

해왔던 삽질..

Flutter - list with paging

대기만성 개발자 2021. 2. 24. 16:21
반응형

여러개의 항목을 Listview를 이용해 출력을 할 때 고민이 생깁니다

 

한번에 많은 항목을 불러오고 보여주기를 하다보면 퍼포먼스에 문제가 생길 수도 있고

 

배터리 소모에도 영향을 주게됩니다

 

그래서 inifite loading 혹은 lazy loading등을 사용하게 되는데

 

보통 서버에 특정개수로 요청을 해서 여러번 가져오는 작업을 수행하는데

 

사용자 입장에서 끊기는 부분이 그렇게 좋지는 않을거 같다고 생각해서

 

한번에 100개정도씩 가져와서 분배해주면 어떨까 싶어서 만들게 되었고

 

Stream을 이용해서 직접 구현을 해봤습니다

 

loading class는 이전에 만들어논것을 그대로 사용했으니

 

예제를 실행해보실분은 로딩 포스트를 참조해주세요

(링크 : devmemory.tistory.com/33 )

 

Main

class LazyLoadingStream extends StatefulWidget {
  @override
  _LazyLoadingStreamState createState() => _LazyLoadingStreamState();
}

class _LazyLoadingStreamState extends State<LazyLoadingStream> {
  List<int> _itemList = [];
  ListData _data = ListData();
  int _indexPage = 0;
  ScrollController _controller = ScrollController();
  StreamController<List<int>> _streamController;
  LoadingHud _hud;

  @override
  void initState() {
    super.initState();
    _data.divideList();

    _itemList.addAll(_data.sortedList[_indexPage]);

    _streamController = StreamController<List<int>>()..add(_itemList);

    _hud = LoadingHud(context);

    _controller.addListener(_listener);
  }

  @override
  void dispose() {
    _streamController?.close();
    _controller?.dispose();
    super.dispose();
  }

  void _listener() {
    if (_controller.offset >= _controller.position.maxScrollExtent &&
        !_controller.position.outOfRange) {
      if (++_indexPage < _data.pageNum) {
        _hud.showHud();

        _itemList.addAll(_data.sortedList[_indexPage]);

        _streamController.add(_itemList);


        Future.delayed(Duration(milliseconds: 500)).whenComplete(() {
          _hud.hideHud();
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    print('Rebuild - build');
    return Scaffold(
        appBar: AppBar(title: Text('Lazy loading example')),
        body: Padding(
            padding: EdgeInsets.all(12),
            child: StreamBuilder<List<int>>(
              stream: _streamController.stream,
              builder: (context, snapshot) {
                print('Rebuild - streamBuilder');
                if(snapshot.hasData) {
                  return Scrollbar(
                    child: ListView.builder(
                        controller: _controller,
                        itemCount: snapshot.data.length,
                        itemBuilder: (context, index) {
                          return ScaleWidget(
                            showWidget: Card(
                              shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(16)),
                              elevation: 2,
                              child: ListTile(
                                leading:
                                CircleAvatar(
                                    child: Text((index + 1).toString())),
                                title: Text('Item - ${snapshot.data[index]}'),
                              ),
                            ),
                          );
                        }),
                  );
                }
                return Center(child: Text('Error'));
              }
            )));
  }
}

data

class ListData {
  List<int> _list = List.generate(90, (index) => index);
  List<List<int>> sortedList = [];
  int listLength;

  void divideList() {
    int total = (_list.length / 20).ceil();
    listLength = total;
    bool isFinished = (_list.length % 20 == 0);
    int lastIndex, start, end;

    if (isFinished) {
      lastIndex = _list.length % 20 + total * 20;
    } else {
      lastIndex = _list.length % 20 + (total - 1) * 20;
    }

    for (int i = 1; i <= total; i++) {
      start = (i - 1) * 20;
      end = i * 20;
      if (total == i) {
        sortedList.add(_list.sublist(start, lastIndex));
      } else {
        sortedList.add(_list.sublist(start, end));
      }
    }
  }
}

 

먼저 데이터 부분을 보면 _list를 20개씩 분배하는 작업이 있습니다

 

규칙성을 먼저 찾아보면 0 ~ 19, 20 ~ 39, 40 ~ 59

 

이걸 수식으로 표현해보면 (n-1) x 20 ~ 20n - 1로 표현이 되고

 

sublist 조건 때문에 20n - 1을 20n으로 처리합니다

(ex: sublist(0,20) = 0...19)

 

total값은 ceil을 이용해서 20으로 떨어지지 않으면 + 1을 해줍니다

(ex: total = 21 1 : 0...19, 2 : 20)

 

그리고 항상 리스트가 20개 단위로 떨어지지 않기 때문에

 

마지막 index부분을 처리해줘야됩니다

 

lastIndex = total % 20 + (index -1) x 20

ex) total = 90, lastIndex = 10 + (5 - 1) x 20 = 90

     total = 80, lastIndex = 0 + 4 x 20 = 80

 

간략하게 차트로 나타내보면

 

< 위 과정 차트 >

(분배과정은 더 좋은방법이 있을수도 있는데 필자가 생각나는대로 구현해봤습니다)

 

한번 출력해보면

//80일 때
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]]

//90일 때
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79],
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89]]

 

잘 분배된것을 확인해 볼 수 있습니다

 

이제 메인부분을 설명리자면

 

ListData class를 호출하면서 list generate가 작동하고

 

initState부분에서 divideList를 통해서 위와 같이 리스트를 분배해줍니다

 

그 후 StreamController 초기화 과정에서 첫번째 리스트(0...19)를 넣어줍니다

 

그리고 listview에서 마지막 항목을 보여줄때를 확인하기 위해 controller를 정의해줍니다

 

이미 이전에 다룬부분이므로 링크로 대체합니다

(링크 : devmemory.tistory.com/6)

 

그리고 listener에서 index를 비교해주고 조건에 맞으면

 

loading hud를 띄워준 후 list를 add해주고 StreamController를 이용해서 화면에 다시 뿌려줍니다

 

그 후 hud를 닫아주는데 delay를 준 이유는 loading이 되고있는지 확실히 표시해주기 위해서 사용했습니다

 

그 다음은 일반적인 StreamBuilder와 ScrollController의 설명이므로 자세한 설명은 생략하겠습니다

 

Scale Widget은 기존에 포스트했던 Animation을 이용해서 효과를 줘봤습니다

class ScaleWidget extends StatefulWidget {
  final Widget showWidget;

  ScaleWidget({this.showWidget});

  @override
  _ScaleWidgetState createState() => _ScaleWidgetState();
}

class _ScaleWidgetState extends State<ScaleWidget>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;

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

    _controller =
        AnimationController(vsync: this, duration: Duration(milliseconds: 800));

    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
        alignment: Alignment.bottomCenter,
        scale: Tween(begin: 0.0, end: 1.0).animate(
            CurvedAnimation(parent: _controller, curve: Curves.easeInOutExpo)),
        child: widget.showWidget);
  }
}

 

< 결과물 >

사실 builder를 사용하면 화면에 보이는 부분만 렌더링을 해주기 때문에

 

고작 이걸 위해 사용하는건 삽질이긴 한데..

 

shrinkWrap을 사용해서 만들때는 상황이 조금 달라집니다

 

필자의 2번째 앱이 그런식으로 되어있어서 이렇게 처리를 했는데 굳이 이럴필요 없습니다..

반응형
Comments