ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RiverPod 배우기
    flutter 2025. 2. 13. 13:55
    반응형

    오늘은 많은? 회사에서 사용하는 RiverPod에 대해 글을 작성하려고 해요!

    provider랑 bloc, GetX는 사용을 해봤지만 Riverpod는 이번이 처음이라 한번 사용한걸 정리하려고 해요

     

    MVVM + Riverpod를 활용한 사용자 정보를 가져와 목록을 보여주는 기능을 예제로 만들어 볼게요

    MVVM + Riverpod의 장점

    비즈니스 로직과 UI 분리 → 유지보수가 쉬움
    StateNotifierProvider를 활용하여 효율적인 상태 관리
    AsyncValue를 활용하여 로딩, 성공, 에러 처리 간편

     

    AsyncValue란? (Flutter Riverpod)

    AsyncValue<T>는 Flutter Riverpod에서 비동기 데이터를 안전하고 쉽게 관리할 수 있도록 도와주는 타입입니다.
    비동기 API 요청 결과를 로딩, 성공, 에러 상태로 구분하여 처리할 수 있습니다.

     

    기존 방식 불편함

    FutureBuilder<List<User>>(
      future: fetchUsers(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        } else if (snapshot.hasError) {
          return 
        } else {
          return ListView.builder(
            itemCount: snapshot.data!.length,
            itemBuilder: (context, index) {
              return 
            },
          );
        }
      },
    );

     

     

    • FutureBuilder 내부에서 로딩, 성공, 에러를 직접 처리해야 해서 코드가 길어짐
    • snapshot.hasError 등을 계속 확인해야 함

    AsyncValue를 사용하면? (더 깔끔한 코드)

    final userProvider = FutureProvider<List<User>>((ref) async {
      return fetchUsers();
    });
    
    ConsumerWidget(
      builder: (context, ref, child) {
        final users = ref.watch(userProvider);
    
        return users.when(
          loading: () => CircularProgressIndicator(),
          error: (error, stack) => Text("Error: $error"),
          data: (users) => ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              return 
            },
          ),
        );
      },
    );

     

    • when()을 사용하여 로딩, 성공, 에러 상태를 깔끔하게 분리
    • 더 직관적이고 유지보수하기 쉬운 코드

     

    AsyncValue<T>는 세 가지 상태를 가질 수 있습니다.

    1. 로딩 중 (AsyncValue.loading()) → 데이터를 불러오는 중
    2. 성공 (AsyncValue.data(value)) → 데이터 불러오기 성공
    3. 오류 (AsyncValue.error(error, stackTrace)) → 데이터 불러오기 실패

    maybeWhen()을 사용하여 일부 상태만 처리

    maybeWhen()을 사용하면 특정 상태만 다루고, 나머지는 기본값으로 처리할 수 있습니다.

    users.maybeWhen(
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) => Text(users[index].name),
      ),
      orElse: () => CircularProgressIndicator(),
    );

    data 상태만 별도로 처리하고, 나머지는 orElse에서 처리함.

     

    AsyncNotifier와 함께 사용

    비동기 상태를 업데이트해야 하는 경우, AsyncNotifier와 함께 사용하면 더욱 효과적입니다.

    final userNotifierProvider = AsyncNotifierProvider<UserNotifier, List<User>>(() => UserNotifier());
    
    class UserNotifier extends AsyncNotifier<List<User>> {
      @override
      Future<List<User>> build() async {
        return fetchUsers();
      }
    
      Future<void> refreshUsers() async {
        state = const AsyncValue.loading();
        try {
          final users = await fetchUsers();
          state = AsyncValue.data(users);
        } catch (e, stack) {
          state = AsyncValue.error(e, stack);
        }
      }
    }

    AsyncNotifier를 사용하면 상태를 갱신하거나 API를 다시 호출하는 기능도 쉽게 추가 가능

     

    AsyncValue와 FutureProvider 비교

    특징AsyncValueFutureProvider

    특징 AsyncValue FutureProvider
    비동기 상태 관리 지원 지원
    loading, error, data 상태 자동 처리 있음  없음
    when()으로 UI 처리 가능 직접 처리해야 함
    isLoading, hasError 상태 확인 가능 불가능
    state 업데이트 가능 (AsyncNotifier 사용) 가능 불가능

     

    - AsyncValue는 Riverpod에서 비동기 상태를 쉽게 관리할 수 있도록 도와줌
    - API 호출 결과를 loading, data, error 상태로 구분하여 처리 가능
    - when()을 사용하면 UI 코드가 깔끔해지고 유지보수 쉬워짐
    - AsyncNotifier와 함께 사용하면 API 호출 및 상태 갱신을 더 쉽게 관리 가능


     

    먼저 riverpod를 추가

    https://pub.dev/packages/riverpod

     

    riverpod | Dart package

    A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.

    pub.dev

     

    패키지를 추가후 아래와 같이 만들어 줍니다.

     

    프로젝트 구조로는

    lib 밑에 

    models
    viewmodels
    views
    services

     

    이렇게 세가지를 만들었어요.

     

    • models/ → 데이터 모델
    • viewmodels/ → 상태 관리 (Riverpod 사용)
    • services/ → API 및 비즈니스 로직
    • views/ → UI 화면

     

     

    그럼 먼저 views/UserScreen.dart  를 만들고아래와 같이 main에 연결합니다.

    class MyApp extends StatelessWidget {
      const MyApp({super.key});
      @override
      Widget build(BuildContext context) {
        return ProviderScope(
          child: MaterialApp(
            debugShowCheckedModeBanner: false,
            home: UserScreen(),  // MaterialApp 내부에서 UserScreen 사용
          ),
        );
      }
    }

     

    ProviderScope가 하는 역할

    1. ProviderScope을 추가하면, 모든 Provider들이 사용할 수 있는 전역 상태 저장소가 생성됩니다.
    2. 이를 통해 ref.watch(), ref.read(), ref.listen() 등이 정상적으로 작동합니다.

     

     

    이후 user_screen.dart에 아래의 코드를 넣어주세요.

    import 'package:flutter/material.dart';
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    import '../viewmodels/user_viewmodel.dart';
    
    class UserScreen extends ConsumerWidget {
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final userState = ref.watch(userViewModelProvider);
    
        return Scaffold(
          appBar: AppBar(title: Text('Users')),
          body: userState.when(
            data: (users) => ListView.builder(
              itemCount: users.length,
              itemBuilder: (context, index) {
                final user = users[index];
                return ListTile(
                  title: Text(user.name),
                  subtitle: Text(user.email),
                );
              },
            ),
            loading: () => Center(child: CircularProgressIndicator()),
            error: (error, _) => Center(child: Text('Error: $error')),
          ),
        );
      }
    }

    ref.watch(userViewModelProvider)를 사용하여 ViewModel에서 데이터를 가져옵니다.
    when을 사용하여 로딩, 성공, 에러 상태를 구분하여 UI를 렌더링합니다.

    ※ WidgetRef의 역할 (Flutter Riverpod)

    WidgetRef는 Riverpod에서 Provider를 읽고, 감시하고, 관리할 수 있도록 도와주는 객체입니다.
    주로 ConsumerWidget 또는 ConsumerStatefulWidget에서 사용됩니다.

     

     

    메서드 설명 사용 예시
    ref.watch(provider) Provider의 상태를 감지하고 UI를 자동 업데이트 final count = ref.watch(counterProvider);
    ref.read(provider) Provider의 현재 값을 가져오지만, UI를 자동 업데이트하지 않음 ref.read(counterProvider.notifier).state++;
    ref.listen(provider, (previous, next) {}) Provider 상태가 변경될 때 특정 로직 실행 ref.listen(counterProvider, (prev, next) => print(next));
    ref.invalidate(provider) Provider를 다시 초기화하여 최신 상태로 만듦 ref.invalidate(userProvider);

     

    ref.watch() vs ref.read() 차이점

    ref.watch(provider) (UI 자동 업데이트)

    ref.read(provider) (UI 자동 업데이트 X)

     

     

     

     

    이제 나머지 코드를 만들어 주세요

    viewmodels /user_viewmodel.dart

    import 'package:flutter_riverpod/flutter_riverpod.dart';
    import '../models/user_model.dart';
    import '../services/user_service.dart';
    
    final userViewModelProvider = StateNotifierProvider<UserViewModel, AsyncValue<List<User>>>((ref) {
      return UserViewModel(UserService());
    });
    
    class UserViewModel extends StateNotifier<AsyncValue<List<User>>> {
      final UserService _userService;
    
      UserViewModel(this._userService) : super(const AsyncValue.loading()) {
        fetchUsers();
      }
    
      Future<void> fetchUsers() async {
        try {
          state = const AsyncValue.loading();
          final users = await _userService.fetchUsers();
          state = AsyncValue.data(users);
        } catch (e) {
          state = AsyncValue.error(e, StackTrace.current);
        }
      }
    }

    Riverpod을 사용하여 상태 관리를 담당하는 ViewModel을 만듭니다.

    StateNotifierProvider를 사용하여 상태를 관리합니다.
    AsyncValue<List<User>>를 사용하여 로딩, 데이터, 에러 상태를 쉽게 처리할 수 있습니다.

     

     

    models /user_model.dart

    class User {
      final int id;
      final String name;
      final String email;
    
      User({required this.id, required this.name, required this.email});
    
      factory User.fromJson(Map<String, dynamic> json) {
        return User(
          id: json['id'],
          name: json['name'],
          email: json['email'],
        );
      }
    }

    사용자 데이터를 표현하는 모델을 생성합니다.

     

    services /user_service.dart

    import 'dart:convert';
    import 'package:http/http.dart' as http;
    import '../models/user_model.dart';
    
    class UserService {
      Future<List<User>> fetchUsers() async {
        final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
    
        if (response.statusCode == 200) {
          List<dynamic> data = json.decode(response.body);
          return data.map((json) => User.fromJson(json)).toList();
        } else {
          throw Exception('Failed to load users');
        }
      }
    }

    실제 API에서 데이터를 가져오는 서비스입니다.

     

     

    결과

    반응형

    댓글

Designed by Tistory.