flutter riverpod - 1

Pyo·2023년 10월 1일
0

RiverPod

상태관리 패키지 중 하나로, GetX, Provider, BLoC등 플러터에서 주로사용하는 상태관리를 위한 패키지중 하나이다. Riverpod는 실제 Provider의 개발자가 개발 하였고, Provider 패턴을 기반으로 하며, Provider의 문제점을 개선하고 상태 관리를 보다 쉽고 간단하게 구현할 수 있도록 도와준다.

pubspec.yaml 설정

dependencies:	 
    flutter_riverpod: 

플러터에서 riverpod를 사용하기 위해 pubspec.yaml에 flutter_riverpod를 추가해준다.

provider 종류

우선 RiverPod를 구성하는 provider의 종류들을 코드를 통해 알아보겠다.

StateProvider

단순한 데이터 , UI 내에서 직접 변경을할때 사용하며 복잡한로직 보다는 비교적 간단한로직에서 사용이 적합하다.

state_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

final numberProvider = StateProvider<int>((ref) => 0);

전역변수로 StateProvider를 생성한다.

state_provider_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/layout/default_layout.dart';
import 'package:flutter_riverpod_playground/riverpod/state_provider.dart';

class StateProviderScreen extends ConsumerWidget {
  const StateProviderScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final provider = ref.watch(numberProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'StateProviderScreen',
        ),
      ),
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                ref.read(numberProvider.notifier).update((state) => state + 1);
              },
              child: const Text('Up'),
            ),
            const SizedBox(
              width: 16,
            ),
            Text(provider.toString()),
            const SizedBox(
              width: 16,
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(numberProvider.notifier).state =
                    ref.read(numberProvider.notifier).state - 1;
              },
              child: const Text('Down'),
            ),
          ],
        ),
      ),
    );
  }
}

RiverPod 구조

기본적인 RiverPod 구조이다. 위젯은 ConsumerWidget을 상속받고, WidgetRef ref를 사용하여 provider에 접근 할수 있다. 보통 업데이트가 있을때 지속적으로 build가 필요한 경우에는 ref.watch()를 이용하여 provider의 변화를 감지할때 사용한다. 반면, read를 사용하는 경우는 실행되는 순간 한번 provider 가져오는데 , onPressed 콜백 처럼 특정 액션 뒤 read를 사용한다. read는 보통 2가지 방법으로 사용한다.
방법 1. ref.read(numberProvider.notifier).update((state) => state + 1);
방법 2. ref.read(numberProvider.notifier).state = ref.read(numberProvider.notifier).state - 1;

StateNotifierProvider

클래스 메소드를 이용한 상태관리로 StateProvider보다는 복잡한 데이터 관리에 적합. StateNotifier를 상속한클래스를 반환한다.

work_out_model.dart

class WorkoutModel {
  final String name;
  final int weight;
  final bool isSuccess;

  WorkoutModel({
    required this.name,
    required this.weight,
    required this.isSuccess,
  });

  WorkoutModel copyWith({
    String? name,
    int? weight,
    bool? isSuccess,
  }) {
    return WorkoutModel(
        name: name ?? this.name,
        weight: weight ?? this.weight,
        isSuccess: isSuccess ?? this.isSuccess);
  }
}

StateNotifierProvider에서 사용할 모델이다.

state_notifier_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/model/workout_model.dart';

final workoutListProvider =
    StateNotifierProvider<WorkOutListNotfier, List<WorkoutModel>>(
        (ref) => WorkOutListNotfier());

class WorkOutListNotfier extends StateNotifier<List<WorkoutModel>> {
  WorkOutListNotfier()
      : super(
          [
            WorkoutModel(name: '벤치프레스', weight: 105, isSuccess: true),
            WorkoutModel(name: '데드리프트', weight: 200, isSuccess: true),
            WorkoutModel(name: '스쿼트', weight: 170, isSuccess: false),
          ],
        );

  void toggleIsSuccess(String name) {
    state
        .map((e) => e.name == name
            ? WorkoutModel(
                name: e.name, weight: e.weight, isSuccess: !e.isSuccess)
            : e)
        .toList();
  }
}

StateNotifier를 상속한 WorkOutListNotifier 클래스를 StateNotifierProvider로 반환한다.

state_notifier_provider_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/layout/default_layout.dart';
import 'package:flutter_riverpod_playground/riverpod/state_notifier_provider_2.dart';

class StateNotifierProviderScreen2 extends ConsumerWidget {
  const StateNotifierProviderScreen2({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(workoutListProvider);
    return DefaultLayout(
      title: 'StateNotifierProvider2',
      body:ListView(
          children: state
              .map((e) => CheckboxListTile(
                  title: Text(e.name),
                  value: e.isSuccess,
                  onChanged: (bool? value) {
                    ref
                        .read(workoutListProvider.notifier)
                        .toggleIsSuccess(e.name);
                  }))
              .toList()),
    );
  }
}

FutureProvider

Future 타입을 반환 하며 , 특정행동뒤에 재실행 기능이 없기때문에 유용하지 않다.

future_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

final futureProvider = FutureProvider<List<int>>((ref) async {
  await Future.delayed(const Duration(seconds: 5));

  return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
});

future_provider_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/riverpod/future_provider.dart';

class FutureProviderScreen extends ConsumerWidget {
  const FutureProviderScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue state = ref.watch(futureProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('FutureProviderScreen'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // AsyncValue일 경우, 사용 가능
          state.when(
            data: (data) {
              return Text(
                data.toString(),
                textAlign: TextAlign.center,
              );
            },
            error: (err, stack) => Text(err.toString()),
            loading: () => const Center(
              child: CircularProgressIndicator(),
            ),
          ),
        ],
      ),
    );
  }
}

StreamProvider

Stream을 반환을 하며 , socket 사용시 사용에 용의하다.

stream_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

final streamProvider = StreamProvider<List<int>>((ref) async* {
  for (var i = 0; i < 10; i++) {
    await Future.delayed(Duration(seconds: 1));

    yield List.generate(3, (index) => index * i);
  }
});

stream_provider_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/layout/default_layout.dart';
import 'package:flutter_riverpod_playground/riverpod/stream_provider.dart';

class StreamProviderScreen extends ConsumerWidget {
  const StreamProviderScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(streamProvider);
    return DefaultLayout(
        title: 'StreamProviderScreen',
        body: Center(
            child: state.when(
                data: (data) => Text(data.toString()),
                error: (err, stack) => Text(err.toString()),
                loading: () => const CircularProgressIndicator())));
  }
}

이렇게 Provider들의 종류와 사용방법에 대해 알아보았다.

Listener & Selector

Riverpod의 Listener와 Selector는 상태 관리를 더 효과적으로 수행하고 앱의 성능을 최적화하기 위한 도구로 사용됩니다. 이들을 사용하면 불필요한 렌더링 , 리스닝을 최소화하여 상태 관리 코드를 더 구조화하고 유지 관리하기 쉽게 만들 수 있습니다. 코드와 함께 실행화면을 통해 정리하겠다.

  • selector : 특정 상태나 Provider의 값을 가져올 때 세부적인 선택 및 변환이 되었을때만, build를 재실행 한다.
  • lister : Provider의 값이 변경되면 값을 읽는 것이 아니라 정의한 함수를 실행한다. 이전값과 현재값을 알수있고, SnackBar나 Dialog를 처리하는데 유용하다.

workout_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/model/workout_model.dart';

final workoutProvider = StateNotifierProvider<WorkoutNotifier, WorkoutModel>(
    (ref) => WorkoutNotifier());

class WorkoutNotifier extends StateNotifier<WorkoutModel> {
  WorkoutNotifier()
      : super(
          WorkoutModel(name: '벤치프레스', weight: 110, isSuccess: false),
        );

  upWeigth() {
    state = state.copyWith(weight: state.weight + 5);
  }

  toggleIsSuccess() {
    state = state.copyWith(isSuccess: !state.isSuccess);
  }
}

workout_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/model/workout_model.dart';
import 'package:flutter_riverpod_playground/riverpod/workout_provider.dart';

class WorkoutScreen extends ConsumerWidget {
  const WorkoutScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final model = ref.watch(workoutProvider);
    final state = ref.watch(workoutProvider.select((value) => value.isSuccess));

    ref.listen(workoutProvider.select((value) => value.weight),
        (previous, next) {
      if (next == 140) {
        showDialog(
          context: context,
          builder: (BuildContext context) {
            return const AlertDialog(
              title: Text('목표 성공'),
            );
          },
        );
      }
    });

    return Scaffold(
      appBar: AppBar(
        title: const Text('WorkOutScreen'),
      ),
      body: SizedBox(
        height: MediaQuery.of(context).size.height,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text(model.name),
                Text('${model.weight}'),
                Text(state.toString()),
              ],
            ),
            const SizedBox(
              width: 16,
            ),
            ElevatedButton(
                onPressed: () {
                  ref.read(workoutProvider.notifier).upWeigth();
                },
                child: const Text('Up Weight')),
            const SizedBox(
              width: 16,
            ),
            ElevatedButton(
                onPressed: () {
                  ref.read(workoutProvider.notifier).toggleIsSuccess();
                },
                child: const Text('Success Toggle')),
          ],
        ),
      ),
    );
  }
}

여기 까지 Provider의 기본적인 종류와 selector,listener에 대해 알아보았다. 정리할 내용과 코드들이 조금 많아서 다음에 이어서 정리해보겠다.

참고

https://velog.io/@leeeeeoy/Flutter-Riverpod-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0-1
https://www.inflearn.com/course/%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%8B%A4%EC%A0%84/dashboard

0개의 댓글