플러터(2)

4 minute read

Riverpod

Provider

  • 가장 기본 베이스가 되는 Provider
  • 아무 타입이나 반환 가능
  • 서비스, 계산한 값드을 반활할때 사용
  • 반환값을 캐싱할때 유용하게 사용된다,
  • 여러 Provider의 값들을 묶어서 사용가능
  • ex) 데이터 캐싱
  • 읽기만 가능하고 값 변경 불가능
final provider = Provider<int>((ref){
    return 0;
});

StateProvider

  • UI에서 “직접적으로” 데이터를 변경할 수 있도록 하고싶을때 사용
  • 단순한 형태의 데이터만 관리 (int, double, String 등)
  • Map, List등 복잡한 형태의 데이터는 다루지 않음
  • 복잡한 로직이 필요한경우 사용(number++ 정도의 간단한 로직에서 사용)
  • ex) 간단한 상태값 관리
final stateProvider = StateProvider<int>((ref){
    return 0;
});

Widget build(BuildContext context, WidgetRef ref) {
  final state = ref.watch(stateProvider);
  return Scaffold(
    body: Column(
        children : [
            ElevateButton(
                onPressed: (){
                    ref.read(stateProvider.notifier).update((state) => state+1);
                },
                child: Text(
                    '증가'
                )
            ),
                        ElevateButton(
                onPressed: (){
                    ref.read(stateProvider.notifier).update((state) => state-1);
                },
                child: Text(
                    '감소'
                )
            )
        ]
    )
  );
}

StateNotifierProvider

  • StateProvider와 마찬가지로 UO에서 “직접적으로” 데이터를 변경할 수 있도록 하고싶을때 사용
  • 복잡한 형태의 데이터 관리가능 (클래스의 메소드를 이용한 상태관리)
  • StateNotifier를 상속한 클래스 반환
  • ex) 복잡한 상태값 관리

ShoppingItemModel


class ShoppingItemModel{
    final String name;
    final String quantity;
    final bool hasBought;

    ShoppingItemModel({
        required this.name,
        required this.quantity.
        required this.hasBought
    });
}

StateNotifierPrivoder

final shoppingListProvider = StateNotifierProvider<ShooppingListNotifier,List<ShoppingItemModel>>((ref) => ShoppingListNotifier());

class ShoppingListNotifier extends StateNotifier<List<ShoppingItemModel>>{
    ShoppingListProvider():
        suepr([
            ShoppingItemModel(
                name: '김치',
                quantity: 3,
                hasBought: true
            ),
                        ShoppingItemModel(
                name: '라면',
                quantity: 5,
                hasBought: false
            ),
                        ShoppingItemModel(
                name: '삼겹살',
                quantity: 30,
                hasBought: true
            )
        ]);
    
    void toggleHasBought({required String name}){
        //state는 StateNotifier에 자동으로 사용 가능(super 값으로 자동 초기화)
        state = state.map((e)=> e.name == name ? ShoppingModel(
            name: e.name,
            quantity: e.quantity,
            hasBought: !e.hasBought,
        )
        : e);
    }
    
}

사용예시

class StateNotifierProviderScreen extends ConsumerWidget{


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

        return Container(
            body: ListView(
                children : state
                        .map(
                            (e) => CheckboxListTile(
                                title: Text(e.name)
                                value: e.hasBought
                                onChanged: (value){
                                    ref.read(shoppingListProvider.notifier).toggleHasBought(name: e.name);
                                }
                            )
                        )
            )
        );
    }
}

FutureProvider

  • Future 타입만 반환 가능
  • API 요청의 결과를 반환할때 자주 사용
  • 복잡한 로직 또는 사용자의 특정 행동뒤에 Future를 재실행하는 기능이 없음 -> 필요할 경우 StateNotifierProvider 사용
  • ex) API 요청의 Future 결과값
final futureProvider = FutureProvider<List<int>>((ref) async{
    await future.delayed(
        Duration(
            seconds:2
        )
    );

    return [1,2,3,4,5];
})

StreamProvider

  • Stream 타입만 반환 가능
  • API 요청의 결과를 Stream으로 반환할때 자주 사용(Socket 등)
  • ex) API 요청의 Stream 결과값

Modifier

Family Modifier
//두번째 파라미터를 제너릭이랑 전달값에 넣어줌
final familyModifierProvider = FutureProvider.family<List<int>,int>((ref,data) async{
    await Future.delayed(Duration(seconds:2));

    return [1,2,3];
})
AutoDispose Modifier
//로딩이 한번 되고나서 다른 프로바이더는 state값을 기억하지만 autoDispose는 값을 캐싱하지 않음
final autoDisposedModifierProvider = FutureProvider.autoDispose<List<int>>((ref) {
    await Future.delayed(Duration(seconds:2));

    return [1,2,3];
});

listenProvider

provider

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

screen


//ConsumerStatefulWidget은 WidgetRef 선언하지 않아도 바로 ref 사용 가능
class ListenProviderScreen extends ConsumerStatefulWidget {
  const ListenProviderScreen({Key? key}) : super(key: key);

  @override
  ConsumerState<ListenProviderScreen> createState() => _ListenProviderScreenState();
}

class _ListenProviderScreenState extends ConsumerState<ListenProviderScreen> with
TickerProviderStateMixin{
  late final TabController controller;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    controller = TabController(length: 10, vsync: this,
    initialIndex: ref.read(listenProvider));
  }

  @override
  Widget build(BuildContext context) {
  ref.listen<int>(listenProvider, (previous, next) {
    if(previous!=next){
      controller.animateTo(
        next
      );
    }
  });
    return DefaultLayout(
        title: 'ListenProviderScreen',
        body: TabBarView(
          physics: NeverScrollableScrollPhysics(),
          controller: controller,
          children: List.generate(
              10,
                  (index) => Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(index.toString()),
                      ElevatedButton(
                          onPressed: (){
                            ref.read(listenProvider.notifier).update((state) => state == 10 ? 10 : state+1);
                          },
                          child: Text('다음')),
                      ElevatedButton(
                          onPressed: (){
                            ref.read(listenProvider.notifier).update((state) => state == 0 ? 0 : state-1);
                          },
                          child: Text('뒤로')),
                    ],
                  )),
        )
    );
  }
}

selectProvider

    //isSpicy값만 변경 됐을때 리빌드, state가 isSpicy의 값으로 반환
    final state = ref.watch(selectProvider.select((value) => value.isSpicy));
    ref.listen(selectProvider.select((value) => value.hasBought),
            (previous, next) {

            }
    );

Provider 안에 Provider 사용하기

screen

class ProviderScreen extends ConsumerWidget {
  const ProviderScreen({Key? key}) : super(key: key);

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

    return DefaultLayout(title: 'ProviderScreen',
      actions: [
        PopupMenuButton<FilterState>(
            itemBuilder: (_) => FilterState.values.map(
                     (e) => PopupMenuItem(
                        value: e,
                        child: Text(e.name)
                    )
            ).toList(),
          onSelected: (value){
              ref.read(filterProvider.notifier).update((state) => value);
          },
        )
      ],
      body: ListView(
        children:
            state.map((e) => CheckboxListTile(
          title: Text(e.name),
          value: e.hasBought,
          onChanged: (value){
            ref.read(shoppingListProvider.notifier).toggleHasBought(name: e.name);
            },
          ),
        ).toList(),
      )
    );
  }
}

provider

final filteredShoppingListProvider = Provider<List<ShoppingItemModel>>(
        (ref) {
          final filterState = ref.watch(filterProvider);
          final shoppingListState = ref.watch(shoppingListProvider);

          if(filterState == FilterState.all){
            return shoppingListState;
          }

          return shoppingListState.where((element) =>
            filterState == FilterState.spicy
              ? element.isSpicy
              : !element.isSpicy
          ).toList();
        }
);

enum FilterState{
  notSpicy,
  spicy,
  all
}

final filterProvider = StateProvider<FilterState>((ref) => FilterState.all);

ProviderObserver - 로그 용도로 사용

main.dart

void main() {
  runApp(
    ProviderScope(
      observers: [
        Logger(),
      ],
      child: MaterialApp(
        home: HomeScreen(),
      ),
    )
  );
}

provider

class Logger extends ProviderObserver{
  //하위에 있는 provider를 업데이트 했을 때 무조건 이 코드가 불림
  @override
  void didUpdateProvider(ProviderBase<Object?> provider, Object? previousValue,
      Object? newValue, ProviderContainer container) {
    print('[Provider Update] provider: $provider / pv: $previousValue / nv: $newValue' );

  }
  //프로바이더를 추가했을 때
  @override
  void didAddProvider(ProviderBase<Object?> provider, Object? value, ProviderContainer container) {
    print('[Provider Added] provider: $provider / value: $value' );

  }
  //프로바이더를 삭제했을 때(autoDisposed provider)
  @override
  void didDisposeProvider(ProviderBase<Object?> provider, ProviderContainer container) {
    print('[Provider Dispose] provider: $provider' );
  }
}

ref.read vs ref.watch

  • ref.watch는 반환값의 업데이트가 있을때 지속적으로 build 함수를 다시 실행해준다.
  • ref.watch는 필수적으로 UI관련 코드에만 사용한다.
  • ref.read는 실행되는 순간 단 한번만 provider 값을 가져온다.
  • ref.read는 onPressed 콜백처럼 특정 액션 뒤에 실해되는 함수 내부에서 사용된다.

Leave a comment