[TIL] Flutter - Provider 전역 상태 관리

jeongjwon·2023년 12월 1일
1

이론

목록 보기
15/19

이전 블로그에서는 Stateless WidgetStateful Widget 에 대해 알아보았다.

상태가 존재하는 Stateful Widget 에서 상태가 변경되면 rebuild 함으로써 UI 도 업데이트하게 된다.

그렇다면, 동일한 상태를 여러 위젯에서 사용하고자 할 때는 부모 위젯과 자식 위젯(공유 상위 위젯) 간에 상태를 공유하면서 rebuild 하는 과정이 불필요하게 많아지게 되고 낮은 성능과 유지보수가 많아지게 된다.

이 문제를 해결하기 위해 상태를 전역으로 관리하는 Provider 를 사용한다.




Provider

Provider 은 크게 생성 부분과 소비 부분으로 나눈다.
생성 부분 (공급자) 은 사용할 데이터 타입을 결정하고 해당 데이터에 대한 Provider 를 생성한다.
소비 부분 (소비자) 은 생성된 Provider 를 통해 데이터를 불러오거나 업데이트하는 등의 작업을 수행한다.



Provider 패키지 설치

flutter pub add provier 명령어를 통해 Provider 패키지를 설치한다.
pubspec.yaml 파일에 추가된 패키지를 볼 수 있다.

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.3.2



Provider 생성

import 'package:flutter/material.dart';

class AppState with ChangeNotifier {
  String _username = '';

  String get username => _username;

  void setUserName(String newUsername) {
    _username = newUsername;
    notifyListeners();
  }
}

상태 변수를 정의하고 상태를 변경하는 getter 를 이용한다.
ChangeNotifer 를 상속하고, notifyListeners() 를 호출하여 상태 변경을 알린다.
ChangeNotifierProvider 가 상태 변경을 감지하고 자동으로 위젯 트리(UI)를 업데이트되기 때문에 편리하게 상태를 관리할 수 있다.





Provider 소비

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state.dart'; // 생성한 모델 클래스의 파일

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => AppState(),
      child: MaterialApp(
        title: 'Flutter Provider Example',
        home: MyHomePage(),
      ),
    );
  }
}

먼저 main.dart 파일에서 ChangeNotifierProvider 를 사용하여 전역 상태를 제공한다.



Note! 여러 개의 ChangeNotifierProvider 를 사용하려면 MultiProvider 로 단일 위젯으로 감싸 이를 통해 각각의 프로바이더를 관리할 수 있게 해준다.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state1.dart'; // 첫 번째 모델 클래스의 파일
import 'app_state2.dart'; // 두 번째 모델 클래스의 파일

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return 
    //두 개 이상의 ChangeNotifierProvider 일 경우
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => AppState1()),
        ChangeNotifierProvider(create: (context) => AppState2()),
      ],
      child: MaterialApp(
        title: 'Flutter MultiProvider Example',
        home: MyHomePage(),
      ),
    );
  }
}



Provider.of

class MyHomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // Provider.of를 사용하여 상태에 접근
    final appState = Provider.of<AppState>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Provider Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Username: ${appState.username}'),
            TextField(
              onChanged: (newUsername) {
                // Provider.of를 사용하여 상태 업데이트
                appState.setUserName(newUsername);
              },
            ),
          ],
        ),
      ),
    );
  }
}

가장 기본적이고 간단한 형태의 상태 소비 방법이다.

Provider.of 를 사용하여 위젯 트리 상에서 가장 가까운 ChangeNotifierProvider 찾아
해당 모델 클래스에 접근할 수 있다. (Provider.of<AppState>(context); )
위젯 트리의 어느곳에서든 사용 가능하지만, 주로 build 메서드 내에서 사용된다.

다만, 이 방식은 해당 위젯에서 상태에 접근하므로, 해당 위치에 위젯이 rebuild 될 때마다 새로운 인스턴스가 생성될 수 있다.

이 경우, rebuild 에 영향을 받지 않는 방법으로 Consumer 을 사용한다.



Consumer

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state.dart'; // 생성한 모델 클래스의 파일

class MyHomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Provider Example'),
      ),
      body: Center(
        child: Consumer<AppState>(
          builder: (context, appState, child) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Username: ${appState.username}'),
                TextField(
                  onChanged: (newUsername) {
                    appState.setUserName(newUsername);
                  },
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

Consumer 위젯은 AppState 의 인스턴스를 감지하고, builder 함수를 호출하여 해당 상태가 변경될 때마다 UI 를 업데이트 한다.
이때 appState 매개변수를 통해 state 인 username 을 가져와서 화면에 표시하고, TextField 의 onChanged 콜백에서 setUserName 메서드를 호출하여 상태를 업데이트할 수 있다.

Consumer 는 효율적으로 필요한 부분만 rebuild 되기 때문에 특정 부분만 업데이트 되어 성능을 더욱 향상시킬 수 있다.(특정 상태를 감시하고 해당 상태가 변경될 때만)



read, watch

class MyHomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Provider Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // watch를 사용하여 상태 감시
            Text('Username: ${context.watch<AppState>().username}'),

            // read를 사용하여 상태 읽기
            TextField(
              onChanged: (newUsername) {
                // read를 사용하여 상태 업데이트
                context.read<AppState>().setUserName(newUsername);
              },
            ),
          ],
        ),
      ),
    );
  }
}

read 는 간단한 읽기, watch 는 읽기와 동시에 해당 Provider 가 변경될 때 자동으로 rebuild 한다.
주로 함수형 위젯 내에 사용되며, read 는 어디서든 사용가능하나 watch 는 주로 build 메서드 내에서 사용되어 변경될 때만 rebuild 되어 성능 상 이점이 있다.



종합적으로,

  • 간편한 사용과 특정 위치에서만 사용해야하는 경우에는 Provider.of 를 선택한다.
  • 리빌드 효율성이 중요하고, 특정 Provider 에 의존하는 경우 Consumer를 사용한다.
  • 함수형 스타일이나 특정상황에서는 readwatch를 사용한다.






마치며

Provider 를 사용하고 정리하면서 소비하는 부분에 있어서 Provider.of 외에도 consumer과 read, watch 를 사용하는 방법에 대해서도 더 배웠다. 아무래도 최적화시키기에는 다양한 방법이 있기 마련이다. 가장 기본적인 Provider.of 를 사용했으니 더 성능상 이점이 있는 consumer 를 사용해 보고자하는 욕심도 생긴다.



0개의 댓글