Flutter/Basic

[Flutter] Mixin으로 StatefulWidget의 중복 코드 제거하기(feat. 무한 스크롤)

seunghwaan 2023. 5. 24. 18:41
반응형

이번 포스트에서는 Mixin으로 StatefulWidget의 초기화 중복 코드를 제거하는 방법을 정리하겠습니다.

StatefulWidget의 Controller 중복 코드 발생

StatefulWidget에서 스크롤 또는 애니메이션 작업 등의 애플리케이션의 UI 상태와 연결하고 관리해야 하는 작업을 수행해야 할 때 Controller를 이용합니다. Controller의 종류로는 ScrollController, AnimationController, TextEditingController 등이 존재합니다.

 

예를 들면, ScrollController는 스크롤 상태를 추적하고 제어하는 데 사용됩니다. 이를 통해 스크롤 위치를 제어하고 스크롤 이벤트를 받을 수 있으므로 무한 스크롤, 페이징 또는 스크롤 위치에 따른 어떤 작업을 수행할 수 있습니다.

 

문제점은 이러한 Controller을 사용하려면 StatefulWidget의 initState() 메소드에서 리스너를 달아주어야 하고, dispose() 메소드에서 메모리 해제를 해주어야 합니다. 또한, scrollController를 이용하여 어떠한 로직을 수행했다면 그 로직을 수행하는 메소드도 작성해야합니다. 그렇기 때문에, 동일한 로직을 수행하는 다른 화면을 추가할 때 마다 보일러 플레이트 코드가 발생하게 됩니다.

Dart의 Mixin을 활용하여 보일러 플레이트 코드 제거하기

이러한 보일러 플레이트 코드는 Dart의 Mixin을 활용하면 이러한 반복 작업들을 없앨 수 있습니다.

 

Dart의 Mixin에 대한 내용은 아래 포스트에 자세하게 나와있습니다.

 

https://seosh817.tistory.com/184

 

[Dart] Mixin 알아보기 (feat. 다중 상속?)

이번 포스트에서는 Dart의 Mixin에 대해 정리하겠습니다. Mixin이란? Mixin이란 여러 클래스 계층에서 재사용할 수 있는 코드를 정의하는 방법입니다. 클래스에 with 키워드를 사용하여 Mixin을 추가하

seosh817.tistory.com

 

ScrollController를 이용한 무한 스크롤을 구현하는 예제를 통해 알아보겠습니다.

기존 코드 문제점

무한 스크롤 로직을 수행하는 _handleScroll() 메소드를 작성했습니다. ScrollController를 이용하여 scroll 위치에 도달하면 pageIndex를 증가시키고 아이템을 불러오는 로직으로 아래와 같이 무한 스크롤 코드를 작성할 수 있습니다.

void _handleScroll() async {
  double maxScroll = _scrollController.position.maxScrollExtent;
  double currentScroll = _scrollController.position.pixels;
  if (currentScroll >= maxScroll * _scrollThreshold && !_isLoading) {
    _pageIndex++; 
    await _loadMoreItems(_pageIndex);
  }
}

이 로직을 가지고 전체 코드는 아래와 같이 작성할 수 있습니다.

전체 코드

import 'package:flutter/material.dart';

class InfiniteScrollScreen extends StatefulWidget {
  @override
  _InfiniteScrollScreenState createState() => _InfiniteScrollScreenState();
}

class _InfiniteScrollScreenState extends State<InfiniteScrollScreen> {
  ScrollController _scrollController = ScrollController();
  List<Items> _items = [];

  double _scrollThreshold = 0.2;
  bool _isLoading = false;
  int _pageIndex = 1;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_handleScroll);
  }

  @override
  void dispose() {
    _scrollController.removeListener(_handleScroll);
    _scrollController.dispose();
    super.dispose();
  }

  void _handleScroll() async {
    double maxScroll = _scrollController.position.maxScrollExtent;
    double currentScroll = _scrollController.position.pixels;
    if (currentScroll >= maxScroll * _scrollThreshold && !_isLoading) {
      _pageIndex++; 
      await _loadMoreItems(_pageIndex);
    }
  }

  Future<void> _loadMoreItems(int pageIndex) async {
    // Load more items...
  }

  @override
  Widget build(BuildContext context) {
    return InfiniteScrollWidget();
  }
}

실행 화면

 

 

위 코드는 문제 없이 잘 동작합니다. 하지만, 무한 스크롤을 사용하는 화면을 추가해야한다고 가정하면 초기화, 메모리 해제, 무한 스크롤의 로직 코드(_handleScroll() 메소드) 세 개의 동일한 코드를 작성해야 한다는 문제점이 존재합니다.

Mixin을 활용하여 코드 개선

InfiniteScrollMixin라는 이름의 무한 스크롤 기능을 제공하는 Mixin을 만들어서 재사용 할 수 있도록 코드를 개선 해보겠습니다.

 

먼저, mixin을 생성하고 on 키워드를 이용하여 <T extends StatefulWidget> on State<T>로 범위를 제한해주도록 합니다.

mixin InfiniteScrollMixin<T extends StatefulWidget> on State<T> {}

범위를 State로 제한하면 InfiniteScrollMixin은 해당 State의 범위에 해당하는 클래스에서만 추가할 수 있습니다. 그리고 범위를 제한했으므로, InfiniteScrollMixin은 State의 메소드인 initState, dispose등의 State의 메소드를 사용할 수 있게 됩니다.

 

그러므로, 초기화 동작, 메모리 해제, 무한 스크롤 로직을 작성해줍니다. 또한, InfiniteScrollMixin로 옮겼으므로 loadMore을 콜백으로 만들고, 무한 스크롤 로직에서 조건이 충족되면 호출되게 만들어줍니다.

 

그렇게 작성한 InfiniteScrollMixin 코드는 아래와 같습니다.

import 'package:flutter/material.dart';

abstract class LoadEvent {
  Future<void> loadMore(int pageIndex);
}

mixin InfiniteScrollMixin<T extends StatefulWidget> on State<T> implements LoadEvent {

  ScrollController scrollController = ScrollController();

  double _scrollThreshold = 0.2;

  int pageIndex = 1;
  bool fetchDataLoading = false;

  @override
  void initState() {
    super.initState();
    scrollController.addListener(_handleScroll);
  }

  @override
  void dispose() {
    scrollController.removeListener(_handleScroll);
    scrollController.dispose();
    super.dispose();
  }

  void _handleScroll() async {
    double maxScroll = scrollController.position.maxScrollExtent;
    double currentScroll = scrollController.position.pixels;
    if (currentScroll >= maxScroll * _scrollThreshold && !fetchDataLoading) {
      pageIndex++;
      await loadMore(pageIndex);
    }
  }

  void refresh() async {
    pageIndex = 1;
    await loadMore(pageIndex);
  }
}

이제 InfiniteScrollMixin 코드를 작성하였으니 화면 코드에 mixin을 추가하겠습니다.

 

_InfiniteScrollScreenState 클래스에 with 키워드를 이용하여 InfiniteScrollMixin을 추가해줍니다. 또한, 콜백 함수의 loadMore() 함수를 구현해줍니다.

import 'package:flutter/material.dart';
import 'package:flutter_widgets/screen/infinite_scroll_mixin.dart';

class InfiniteScrollScreen extends StatefulWidget {
  @override
  _InfiniteScrollScreenState createState() => _InfiniteScrollScreenState();
}

class _InfiniteScrollScreenState extends State<InfiniteScrollScreen> with InfiniteScrollMixin {
  List<Items> _items = [];

  Future<void> _loadMoreItems(int pageIndex) async {
    // Load more items...
  }

  void refresh() async {
    pageIndex = 1;
    await _loadMoreItems(pageIndex);
  }

  @override
  Future<void> loadMore(int pageIndex) async {
    await _loadMoreItems(pageIndex);
  }
  
  @override
  Widget build(BuildContext context) {
    return InfiniteScrollWidget();
  }
}

화면의 상태 클래스에서 scrollController에 관련된 코드들이 전부 제거 되었지만 화면에서는 동일한 무한 스크롤 동작을 수행하게 됩니다.

 

여기서 중요한 점은 클래스에 with 키워드로 mixin을 추가한 것 만으로 무한 스크롤 로직을 추가할 수 있다는 점입니다. 그렇기 때문에 다른 무한 스크롤을 수행하는 화면을 만든다고 가정하면 scrollController에 대한 중복 코드 없이 화면의 상태 클래스에 mixin을 추가하는 것만으로도 무한 스크롤을 구현할 수 있습니다.

 

예를 들면, AnotherScreen을 추가한다고 가정하면 아래와 같이 with 키워드로 InfiniteScrollMixin을 추가해주고 데이터를 받아오는 로직만 구현해주면 됩니다.

import 'package:flutter/material.dart';
import 'package:flutter_widgets/screen/infinite_scroll_mixin.dart';

class AnotherScreen extends StatefulWidget {
  @override
  _AnotherScreenState createState() => _AnotherScreenState();
}

class _AnotherScreenState extends State<AnotherScreen> with InfiniteScrollMixin {
  List<Items> _items = [];

  Future<void> _loadMoreItems(int pageIndex) async {
    // Load more items...
  }

  void refresh() async {
    pageIndex = 1;
    await _loadMoreItems(pageIndex);
  }

  @override
  Future<void> loadMore(int pageIndex) async {
    await _loadMoreItems(pageIndex);
  }
  
  @override
  Widget build(BuildContext context) {
    return InfiniteScrollWidget();
  }
}

 

감사합니다!

References

https://dart.dev/language/mixins

 

Mixins

Learn how to add to features to a class in Dart.

dart.dev

https://seosh817.tistory.com/184

 

[Dart] Mixin 알아보기 (feat. 다중 상속?)

이번 포스트에서는 Dart의 Mixin에 대해 정리하겠습니다. Mixin이란? Mixin이란 여러 클래스 계층에서 재사용할 수 있는 코드를 정의하는 방법입니다. 클래스에 with 키워드를 사용하여 Mixin을 추가하

seosh817.tistory.com

https://medium.com/flutter-community/keeping-flutter-widgets-dry-with-dart-mixins-252cada41217

 

Keeping Flutter Widgets Dry with Dart Mixins

Using Dart mixins in Flutter apps: a practical example

medium.com

반응형