728x90

Riverpod 가 좋은 상태관리 라이브러리 이지만 초보자에겐 어렵다는 것에 공감한다.

하나씩 이해하기 위해서 기록하고 수정 보완하려고 한다.

 

Flutter riverpod 자동 생성하는 방법이다.

# 상태관리툴 라이브러리 riverpod 추가
flutter pub add flutter_riverpod riverpod_annotation
flutter pub add -d riverpod_generator

 

 

@riverpod 를 어노테이션을 사용하여 riverpod 자동 생성을 할 수 있다.

함수로 정의하는 경우와 클래스로 정의하는 경우가 있는데, 클래스로 정의하는 경우를 더 많이 사용할 거 같다.

import 'package:riverpod_annotation/riverpod_annotation.dart';
 
part 'sample_provider.g.dart';
 
/***
 * 1) 어떤 Provider를 사용할지 결정할 고민 필요없도록
 * 2) Parameter > Family 파라미터를 일반 함수처럼 사용할 수 있도록
 *
 * 함수명 gState 에서 첫글자를 대문자로 변경하고, Ref 를 붙인다. GStateRef ref
 * 함수 정의 이후  dart run build_runner build 명령어를 터미널 창에서 실행한다.
 */
 
@riverpod
String gState(GStateRef ref) {
  return 'Code Generation Riverpod';
}
 
// final _gStateProvider = Provider<String>((ref) => 'Code Generation Riverpod');
 
@riverpod
int gStateMultiply(GStateMultiplyRef ref, {
  required int number1,
  required int number2,
}){
  return number1 * number2;
}
 
@riverpod
class GNotifier extends _$GNotifier {
  @override
  int build() {
    return 0;
  }
 
  increment(){
    state++;
  }
 
  decrement() {
    state--;
  }
}
 
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  List<String> build() {
    return [];
  }
 
  void addString(String stringToAdd) {
    state = [...state, stringToAdd];
  }
}

 

함수처럼 정의하여 사용하는 경우와 Class 로 정의하여 사용하는 경우를 예시하고 있다.

 

 

자동으로 생성하지 않고 직접 구현하는 경우의 코드.

import 'package:riverpod_annotation/riverpod_annotation.dart';
 
final myNotifierProvider = NotifierProvider<MyNotifier, List<String>>(MyNotifier.new);
 
class MyNotifier extends Notifier<List<String>> {
  // List<String> 이 state 를 의미하고, 반환타입이다.
  @override
  List<String> build() {
    return []; // 상태(state) 초기값 정의
  }
 
  void addString(String stringToAdd){
    state = [...state, stringToAdd];
  }
}

 

자동으로 생성한 코드

아래 코드와 위의 코드는 서로 같은 결과를 반환한다.

import 'package:riverpod_annotation/riverpod_annotation.dart';
 
part 'my_provider.g.dart';
 
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  List<String> build() {
    return [];
  }
 
  void addString(String stringToAdd) {
    state = [...state, stringToAdd];
  }
}

 

 

UI에 출력하여 결과를 확인해보자.

최상위에 ProviderScope를 지정하여 project 전반에 프로바이더 선언/접근을 가능하게 한다.

void main() {
  runApp(
    ProviderScope(
      child: const MyApp(),
    ),
  );
}
 
class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Riverpod Autho Generation Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: HomeScreen(),
    );
  }
}
 

 

 

Riverpod를 읽기 위해서는 ref 객체가 필요하다.
ref 객체는 일반적으로 사용하는 StatelessWidget 에서는 얻을 수 없고, ConsumerWidget을 사용하거나 사용하고 싶은 위젯 부분에서 Consumer 위젯으로 감싸면 ref 객체를 얻을 수 있다.
build() 메서드 내부에 선언한 listOfString 변수는 ref 객체의 watch 메서드를 사용하여 myNotifierProvider의 List<String> 타입의 상태값을 받아온다.
myNotifierProvider의 상태가 변할 때마다 감지할 수 있고 위젯을 리빌드 할 수 있다.

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_v2/provider/sample_provider.dart';
 
class HomeScreen extends ConsumerWidget {
  HomeScreen({Key? key}) : super(key: key);
 
  Random random = new Random();
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final listOfString = ref.watch(myNotifierProvider) as List;
 
    ref.listen<List>(myNotifierProvider, (List? prevState, List newState) {
      print('This function have been called');
    });
 
    return Scaffold(
      appBar: AppBar(
        title: Text('Riverpod Sample'),
        centerTitle: true,
        actions: [
          IconButton(
            onPressed: () {
              ref
                  .read(myNotifierProvider.notifier)
                  .addString('string ${random.nextInt(100)}');
            },
            icon: const Icon(Icons.add),
          ),
        ],
      ),
      body: Center(
        child: Column(
          children: [
            ...listOfString.map(
              (string=> Text(string),
            ),
          ],
        ),
      ),
    );
  }
}

 

 

자동으로 생성하는 CRUD 예제를 더 보강할 예정이다.

Riverpod 를 생성하는 기본 사항을 알고 나면, Dart 언어를 잘 다루는 것이 중요하다는 걸 많이 느끼고 있다.

Dart asMap 에 대한 Dart 문법은 https://link2me.tistory.com/2372 를 참조하면 도움된다.

블로그 이미지

Link2Me

,
728x90

Flutter Provider로 작성된 상태관리 ViewModel이다.

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
 
class CatViewModel extends ChangeNotifier {
 
  final List<String> _catImages = []; // 고양이 사진을 담을 변수
 
  List<String> _favoriteImages = []; // 좋아요 사진을 담을 변수
 
  List<String> get catImages => _catImages;
 
  List<String> get favoriteImages => _favoriteImages;
 
 Dio _dio = Dio();
 
  // 생성자
  CatViewModel() {
   _dio.interceptors.add(LogInterceptor());
   _dio.interceptors.add(CustLogInterceptor());
    getRandomCatImages();
  }
 
  void getRandomCatImages() async {
    Response resp = await _dio.get(
        'https://api.thecatapi.com/v1/images/search?limit=10&mime_types=jpg');
    print(resp.data);
    for (int i = 0; i < resp.data.length; i++) {
      final map = resp.data[i];
      _catImages.add(map['url']); // url만 추출하여 catImages 에 이미지 추가.
    }
    notifyListeners();
  }
 
  // 좋아요 토글
  void toggleFavoriteImage(String catImage) {
    if (_favoriteImages.contains(catImage)) {
      _favoriteImages.remove(catImage); // 이미 좋아요한 경우 제거
    } else {
      _favoriteImages.add(catImage); // 새로운 사진 추가
    }
 
    notifyListeners(); // 새로고침
  }
}

 

ViewModel 로 클래스명을 정의하기도 하고 Service로 정의하기도 한다.

View 에서 코드를 분리하여 ViewModel에 코드를 대부분 구현하고, View에서는 UI중심으로 코드가 최소화되도록 구현하는 것이 중요하다.

ViewModel 에서는 UI에서 직접적으로 수정할 수 없도록 변수를 선언하는 것이 매우 중요하다.

그래서 위 예제에서는 Class 내부에 있는 변수에 언더바(_)를 붙여서 Private로 정의했고, 외부에서 접근하는 것은 Getter를 추가했다.

기능을 추가할 때 ViewModel 안에 선언한 변수를 외부에서 직접적으로 접근할 수 없도록 신경써야 한다.

 

 

CustLogInterceptor는 Interceptor를 상속받았고 특별하게 코드를 추가한 것은 없다.

아래 코드를 잘 활용하면 유용하게 사용할 수 있다.

import 'package:dio/dio.dart';
 
class CustLogInterceptor extends Interceptor {
 
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    print('REQUEST[${options.method}] => PATH: ${options.path}');
    return super.onRequest(options, handler);
  }
 
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) async {
    print('Response_URI >>> ${response.realUri.toString()}');
    return super.onResponse(response, handler);
  }
 
  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    print(
      'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}',
    );
    return super.onError(err, handler);
  }
}

 

 

HomePage UI 클래스

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class HomePage extends StatelessWidget {
  const HomePage({super.key});
  @override
  Widget build(BuildContext context) {
    return Consumer<CatViewModel>(
      builder: (context, viewModel, child) {
        return Scaffold(
          body: GridView.count(
            mainAxisSpacing: 8,
            crossAxisSpacing: 8,
            crossAxisCount: 2,
            padding: EdgeInsets.all(8),
            children: List.generate(
              viewModel.catImages.length,
              (index) {
                String catImage = viewModel.catImages[index];
                return GestureDetector(
                  onTap: () {
                    viewModel.toggleFavoriteImage(catImage);  // 사진 클릭시
                  },
                  child: Stack(
                    children: [
                      // 사진
                      Positioned.fill(
                        child: Image.network(
                          catImage,
                          fit: BoxFit.cover,
                        ),
                      ),
                      // 좋아요
                      Positioned(
                        bottom: 8,
                        right: 8,
                        child: Icon(
                          Icons.favorite,
                          color: viewModel.favoriteImages.contains(catImage)
                              ? Colors.amber
                              : Colors.transparent,
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        );
      },
    );
  }
}

 

 

 

'Flutter 앱 > 상태관리' 카테고리의 다른 글

Flutter riverpod 자동 생성  (0) 2024.01.15
Flutter Riverpod 예제 : Bucket List  (0) 2023.12.27
Flutter StatefulWidget  (0) 2023.12.13
Flutter Riverpod - NotifierProvider.family  (0) 2023.12.11
Flutter Riverpod - NotifierProvider  (0) 2023.12.11
블로그 이미지

Link2Me

,
728x90

온라인 강좌를 듣고 Provider 상태관리를 Riverpod 로 변경해보고 있다.

구글 검색으로 많이 나오는 Todo 에 예제와 비슷한 예제이다.

import 'package:bucket_list/view/home_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
void main() {
  runApp(
    ProviderScope(
      child: const MyApp(),
    ),
  );
}
 
class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: HomePage(),
    );
  }
}

 

import 'package:bucket_list/provider/bucket_provider.dart';
import 'package:bucket_list/view/create_page.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
class HomePage extends ConsumerStatefulWidget {
  const HomePage({super.key});
 
  @override
  ConsumerState<HomePage> createState() => _HomePageState();
}
 
class _HomePageState extends ConsumerState<HomePage> {
 
  @override
  Widget build(BuildContext context) {
    final bucketList = ref.watch(bucketProvider);
 
    return Scaffold(
      appBar: AppBar(
        title: Text("버킷 리스트"),
        centerTitle: true,
      ),
      body: bucketList.isEmpty
          ? Center(child: Text("버킷 리스트를 작성해 주세요."))
          : ListView.builder(
        itemCount: bucketList.length// bucketList 개수 만큼 보여주기
        itemBuilder: (context, index) {
          final bucket = bucketList[index]; // index에 해당하는 bucket 가져오기
          return ListTile(
            // 버킷 리스트 할 일
            title: Text(
              bucket.job,
              style: TextStyle(
                fontSize: 24,
                color: bucket.isDone ? Colors.grey : Colors.black,
                decoration: bucket.isDone
                    ? TextDecoration.lineThrough
                    : TextDecoration.none,
              ),
            ),
            // 삭제 아이콘 버튼
            trailing: IconButton(
              icon: Icon(CupertinoIcons.delete),
              onPressed: () {
                // 삭제 버튼 클릭시
                showDeleteDialog(context, index);
              },
            ),
            onTap: () {
              bucket.isDone = !bucket.isDone;
              //bucketService.updateBucket(bucket, index);
              ref.read(bucketProvider.notifier).updateBucket(bucket);
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () async {
          // + 버튼 클릭시 버킷 생성 페이지로 이동
          String? job = await Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => CreatePage()),
          );
        },
      ),
    );
  }
 
  void showDeleteDialog(
      BuildContext context, int index) {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("정말로 삭제하시겠습니까?"),
          actions: [
            // 취소 버튼
            TextButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: Text("취소"),
            ),
            // 확인 버튼
            TextButton(
              onPressed: () {
                //bucketService.deleteBucket(index);
                ref.read(bucketProvider.notifier).deleteBucket(index);
                Navigator.pop(context);
              },
              child: Text(
                "확인",
                style: TextStyle(color: Colors.pink),
              ),
            ),
          ],
        );
      },
    );
  }
}
 

 

import 'package:bucket_list/provider/bucket_provider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
class CreatePage extends ConsumerStatefulWidget {
  const CreatePage({super.key});
 
  @override
  ConsumerState<CreatePage> createState() => _CreatePageState();
}
 
class _CreatePageState extends ConsumerState<CreatePage> {
  // TextField의 값을 가져올 때 사용합니다.
  TextEditingController textController = TextEditingController();
 
  // 경고 메세지
  String? error;
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("버킷리스트 작성"),
        // 뒤로가기 버튼
        leading: IconButton(
          icon: Icon(CupertinoIcons.chevron_back),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // 텍스트 입력창
            TextField(
              controller: textController, // 연결해 줍니다.
              autofocus: true,
              decoration: InputDecoration(
                hintText: "하고 싶은 일을 입력하세요",
                errorText: error,
              ),
            ),
            SizedBox(height: 32),
            // 추가하기 버튼
            SizedBox(
              width: double.infinity,
              height: 48,
              child: ElevatedButton(
                child: Text("추가하기", style: TextStyle(fontSize: 18)),
                onPressed: () {
                  // 추가하기 버튼 클릭시
                  String job = textController.text; // 값 가져오기
                  if (job.isEmpty) {
                    setState(() {
                      error = "내용을 입력해주세요."// 내용이 없는 경우 에러 메세지
                    });
                  } else {
                    setState(() {
                      error = null// 내용이 있는 경우 에러 메세지 숨기기
                    });
 
                    // BucketService 가져오기
                    //BucketService bucketService = context.read<BucketService>();
                    //bucketService.createBucket(job);
                    ref.read(bucketProvider.notifier).createBucket(job);
 
                    Navigator.pop(context, job); // job 변수를 반환하며 화면을 종료합니다.
                  }
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}
 
 

 

/// 버킷 클래스
class Bucket {
  String id;
  String job; // 할 일
  bool isDone; // 완료 여부
 
  Bucket(this.id, this.job, this.isDone); // 생성자
}
 

 

import 'package:bucket_list/model/bucket.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
final bucketProvider = NotifierProvider<BucketNotifier, List<Bucket>>(BucketNotifier.new);
 
class BucketNotifier extends Notifier<List<Bucket>> {
  @override
  List<Bucket> build() => [];
 
  /// bucket 추가
  void createBucket(String job){
    state = [...state, Bucket((state.length + 1).toString(),job, false)];
  }
 
  /// bucket 수정
  void updateBucket(Bucket bucket){
    state = [
      for(final item in state)
        if(item.id == bucket.id)
          Bucket(item.id, bucket.job, bucket.isDone)
        else
          item
    ];
  }
 
  /// bucket 삭제
  void deleteBucket(int index){
    //state = state.where((e) => e.id != bucket.id).toList(); // Bucket bucket 인자로 받을 때
    state = List.from(state)..removeAt(index);
  }
}

 

Riverpod Notifier로 구현한 코드와

Provider 로 구현한 BucketService 코드를

비교해서 보면 도움이 될 거 같다.

 

import 'package:burket_list/model/bucket.dart';
import 'package:flutter/material.dart';
 
class BucketService extends ChangeNotifier {
  List<Bucket> bucketList = [];
 
  /// bucket 추가
  void createBucket(String job){
    bucketList.add(Bucket(job, false));
    notifyListeners();
  }
 
  /// bucket 수정
  void updateBucket(Bucket bucket, int index){
    bucketList[index] = bucket;
    notifyListeners();
  }
 
  /// bucket 삭제
  void deleteBucket(int index){
    bucketList.removeAt(index);
    notifyListeners();
  }
}
 
/***
 * 전역적으로 사용되는 데이터를 담당할 서비스를 만들고,
 * 해당 데이터에 대한 CRUD를 모두 해당 서비스에서 구현한다.
 */

 

 

'Flutter 앱 > 상태관리' 카테고리의 다른 글

Flutter riverpod 자동 생성  (0) 2024.01.15
Flutter Provider 상태관리 예제  (0) 2024.01.06
Flutter StatefulWidget  (0) 2023.12.13
Flutter Riverpod - NotifierProvider.family  (0) 2023.12.11
Flutter Riverpod - NotifierProvider  (0) 2023.12.11
블로그 이미지

Link2Me

,
728x90

가장 기본적인 상태관리를 하는 방법이다.

부모위젯

- StatefulWidget으로 생성한다.

- 모든 공유 상태를 가지고 있다.

- 상태 변경시 setState()로 자식 위젯들을 갱신한다.

 

자식 위젯

-  부모 위젯의 공유 상태를 전달받는다.

- 이벤트 발생시, 부모 위젯에게 전달하여 공유 상태를 변경한다.

 

import 'package:flutter/material.dart';
 
void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MyApp(),
    ),
  );
}
 
class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}
 
class _MyAppState extends State<MyApp> {
  // 공유 상태
  int state = 0;
 
  // 공유 상태 업데이트
  void increaseState() {
    setState(() {
      state += 1;
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("state : $state"),
            Child(
              state: state, // 부모로부터 공유 상태를 전달 받는다.
              onPressed: increaseState, // 이벤트를 부모에게 전달한다.
            ),
          ],
        ),
      ),
    );
  }
}
 
class Child extends StatelessWidget {
  const Child({
    required this.state,
    required this.onPressed,
  });
 
  final int state;
  final void Function() onPressed;
 
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: Text("Child : $state"),
    );
  }
}
 

 

 

 

블로그 이미지

Link2Me

,
728x90

family를 이용하면 Provider에 매개변수를 추가할 수 있다.

.family를 붙이고, 매개변수 타입을 명시한다.

 

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
final counterProvider = NotifierProvider.family<Counter, intint>(() {
  return Counter();
});
 
class Counter extends FamilyNotifier<intint> {
  @override
  int build(int initValue) => initValue;
 
  void increment() => state++;
}
 
 
void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        home: MyApp(),
      ),
    ),
  );
}
 
class MyApp extends ConsumerWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    int counter = ref.watch(counterProvider(10));
    return Scaffold(
      body: Center(
        child: Text(
          "$counter",
          style: const TextStyle(
            fontSize: 24,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: ref.read(counterProvider(10).notifier).increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
 

 

counterProvider(10)과 같이 원하는 초기값을 전달할 수 있다.
ref.read(counterProvider(10))에서도 처음 전달한 매개 변수와 동일한 값을 전달해야 한다.

 

'Flutter 앱 > 상태관리' 카테고리의 다른 글

Flutter Riverpod 예제 : Bucket List  (0) 2023.12.27
Flutter StatefulWidget  (0) 2023.12.13
Flutter Riverpod - NotifierProvider  (0) 2023.12.11
Flutter riverpod - StateProvider  (0) 2023.11.23
Flutter GetX (반응형 상태관리)  (0) 2022.06.24
블로그 이미지

Link2Me

,
728x90

Notifier is a replacement of StateNotifier + ChangeNotifier.

 

아래 예제는 하나의 파일로 작성되었지만 별도의 파일로 Provider 를 추가하는 것을 권장한다.

- 클래스를 구현하고, Notifier 를 상속받고 제네릭 타입을 명시한다.

- 아래 예시는 숫자를 증가시키는 Provider 이므로 counterProvider 를 명명한다.

 

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
final counterProvider = NotifierProvider<Counter, int>(() {
  return Counter();
});
 
// Notifier를 상속 받은 클래스만 NotifierProvider에 등록 가능
 
class Counter extends Notifier<int> {
  @override
  int build() => 0// state 초기값
 
  void increment() => state++;
}
 
 
void main() {
  runApp(
    const ProviderScope(
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        home: MyApp(),
      ),
    ),
  );
}
 
class MyApp extends ConsumerWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    int counter = ref.watch(counterProvider);
    return Scaffold(
      body: Center(
        child: Text(
          "$counter",
          style: const TextStyle(
            fontSize: 24,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: ref.read(counterProvider.notifier).increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
 

 

ref.watch는 provider를 입력값으로 받고, state를 리턴하는 함수다.

 

https://riverpod.dev/docs/providers/notifier_provider 를 참조하면 도움된다.

 

 

'Flutter 앱 > 상태관리' 카테고리의 다른 글

Flutter StatefulWidget  (0) 2023.12.13
Flutter Riverpod - NotifierProvider.family  (0) 2023.12.11
Flutter riverpod - StateProvider  (0) 2023.11.23
Flutter GetX (반응형 상태관리)  (0) 2022.06.24
Flutter GetX (단순 상태관리)  (0) 2022.06.23
블로그 이미지

Link2Me

,
728x90

Riverpod 라이브러리를 이용한 상태관리 방법에 대해 알아보고 적어둔다.

 

Provider는 상태를 저장하고, 자손 위젯에서 상태에 접근할 수 있도록 제공해주는 InheritedWidget를 래핑한 라이브러리이다. Provider 개발자가 단점을 보완하여 재작성된 라이브러리가 Riverpod 상태 관리 라이브러리이다.

 

1. 먼저 https://pub.dev/packages/riverpod 사이트에서 flutter_riverpod 의 최신버전을 확인하여 pubspec.yaml 에 추가한다.

 

name: riverpod_ex
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 
version: 1.0.0+1
 
environment:
  sdk: '>=3.1.5 <4.0.0'
 
dependencies:
  flutter:
    sdk: flutter
 
  cupertino_icons: ^1.0.2
  flutter_riverpod: ^2.4.8
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  flutter_lints: ^2.0.0
 
 
flutter:
 
  uses-material-design: true
 

 

 

2. main.dart 파일에서 아래와 같이

_MyApp을 ProviderScope 로 감싸준다.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_ex/screen/home_screen.dart';
 
void main() {
  runApp(ProviderScope(
    child: _MyApp(),
  ));
}
 
class _MyApp extends StatelessWidget {
  const _MyApp({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    );
  }
}

 

 

3. state_provider.dart

상태관리를 할 StateProvider를 구현한다.

import 'package:flutter_riverpod/flutter_riverpod.dart';
 
final numberProvider = StateProvider<int>((ref) => 0);

 

 

4.state_provider_screen.dart

ConsumerWidget 을 상속하고, Widget build(BuildContext context) 를 Widget build(BuildContext context, WidgetRef ref) 로 변경해준다.

Riverpod에서 정의된 추상클래스 WidgetRef를 통해서 Provider에 접근 가능하고,
ref.watch를 통해서 변경을 감지하면 해당 위젯을 다시 build한다.

 

final provider = ref.watch(numberProvider); 를 추가하여 상태 변화를 감지한다.

Provider의 값이 변경되면 자체적으로 다시 build된다.

 

WidgetRef의 read 메소드를 호출(Provider의 값을 읽어오기만 함)하며, 상태를 변경할 StateProvider의 notifier를 전달한다.

ref.read(numberProvider.notifier).update((state) => state + 1); 로 상태를 증가시키거나,

ref.read(numberProvider.notifier).state = ref.read(numberProvider.notifier).state - 1; 와 같은 방법으로 상태를 감소/증가 시킬 수 있다.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
 
import '../layout/default_layout.dart';
import '../riverpod/state_provider.dart';
 
class StateProviderScreen extends ConsumerWidget {
  const StateProviderScreen({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final provider = ref.watch(numberProvider);
 
    return DefaultLayout(
      title: 'Basic Provider',
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              provider.toString(),
              textAlign: TextAlign.center,
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(numberProvider.notifier).update((state) => state + 1);
              },
              child: Text('UP'),
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(numberProvider.notifier).state =
                    ref.read(numberProvider.notifier).state - 1;
              },
              child: Text(
                'DOWN',
              ),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute(builder: (_) => _NextScreen()),
                );
              },
              child: Text(
                'Next Screen',
              ),
            ),
          ],
        ),
      ),
    );
  }
}
 
class _NextScreen extends ConsumerWidget {
  const _NextScreen({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final provider = ref.watch(numberProvider);
 
    return DefaultLayout(
      title: 'Basic Provider',
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              provider.toString(),
              textAlign: TextAlign.center,
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(numberProvider.notifier).update((state) => state + 1);
              },
              child: Text('UP'),
            ),
          ],
        ),
      ),
    );
  }
}

 

블로그 이미지

Link2Me

,
728x90

반응형 상태관리는  GetxController 를 상속받지 않아도 된다.

대신 하나 하나의 변수에 obs를 붙여서 listening을 한다. 메소드 내에 update()도 필요 없다.

 

GetX 반응형 상태관리 예제

import 'package:get/get.dart';
 
class CountController extends GetxController {
  RxInt counter = 0.obs;
  // 반응형은 obs 를 붙이고 int 형에서 RxInt 형으로 변경
  // RxDouble, RxString, Rx<Class명>, RxList<String>, Rx<ENUM명>
 
  void increment() {
    counter++;
    // update(); 는 필요 없다.
  }
 
  void decrement() {
    counter--;
  }
 
  void putNumber(int val){
    counter(val);
  }
 
  @override
  void onInit() {
    //ever(counter, (_) => print('매번 호출'));
    //once(counter, (_) => print('한번만 호출'));
    //debounce(counter, (_) => print('마지막 변경에 한번만 호출'), time: Duration(seconds: 1) );
    super.onInit();
  }
}
 

 

 

MaterialApp → GetMaterialApp 으로 변경한다.

실제 사용하는 곳에서 인스턴스를 생성해야 한다.  Get.put(CountController());

단순 상태관리는 GetBuilder를 사용했다면, 반응형은 Obx 와 GetX 두 가지 방법이 있다. 

비교적 사용법이 간단한 Obx 를 많이 사용한다.

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
 
void main() {
  runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
 
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
 
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
 
class _MyHomePageState extends State<MyHomePage> {
 
  @override
  Widget build(BuildContext context) {
    Get.put(CountController());
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button and count :',
            ),
            Obx(() {
              //print('Update UI'); // 화면 렌더링을 확인하고 싶을 때
              return Text(
                '${Get.find<CountController>().counter.value}',
                style: Theme.of(context).textTheme.headline4,
              );
            } ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
              onPressed: () {
                Get.find<CountController>().increment();
              },
              backgroundColor: Colors.green,
              child: const Icon(Icons.add)),
          SizedBox(width: 10, height: 10,), // 여백을 만들기 위해서 넣음.
          FloatingActionButton(
              onPressed: () {
                Get.find<CountController>().decrement();
              },
              backgroundColor: Colors.pink,
              child: const Icon(Icons.remove)),
          SizedBox(width: 10, height: 10,), // 여백을 만들기 위해서 넣음.
          FloatingActionButton(
              onPressed: () {
                Get.find<CountController>().putNumber(5);
              },
              backgroundColor: Colors.blue,
              child: const Text('5')),
        ],
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Obx 는 화살표 함수(람다식 표현)로 수정할 수 있으나, 로그 확인을 위해서 print 함수를 찍어서 알아보기 위함이다.

 

 

Rx 타입 자료형

class User{
  String name;
  int age;
  User({this.name, this.age})
}

enum NUM {FIRST,SECOND}

class CountReactiveGetx extends GetxController{
  RxInt count = 0.obs;
  RxDouble _double = 0.0.obs;
  RxString value = "".obs;
  Rx<NUM> nums = NUM.FIRST.obs;
  Rx<User> user = User(name:'Json',age:10).obs;
  RxList<String> list=[].obs;
  
  void increase(){
    count++;
    _double++;
    _double(424);
    
    nums(NUM.SECOND);
    user(User());
    user.update((_user){ _user.name='James'});
    
    //list.addAll();
    //list.add();
    //list.addIf(user.value.name == 'Json','OKAY') //(조건,추가할 요소)
  } 
  
}

출처 : https://www.youtube.com/watch?v=TjC1ka8fZJw 유투브 영상

블로그 이미지

Link2Me

,
728x90

https://pub.dev/packages/get/install  에 접속한다.

설치 안내에 따라 dependencies에 get 을 추가한다.

 

터미널창을 열고 아래 명령어를 입력하고 엔터키를 친다.

flutter pub add get

 

 

단순 상태관리 예제

본문에 포함된 주석을 참조하면 이해하는데 도움된다.

import 'package:get/get.dart';
 
class CountController extends GetxController {
  // Flutter에서 상태 관리에 가장 많이 사용되는 패키지인 GetX
  // 상태 관리 외에도 route 관리, 다국어 지원, 화면크기 가져오기, API 호출 기능
 
  int _counter = 0// _를 붙이면 private 변수
  int get counter => _counter;
 
  void increment() {
    _counter++;
    update();
  }
 
  void decrement() {
    _counter--;
    update();
  }
}

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
 
void main() {
  runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      // MaterialApp 대신에 GetMaterialApp
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
 
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
 
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
 
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    CountController controller = Get.put(CountController());
 
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times:',
            ),
            // 상태의 변화를 감지하고 변경된 상태값을 적용하기 위해서는
            // GetBuilder를 사용해야 한다.
            GetBuilder<CountController>(builder: (_) {
              return Text(
                '${controller.counter}',
                style: Theme.of(context).textTheme.headline4,
              );
            }),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
              onPressed: () {
                controller.increment();
              },
              backgroundColor: Colors.green,
              child: const Icon(Icons.add)),
          SizedBox(width: 10, ), // 여백을 만들기 위해서 넣음.
          FloatingActionButton(
              onPressed: () {
                controller.decrement();
              },
              backgroundColor: Colors.pink,
              child: const Icon(Icons.remove)),
        ],
      ),
      // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

 

CountController controller = Get.put(CountController());
controller.counter;
controller.increment();
controller.decrement();
 
// 위의 코드 대신에 아래 코드로 대체 사용할 수 있다.
Get.find<CountController>().counter;
Get.find<CountController>().increment();
Get.find<CountController>().decrement();

 

 

 

블로그 이미지

Link2Me

,
728x90

플러터 위젯 상태(state)를 관리하는 기본 방법은 setState인데, 이는 state를 업데이트 하고 위젯을 다시 빌드하는 메소드이다. 

※ Class 내에서 사용하는 function 은 메소드로 통칭한다.

부모 자식 위젯간에는 유용하지만 위젯 Depth가 커지면, 즉 조부모 → 손자 간에는 데이터 전달이 쉽지 않다.

앱의 규모가 커지면 코드는 복잡해지고 효과적인 상태 관리가 필요하다는 것을 느끼게 된다.

 

https://pub.dev/packages/provider/install 에서 최신 버전을 확인하고 pubspec.yaml 파일에 추가한다.

dependencies:
  provider: ^6.0.3

import 'package:provider/provider.dart';

를 하면 기본 준비 완료이다.

 

기존 main 함수

void main() {
  runApp(
      const MyApp()
  );
}

 

변경 main 함수

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

 

상태관리 클래스를 추가한다.

provider/counter_provider.dart

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
 
class CounterProvider extends ChangeNotifier {
  int _count = 0;
 
  int get count => _count;
 
  void increment() {
    _count++;
    notifyListeners();
  }
 
  void decrement() {
    _count--;
    notifyListeners();
  }
}
 

 

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/foundation.dart';
import 'counterview_home.dart';
import './provider/counter_provider.dart';
 
void main() {
  runApp(
    MultiProvider(providers: [
      ChangeNotifierProvider(create: (c) => CounterProvider()),
    ],
    child: const MyApp(),
    )
  );
}
 
class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);
 
  @override
  State<MyApp> createState() => _MyAppState();
}
 
class _MyAppState extends State<MyApp> {
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // home: const DrawerMenu(title: 'Drawer Demo'),
      home: CounterView_Home(),
    );
  }
}

 

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './provider/counter_provider.dart';
 
class CounterView_Home extends StatelessWidget {
  const CounterView_Home({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    CounterProvider counterProvider = Provider.of<CounterProvider>(context,listen: false);
    //print('CounterView rendering');
    return Scaffold(
      appBar: AppBar(
        title: const Text('Provider Counter Example'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text('You have pushed the button this many times:'),
            Count(),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          IconButton(
            //onPressed: () => context.read<CounterProvider>().increment(),
            onPressed: () => counterProvider.increment(),
            icon: Icon(Icons.add),
          ),
          IconButton(
              //onPressed: () => context.read<CounterProvider>().decrement(),
              onPressed: () => counterProvider.decrement(),
              icon: Icon(Icons.remove)),
        ],
      ),
    );
  }
}
 
class Count extends StatelessWidget {
  const Count({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return Text('${context.watch<CounterProvider>().count}',
        key: const Key('counterState'),
        style: Theme.of(context).textTheme.headline4);
  }
}

 

 

블로그 이미지

Link2Me

,