이번 포스트에서는 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