13. Flutter 수업-StateProvider,StateNotifierProvider 연습 코드

StateProvider,StateNotifierProvider
홍윤's avatar
Oct 07, 2024
13. Flutter 수업-StateProvider,StateNotifierProvider 연습 코드

StateProvider

💡
StateProvider는 주로 React 애플리케이션에서 전역 상태 관리를 위해 사용되는 패턴입니다. 이 패턴은 Context API와 함께 useStateuseReducer 훅을 사용하여 상태를 관리하며, 애플리케이션의 여러 컴포넌트가 공통으로 사용하는 상태를 공유하고 업데이트할 수 있게 도와줍니다.

역할

  • 전역 상태 관리: 여러 컴포넌트에서 공통적으로 사용되는 데이터를 하나의 중앙화된 저장소(컨텍스트)에서 관리합니다. 이를 통해 트리의 깊은 하위 컴포넌트에서도 간편하게 상태를 접근하거나 수정할 수 있습니다.
  • 상태 및 로직 공유: StateProvider는 상태와 상태를 변경하는 로직을 Provider 컴포넌트를 통해 공유하고, 하위 컴포넌트에서 useContext 훅을 사용해 이를 구독합니다.

주요 구성 요소

  1. Context API: React의 내장 기능으로, 전역 데이터를 저장하고 이를 여러 컴포넌트에서 구독할 수 있게 해줍니다.
  1. Provider 컴포넌트: StateProviderContext.Provider로 상태를 전달하는 컴포넌트입니다. 모든 하위 컴포넌트는 이 Provider로부터 상태를 받을 수 있습니다.
  1. Reducer (선택 사항): useReducer 훅을 통해 상태의 변경 로직을 정의할 수 있습니다. 상태가 복잡하거나 다양한 액션이 발생하는 경우 useReduceruseState보다 더 적합합니다.
notion image

StateProvider 장단점

StateProvider의 장점:

  1. 간단한 상태 관리: 단일 상태 값을 관리하기 매우 간편합니다. 단순한 카운터, 토글, 기본 데이터 저장 등 단순한 상태 데이터를 보관할 때 적합합니다.
  1. 초기 학습 곡선이 낮음: Riverpod 상태 관리 도구 중 가장 간단하고 직관적이어서, 초기 사용자나 간단한 프로젝트에서 쉽게 사용할 수 있습니다.
  1. UI와의 연동 용이: 상태가 변경될 때 UI가 자동으로 업데이트되므로, 작은 규모의 애플리케이션에서는 쉽게 사용 가능합니다.
  1. 동기적 상태 변경: StateProvider는 동기적인 상태 관리에 최적화되어 있어 복잡한 비동기 작업 없이도 상태를 관리할 수 있습니다.
  1. 테스트가 용이함: 테스트가 단순하고 상태 변화가 예측 가능하므로, 간단한 상태에 대한 테스트 코드 작성이 용이합니다.

StateProvider의 단점:

  1. 복잡한 상태 관리에 비효율적: StateProvider는 단일 값을 다루는 데 적합하지만, 복잡한 비즈니스 로직이나 여러 상태 간의 상호작용이 필요한 상황에서는 적합하지 않습니다. 복잡한 상태를 다룰 때는 StateNotifierProviderFutureProvider, StreamProvider 같은 더 강력한 도구가 필요합니다.
  1. 비동기 처리 제한: StateProvider는 기본적으로 동기적 상태 관리에 초점을 맞추고 있기 때문에, 비동기 작업을 다룰 때는 추가적인 로직이 필요하며, 복잡해질 수 있습니다. 예를 들어, API 호출과 같은 비동기 작업이 필요한 경우 FutureProvider 또는 StreamProvider를 사용하는 것이 더 적합합니다.
  1. 보일러플레이트 코드 증가: 간단한 상태 관리에는 적합하지만, 상태가 복잡해지면 그에 따라 StateProvider와 관련된 코드가 복잡해질 수 있습니다. 이 경우 StateNotifierProviderChangeNotifierProvider 같은 더 구조화된 방식이 더 효율적입니다.
  1. 리액티브 프로그래밍에 약함: StateProvider는 리액티브 프로그래밍에 적합하지 않으며, 상태 변화가 발생할 때마다 값만 갱신하는 방식입니다. 비동기 데이터 스트림이나 반응형 데이터를 다룰 때는 더 복잡한 상태 관리 도구가 필요합니다.

StateNotifierProvider

💡
StateNotifierProviderRiverpod 상태 관리 라이브러리에서 사용되는 주요 프로바이더 중 하나입니다. StateNotifier와 함께 사용하여 상태를 관리할 때 유용합니다. StateNotifier는 상태의 변화를 관리하고, StateNotifierProvider는 이 상태 변화를 구독하고 컴포넌트에 전달하는 역할을 합니다.

StateNotifierProvider 기본 개념

  • StateNotifier: 상태 변경 로직을 포함한 클래스입니다. 이 클래스를 사용하면 상태를 immutable하게 관리하면서 특정 상태를 변경할 수 있습니다.
  • StateNotifierProvider: StateNotifier가 관리하는 상태를 외부에서 접근하고 구독할 수 있도록 제공하는 역할을 합니다. 이를 통해 StateNotifier가 관리하는 상태를 Riverpod 컨테이너를 통해 전역적으로 사용할 수 있게 됩니다.
notion image

장점

  1. Immutable한 상태 관리: StateNotifier를 통해 상태를 불변으로 유지할 수 있습니다. 상태 변경은 항상 새로운 상태를 생성하는 방식으로 이루어지기 때문에, 상태 관리의 일관성이 높아집니다.
  1. 분리된 상태 변경 로직: 상태와 상태 변경 로직이 StateNotifier에 분리되어 있어 코드 구조가 깔끔하고 유지보수하기 쉽습니다.
  1. 간결한 상태 관리: StateNotifierProvider는 상태 관리가 필요한 곳에 쉽게 상태를 전달하고 구독할 수 있게 해줍니다. 이로 인해 상태 관리 코드가 간결해집니다.
  1. 전역 상태 관리: Riverpod의 특성상 상태를 전역적으로 쉽게 관리할 수 있으며, 리액티브하게 변경 사항을 구독할 수 있습니다.

단점

  1. 학습 곡선: Flutter의 기본 상태 관리(setState)보다는 복잡하고, Riverpod와 StateNotifier 개념을 처음 접하는 경우에는 학습에 시간이 필요합니다.
  1. 보일러플레이트 코드: 단순한 상태 관리라면 StateNotifierStateNotifierProvider의 사용이 오히려 불필요한 코드를 늘릴 수 있습니다. 작은 앱이나 간단한 상태 관리에는 조금 과할 수 있습니다.
  1. 복잡한 상태 변경 관리: 상태가 복잡해질수록 상태 관리 로직도 복잡해질 수 있습니다. 특히 여러 액션과 다양한 상태를 관리해야 하는 경우에는 로직이 복잡해질 수 있습니다.

예제코드

1. main.dart

import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverapp/num_notify_provider.dart'; void main() { runApp(ProviderScope(child: MyApp())); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: HomePage(), ); } } class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { print("HomePage"); return Scaffold( body: Column( children: [ Expanded( child: Top(), ), Expanded( child: Bottom(), ), ], ), ); } } class Bottom extends ConsumerWidget { const Bottom({ super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { print("Bottom"); /* NumStore store = ref.read(numProvider); */ BunStore store = ref.read(bunProvider.notifier); return Center( child: Container( child: InkWell( onTap: () { print("증가 클릭됨"); store.increase(); }, child: Text( "증가", style: TextStyle(fontSize: 30), ), ), ), ); } } //riverpod 사용 할 때만 ConsumerWidget 사용한다. class Top extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { print("Top"); /* Provider 사용! 싱글톤으로 관리한다. 여러번 실행되도 한번만 뜬다. prvicer NumStore store = ref.read(numProvider); //ref.read(numProvider) -> provider의 익명함수가 실행됨! (창고에 접근) NumStore num2 = ref .read(numProvider); //ref.read(numProvider) -> provider의 익명함수가 실행됨! (문법) */ //notifyProvider는 값에 바로 접근이 가능하다. BunModel model = ref.watch(bunProvider); // provider의 익명함수가 실행됨! (모델에 접근) return Center( child: Container( // "${Store.num}" = Provider , "${model.bun}" = notifyProvider child: Text("${model.bun}", style: TextStyle(fontSize: 30)), ), ); } }
💡

코드설명

1.HomePage 클래스

class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { print("HomePage"); return Scaffold( body: Column( children: [ Expanded( child: Top(), ), Expanded( child: Bottom(), ), ], ), ); } }
  • Scaffold: Flutter에서 기본 레이아웃을 설정하는 위젯입니다. 상단에 앱바를 넣거나 하단에 플로팅 액션 버튼을 추가할 수 있습니다.
  • Column: 두 개의 자식 위젯(Top, Bottom)을 세로로 배치합니다.
  • Expanded: 자식 위젯이 부모 위젯의 남은 공간을 모두 차지하도록 설정합니다.

2.Bottom 클래스 (ConsumerWidget)

class Bottom extends ConsumerWidget { const Bottom({ super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { print("Bottom"); BunStore store = ref.read(bunProvider.notifier); return Center( child: Container( child: InkWell( onTap: () { print("증가 클릭됨"); store.increase(); }, child: Text( "증가", style: TextStyle(fontSize: 30), ), ), ), ); } }
  • ConsumerWidget: Riverpod에서 제공하는 상태 관리 위젯입니다. WidgetRef를 통해 프로바이더에 접근할 수 있습니다.
  • ref.read(bunProvider.notifier): bunProvider에서 상태를 관리하는 BunStore 객체에 접근합니다. increase() 메소드를 통해 상태를 변경할 수 있습니다.
  • InkWell: 클릭 이벤트를 감지하여 store.increase() 메소드를 호출합니다. 이 메소드는 BunStore에서 정의된 로직을 통해 숫자를 증가시키는 역할을 합니다.

3.Top 클래스 (ConsumerWidget)

class Top extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { print("Top"); BunModel model = ref.watch(bunProvider); return Center( child: Container( child: Text("${model.bun}", style: TextStyle(fontSize: 30)), ), ); } }
  • ref.watch(bunProvider): Riverpod의 watch 메소드를 통해 bunProvider의 상태 변화를 구독합니다. 상태가 변경되면 Top 위젯이 리렌더링됩니다.
  • BunModel: bunProvider의 상태로, model.bun을 통해 숫자 값을 가져옵니다.

상태 관리

코드의 상태 관리 핵심은 num_notify_provider.dart에 정의된 bunProvider입니다. 이는 아마도 StateNotifierStateNotifierProvider를 사용하여 상태를 관리하는 방식일 것입니다.
 

2. num_provider.dart

//sinppets //관리자 import 'package:flutter_riverpod/flutter_riverpod.dart'; // 항상 이형태를 유지하자! , 항상 일관성있게 코드를 만들자 //창고 데이터 (책임: 데이터) class NumModel { int num = 1; } //창고 (책임: 비즈니스 로직 관리) 부모꺼라서 super 써주기! class NumStore extends NumModel { void add() { super.num++; } void minus() { super.num--; } } //창고 관리자는 return 값을 가진다. (책임: 창고 관리) final numProvider = StateProvider<NumStore>((ref) { print("StateProvider 창고 생성됨"); return NumStore(); });
💡

코드설명

1. NumModel 클래스

class NumModel { int num = 1; }
  • NumModel: 상태를 나타내는 데이터 모델입니다. 이 클래스는 단순히 num이라는 정수 변수를 관리하며, 초기값으로 1을 가집니다.
  • 이 모델은 상태의 기본 구조를 정의합니다.

2. NumStore 클래스

class NumStore extends NumModel { void add() { super.num++; } void minus() { super.num--; } }
  • NumStore: NumModel을 상속받아 상태 변경을 관리하는 클래스입니다.
  • 여기서 비즈니스 로직이 포함되며, add() 메소드는 num 값을 증가시키고, minus() 메소드는 num 값을 감소시킵니다.
  • super.num++은 부모 클래스인 NumModelnum 값을 직접 조작하는 방식입니다.

3. StateProvider 설정

final numProvider = StateProvider<NumStore>((ref) { print("StateProvider 창고 생성됨"); return NumStore(); });
  • StateProvider: Riverpod에서 제공하는 프로바이더 중 하나로, 상태를 관리하는 객체를 제공합니다.
  • 여기서 numProviderNumStore 인스턴스를 제공하며, 이를 통해 앱에서 NumStore의 상태를 전역적으로 접근하고 변경할 수 있습니다.
  • StateProvider를 사용하면 간단한 상태 관리를 할 수 있습니다. 여기서는 NumStore 객체가 관리되며, 이를 통해 add()minus()를 호출하여 num 값을 변경할 수 있습니다.

동작 원리

  1. 초기 상태: 앱이 시작될 때 StateProvider가 실행되면서 NumStore 객체가 생성됩니다. 이 객체는 초기값으로 num = 1을 가집니다.
  1. 상태 변경: NumStore 객체의 add()minus() 메소드를 호출하면 num 값이 변경됩니다.
  1. UI 반영: Riverpod을 통해 numProvider를 구독하고 있는 UI가 있으면, 상태 변경이 발생할 때마다 UI가 리렌더링되어 변경된 값을 반영합니다.

일관된 코드 스타일 유지

  • 일관성 있는 코드 작성: 여기서 강조한 부분은 일관성 있게 코드를 작성하는 것입니다. 이 코드는 항상 상태를 모델(NumModel), 비즈니스 로직(NumStore), 상태 관리자(StateProvider)로 나누어 관리하고 있습니다. 이를 통해 코드의 유지보수성과 가독성을 높일 수 있습니다.
 

3. num_notify_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart'; //창고데이터 (책임: 데이터) class BunModel { int bun; BunModel(this.bun); } //창고 (책임: 비즈니스 로직관리) class BunStore extends StateNotifier<BunModel> { BunStore(super.state); void increase() { //기존 값을 알고 나서 값을 다시 넘겨준다. 깊은 복사를 해줘야한다. new를 새로해줘야한다. BunModel model = state; model.bun++; //변경된 값 깊은 복사 super.state = BunModel(model.bun); } void decrease() { BunModel model = state; super.state = BunModel(model.bun--); } } //창고관리자 (책임: 창고 관리) final bunProvider = StateNotifierProvider<BunStore, BunModel>((ref) { BunModel model = BunModel(1); return BunStore(model); });
💡

코드설명

1. BunModel 클래스

class BunModel { int bun; BunModel(this.bun); }
  • BunModel: 상태 데이터를 정의하는 클래스입니다. 이 클래스는 bun이라는 정수 값을 가지고 있으며, 이를 통해 현재 상태를 나타냅니다.
  • 생성자: BunModel(this.bun)을 통해 bun 값을 초기화할 수 있습니다.

2. BunStore 클래스 (StateNotifier 상속)

class BunStore extends StateNotifier<BunModel> { BunStore(super.state); void increase() { BunModel model = state; model.bun++; super.state = BunModel(model.bun); // 깊은 복사를 통해 상태를 변경 } void decrease() { BunModel model = state; super.state = BunModel(model.bun--); // 감소 처리 } }
  • BunStore: StateNotifier<BunModel>를 상속받아 상태 변경 로직을 관리합니다. StateNotifier는 상태 변경 시 UI를 자동으로 갱신할 수 있도록 해줍니다.
  • increase 메소드: increase()는 현재 상태(state)를 가져와 bun 값을 증가시킨 후, 새로 생성한 BunModel로 상태를 갱신합니다.
    • 깊은 복사: 상태는 불변성(immutable)을 유지해야 하므로, 상태를 직접 수정하지 않고 새로운 인스턴스를 생성하여 상태를 업데이트합니다.
  • decrease 메소드: decrease()도 유사한 방식으로 bun 값을 감소시키며, 새로운 BunModel을 만들어 상태를 갱신합니다.

3. bunProvider (StateNotifierProvider)

final bunProvider = StateNotifierProvider<BunStore, BunModel>((ref) { BunModel model = BunModel(1); return BunStore(model); });
  • StateNotifierProvider: bunProvider는 Riverpod에서 상태 관리 및 상태 변경을 관리하는 프로바이더입니다.
    • BunStore: 상태를 변경할 로직을 담고 있습니다.
    • BunModel: bunProvider가 관리하는 초기 상태로, bun 값은 1로 초기화됩니다.
  • 사용법: 이 프로바이더는 UI에서 ref.watchref.read를 통해 상태를 구독하고 변경할 수 있습니다.

상태 관리 흐름

  1. 초기 상태: 앱이 시작될 때 bunProviderBunModel의 초기값 1을 가진 BunStore 객체를 생성하여 상태를 관리합니다.
  1. 상태 변경: 사용자가 UI에서 increase() 또는 decrease()를 호출하면, BunStore에서 상태를 새로 생성하여 Riverpod의 상태로 반영됩니다.
  1. UI 업데이트: 상태가 변경되면 StateNotifier를 통해 UI에 자동으로 상태 변경이 반영됩니다.

4. 결과 화면

notion image
 
Share article

Uni