[Flutter] Mixin으로 StatefulWidget의 중복 코드 제거하기(feat. 무한 스크롤)
이번 포스트에서는 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
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
https://seosh817.tistory.com/184
https://medium.com/flutter-community/keeping-flutter-widgets-dry-with-dart-mixins-252cada41217