Flutter 상태관리 필승법 찾기
나는 Flutter 상태 관리 패키지 중 Provider를 선호하는데 상태 관리와 UI 간의 분리가 쉽기 때문에 구조적으로 설계를 할 수 있기 때문이다. 보통의 경우 Provider 폴더를 따로 만들고 Flutter의 상태 관리를 위해 사용되는 ChangeNotifier를 기반으로 한 데이터 모델 클래스를 정의한 후 ChangeNotifierProvider를 통해 등록하여 사용한다.
위젯 리빌드는 Consumer<T>는 T 타입의 상태가 변경될 때마다 빌드되는데 Provider클래스에서 notifyListeners()가 호출되면, 해당 상태를 구독하고 있는 모든 Consumer<T> 위젯은 다시 빌드된다. 나는 이러한 불필요한 위젯 리빌드를 방지하기 위해 Provider 패키지의 Selector를 사용하기도 하는데 Selector는 Flutter에서 효율적인 상태 관리를 위해 사용되는 위젯으로 특정 상태의 일부만 리스닝하도록 설정하여, 불필요한 리빌드를 방지한다. 쉽게 말해, 전체 상태 대신 상태 객체의 특정 속성만 관찰하여, 해당 속성이 변경될 때만 위젯을 다시 빌드한다는 뜻이다.
모든게 완벽한 Provider에서 딱 하나 불편한 점이 있다면 Provider로 관리하는 상태 객체 안에 있는 변수나 메서드에 접근하려면 context를 필수로 사용해야한다는 것이다. Provider를 사용하면 데이터(상태)를 위젯 트리의 특정 위치에 저장할 수 있고 자식 위젯들이 굳이 부모를 거치지 않아도 context를 통해 데이터에 바로 접근할 수 있다.
쉽게 말하자면 Flutter의 BuildContext는 위젯 트리에서 상태와 위젯 간의 연결 고리 역할을 한고 그를 통해 context는 위젯들이 어디에 있고, 어떤 데이터를 사용할 수 있는지 알려주는 역할을 하는데 현재 위젯의 위치와 관련된 정보를 제공하는 context를 통해 Provider가 저장한 데이터에 접근할 수 있게 된다.
그런데 문제되는 부분은 PageProvider부분인다. 현재 PageProvider에 현재 페이지를 저장하고 RouteObserverService 이용해서 추적하고 있는데 RouteObserverService는 위젯이 빌드되는 부분이 아니기 때문에 context가 필요없다. 다른 상태관리 패키지를 쓸까 하다가 너무 많은 패키지는 오히려 성능에 방해될것 같아서 context없이 Provider로 관리하는 상태 객체 안에 있는 변수에 데이터를 저장하는 로직을 짜보기로 하였다.
1. get_ it 패키지
https://pub.dev/packages/get_it
get_it | Dart package
Simple direct Service Locator that allows to decouple the interface from a concrete implementation and to access the concrete implementation from everywhere in your App"
pub.dev
1. 정의
GetIt은 서비스 로케이터 패턴을 구현한 패키지로 이 패턴은 전역적으로 객체를 관리하고 가져오는 도구이다. DI(Dependency Injection) 의존성 주입 Library로서 InheritedWidget 및 provider를 대체할 수 있으며 주된 사용으로는 REST API 클라이언트 등을 포함한 데이버베이스 서비스 객체에 접근해서 사용할 때, View, BLoC 등 여러가지 객체에 접근해야 될 때 사용된다.
Provider처럼 위젯 트리 안에서 데이터를 찾는 방식이 아니고 객체를 메모리에 등록해 두어, 위젯 트리와 독립적으로 관리하기 때문에 어디서든 바로 가져올 수 있다.
- 객체 등록
- setupLocator() 함수에서 locator.registerLazySingleton등을 사용해 객체를 등록하고 한번 생성된 객체는 앱 전체에서 재사용 가능하다.
- 객체 조회
- 필요할 때 locator<>()형태로 객체를 바로 가져오고 이때 GetIt은 내부에서 이미 생성된 객체를 반환해준다.
3. 등록 방식
- registerLazySingleton
- 객체를 필요할 때 처음 한 번만 생성한다.
- 이후에는 같은 인스턴스(객체)를 계속 재사용한다.
- 앱 전체에서 하나의 인스턴스만 유지해야 할 때 사용한다 (예: 네트워크 요청을 처리하는 API 서비스, 데이터베이스 관리 객체 등)
locator.registerLazySingleton(() => MyService());
- registerSingleton
- 즉시 객체를 생성해서 등록한다.
- 이후에도 같은 인스턴스(객체)를 계속 재사용한다.
- 앱 시작과 동시에 객체를 미리 준비해야 할 때 사용한다 (예: 앱 시작 시 초기화가 필요한 서비스)
locator.registerSingleton(MyService());
- registerFactory
- 요청할 때마다 새로운 객체를 생성한다.
- 매번 다른 인스턴스(객체)를 반환한다.
- 객체가 짧은 시간 동안만 사용되거나,매번 새로운 상태를 유지해야 할 때 사용한다. (예: 화면에서만 사용하는 임시 데이터 모델)
locator.registerFactory(() => MyService());
- registerSingletonWithDependencies
- 의존성이 있는 객체를 등록할 때 사용한다.
- 등록할 객체를 생성하기 전에, 다른 객체들을 먼저 준비할 수 있다.
- 객체를 생성하기 전에 다른 서비스나 객체가 미리 준비되어 있어야 할 때 사용한다.
locator.registerSingletonWithDependencies<MyService>(
() => MyService(locator<DependencyService>()),
dependsOn: [DependencyService],
);
- registerLazySingletonAsync / registerSingletonAsync
- 비동기적으로 객체를 생성할 때 사용한다. (예 : API 초기화, 데이터베이스 연결 등)
- 객체 생성 과정에서 비동기 작업이 필요한 경우 사용한다.
locator.registerLazySingletonAsync<MyService>(
() async {
final service = await MyService.init(); // 비동기 초기화
return service;
},
);
//registerLazySingletonAsync: 객체를 필요할 때 비동기로 생성.
//registerSingletonAsync: 앱 시작 시 비동기로 생성.
2. get_it과 Provider를 사용하여 로직 설계
1. Get It의 싱글톤(Singleton) 인스턴스를 가져오기
싱글톤(Singleton)이란?
싱글톤은 앱 전체에서 하나의 객체만 생성하고, 이를 어디서든 공유할 수 있는 디자인 패턴이다.
final locator = GetIt.instance;
GetIt.instance를 호출하여 이미 존재하는 인스턴스를 반환하거나, 없으면 새로 생성해서 반환하게 한다.
2. setupLocator 함수 호출
void setupLocator() {
locator.registerLazySingleton(() => PageRouteProvider());
}
setupLocator 호출 하여 객체들을 GetIt에 등록해 앱의 전역 저장소 에 저장한다.
3. Provider 등록
ChangeNotifierProvider<PageRouteProvider>(
create: (context) => locator<PageRouteProvider>(),
),
3. 최종코드
final locator = GetIt.instance;
void setupLocator() {
locator.registerLazySingleton(() => PageRouteProvider());
}
DefaultTabController(
initialIndex: 0,
length: 5,
child: Scaffold(
resizeToAvoidBottomInset: true,
body: Navigator(
key: _navigatorKey,
initialRoute: _routeNames[0],
onGenerateRoute: _onGenerateRoute,
observers: [RouteObserverService()],
),
),
)
import 'package:flutter/material.dart';
import '../provider/page_route_provider.dart';
import 'locator.dart';
class RouteObserverService extends RouteObserver<PageRoute<dynamic>> {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
_saveScreenView(route, previousRoute);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPop(route, previousRoute);
_saveScreenView(previousRoute, route);
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
_saveScreenView(newRoute, oldRoute);
}
void _saveScreenView(Route<dynamic>? newRoute, Route<dynamic>? oldRoute) {
if (newRoute is PageRoute) {
WidgetsBinding.instance.addPostFrameCallback((_) {
locator<PageRouteProvider>().currentPageRoute = newRoute;
});
}
}
}
import 'package:flutter/material.dart';
class PageRouteProvider extends ChangeNotifier {
//싱글턴 패턴을 적용하여 하나의 객체만 사용하여 공유
static final PageRouteProvider _instance = PageRouteProvider._internal();
factory PageRouteProvider() => _instance;
PageRouteProvider._internal();
String? _currentRoute;
PageRoute<dynamic>? _currentPageRoute;
String? get currentRoute => _currentRoute;
PageRoute<dynamic>? get currentPageRoute => _currentPageRoute;
set currentPageRoute(PageRoute<dynamic>? _previousPageRoute) {
_currentPageRoute = _previousPageRoute;
_currentRoute = currentPageRoute!.settings.name;
notifyListeners();
}
int? get getCurrentPageNum {
switch (_currentRoute) {
case '/':
return 0;
case '/search':
return 1;
case '/add':
return 2;
case '/bookmark':
return 3;
case '/user':
return 4;
default:
return null;
}
}
}
끝으로
페이지 추적부분에서 애를 먹었다. 아무래도 계속 pushNavigator만 할 경우 스택이 쌓여서 결국 과부화가 일어날게 뻔하니 좀 더 깔끔하게 만들어야했다. 현재는 위의 로직으로 같은 페이지 내에서 bottom appbar tab시 계속 페이지가 Push되는걸 막았고 RouteObserverService를 이용하여 이전 페이지 추적 기능 까지는 구현할 수 있었다. 지금은 bottom appbar를 누르면 모든 페이지가 제거되어서 상태 유지가 안되고 있는데 이 후에는 페이지 상태 유지 부분을 중점으로 방향을 잡고 리팩토링 해나아가야겠다.
'개발 > flutter' 카테고리의 다른 글
[Flutter] draggable_list 패키지 개발 (0) | 2025.01.24 |
---|---|
[Flutter] 기본 패키지 불러오지 못할 때 발생한 에러 해결 방법 (0) | 2025.01.19 |
[Flutter] cached_network_image (1) | 2025.01.17 |
[Flutter] Supabase Realtime (0) | 2025.01.16 |
[Flutter] 상태 관리 패키지 (0) | 2024.09.15 |