본문 바로가기

개발/Study

[GoToLearn] Flutter Riverpod을 사용한 CounterApp 만들기

GoToLearn 6주차

GoToLearn 6주차 과제는 Codelab을 학습하는것이 아닌 상태 관리 패키지인 Riverpod을 활용하는 과제입니다. Riverpod을 사용하여 Counter App을 만들어보겠습니다.

 

Riverpod ?

Riverpod이란 무엇일까?

공식문서에 따르면 Riverpod이란 Fluuter/Dart를 위한 반응형 캐싱 프레임워크라고 합니다.  반응형 캐싱 프레임워크 하는 이유를 찾아보니 Rivierpod은 1. 자동 캐싱(상태를 자동으로 캐싱하여 동일한 상태를 여러 번 생성할 필요 없어 효율적으로 재사용할 수 있고, 상태나 데이터를 요청할 때 이미 존재하는 데이터가 있다면 계산하지 않고 캐싱된 데이터를 사용)과 2. 반응형 업데이트(상태가 변경될 때 감지하고 자동으로 UI를 업데이트 함)의 기능을 하기 때문입니다.

 

또한 Riverpod은 기존 Provider 패키지에서 영감을 받아 만들어진 라이브러리로 기존에 Provider 패키지의 여러 문제점과 제한 사항을 해결하고, 상태 관리의 복잡성을 줄이며, 더 나은 개발 경험을 제공하기 위해 만들어졌습니다.

 

Riverpod은 Flutter/Dart에서 사용하기 좋은 상태 관리 라이브러리입니다.

 

Riverpod vs Provider

Riverpod은 Provider의 여러 문제점을 개선했다고 하는데 어떤 점이 다를까요?

1. Provider 정의 방법

Provider 패키지는 providers가 위젯이므로 위젯 트리안에 배치합니다.

class Counter extends ChangeNotifier {
 ...
}

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<Counter>(create: (context) => Counter()),
      ],
      child: MyApp(),
    )
  );
}

riverpod 패키지의 providers는 Dart 객체로 위젯트리에 정의되지 않고 전역 final(global final) 변수로 정의되고, riverpod을 사용하려면 ProviderScope라는 위젯을 감싸주기만하면 어디서든 접근이 가능합니다.

// 전역 파이널 변수로 선언
final counterProvider = ChangeNotifierProvider<Counter>((ref) => Counter());

void main() {
  runApp(
    // 한번 감싸면 아래의 위젯들은 provider에 접근 가능
    ProviderScope(
      child: MyApp(),
    ),
  );
}

2. Providers를 사용하는 방법 : BuildContext

Provider 패키지는 providers를 사용하기 위해 BuildContext를 사용합니다.

Provider<Model>(...);

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Model model = context.watch<Model>(); // context.watch를 통한 T 클래스의 providers 접근
  }
}

riverpod 패키지는 StatelessWidget을 확장한 ConsumerWidget을 사용하여 WidgetRef를 통해 porvider를 사용합니다.

final modelProvider = Provider<Model>(...);

class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    Model model = ref.watch(modelProvider); // ref.watch를 통한 modelProvider에 접근
  }
}

위의 차이점으로는 Provider 패키지는 정의된 제너릭 타입에 의존하고, riverpod 패키지는 정의된 변수에 의존하게 됩니다.

 

3. Providers를 사용하는 방법 : Consumer

일단 Consumer 위젯은 변경 사항이 발생했을 때만 빌드할 수 있게 도와주는 위젯으로 성능 최적화에 도움이 되는 위젯입니다.

Provider 패키지에서 Consumer를 사용하는 방법입니다.

Consumer<Model>(
  builder: (BuildContext context, Model model, Widget? child) {

  }
)

Riverpod 패키지에서는 Consumer를 사용하는 방법입니다.

Consumer(
  builder: (BuildContext context, WidgetRef ref, Widget? child) {
    Model model = ref.watch(modelProvider);

  }
)

Consumer 위젯을 사용하는 차이점으로는 Provider는 Model 을 바로 사용하고, Riverpod은 WidgerRef를 통해 접근하고 싶은 provider 변수를 사용해서 필요한 상태에 접근하게 됩니다.

 

Provider는 한 제너릭 타입에만 접근할 수 있어 Consumer1, 2, 3을 제공하지만 Rivderpod은 WidgetRef와 접근하고 싶은 여러 providers를 사용할 수 있기 때문에 Consumer1,2,3 같은 위젯이 필요 없게 됩니다.

Consumer(
  builder: (BuildContext context, WidgetRef ref, Widget? child) {
    Model1 model1 = ref.watch(model1Provider);
	Model2 model2 = ref.watch(model2Provider);
    Model3 model3 = ref.watch(model3Provider);
  }
)

4. Providers를 결합하는 방법 : ProxyProvider와 Stateless Objects

Provider 패키지에서 providers를 결합하는 첫번째 방법은 ProxyProvider를 사용하는 방법이 있습니다.

// UserIdNotifier 정의
class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// UserIdNotifier를 생성하고 사용할 수 있도록 정의
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
// userId가 변경될 때마다 새 String 반환
ProxyProvider<UserIdNotifier, String>(
  update: (context, userIdNotifier, _) {
    return 'The user ID of the the user is ${userIdNotifier.userId}';
  }
)

Riverpod에서는 위 ProxyProvider를 동작하기 위한 방법이 조금 다릅니다.

class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// 전역 파이널 변수로 선언
final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
  (ref) => UserIdNotifier(),
);
// userIdNotifierProvider가 변경될때마다
// 해당 문자열 값을 반환하는 labelProvider 생성
final labelProvider = Provider<String>((ref) {
  UserIdNotifier userIdNotifier = ref.watch(userIdNotifierProvider);
  return 'The user ID of the the user is ${userIdNotifier.userId}';
});

위처럼 정의하면 labelProvider를 userIdNotifierProvider를 바라보면서 변경사항에 대한 값을 바로바로 수신할 수 있습니다.

 

5. Providers를 결합하는 방법 : ProxyProvider와 Stateful Objects

Provider 패키지에서 providers를 결합하는 다른 방법은 ChangeNotifer와 같은 상태 저장 객체를 노출시키고, ChangeNotifierProxyProvider를 사용하는 방법이 있습니다. 아래의 방법을 설명하자면 userId를 가지고 있는 UserIdNotifier의 userId값을 UserNotifier의 userId로 ChangeNotifierProxyProvider를 사용해서 변환하는 코드입니다.

// UserIdNotifier 정의
class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

// UserIdNotifier 생성
ChangeNotifierProvider<UserIdNotifier>(create: (context) => UserIdNotifier()),
/// UserNotifer 정의
class UserNotifier extends ChangeNotifier {
  String? _userId;

  void setUserId(String? userId) {
    if (userId != _userId) {
      print('The user ID changed from $_userId to $userId');
      _userId = userId;
    }
  }
}

// UserIdNotifer의 userId값을 사용해서 UserNotifier의 userId를 업데이트
ChangeNotifierProxyProvider<UserIdNotifier, UserNotifier>(
  create: (context) => UserNotifier(),
  update: (context, userIdNotifier, userNotifier) {
    return userNotifier!
      ..setUserId(userIdNotifier.userId);
  },
);

riverpod에서 위와 같은 동작을 하기 위해서는 <ref.listen>을 사용하게 됩니다.

ref.listen은 provider의 수신을 대기하고, provider가 변경될 떄마다 함수를 실행합니다.

class UserIdNotifier extends ChangeNotifier {
  String? userId;
}

final userIdNotifierProvider = ChangeNotifierProvider<UserIdNotifier>(
  (ref) => UserIdNotifier(),
),
class UserNotifier extends ChangeNotifier {
  String? _userId;

  void setUserId(String? userId) {
    if (userId != _userId) {
      print('The user ID changed from $_userId to $userId');
      _userId = userId;
    }
  }
}

final userNotifierProvider = ChangeNotifierProvider<UserNotifier>((ref) {
  final userNotifier = UserNotifier();
  
  // ref.listen을통해 userIdNotifier가 변경이 감지되면 함수 실행
  ref.listen<UserIdNotifier>(
    userIdNotifierProvider,
    (previous, next) {
      if (previous?.userId != next.userId) {
        userNotifier.setUserId(next.userId);
      }
    },
  );

  return userNotifier;
});

6. 범위 지정 Providers vs family + autoDispose

Provider 패키지에서 상태의 범위는 페이지 이탈시 상태 소멸/페이지당 커스텀 상태 보유 2가지 용도로 사용되었습니다. 이때 문제는 대규모 애플리케이션에서는 제대로 동작하지 않을 수 있습니다. 예를들어 한 페이지에서 상태가 생성되고, 다른 페이지에서 소멸되는 경우가 있는데 이렇게하면 여러 페이지에서 여러개의 캐시를 활성화할 수 없는 문제가 생기게됩니다. 따라서 모달이나 다단계 양식과 같이 해당 상태를 다른 위젯트리와 공유해야하는 경우 처리하기 어려워질 수 있습니다.

 

반면에 riverpod 패키지는 다른 접근 방식으로 family와 autoDispose를 사용하게 됩니다.

autoDispose는 더이상 providers가 사용하지 않을때 자동으로 해당 상태를 소멸시키게됩니다. 상태가 소멸되고 사용하지 않음을 확인하기 위해서는 onCancel, onDipose 함수를 사용할 수 있습니다.

ref.onCancel((){
  // 더이상 사용하지 않음
});

ref.onDispose((){
  // autodispose로 정의된 경우 해당 상태가 소멸된 경우
});

또한 family를 사용해서 provides에게 매개변수를 전달할 수 있습니다. 이를 통해 내부적으로 여러 providers를 생성하고 추적할 수 있습니다.

class ParamsType extends Equatable {
  const ParamsType({required this.seed, required this.max});

  final int seed;
  final int max;

  @override
  List<Object?> get props => [seed, max];
}

// int값을 매개변수로 넘김
final randomProvider =
    Provider.family.autoDispose<int, ParamsType>((ref, params) {
  return Random(params.seed).nextInt(params.max);
});

위를 통해 더 이상 상태는 특정 페이지에 묶여있지 않을 수 있습니다.

Riverpod 사용 방법

1. Riverpod 패키지 설치

flutter pud add flutter_riverpod

 

2. 최상위 위젯에 ProviderScope를 감싸주기

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

3. Provider를 정의하고 WidgetRef를 통해 Provider와 상호작용하기

WidgetRef를 통해 Provider와 상호작용하기 위해 먼저 문자열 'hello world'를 반환하는 provider를 정의합니다.

// 문자열 'helloWorld'를 반환하는 Provider 정의
final helloWorldProvider = Provider<String>(
  (ref) => 'helloWorld',
);

 

첫 번째 방법은 ConsumerWidget입니다.

ConsumerWidget을 상속받고, build 메소드의 매개 변수로 전달되는 WidgetRef를 통해 Provider를 사용합니다.

ConsumerWidget은 StatelessWidget에 build 메소드에 WidgetRef 매개 변수가 추가된 형태입니다.

// 1. ConsumerWidget 상속
class HelloWorldWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
  	// build 메소드의 ref를 통해 ref.watch를 사용하여 helloWorlProvider 사용
    final helloWorldString = ref.watch(helloWorldProvider);
    return Text(
      helloWorldString
    );
  }
}

 

ConsumerStatefulWidget과 ConsumerState입니다.

ConsumerStatefulWidget을 상속받고, ConsumerState 상태를 반환하게 변경하면 상태내에서 WidgetRef를 사용할 수 있습니다. 

build 메소드외에도 ref를 접근할 수 있게 됩니다.

ConsumerStatefulWidget와 은 ConsumerState는 StatefulWidget을 ConsumerStatefulWidget로 State를 ConsumerState로 변경한 형태입니다.

// ConsumerStatefuleWidget 상속
class HelloWorldWidget extends ConsumerStatefulWidget {
  // ConsumerState 상태를 반환
  @override
  ConsumerState<HelloWorldWidget> createState() => _HelloWorldWidgetState();
}

class _HelloWorldWidgetState extends ConsumerState<HelloWorldWidget> {
  @override
  Widget build(BuildContext context) {
  	// 상태 내 어디서든 사용할 수 있는 ref를 통해 Provider 사용
    final helloWorldString = ref.watch(helloWorldProvider);
    return Text(
      helloWorldString,
    );
  }
}

 

마지막 방법은 Consumer 입니다.

Consumer 위젯을 사용하여 builder 매개 변수에 있는 WidgetRef를 사용하여 provider에 접근할 수 있습니다.

class HelloWorldWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
  	// Consumer 위젯 사용
    return Consumer(
      // builder 매개변수 ref 사용
      builder: (context, ref, child) {
        final helloWorldString = ref.watch(helloWorldProvider);
        return Text(
          helloWorldString,
        );
      },
    );
  }
}​

 

WidgetRef?

위의 방법들을 살펴보면 WidgetRef를 통해 Provider를 접근하고 사용합니다. WidgetRef는 무엇일까요?

공식 문서에 따르면 WidgetRef는 위젯이 Provider와 상호작용할 수 있도록 하는 객체라고 합니다. 

WidgetRef를 통해 상호작용하는 방법은 어떤게 있을까요 ? (https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/WidgetRef-class.html)

  • ref.watch - Provider의 상태를 구독하여 상태가 변경될 때마다 UI를 리빌드합니다. 주로 화면에 즉각 반영해야할 때 사용합니다.
  • ref.read - Provider의 상태를 읽기만 할 때 사용합니다. 상태 변경을 구독하지 않아 UI는 리빌드하지않고, 주로 사용자와의 상호작용에 의한 이벤트를 사용할 때 사용합니다.
  • ref.listen - watch와 동일하기 상태를 구독하지만 리빌드는 하지 않지만 상태 값이 변경함에 따라 정의한 함수를 실행합니다. 주로 상태 변경시 함수를 실행하여 특정 상태인 경우 처리하는데 사용합니다.

 

Provider의 종류

1. Provider

가장 기본이 되는 Provider로 단순히 값을 읽을 수 있는 Provider입니다. 

final helloWorldProvider = Provider<String>(
  (ref) => 'helloWorld',
);

2. StateProvider

StateProvider는 상태를 수정하는 방법을 노출하는 Provider로 가장 간단한 상태를 저장하는 용도로 적합합니다.

// int 상태를 저장하는 StateProvider 정의
final counterStateProvider = StateProvider((ref) => 0);

class HelloWorldWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterStateProvider);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$counter'),
        ElevatedButton(
          onPressed: () {
            // counterStateProvider의 상태(int)를 증가
            ref.read(counterStateProvider.notifier).state++;
          },
          child: const Text('increment'),
        )
      ],
    );
  }
}

 

3. StateNotifierProvider

StateNotifierProvider는 StateNotifier를 수신하고 노출하는데 사용하는 Provider입니다. 사용자와의 상호 작용이나 이벤트로 계속해서 변화하는 상태를 관리할때 사용합니다.

// StateNotifierProvider 정의
final counterNotifierProvider = StateNotifierProvider<CounterNotifier, int>(
  (ref) => CounterNotifier(),
);

// CounterNotifier 정의
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  // 증가함수
  void increment() {
    state++;
  }

  // 감소함수
  void decrement() {
    state--;
  }
}

class HelloWorldWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterNotifierProvider);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$counter'),
        ElevatedButton(
          onPressed: () => ref.read(counterNotifierProvider.notifier).increment(), // CounterNotifier의 증가함수 사용
          child: const Text('increment'),
        ),
        ElevatedButton(
          onPressed: () => ref.read(counterNotifierProvider.notifier).decrement(), // CounterNotifier의 감소함수 사용
          child: const Text('decrement'),
        ),
      ],
    );
  }
}

 

4. FutureProvider

FutureProvider는 Provider와 동일하지만 비동기 작업을 위해 사용하는 Provider입니다.

주로 네트워크 요청, 파일 입출력, 데이터베이스 입출력의 비동기작업을 할 때 사용합니다.

// 비동기 작업을 반환하는 Provider 정의
final futureStringProvider = FutureProvider(
  (ref) async {
    await Future.delayed(Duration(seconds: 10));
    return 'helloWorld';
  },
);


class HelloWorldWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(futureStringProvider);

	// 비동기 상태에 따라 화면 렌더링
    return asyncValue.when(
      data: (data) => Text(data),
      error: (error, stackTrace) => Text('error : $error'),
      loading: () => CircularProgressIndicator(),
    );
  }
}

5. StreamProvider

StreamProvider는 FutureProvider와 비슷하지만 Stream을 처리 할 때 사용하는 Provider입니다.

 

// 1초마다 증가하는 Stream을 반환하는 Provider 정의
final counterStreamProvider = StreamProvider(
  (ref) {
    return Stream.periodic(
      const Duration(seconds: 1),
      (count) => count,
    );
  },
);

class HelloWorldWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(counterStreamProvider);

	// 받아오는 Stream에 값에 따라 랜더링
    return asyncValue.when(
      data: (count) => Text('$count'),
      error: (error, stackTrace) => Text('error : $error'),
      loading: () => CircularProgressIndicator(),
    );
  }
}

6. ChangeNotifierProvider

ChangeNotifierProvider는 Flutter 자체에서 ChangeNotifier를 수신하고 노출하는데 사용하는 Provider입니다.

Riverpod 패키지에서는 ChangeNotifierProvider를 권장하지 않는다고 합니다.

7. (async)NotifierProvider

NotifierProvider와 AsyncNotifierProvider는 Riverpod 2.0에서 새롭게 추가된 Provider로 사용자 상호작용에 의해 변경될 수 있는 상태를 관리하는 Riverpod에서 권장하는 솔루션입니다.

NotifierProvider는 Notifier를 수신하고 노출하는데 사용되는 Provider이며, AsyncNotifierProvider는 비동기적으로 초기화되는 AsyncNotifier를 사용하는 Provider라고합니다.

 

NotifierProvider 예시

@immutable
class Todo {
  const Todo({
    required this.id,
    required this.description,
    required this.completed,
  });

  // 모든 속성은 final로 정의
  final String id;
  final String description;
  final bool completed;

  // Todo 클래스는 불변 객체라 변경이 불가능하여 copyWith 구현
  Todo copyWith({String? id, String? description, bool? completed}) {
    return Todo(
      id: id ?? this.id,
      description: description ?? this.description,
      completed: completed ?? this.completed,
    );
  }
}

// NotifierProvider에 전달할 Notifier 클래스
// 이 클래스의 'state'는 외부에 공개하면 안됌 > public getter 없어야함
// 이 클래스의 공용 메서드는 UI가 상태를 수정할 수 있게 함
class TodosNotifier extends Notifier<List<Todo>> {
  // 빈 리스트를 갖는 state 초기화
  @override
  List<Todo> build() {
    return [];
  }

  // 추가
  void addTodo(Todo todo) {
    // 상태가 불변이므로 아예 새로운 상태를 만들어야 함
    // state.add - X
    // state = [...state, todo];
    state = [...state, todo];
  }

  // 삭제
  void removeTodo(String todoId) {
    state = [
      for (final todo in state)
        if (todo.id != todoId) todo,
    ];
  }

  // todoId를 갖는 Todo를 토글 처리
  void toggle(String todoId) {
    state = [
      for (final todo in state)
        if (todo.id == todoId)
          todo.copyWith(completed: !todo.completed)
        else
          todo,
    ];
  }
}

// NotifierProvider를 사용해서 UI와 상호작용할 수 있도록 정의
final todosProvider = NotifierProvider<TodosNotifier, List<Todo>>(() {
  return TodosNotifier();
});

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // todo list 변경시 재빌드
    List<Todo> todos = ref.watch(todosProvider);

    return ListView(
      children: [
        for (final todo in todos)
          CheckboxListTile(
            value: todo.completed,
            // todo 선택 시 토글 처리
            onChanged: (value) => ref.read(todosProvider.notifier).toggle(todo.id),
            title: Text(todo.description),
          ),
      ],
    );
  }
}

 

AsyncNotifierProvider를 사용한 예시

@immutable
class Todo {
  const Todo({
    required this.id,
    required this.description,
    required this.completed,
  });

  // 모든 속성은 final로 정의
  final String id;
  final String description;
  final bool completed;

  // Todo 클래스는 불변 객체라 변경이 불가능하여 copyWith 구현
  Todo copyWith({String? id, String? description, bool? completed}) {
    return Todo(
      id: id ?? this.id,
      description: description ?? this.description,
      completed: completed ?? this.completed,
    );
  }
}

// NotifierProvider에 전달할 AsyncNotifier 클래스
class AsyncTodosNotifier extends AsyncNotifier<List<Todo>> {
  Future<List<Todo>> _fetchTodo() async {
    final json = await http.get('api/todos');
    final todos = jsonDecode(json) as List<Map<String, dynamic>>;
    return todos.map(Todo.fromJson).toList();
  }

  @override
  Future<List<Todo>> build() async {
    // 원격 저장소에서 todo list를 가져오는 초기값 future
    return _fetchTodo();
  }

  Future<void> addTodo(Todo todo) async {
    // 로딩 상태로 설정
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      // 새로운 할일을 추가
      await http.post('api/todos', todo.toJson());
      // 이후 재로드
      return _fetchTodo();
    });
  }

  
  Future<void> removeTodo(String todoId) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      // 새로운 할일을 삭제
      await http.delete('api/todos/$todoId');
      // 이후 재로드
      return _fetchTodo();
    });
  }

  Future<void> toggle(String todoId) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      // 토글 처리
      await http.patch(
        'api/todos/$todoId',
        <String, dynamic>{'completed': true},
      );
      // 이후 재로드
      return _fetchTodo();
    });
  }
}

// AsyncNotifierProvider를 사용해서 UI와 상호작용할 수 있도록 정의
final asyncTodosProvider = AsyncNotifierProvider<AsyncTodosNotifier, List<Todo>>(() {
  return AsyncTodosNotifier();
});

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // todo list 변경시 재빌드
    final asyncTodos = ref.watch(asyncTodosProvider);

    return switch (asyncTodos) {
      AsyncData(:final value) => ListView(
          children: [
            for (final todo in value)
              CheckboxListTile(
                value: todo.completed,
                // 토글 처리
                onChanged: (value) {
                  ref.read(asyncTodosProvider.notifier).toggle(todo.id);
                },
                title: Text(todo.description),
              ),
          ],
        ),
      AsyncError(:final error) => Text('Error: $error'),
      _ => const Center(child: CircularProgressIndicator()),
    };
  }
}

 

StateNotifierProvider와 NotifierProvider는 비슷하지만 확인해보면 NotifierProvider는 build() 함수를 오버라이딩해야하는 차이점과 AsyncNotifierProvider는 비동기 상태를 가진다는 차이점이 있는데 또 다른 차이점이 무엇이 있을지 더 공부해봐야할 것 같습니다.

 

Riverpod을 통한 Counter App

Rivepod에 관한 간단한 내용을 확인했으니 Counter App을 만들어봅니다.

 

위의 Provider 중 카운터 앱을 만들기 위해서는 StateProvider, StateNotifierProvider, NotifierProvider 정도를 사용하면 구현 할 수 있을 것 같네요. 저는 StateNotifierProvider를 통해 구현해보겠습니다. 패키지는 설치되어있다고 가정하고 진행하겠습니다.

1. ProviderScope 감싸기

Rivderpod을 사용하기 위해 ProviderScope를 최상단 위젯에 먼저 감싸줍니다.

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

2. StateNotifier 정의하기

클릭한 count와 count 증가 함수를 갖는 StateNotifier를 정의합니다.

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() {
    state++;
  }
}

3. StateNotifierProvider 정의하기

2번에서 정의한 CounterNotifier를 사용자와 상호작용할 수 있도록 StateNotifierProvider를 정의합니다.

final counterNotifierProvider = StateNotifierProvider<CounterNotifier, int>(
  (ref) => CounterNotifier(),
);

4.  ConsumerWidget을 사용한 화면 및 이벤트 처리

화면은 간단하게 중앙에 클릭한 숫자와 증가 플로팅 버튼을 배치합니다.

ConsumerWidget을 상속해주고 WidgetRef를 통해 count 상태값과 count 증가함수를 사용해 줍니다.

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
  	// 현재 count 상태값 - 변경 시 rebuild
    final count = ref.watch(counterNotifierProvider);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Riverpod Counter App'),
      ),
      body: Center(
        child: Text(
          '$count',
          style: const TextStyle(
            fontSize: 60,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
      	// 증가 처리
        onPressed: () => ref.read(counterNotifierProvider.notifier).increment(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

5.  완성

후기

이번주 과제를 통해 Riverpod과 Provider의 차이, Riverpod에서 제공되는 여러가지 Provider들을 확인할 계기였습니다. Riverpod에 관한 공식 문서도 꼼꼼히 읽게 되고 여러 자료들을 참고할 수 있는 시간이었습니다. 현업에서 Provider를 상태관리로 사용하고 있어 Rivderpod을 공부하고 사용할 계기가 없었는데 이렇게 좋은 시간을 보낸 것 같습니다. 아직 모르는 부분이 많은것 같은데(WidgetRef, Notifier, StateNotifierProvider vs NotifierProvider 등) 시간을 내서 더 공부해봐야겠습니다.