플러터 상태관리

LeeWonjin·2024년 4월 25일
0

플러터

목록 보기
8/15

https://docs.flutter.dev/data-and-backend/state-mgmt/intro

  • Dart의 함수는 일급객체다 : 링크

선언적 UI

안드로이드, ios는 명령형 UI 프레임워크를 채택했다. 하나하나 알려줘야 한다.
플러터는 선언적 UI 프레임워크다. 상태가 바뀌면 UI가 바뀐다.

  • View 구성을 사람이 직접 바꿀 수 없다고 가정한다. (Immutable) 뷰는 블루프린트에 불과하고, 상태 변화에 따라 플러터가 알아서 재구성한다.

https://docs.flutter.dev/get-started/flutter-for/declarative

상태(State)는 무엇인가

앱이 실행될 때 메모리에 올라가 있는 모든 것이 상태다.

  • (에셋, 변수, 애니메이션 상태, 폰트, 텍스쳐)

그러나 모든 상태를 개발자가 관리하지는 않는다. 플러터만 건드리는 상태도 있다. (e.g, 텍스쳐)
사용자가 관리하는 상태는 두 가지로 분류할 수 있다.

  • Ephemeral State (임시 상태) = UI state, Local state
  • App State (앱 상태) = Shared state
  • 참고

임시 상태

= UI상태, 로컬 상태

한 위젯에 포함될 수 있는 상태.

  • 임시상태는 다른 위젯에서 건드릴 일 없다
  • 상태관리 툴로 관리 할 필요 없다.
  • 직렬화도 필요 없다.

임시라는 말 그대로 UI를 다 쓰고 난 뒤에 저장될 필요도 없다.
(앱 껐다가 다시 키면 초기화되는게 맞다)

e.g., 게시판 페이지 넘버, 애니메이션 진행도

앱 상태

= 공유 상태

임시 상태와 대치된다.

  • 앱의 많은 부분에서 (다른 위젯에서?) 공유될 수 있다.
  • 사용자 유저 세션 간에 유지될 수 있다.

e.g., 유저 설정, 로그인 정보, 쇼핑몰 장바구니, 메일함 unread/read 여부.

사실 둘의 구분은 모호하다

알아서 결정하면 된다. 정해진거 없다. 실제로 개발하는 사람들만이 판단할 수 있음. 원래는 임시 상태였던게 시간이 지나서 앱 상태가 돼야 할 수도 있다.

  • 무엇이 앱 상태인가? 무엇이 임시 상태인가? --> 알잘딱하셈
    • 중앙에서 관리해야 하는 게시판 페이지넘버는 과연 임시 상태일까? 아님
    • 매번 초기화 돼야 하는 (기괴한)메일함의 read여부는 앱 상태일까? 아님
  • 특정한 앱 상태를 어떤 식으로 관리할 것인가? --> 알잘딱하셈

어떤 상태든, 상태 관리를 위해 State클래스와 setState메소드를 쓰면 된다.
대충 일반적인 가이드라인을 잡아주는 다이어그램은 아래와 같이 그려볼 수 있다.

플러터가 선언적 UI를 채택했다는 점을 생각하면 상태를 어느 계층에 둘 것인지 판단할 수 있다. (데이터에 접근하기 쉬운 방법을 채택)


https://docs.flutter.dev/assets/images/docs/development/data-and-backend/state-mgmt/ephemeral-vs-app-state.png

Provider 패키지

상태관리의 대표적 Bad Practice

다른 위젯의 상태를 가져와서 처리하고, 각자 위젯 마다 같은 내용의 다른 상태변수를 업데이트 하는 것
--> 이거 하면 인생이 피곤해진다. 이럴거면 프레임워크 쓰지마라.
--> 차라리 트리 상위계층에서 하나의 상태를 관리하고, 하위 위젯에 뿌려줘라

아이디어

자신보다 위에 있는 위젯의 데이터 접근 방법
: 위에서 접근점 역할을 할 수 있는 콜백함수를 내려준다.

다만 트리 깊이가 깊어질 수록 웹-프론트엔드의 props-drilling같은 일이 발생할 수 있는데, 플러터는 자식보다 아래에 위치한 위젯에 파라미터를 전달하기 위한 기능을 제공한다. 그 기능은 위젯으로 제공된다.

  • e.g., InheritedWidget, InheritedNotifier, InheritedModel

이 위젯들을 사용하는 대신 더 쉬운 방법인 Provider로 전역상태 관리 문제를 해결할 수 있다.

flutter pub add provider

아래의 기능을 사용해 상태를 관리한다.

  • 상태 제공
    • ChangeNotifier
    • ChangeNotifierProvider
  • 상태 소비
    • context.watch<T> / context.read<T>
    • Provider.of
    • Consumer

제공 : ChangeNotifier

  • 리스너에게 변화를 알려줄 수 있는 클래스. (구독 가능 객체)
  • 단방향.
  • 상태가 변해 UI리렌더링이 필요할 때 notifyListeners()를 호출
    • 리스너들에게 변화를 알림 (마치 setState하는 것과 같다)
class CounterModel extends ChangeNotifier {
  int _num = 0; // 언더스코어는 private이란 뜻
  int get num => _num; // getter를 이렇게 쓴다.
  
  // // setter 쓰고싶으면 이렇게 할 수 있다
  // set num (int a) {_num = a;}
  
  void increment() {
    _num++;
    notifyListeners();
  }
}

제공 : ChangeNotifierProvider

ChangeNotifier로 상태를 관리하는 모델을 만들었다면,
이제 그 모델을 어떤 위젯이 접근할 수 있도록 앱 내에 박아줘야 한다.

아래 경우는 MaterialApp이 모델(의 상태와 메소드)에 접근할 수 있도록 하는 코드.
이 경우 제공할 모델이 하나라서 ChangeNotifierProvider<T> 를 사용했다.
만약 모델이 여러 개라면 MultiProvider를 사용할 수 있다.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    const appTitle = 'Theme Demo';

    return ChangeNotifierProvider<CounterModel>(
      create: (_) => CounterModel(),
      child: MaterialApp(
        title: appTitle,
        home: Scaffold(
          body: SafeArea(
            child: DemoBody(),
          ),
        ),
      ),
    );
  }
}

이제 ChangeNotifier가 ChangeNotifierProvider(혹은 MultiProvider)를 통해 앱에 박혔다면, 하위 위젯에서 써먹어야 한다.

세 가지 방법으로 나누어 아래에 적는다.

소비 : context.watch<T> / context.read<T>

context.watch는 상태가 변경되면 다시 빌드한다.
read는 다시 빌드하지 않는다.

class DemoBody extends StatelessWidget {
  const DemoBody({super.key});

  
  Widget build(BuildContext context) {
    final int num = context.watch<CounterModel>().num;

    return Center(
      child: ElevatedButton(
        onPressed: () {
          context.read<CounterModel>().increment();
        },
        child: Text('$num'),
      ),
    );
  }
}

소비 : Provider.of<T>

context.watch/read와 유사한 사용법
옛날에는 어떤 제약때문에 지금처럼 언제나 일대일대응이 되지는 못했다고 한다.

class DemoBody extends StatelessWidget {
  const DemoBody({super.key});

  
  Widget build(BuildContext context) {
    final int num = Provider.of<CounterModel>(context).num;

    return Center(
      child: ElevatedButton(
        onPressed: () {
          Provider.of<CounterModel>(context, listen: false).increment();
        },
        child: Text('$num'),
      ),
    );
  }
}

소비 : Consumer<T>

이렇게도 된다.

  • Provide하는 부분과 메소드/멤버에 접근하는 코드가
  • 같은 build메소드에 있을 때는
  • context, Provider.of가 아니라 --> Consumer위젯으로 해결봐야 한다고 함.
class DemoBody extends StatelessWidget {
  const DemoBody({super.key});

  
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<CounterModel>(
        builder: (_, counter, __) {
          final int num = counter.num;
          return ElevatedButton(
            onPressed: () {
              counter.increment();
            },
            child: Text('$num'),
          );
        },
      ),
    );
  }
}

선택가능한 다른 옵션

Provider말고도 상태관리 도구 많이 있다. 알아서 골라써라.
https://docs.flutter.dev/data-and-backend/state-mgmt/options

전체 코드

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    const appTitle = 'Theme Demo';

    return ChangeNotifierProvider<CounterModel>(
      create: (_) => CounterModel(),
      child: MaterialApp(
        title: appTitle,
        home: Scaffold(
          body: SafeArea(
            child: DemoBody(),
          ),
        ),
      ),
    );
  }
}

class DemoBody extends StatelessWidget {
  const DemoBody({super.key});

  // ---------------------------------------------
  // context.read/watch로 구현
  // @override
  // Widget build(BuildContext context) {
  //   final int num = context.watch<CounterModel>().num;

  //   return Center(
  //     child: ElevatedButton(
  //       onPressed: () {
  //         context.read<CounterModel>().increment();
  //       },
  //       child: Text('$num'),
  //     ),
  //   );
  // }

  // ---------------------------------------------
  // // Provider.of로 구현
  // @override
  // Widget build(BuildContext context) {
  //   final int num = Provider.of<CounterModel>(context).num;

  //   return Center(
  //     child: ElevatedButton(
  //       onPressed: () {
  //         Provider.of<CounterModel>(context, listen: false).increment();
  //       },
  //       child: Text('$num'),
  //     ),
  //   );
  // }

  // ---------------------------------------------
  // Consumer로 구현
  
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<CounterModel>(
        builder: (_, counter, __) {
          final int num = counter.num;
          return ElevatedButton(
            onPressed: () {
              counter.increment();
            },
            child: Text('$num'),
          );
        },
      ),
    );
  }
}

class CounterModel extends ChangeNotifier {
  int _num = 0;
  int get num => _num;

  void increment() {
    _num++;
    notifyListeners();
  }
}
profile
노는게 제일 좋습니다.

0개의 댓글

관련 채용 정보