[Flutter/플러터] Riverpod 장점 및 사용법
getx를 사용하다가 프로젝트의 규모가 점점 더 커질 것 같아서, riverpod를 공부하기로 마음먹었다. getx를 사용했을 때, 코드가 간결해져서 생산성이 높고, global context로 어디에서나 객체 접근이 가능하지만 이것이 나중에는 단점이 될 수도 있다는 생각에 flutter에서 공식으로 밀고있는 provider와 그것의 개선안인 riverpod를 사용할 예정이다.. 일단 본격적으로 riverpod에 대해 알아보자.. Riverpod가 생겨난 배경 ▶ provider의 단점 provider를 사용하다 보면 결국에는 UI 코드와 섞이게 되는 문제를 볼 수 있다. main.dart class MyWidget extends StatelessWidget { @override Widget build(..
2022.09.28
no image
[Flutter/플러터] Riverpod를 통해 알아보는 플러터 아키텍처 - (2) Domain Model
2편은 Domain Model에 관한 것이다. 원글은 .. https://codewithandrea.com/articles/flutter-app-architecture-domain-model/ Flutter App Architecture: The Domain Model An introduction to the domain model and its role in defining entities and the business logic for manipulating them in the context of Flutter app architecture. codewithandrea.com Domain-Driven Design(DDD)에서 중요한 것은 model 이다. 좋은 도메인 모델을 갖고 있는 지의 여부는 ..
2022.09.27
no image
[Flutter/플러터] Riverpod를 통해 알아보는 플러터 아키텍처 - (1) Repository pattern
riverpod를 상태관리에 대해 자세히 알아보던 도중, 잘 정리되어 있는 글을 발견했다. 해당 글을 정리 및 번역한 글이며 원본 글은 아래에서 확인할 수 있다. https://codewithandrea.com/articles/flutter-repository-pattern/ Flutter App Architecture: The Repository Pattern An in-depth overview of the repository pattern in Flutter: what it is, when to use it, and various implementation strategies along with their tradeoffs. codewithandrea.com Riverpod 상태관리를 활용한 아키텍..
2022.09.27
320x100
728x90

 

getx를 사용하다가 프로젝트의 규모가 점점 더 커질 것 같아서,

riverpod를 공부하기로 마음먹었다.

 

getx를 사용했을 때, 코드가 간결해져서 생산성이 높고,

global context로 어디에서나 객체 접근이 가능하지만 

이것이 나중에는 단점이 될 수도 있다는 생각에 flutter에서 공식으로 밀고있는 provider와 

그것의 개선안인 riverpod를 사용할 예정이다..

 

 

 


 

 

일단 본격적으로 riverpod에 대해 알아보자..

 

 

Riverpod가 생겨난 배경

    ▶   provider단점

 

  •   provider를 사용하다 보면 결국에는 UI 코드와 섞이게 되는 문제를 볼 수 있다.

 

main.dart

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (context) => MyFirstClass(),
      child: ProxyProvider<MyFirstClass, MySecondClass>(
        update: (context, firstClass, previous) => MySecondClass(firstClass),
        child: MyVisibleWidget(),
      ),
    );
  }
}

 

위의 코드를 보면 알 수 있다.. 

Provider로 위젯을 감싼 뒤 그 안에서 변화를 체크해야 하니 결국에는 UI와 복잡하게

얽혀있는 모습이다.. 

 

 

 

  • 타입에 의존하는 provider

 

main.dart

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (context) => 'A String far away.',
      child: Provider(
        create: (context) => 'A String that is close.',
        builder: (context, child) {
          return Text(Provider.of<String>(context));
        },
      ),
    );
  }
}

String 타입의 객체를 동일하게 제공하면 제일 가까운 놈에만 접근 가능하여

결국 A String far away의 값을 얻지 못한다..

 

 

 

그 외에도 여러번 접근하려면 그만큼의 선언이 필요하여 불편하고 제한적이다..

MultiProvider(
  providers: [
    Provider<Something>(create: (_) => Something()),
    Provider<SomethingElse>(create: (_) => SomethingElse()),
    Provider<AnotherThing>(create: (_) => AnotherThing()),
  ],
  child: Container(),
)

해당과 같이 main에서 사용할 provider들에 대해 명시해 주어야 한다. 결국엔 파일을 왔다 갔다 해야 할 것이다.

 

 

 

 

 

이러한 단점들을 보완한 것이 riverpod 이다.

 Riverpod장점

  •  provider를 일단 한번 선언하면 어디에서든 global 접근이 가능하다 
final greetingProvider = Provider((ref) => 'Hello Riverpod!');

 

 

  하지만 provider 자체는 flutter 위젯 트리에 종속된다. 따라서

  전체 위젯트리를 ProviderScope으로 감쌀 필요가 있다.

 

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

 

 

 

  • ConsumerWidget의 사용

 

    이제 greetingProvider의 String 문자를 Text 위젯에 띄우는 방법에 대해 알아보자 

 

 

    1.  superclass를 ConsumerWidget으로 변경

class MyApp extends ConsumerWidget{
	...

 

 

  2. 위젯의 build 메소드에 ScopedReader function 추가

Widget build(BuildContext context, ScopedReader watch){
	...

 

 

  3. 위젯의 상태를 체크하여 rebuild 시켜주는 watch를 통해 provider 전달

final greeting = watch(greetingProvider);

 

 

 

전체 코드 

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final greeting = watch(greetingProvider);

    return MaterialApp(
      title: 'Riverpod Tutorial',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Riverpod Tutorial'),
        ),
        body: Center(
          child: Text(greeting),
        ),
      ),
    );
  }
}

 

 

 

오직 변화하는 Text만 감지를 해서 rebuild 해주고 싶을 때는 Consumer widget을 사용하면 된다.

 

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        ...
        body: Center(
          child: Consumer(
            builder: (context, watch, child) {
              final greeting = watch(greetingProvider);
              return Text(greeting);
            },
         ...
  }
}

 

 

 

  • AsyncValue의 사용으로 Future 처리를 더 쉽게 만들어준다.
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      		...
          child: Consumer(
            builder: (context, watch, child) {
              final responseAsyncValue = watch(responseProvider);
              return responseAsyncValue.map(
                data: (_) => Text(_.value),
                loading: (_) => CircularProgressIndicator(),
                error: (_) => Text(
                  _.error.toString(),
                  style: TextStyle(color: Colors.red),
                ),
              );
            ...
}

 

   map을 통해 data가 존재할 때, 데이터를 불러오는 중인 loading 상태일 때, 데이터를 불러올 수 없는 error 상태일 때

   각각 상태 처리를 한번에 가능하다. 

 

 

 

  • family를 통한 arguments 전달 
final greetingFamilyProvider = Provider.family<String, String>(
  (_, name) {
    return "Welcome, $name!";
  },
);

 

  family를 통해 String 인자를 넘겨주는 provider를 선언한다

 

sendArguments(WidgetRef ref) {
  ref.read(
    greetingFamilyProvider('yerim')
  );
}

 

  sendArguments 함수에서 greetingFamilyProvider에 name 인자값을 넘겨주면 된다.

 

 

 

 

  • autoDispose의 사용으로 자동으로 상태를 dispose 해줌

   

   fake request가 있을 때, 직접 dispose 해주지 않는 이상 계속 존재하였다. 

   하지만 autoDispose는 provider가 사용되지 않을 때, 알아서 dispose 해주는 역할을 한다. 

 

final responseProvider =
    FutureProvider.autoDispose.family<String, String>((ref, url) async {
  final httpClient = ref.read(fakeHttpClientProvider);
  return httpClient.get(url);
});

 


 

 

정리해보자면,

 

Riverpod

1. provider와 달리 compile time에 오류를 캐치할 수 있으며 (provider는 runtime에 오류가 난다..)

2. 동일한 유형의 여러 종속성을 주입할 수 있으며,

3. No BuildContext  즉, BuildContext가 더 이상 필요하지 않는다 

 

 

 

 

간단한 튜토리얼에 대해 알아보았다..

다음엔 더 깊게 들어가 보도록 하자

 

 

 

참고 : https://resocoder.com/

 

728x90
반응형
320x100
728x90

 

 

2편은 Domain Model에 관한 것이다.

원글은 ..

https://codewithandrea.com/articles/flutter-app-architecture-domain-model/

 

Flutter App Architecture: The Domain Model

An introduction to the domain model and its role in defining entities and the business logic for manipulating them in the context of Flutter app architecture.

codewithandrea.com

 

 


 

 

 

Domain-Driven Design(DDD)에서 중요한 것은 model 이다.

좋은 도메인 모델을 갖고 있는 지의 여부는 프로젝트의 성공과 실패를 결정하는 것과도 같다..

 

 

플러터 아키텍처를 다시 살펴보자

 

전에서 데이터 계층(Data layer)은 

외부 API 및 데이터 소스와 통신하는 역할을 하며 repository를 포함하고 있다고 말했다.

 

 

그 바로 위에 있는 모델 계층애플리케이션 계층모델들과 비즈니스 로직을 갖고 있으므로

매우 중요한 역할을 한다.

 

 

 

일단,

도메인 모델이란 무엇일까?

도메인 모델은 행동과 데이터를 모두 통합하는 도메인의 개념적 모델이다.

 

무슨 말인지 모르겠다면.. 그냥 도메인 자체를 이해하기 위한 개념 모델이라고 생각하자

 

 

· 데이터(Data) : 엔티티 집합과 그들의 관계로 이루어져 있음

· 행동(Behavior) : 비즈니스 로직을 변환한 것으로 엔티티들을 조작하는 역할을 함

 

 

아래는 엔티티 관계 예시이다

이커머스 앱 엔티티 및 관계

 

 

다음과 같이 작성된 다이어그램을 통해 

Model을 작성해 보도록 하자..

 

typedef ProductID = String;

class Product {
  Product({
    required this.id,
    required this.imageUrl,
    required this.title,
    required this.price,
    required this.availableQuantity,
  });

  final ProductID id;
  final String imageUrl;
  final String title;
  final double price;
  final int availableQuantity;

  // serialization code
  factory Product.fromMap(Map<String, dynamic> map, ProductID id) {
    ...
  }

  Map<String, dynamic> toMap() {
    ...
  }
}

 

model 안에는 UI를 그리기 위해 필요한 속성들은 필수적으로 가지고 있어야 한다.

아래 그림과 같이 품목에 대해 정보를 제공해주는 요소들을 말이다..

 

 

 

 

이렇게 simple data classes가 있는 반면, 

(위 product 모델은 리파지토리 및 서비스 혹은 다른 객체에 접근할 수 없는 그저 간단한 모델 클래스이다.)

 

비즈니스 로직을 갖고 있는 모델 클래스도 존재한다.

 

 


 

 

◆     비즈니스 로직을 가진 Model class

 

예를 들어서, 쇼핑 앱에서 피자와 여러 채소들을 구매하고자 장바구니에 담았다고 가정해보자.

장바구니에 담긴 항목들은 사용자의 선택에 따라 금액, 수량, 품목들을 갱신해야 한다.

 

 

이때, 장바구니를 갱신하는 코드를 상품 정보를 갖고있는 클래스 안에서 만들기 보단

해당 클래스를 플러터 확장 매서드인 extension을 사용하여 나타낸다.

 

 

 

상품 ID와 수량을 key-value 쌍의 Map 형태로 가지고 있는 Cart 클래스를 먼저 보자

class Cart {
  const Cart([this.items = const {}]);
  
  /// - key: product ID
  /// - value: quantity
  final Map<ProductID, int> items;

  factory Cart.fromMap(Map<String, dynamic> map) { ... }
  Map<String, dynamic> toMap() { ... }
}

 

cart에서 상품을 추가하거나 제거할 수도 있다.

해당 기능을 cart 클래스 extension 해서 만들어 보자

 

extension MutableCart on Cart {
  //카드 갱신
  Cart addItem({required ProductID productId, required int quantity}) {
    final copy = Map<ProductID, int>.from(items);
    
    //기존의 것을 복사해서 새로 cart를 update 함
    copy[productId] = quantity + (copy[productId] ?? 0);
    return Cart(copy);
  }
  
  //카드에서 삭제
  Cart removeItemById(ProductID productId) {
    final copy = Map<ProductID, int>.from(items);
    copy.remove(productId);
    return Cart(copy);
  }
}

이렇게 하면 카트 안의 값들은 변경되지만 최종적으로 immutable한 새로운 카트 객체를 얻게 된다.

 

  →   많은 상태관리가 immutable한 객체에 의존하고 있기 때문에,

         모델의 상태를 변경하기 위해서는 새로운 immutable한 객체를 복사한 뒤, 그 안에서 변경해야 한다. 

 

 

 

★ 핵심

모델/엔티티에 대해서 immutable data class로 만드는 것이 중요하다 

 

 

모델의 단위테스트에 대해서는 나중에 따로

테스트 part에 모아서 정리한 뒤 올리도록 하겠당..

728x90
반응형
320x100
728x90

 

 

riverpod를 상태관리에 대해 자세히 알아보던 도중,

잘 정리되어 있는 글을 발견했다. 

해당 글을 정리 및 번역한 글이며 원본 글은 아래에서 확인할 수 있다.

 

 

https://codewithandrea.com/articles/flutter-repository-pattern/

 

Flutter App Architecture: The Repository Pattern

An in-depth overview of the repository pattern in Flutter: what it is, when to use it, and various implementation strategies along with their tradeoffs.

codewithandrea.com

 

 

 

 


 

 

 

 

 

Riverpod 상태관리를 활용한 아키텍처에 대해 설명해보려 한다.

 

Riverpod 아키텍처는 4개의 레이어로 구성된다. 

(Data, Domain, Application, Presentation)

 

 

위의 각 화살표들은 레이어들 간의 종속성을 의미한다. 

하나씩 자세히 알아보도록 하자. 

 

 

 

1. 데이터 계층(Data Layer) 

  • Data Sources에 있는 세부 정보들로부터 도메인 모델(엔티티)를 분리하는 역할을 한다.
  • 데이터 전송 객체를 도메인 계층에서 이해할 수 있는 데이터로 변환시켜 준다.
  • 데이터 캐싱 역할 또한 한다.

 

 

 - Repository Pattern

     데이터 계층에서 repository pattern을 사용하여 Backend의 API와 같은 다양한 데이터 개체에 접근할 수 있으며, 

     앱의 도메인 계층에서 엔티티들이 안전한 유형으로 존재할 수 있도록 한다.

 

    즉, 데이터 로직비즈니스 로직분리하여 도메인에서 일관된 인터페이스를 통해

          데이터를 요청할 수 있도록 해주는 패턴이다.

 

 

 

 

★ 언제 이 패턴을 사용하나?

     비정형 데이터(주로 Json 데이터)를 반환할 때 주로 사용..

  1.  REST API를 사용할 때,
  2.  local 혹은 remote DB 저장소를 사용할 때(Hive, Firestore, etc..)
  3.  기기별 특정 API 호출할 때(permissions, camera, location, etc..)

 

 

 

 

 

예를 들어서 이해해 보자. 

아래의 api에서 날씨 정보를 flutter 앱에 받아오려고 한다. 

https://openweathermap.org/api

 

Weather API - OpenWeatherMap

Please, sign up to use our fast and easy-to-work weather APIs. As a start to use OpenWeather products, we recommend our One Call API 3.0. For more functionality, please consider our products, which are included in professional collections.

openweathermap.org

 

이를 받아오는 repository 인터페이스를 만들면 다음과 같다.

 

abstract class WeatherRepository {
	Future<Weather> getWeather({required String city)};
}

 

http 또는 dio를 통해 api를 호출할 수 있다.

다음은 http를 통해 weatherrepository를 구체화한 httpWeatherRepository 클래스 이다. 

 

import 'package:http/http.dart' as http;

class HttpWeatherRepository implements WeatherRepository {
  HttpWeatherRepository({required this.api, required this.client});
  
  final OpenWeatherMapAPI api;

  final http.Client client;

  Future<Weather> getWeather({required String city}) {
    	...
  }
}

    이렇게 따로 파일을 두어 클래스를 생성하면, 앱의 다른 부분은 이에 대해 신경쓰지 않고 각자의 일을 수행할 수 있다.

 

 

 

 

 

  - JSON data Parsing하기

    api를 통해 불러온 데이터는 Weather Model 클래스에 정의되어야 한다. 

 

   예전에 자동으로 model 생성해주는 quicktype.io 를 소개했지만, 이번에는 

    freezed 패키지를 통해 만들어 보았다. 

 

     Freezed에 대한 소개는 다음 글을 참고하면 된다..

    https://yerim-coding.tistory.com/26

 

[Flutter/플러터] Freezed 플러그인 사용하기 - (1)

Freezed란? 데이터 클래스에 필요한 편의 기능들을 Code Generation으로 제공하는 라이브러리 아래 공식문서를 살펴보자 freezed | Dart Package Code generation for immutable classes that has a simple syntax..

yerim-coding.tistory.com

 

@Freezed
class Weather with _$Weather{
   const factory({
   		required String city,
        ...
  factory Weather.fromJson(Map<String, dynamic> json) {
      ...
    }
}

 

생성한 model과 repository를 앱에서 초기화 하는 방법이다.

나는 riverpod를 사용할것 이기 때문에 

 

import 'package:flutter_riverpod/flutter_riverpod.dart';

final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
  return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client());
});

다음과 같이 초기화 해 주면 된다. 

 

 

 

 

 

- 추상클래스(abstract class)구체 클래스(concrete class)언제 사용하면 될까?

 

각각의 장단점을 알아보자

 

추상 클래스를 사용할 때

장점 :  복잡하지 않다. 인터페이스를 한 곳에 볼 수 있어서 좋다.

            함수명을 바꾸고 싶을 때, 초기화 코드에서 한줄만 바꾸면 된다.

단점 : 상용구 코드가 길어질 수 있다

 

구체적인 클래스 사용할 때

장점 : 상용구 코드가 적다

단점 : repository 명을 바꾸려면 많은 곳에서의 수정이 필요하다

 

 

 


 

 

 

 

repository 패턴을 사용하면 데이터 계층의 구현을 따로 관리할 수 있다.

따라서, 도메인과 프레젠테이션 계층은 각자의 역할에만 충실할 수 있으며

모델 클래스와 엔티티들이 안전하게 타입을 유지할 수 있다. 

 

따라서 각 계층들을 분리하여 코드를 작성하는 것이 중요하다고 할 수 있다.

 

 

 

 

 

 

 

 

 

Repository 테스트 방법은 건너 뛰었다.. 알고싶으면 상단의 링크로 들어가보자

source code 또한 제공해 준다.

예제를 보면서 더 익혀 봐야 겠다..

https://github.com/bizz84/open_weather_example_flutter

728x90
반응형