flutter의 다양한 상태관리 라이브러리

김연후·2022년 8월 11일
0

출처
[Flutter Festival GDG Songdo] GetX, provider, bloc 패턴 비교 분석 - 유병욱

요약

  • 간단한 Counter를 구현하여 총 5가지 코드 비교
    • Plain(Stateful Widget)
    • Bloc (with Cubit)
    • Provider
    • GetX
    • riverpod

Plain(Stateful Widget)

count state를 Stateful Widget 내에 선언

// stateful widget
class CountPage extends StatefulWidget {
  const CountPage({Key? key}) : super(key: key);

  @override
  _CountPageState createState() => _CountPageState();
}

// state of the widget
class _CountPageState extends State<CountPage> {
  int _count = 0; // state 선언

  @override
  Widget build(BuildContext context) {
  ...

counter의 +, - 버튼에 setState로 state 업데이트

children: [
  FloatingActionButton(
    onPressed: () => setState(() {
      _count++;
    }),
    child: const Icon(Icons.add),
  ),
  const SizedBox(height: 8),
  FloatingActionButton(
    onPressed: () => setState(() {
      _count--;
    }),
    child: const Icon(Icons.remove),
  ),
],

setState 코드

@protected
  void setState(VoidCallback fn) {
  
  	...
    
  	_element!.markNeedsBuild();
  }

markNeedsBuild 메서드는 flutter에 다음 프레임에 UI를 re-render하도록 알린다.
markNeedsBuild 메서드를 보자.

/// Marks the element as dirty and adds it to the global list of widgets to
/// rebuild in the next frame.
///
/// Since it is inefficient to build an element twice in one frame,
/// applications and widgets should be structured so as to only mark
/// widgets dirty during event handlers before the frame begins, not during
/// the build itself.
void markNeedsBuild() {
	
    ...
    if (dirty)
      return;
    _dirty = true;
    owner!.scheduleBuildFor(this);
}
  • dirty 값을 true로 두어 element에 마킹을 하여 다음 프레임에 rebuild할 전역 widget 리스트에 추가해둠

setState()를 이용하여
1. state를 업데이트 하고,
2. flutter에게 UI를 re-render하도록 알림

언제쓸까?

  • 특정한 Widget 내에서 단기적으로 쓰이고 말 때, 사용하면 편한 상태관리
    (한 파일 내에서 쓰기에 편한 상태관리 방식)

Bloc

Stream을 Flutter Project 내에 적극적으로 사용해야한다면 사용해야하는 라이브러리!

Cubit과 Bloc

Cubit은 function으로 state를 관리 할 수 있게해주는 class

  • 단순 function 호출로 state 변경
    Bloc은 event 발생으로 state를 관리 할 수 있게해주는 class
  • event를 감지해 state 변경

Cubit 생성

import 'package:flutter_bloc/flutter_bloc.dart';

class CountCubit extends Cubit<int> {

  CountCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}
  • Cubit을 생성할 때, type(위의 경우 int) 지정 필요, 원시 타입 외에 사용자 지정 class를 생성해서 사용할 수도 있다.
  • emit함수는 Cubit 클래스 내에서만 사용 가능하며, 새로운 state 값을 지정해준다.

Cubit 사용

class CountPage extends StatelessWidget {
  const CountPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CountCubit(),
      child: const CountView(),
    );
  }
}
  • CountCubit을 CountPage에서 사용하기 위해 BlocProvider의 create 함수에 지정

BlocProvider

  • Flutter widget
  • create함수로 새로운 Bloc을 생성
  • 의존성 주입(DI)으로 하위 widget(child)들에게 생성한 하나의 Bloc(or Cubit) 인스턴스 전달
  • 따라서 CountView widget에서 CountCubit 사용 가능

BlocBuilder

class CountView extends StatelessWidget {
  const CountView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('State Manager - BloC'),
      ),
      body: Center(
        child: BlocBuilder<CountCubit, int>(
          builder: (BuildContext context, int state) {
            return Text('$state', style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold));
          },
        ),
      ),
	...
}
  • Flutter widget으로 bloc과 builder로 구성됨 (StreamBuilder와 유사)
  • builder 함수에서 state에 따라 widget을 반환

context.read

floatingActionButton: Column(
  mainAxisAlignment: MainAxisAlignment.end,
  crossAxisAlignment: CrossAxisAlignment.end,
  children: [
    FloatingActionButton(
      onPressed: () => context.read<CountCubit>().increment(),
      child: const Icon(Icons.add),
    ),
    const SizedBox(height: 8),
    FloatingActionButton(
      onPressed: () => context.read<CountCubit>().decrement(),
      child: const Icon(Icons.remove),
    ),
  ],
),
  • CountCubit 클래스 형식의 가장 근접한 조상의 인스턴스를 가리킴
  • 여기서는 BlocProvider에서 생성한 CountCubit 클래스로 만들어진 인스턴스
  • increment(), decrement() 함수로 state값을 변경
    • Cubit이므로 state를 단순 함수로 변경

BLoc 사용
추후 flutter로 로그인 기능 구현에서...

Cubit

  • state 변경을 function으로 관리
  • 변경 된 값을 가져올 때, emit / onChanged 등으로 개발자가 시점을 특정할 수 있음
  • 단순 UI 핸들링에 효과적

Bloc

  • state 변경을 event로 관리
  • 여러 요인으로 인한 잦은 변경이 많은 관리
  • 주체에 대해 event로 관리되기에 변화에 대한 추적 가능
  • 로그인 유무 / 특정 Action에 대한 추적을 Stream으로 관리하기에 효과적

언제쓸까?

  • Stream을 적극적으로 활용해야될 때 (event기반의 Bloc, function기반의 Cubit)
  • View와 비즈니스Logic에 대한 분명한 분리

Provider

state 관리 그 자체만을 위하여...

관리할 state model 생성

class CountModel extends ChangeNotifier {
  int counter = 0;

  void incrementCounter() {
    counter++;
    notifyListeners();
  }

  void decrementCount() {
    counter--;
    notifyListeners();
  }
}

ChangeNotifier class

  • 변경 알림(change notification) 기능을 제공하는 클래스
  • notifyListeners() 함수가 counter state가 변경되었음을 ChangeNotifierProvider에 알림

Provider 사용하기

class CountApp extends StatelessWidget {
  const CountApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'State Manager - Provider',
      home: MultiProvider(
        providers: [
          ChangeNotifierProvider<CountModel>(
            create: (_) => CountModel(),
            child: const CountPage(),
          ),
        ],
      ),
    );
  }
}

Root Widget에서 provider 사용 (하위(자식) Widget에서 state관리 가능)

  • 하나의 Provider만 사용하고자 하면 home에 ChangeNotifierProvider를 바로 작성해도 된다.

ChangeNotifierProvider

  • ChangeNotifier의 인스턴스를 하위 Widget들에게 제공 (CountModel)
  • ChangeNotifierProvider가 widget트리에서 제거되면 이전에 생성한 ChangeNotifier 인스턴스도 자동으로 삭제(dispose)

Consumer와 Provider.of(context)
CountModel이 ChangeNotifierProvider에 의해 하위 Widget들에게 제공되는 상황에서 이를 사용하기 위한 두 가지의 방식이 있다.

  • Consumer
@override
Widget build(BuildContext context) {
  return Consumer<CountModel>(
    builder: (context, model, child) {
      return Center(
        child: Text(
          '${model.counter}',
          style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
        ),
      );
    },
  );
}
  • Provider.of(context)
@override
Widget build(BuildContext context) {
  final countModel = Provider.of<CountModel>(context);
  return Center(
    child: Text(
      '${countModel.counter}',
      style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
    ),
  );
}
  • Consumer와 Provider.of(context)의 성능은 동일
  • Provider.of(context)의 경우 사용하는 Widget 전체가 rebuild 된다.
    • 따라서 Widget을 잘게 쪼개서 rebuild하는 코드를 줄이거나
    • Consumer를 사용하여 일부분만 rebuild 할 수 있다.
return Consumer<CountModel>(
  builder: (context, model, child) => Stack(
    children: [
      // Use SomeExpensiveWidget here, without rebuilding every time.
      if (child != null) child,
      Text('Counter: ${model.counter}'),
    ],
  ),
  // Build the expensive widget here.
  child: const SomeExpensiveWidget(),
);

builder 함수의 child argument에 할당한 SomeExpensiveWidget()은 CountModel의 state가 바뀌어도 rebuild하지 않는다.

위와 같은 경우는 큰 Widget의 부모로 Consumer Widget이 필수적으로 있어야 하는 경우 사용할 수 있는 방법이지만, 굳이 부모에 있을 필요가 없다면 최대한 하위 Widget으로 Consumer를 배치하는 것이 성능적으로 유리하다. flutter docs

// DON'T DO THIS
return Consumer<CountModel>(
  builder: (context, model, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Counter: ${model.counter}'),
      ),
    );
  },
);
// DO THIS
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CountModel>(
      builder: (context, model, child) {
        return Text('Counter: ${model.counter}');
      },
    ),
  ),
);

Consumer와 Provider.of(context) 둘 다 함수를 이용하여 state 변경 가능

return Scaffold(
  appBar: AppBar(
    title: const Text('State Manager - Provider'),
  ),
  body: const CountView(),
  floatingActionButton: Consumer<CountModel>(
    builder: (context, model, child) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => model.incrementCounter(),
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            onPressed: () => model.decrementCount(),
            child: const Icon(Icons.remove),
          ),
        ],
      );
    },
  ),
);
final countModel = Provider.of<CountModel>(context);
return Scaffold(
  appBar: AppBar(
    title: const Text('State Manager - Provider'),
  ),
  body: const CountView(),
  floatingActionButton: Column(
    mainAxisAlignment: MainAxisAlignment.end,
    crossAxisAlignment: CrossAxisAlignment.end,
    children: [
      FloatingActionButton(
        onPressed: () => countModel.incrementCounter(),
        child: const Icon(Icons.add),
      ),
      const SizedBox(height: 8),
      FloatingActionButton(
        onPressed: () => countModel.decrementCount(),
        child: const Icon(Icons.remove),
      ),
    ],
  ),
);

Provider

  • InheritedWidget을 좀 더 쓰기 편한 형태로 발전시킨 상태관리 모델
  • Flutter에서 적극 추천하는 상태관리 모델
  • Provider는 Flutter 기반을 최대한 유지 및 활용하는 형태로 구현 가능 (Provider 객체를 가지고 올 때 Flutter 기존의 방식인 BuildContext 객체를 활용 할 정도)
  • MultiProvider 형태로 동시에 여러 Provider 객체를 운용 가능
  • ProxyProvider로 여러 Provider 값을 한데 모아 활용 가능

언제쓸까?

  • 단순 전역적인 상태관리가 필요할 때 사용하기 좋을 것 같다

GetX

Flutter 안에서 쓸 수있는 새로운 프레임워크 같은 라이브러리...
Flutter를 쓰면서 다소 귀찮아질 수 있는 BuildContext와 StatefulWidget의 State객체의 존재를 지우고 많은 것을 GetX로 쓸 수 있다. (Navigator부터 Connection까지...)

Flutter가 어떤 구조로 돌아가는지 생각하게 해주지 않고 GetX 기능을 쓰면 한방에 해결되기에 개발 공부에 필요한 생각을 하지 않게된다는 말도 나온다...

추후에...

profile
개발 지식 공부

0개의 댓글