🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Riverpod 코드 제너레이션 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 30. · 13 Views

Riverpod 코드 제너레이션 완벽 가이드

Flutter 상태 관리의 강자 Riverpod을 더욱 편리하게 사용할 수 있는 코드 제너레이션 방식을 알아봅니다. @riverpod 어노테이션부터 build_runner 활용까지, 초급 개발자도 쉽게 따라할 수 있도록 설명합니다.


목차

  1. riverpod_generator 설치
  2. @riverpod 어노테이션
  3. 함수형 Provider 생성
  4. 클래스형 Notifier 생성
  5. @Riverpod(keepAlive: true) 옵션
  6. build_runner 사용법

1. riverpod generator 설치

김개발 씨는 새로운 Flutter 프로젝트를 시작하면서 Riverpod을 사용하기로 결정했습니다. 그런데 선배 개발자가 "요즘은 코드 제너레이션 방식을 많이 써요"라고 말합니다.

코드 제너레이션이라니, 뭔가 어려워 보이는데 어떻게 시작해야 할까요?

riverpod_generator는 Riverpod Provider를 자동으로 생성해주는 도구입니다. 마치 요리사가 재료만 준비하면 자동으로 요리가 완성되는 것처럼, 개발자가 핵심 로직만 작성하면 나머지 보일러플레이트 코드를 자동으로 만들어줍니다.

이를 통해 타입 안전성과 개발 생산성을 동시에 확보할 수 있습니다.

다음 코드를 살펴봅시다.

# pubspec.yaml 파일에 추가
dependencies:
  flutter_riverpod: ^2.4.9
  riverpod_annotation: ^2.3.3

dev_dependencies:
  riverpod_generator: ^2.3.9
  build_runner: ^2.4.8

# 터미널에서 패키지 설치
# flutter pub get

김개발 씨는 드디어 첫 번째 Flutter 프로젝트를 맡게 되었습니다. 상태 관리 라이브러리로 Riverpod을 선택했는데, 문서를 읽다 보니 두 가지 방식이 있다는 것을 알게 되었습니다.

하나는 전통적인 방식이고, 다른 하나는 코드 제너레이션 방식입니다. "코드 제너레이션이 뭐예요?" 김개발 씨가 옆자리 박시니어 씨에게 물었습니다.

박시니어 씨가 친절하게 설명해주었습니다. "쉽게 말해서, 네가 핵심 로직만 작성하면 컴퓨터가 나머지 반복적인 코드를 자동으로 만들어주는 거야.

마치 자동 번역기처럼 말이지." 그렇다면 왜 코드 제너레이션 방식을 사용할까요? 전통적인 Riverpod 방식에서는 Provider를 직접 선언해야 했습니다.

StateNotifierProvider, FutureProvider, StreamProvider 등 상황에 맞는 Provider 타입을 선택하고, 제네릭 타입도 정확하게 명시해야 했습니다. 초보자에게는 이 과정이 꽤 복잡하게 느껴질 수 있습니다.

코드 제너레이션 방식을 사용하면 이런 복잡함이 사라집니다. @riverpod 어노테이션만 붙이면, 어떤 Provider 타입을 사용해야 하는지 자동으로 판단해줍니다.

개발자는 비즈니스 로직에만 집중하면 됩니다. 패키지 설치를 살펴보겠습니다.

먼저 flutter_riverpod은 Riverpod의 핵심 라이브러리입니다. Flutter 앱에서 Riverpod을 사용하기 위한 기본 패키지입니다.

다음으로 riverpod_annotation은 @riverpod 어노테이션을 제공하는 패키지입니다. 이 패키지는 런타임에도 필요하므로 dependencies에 추가합니다.

riverpod_generator는 실제로 코드를 생성하는 도구입니다. 개발 중에만 필요하므로 dev_dependencies에 추가합니다.

마지막으로 build_runner는 코드 생성을 실행하는 도구입니다. 이 역시 개발 도구이므로 dev_dependencies에 들어갑니다.

버전 번호는 시간이 지나면 변경될 수 있습니다. pub.dev에서 최신 버전을 확인하는 것이 좋습니다.

하지만 각 패키지 간 호환성이 중요하므로, 공식 문서에서 권장하는 버전 조합을 따르는 것이 안전합니다. 김개발 씨는 pubspec.yaml 파일을 열고 패키지들을 추가했습니다.

터미널에서 flutter pub get을 실행하니 모든 패키지가 설치되었습니다. 이제 코드 제너레이션을 시작할 준비가 완료되었습니다.

실전 팁

💡 - 패키지 버전은 pub.dev에서 최신 버전을 확인하되, 호환성을 위해 공식 문서 권장 버전을 따르세요

  • dev_dependencies는 배포 시 포함되지 않으므로 앱 크기에 영향을 주지 않습니다

2. @riverpod 어노테이션

패키지 설치를 마친 김개발 씨는 이제 본격적으로 코드를 작성하려고 합니다. 그런데 @riverpod 어노테이션을 어디에 어떻게 붙여야 하는지 막막합니다.

선배에게 다시 한번 도움을 요청했습니다.

@riverpod 어노테이션은 함수나 클래스에 붙여서 "이것을 Provider로 만들어주세요"라고 표시하는 역할을 합니다. 마치 우체국에서 편지에 우표를 붙이면 배달이 시작되는 것처럼, @riverpod을 붙이면 코드 생성이 시작됩니다.

이 어노테이션 하나로 복잡한 Provider 선언이 자동화됩니다.

다음 코드를 살펴봅시다.

// counter_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

// 생성될 파일을 연결하는 part 지시문
part 'counter_provider.g.dart';

// @riverpod 어노테이션으로 Provider 생성
@riverpod
int counter(CounterRef ref) {
  // 초기값 0을 반환
  return 0;
}

박시니어 씨가 화이트보드에 그림을 그리며 설명하기 시작했습니다. "Riverpod 코드 제너레이션의 핵심은 @riverpod 어노테이션이야.

이걸 이해하면 절반은 끝난 거나 마찬가지지." @riverpod은 Dart의 메타데이터 어노테이션입니다. 어노테이션이란 코드에 추가 정보를 붙이는 방법인데, 마치 책에 포스트잇을 붙여 중요한 부분을 표시하는 것과 비슷합니다.

riverpod_generator는 프로젝트의 모든 Dart 파일을 스캔하면서 @riverpod이 붙은 함수와 클래스를 찾습니다. 발견하면 해당 코드를 분석하여 적절한 Provider 코드를 자동으로 생성합니다.

코드를 자세히 살펴보겠습니다. 먼저 import 문에서 riverpod_annotation 패키지를 가져옵니다.

이 패키지가 @riverpod 어노테이션을 제공합니다. 다음으로 중요한 것이 part 지시문입니다.

part 'counter_provider.g.dart'라고 작성했는데, 이것은 "counter_provider.g.dart 파일이 이 파일의 일부입니다"라는 의미입니다. 여기서 .g.dart는 generated의 약자로, 자동 생성된 파일임을 나타내는 관례입니다.

함수를 보면 @riverpod이 붙어있고, 그 아래에 counter라는 함수가 있습니다. 이 함수의 이름이 매우 중요합니다.

함수 이름이 counter이면, 생성되는 Provider의 이름은 counterProvider가 됩니다. 함수 이름 뒤에 Provider가 자동으로 붙는 규칙입니다.

함수의 매개변수로 CounterRef ref가 있습니다. 여기서 CounterRef는 자동으로 생성되는 타입인데, 함수 이름의 첫 글자를 대문자로 바꾸고 Ref를 붙인 것입니다.

이 ref를 통해 다른 Provider를 읽거나 생명주기 콜백을 등록할 수 있습니다. "그런데 아직 .g.dart 파일이 없는데요?" 김개발 씨가 물었습니다.

"맞아, 아직 코드 생성을 실행하지 않았으니까. 나중에 build_runner를 실행하면 그 파일이 만들어질 거야." 박시니어 씨가 답했습니다.

처음에는 part 지시문 때문에 에러가 표시될 수 있습니다. 하지만 걱정하지 마세요.

build_runner를 실행하면 .g.dart 파일이 생성되고 에러가 사라집니다.

실전 팁

💡 - 파일 이름과 part 지시문의 이름이 일치해야 합니다 (예: my_file.dart -> my_file.g.dart)

  • 함수 이름은 소문자로 시작하는 camelCase를 사용하세요

3. 함수형 Provider 생성

김개발 씨는 이제 @riverpod의 기본 개념을 이해했습니다. 하지만 실제로 다양한 상황에서 어떻게 활용하는지 궁금해졌습니다.

단순한 값뿐만 아니라 API 호출 결과도 Provider로 만들 수 있을까요?

함수형 Provider는 가장 간단한 형태의 코드 제너레이션입니다. 함수가 반환하는 값의 타입에 따라 일반 Provider, FutureProvider, StreamProvider가 자동으로 결정됩니다.

마치 음식 재료를 넣으면 자동으로 적절한 요리법을 선택해주는 스마트 오븐과 같습니다.

다음 코드를 살펴봅시다.

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'providers.g.dart';

// 동기 Provider - 즉시 값 반환
@riverpod
String greeting(GreetingRef ref) {
  return '안녕하세요!';
}

// 비동기 Provider - Future 반환
@riverpod
Future<List<String>> fetchUsers(FetchUsersRef ref) async {
  // API 호출 시뮬레이션
  await Future.delayed(Duration(seconds: 1));
  return ['김철수', '이영희', '박민수'];
}

// 매개변수가 있는 Provider (family)
@riverpod
String userGreeting(UserGreetingRef ref, String name) {
  return '$name님, 환영합니다!';
}

박시니어 씨가 새로운 예제 코드를 보여주며 설명을 이어갔습니다. "함수형 Provider의 가장 큰 장점은 자동 타입 추론이야.

네가 어떤 타입을 반환하느냐에 따라 알아서 적절한 Provider를 만들어줘." 첫 번째 예제인 greeting 함수를 봅시다. 이 함수는 단순히 문자열을 반환합니다.

동기적으로 즉시 값을 반환하므로, 이것은 일반 Provider로 생성됩니다. 전통적인 방식으로 작성했다면 Provider<String>((ref) => '안녕하세요!')처럼 작성해야 했을 것입니다.

두 번째 예제인 fetchUsers 함수는 더 흥미롭습니다. 이 함수는 Future를 반환합니다.

async 키워드가 붙어있고, 네트워크 호출처럼 시간이 걸리는 작업을 수행합니다. riverpod_generator는 이것을 감지하고 자동으로 FutureProvider를 생성합니다.

"만약 Stream을 반환하면요?" 김개발 씨가 물었습니다. "그러면 StreamProvider가 만들어지지.

실시간 데이터를 다룰 때 유용해." 박시니어 씨가 답했습니다. 세 번째 예제는 매개변수가 있는 Provider입니다.

전통적인 Riverpod에서는 이것을 family라고 불렀습니다. 코드 제너레이션에서는 그냥 함수에 매개변수를 추가하면 됩니다.

name이라는 String 매개변수를 받아서 개인화된 인사말을 반환합니다. 이 Provider를 사용할 때는 ref.watch(userGreetingProvider('김개발'))처럼 매개변수를 전달합니다.

같은 매개변수로 호출하면 캐시된 값을 반환하고, 다른 매개변수로 호출하면 새로운 값을 계산합니다. 함수형 Provider의 특징은 읽기 전용이라는 점입니다.

한번 생성된 값을 외부에서 변경할 수 없습니다. 값을 변경하려면 의존하는 다른 Provider가 변경되어야 합니다.

"그럼 상태를 변경해야 할 때는 어떻게 해요?" 김개발 씨가 다음 질문을 던졌습니다. "그때 필요한 게 바로 클래스형 Notifier야.

다음에 알려줄게." 박시니어 씨가 미소를 지었습니다. 함수형 Provider는 간단한 계산이나 API 호출 결과처럼 파생된 값을 다룰 때 적합합니다.

사용자의 입력에 따라 상태가 변경되어야 한다면 클래스형 Notifier를 사용해야 합니다.

실전 팁

💡 - 함수형 Provider는 읽기 전용이므로 상태 변경이 필요없는 경우에 사용하세요

  • 매개변수가 있는 Provider는 자동으로 family 패턴이 적용됩니다

4. 클래스형 Notifier 생성

카운터 앱을 만들던 김개발 씨는 버튼을 누르면 숫자가 증가하는 기능을 구현해야 했습니다. 함수형 Provider로는 값을 변경할 수 없다고 했는데, 어떻게 해야 할까요?

박시니어 씨가 드디어 클래스형 Notifier를 알려줄 시간입니다.

클래스형 Notifier는 상태를 변경할 수 있는 Provider를 만드는 방법입니다. _$클래스명을 상속받는 클래스를 만들고 build 메서드에서 초기값을 반환합니다.

마치 리모컨처럼 상태를 조작하는 메서드들을 추가할 수 있어서, 사용자 인터랙션이 필요한 기능에 적합합니다.

다음 코드를 살펴봅시다.

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter_notifier.g.dart';

@riverpod
class Counter extends _$Counter {
  // build 메서드에서 초기 상태 반환
  @override
  int build() {
    return 0;
  }

  // 상태 변경 메서드들
  void increment() {
    state = state + 1;
  }

  void decrement() {
    state = state - 1;
  }

  void reset() {
    state = 0;
  }
}

박시니어 씨가 화면에 새로운 코드를 띄웠습니다. "자, 이게 바로 클래스형 Notifier야.

상태를 마음대로 변경할 수 있지." 클래스형 Notifier의 구조를 하나씩 살펴보겠습니다. 먼저 클래스 선언부를 보면 _$Counter를 상속받고 있습니다.

이 _$Counter는 아직 존재하지 않는 클래스입니다. build_runner가 실행되면 자동으로 생성됩니다.

앞에 밑줄과 달러 기호가 붙는 것이 규칙입니다. build 메서드는 Notifier의 핵심입니다.

이 메서드에서 반환하는 값이 초기 상태가 됩니다. 여기서는 0을 반환하므로, 카운터의 초기값은 0입니다.

@override 어노테이션이 붙어있는데, 이는 부모 클래스의 메서드를 재정의한다는 의미입니다. "build라는 이름이 특별한 이유가 있나요?" 김개발 씨가 질문했습니다.

"좋은 질문이야. build는 Provider가 처음 읽힐 때 호출되고, 의존하는 Provider가 변경되면 다시 호출돼.

그래서 초기화뿐만 아니라 재초기화 로직도 여기에 작성할 수 있어." 상태를 변경하는 메서드들을 살펴봅시다. increment, decrement, reset 메서드가 있습니다.

각 메서드에서 state 속성을 사용하고 있는데, 이것이 핵심입니다. state는 현재 상태값을 가지고 있고, 새로운 값을 할당하면 상태가 변경됩니다.

state = state + 1이라고 작성하면, 현재 상태에 1을 더한 값이 새로운 상태가 됩니다. 이렇게 state에 새로운 값을 할당하면 Riverpod이 자동으로 변경을 감지하고, 이 Provider를 구독하고 있는 모든 위젯에 알립니다.

UI에서 이 Notifier를 사용하는 방법도 간단합니다. 값을 읽을 때는 ref.watch(counterProvider)를 사용하고, 메서드를 호출할 때는 ref.read(counterProvider.notifier).increment()를 사용합니다.

notifier를 통해 클래스 인스턴스에 접근할 수 있습니다. 전통적인 StateNotifier와 비교하면 코드가 훨씬 간결합니다.

제네릭 타입을 명시할 필요도 없고, StateNotifierProvider를 따로 선언할 필요도 없습니다. 클래스와 @riverpod 어노테이션만으로 모든 것이 해결됩니다.

"훨씬 간단하네요!" 김개발 씨가 감탄했습니다.

실전 팁

💡 - build 메서드의 반환 타입이 state의 타입을 결정합니다

  • 비동기 초기화가 필요하면 build 메서드를 async로 만들면 AsyncNotifier가 됩니다

5. @Riverpod(keepAlive: true) 옵션

김개발 씨가 만든 앱에서 이상한 현상이 발생했습니다. 다른 화면으로 이동했다가 돌아오니 상태가 초기화되어 있었습니다.

사용자가 입력한 데이터가 사라진 것입니다. 이 문제를 어떻게 해결할 수 있을까요?

keepAlive 옵션은 Provider가 더 이상 사용되지 않아도 상태를 유지하도록 설정합니다. 기본적으로 Provider는 구독자가 없으면 상태가 폐기되는데, keepAlive: true를 설정하면 앱이 실행되는 동안 상태가 유지됩니다.

마치 냉장고에 음식을 보관하는 것처럼, 당장 먹지 않아도 신선하게 보존됩니다.

다음 코드를 살펴봅시다.

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'auth_provider.g.dart';

// keepAlive: true로 상태 유지
@Riverpod(keepAlive: true)
class Auth extends _$Auth {
  @override
  User? build() {
    return null; // 초기에는 로그인 안됨
  }

  Future<void> login(String email, String password) async {
    // 로그인 API 호출
    final user = await AuthService.login(email, password);
    state = user;
  }

  void logout() {
    state = null;
  }
}

class User {
  final String id;
  final String name;
  User({required this.id, required this.name});
}

박시니어 씨가 김개발 씨의 코드를 살펴보며 문제점을 발견했습니다. "아, 이거구나.

Provider의 생명주기 때문이야." Riverpod의 기본 동작을 이해해야 합니다. Provider는 자동 폐기(auto-dispose) 기능을 가지고 있습니다.

어떤 위젯도 이 Provider를 구독하지 않으면, Riverpod은 메모리를 절약하기 위해 상태를 폐기합니다. 그리고 다시 구독하면 build 메서드가 호출되어 초기 상태로 돌아갑니다.

대부분의 경우 이것은 좋은 동작입니다. 사용하지 않는 데이터를 메모리에 계속 들고 있을 필요가 없으니까요.

하지만 로그인 상태사용자 설정처럼 앱 전체에서 유지되어야 하는 데이터는 다릅니다. 이런 상황을 위해 keepAlive 옵션이 존재합니다.

@riverpod 대신 @Riverpod(keepAlive: true)를 사용하면, 구독자가 없어도 상태가 유지됩니다. 마치 VIP 회원처럼 특별 대우를 받는 것입니다.

코드를 살펴보면, 소문자 @riverpod이 아닌 대문자 @Riverpod을 사용하고 있습니다. 옵션을 전달할 때는 대문자로 시작하는 생성자 형태를 사용해야 합니다.

괄호 안에 keepAlive: true를 명시합니다. Auth 클래스는 사용자 인증 상태를 관리합니다.

초기값은 null로, 로그인하지 않은 상태를 나타냅니다. login 메서드가 호출되면 API를 통해 인증하고, 성공하면 사용자 정보를 state에 저장합니다.

keepAlive가 true이므로, 사용자가 앱의 어떤 화면으로 이동하든 로그인 상태가 유지됩니다. 로그인 화면에서 홈 화면으로, 홈 화면에서 설정 화면으로 이동해도 사용자 정보는 그대로입니다.

"그럼 모든 Provider에 keepAlive를 붙이면 되지 않나요?" 김개발 씨가 물었습니다. 박시니어 씨가 고개를 저었습니다.

"그러면 메모리 누수가 발생할 수 있어. 정말 필요한 것만 keepAlive를 붙여야 해.

대부분의 Provider는 자동 폐기가 더 효율적이야." keepAlive를 사용하기 좋은 상황은 명확합니다. 로그인 상태, 테마 설정, 언어 설정처럼 앱 전체에서 공유되고 자주 변경되지 않는 데이터입니다.

반면 특정 화면에서만 사용하는 목록 데이터나 폼 입력값은 자동 폐기가 더 적합합니다.

실전 팁

💡 - keepAlive는 정말 필요한 전역 상태에만 사용하세요

  • 소문자 @riverpod과 대문자 @Riverpod(keepAlive: true)를 구분해서 사용하세요

6. build runner 사용법

모든 코드를 작성한 김개발 씨. 하지만 IDE에서는 여전히 빨간 줄이 가득합니다.

_$Counter를 찾을 수 없다, counterProvider가 정의되지 않았다는 에러들입니다. 이제 마지막 단계인 코드 생성을 실행할 시간입니다.

build_runner는 Dart 코드를 분석하고 새로운 코드를 생성하는 도구입니다. @riverpod 어노테이션이 붙은 코드를 찾아 .g.dart 파일을 생성합니다.

마치 밀가루 반죽을 제빵기에 넣으면 빵이 나오는 것처럼, 우리의 코드를 넣으면 완성된 Provider 코드가 나옵니다.

다음 코드를 살펴봅시다.

# 한 번만 코드 생성 (일반적인 사용)
dart run build_runner build

# 파일 변경 감지하여 자동 재생성 (개발 중 권장)
dart run build_runner watch

# 기존 생성 파일 삭제 후 새로 생성 (충돌 시)
dart run build_runner build --delete-conflicting-outputs

# 생성된 파일 예시 (counter_notifier.g.dart)
# part of 'counter_notifier.dart';
#
# final counterProvider =
#     AutoDisposeNotifierProvider<Counter, int>.internal(
#   Counter.new,
#   name: 'counterProvider',
#   ...
# );

박시니어 씨가 터미널을 열며 말했습니다. "자, 이제 마법의 주문을 외워볼 시간이야." build_runner를 실행하는 방법은 크게 두 가지입니다.

첫 번째는 build 명령어입니다. dart run build_runner build를 실행하면, 프로젝트의 모든 파일을 스캔하고 필요한 코드를 생성합니다.

한 번 실행하고 끝나는 방식입니다. 두 번째는 watch 명령어입니다.

dart run build_runner watch를 실행하면, 터미널이 대기 상태로 들어갑니다. 파일이 변경될 때마다 자동으로 코드를 재생성합니다.

개발 중에는 이 방식이 훨씬 편리합니다. "watch 모드를 실행해놓으면 저장할 때마다 자동으로 .g.dart 파일이 업데이트되는 거예요?" 김개발 씨가 물었습니다.

"맞아. 새로운 Provider를 추가하거나 기존 Provider를 수정하면 몇 초 안에 반영돼." 박시니어 씨가 답했습니다.

가끔 에러가 발생할 수 있습니다. 특히 기존에 생성된 파일과 충돌이 일어날 때 그렇습니다.

이럴 때는 --delete-conflicting-outputs 옵션을 사용합니다. 이 옵션은 충돌하는 기존 파일을 삭제하고 새로 생성합니다.

생성된 .g.dart 파일을 열어보면 꽤 복잡한 코드가 있습니다. AutoDisposeNotifierProvider라는 긴 이름의 클래스, 각종 설정값들이 있습니다.

하지만 이 파일을 직접 수정하면 안 됩니다. 다음에 build_runner를 실행하면 덮어써지기 때문입니다.

.g.dart 파일은 .gitignore에 추가하지 않는 것이 일반적입니다. 버전 관리에 포함시켜야 CI/CD 환경에서 별도로 코드 생성을 실행하지 않아도 됩니다.

하지만 팀 규칙에 따라 다를 수 있으니 확인해보세요. 김개발 씨가 터미널에서 dart run build_runner build를 실행했습니다.

몇 초간 처리가 진행되더니, 성공 메시지가 출력되었습니다. IDE를 확인해보니 모든 빨간 줄이 사라졌습니다.

"와, 진짜 됐네요!" 김개발 씨가 기뻐했습니다. 박시니어 씨가 웃으며 말했습니다.

"이제 네가 만든 Provider를 마음껏 사용할 수 있어. ref.watch(counterProvider)로 값을 읽고, ref.read(counterProvider.notifier).increment()로 증가시키면 돼." 코드 제너레이션 방식의 Riverpod은 처음 설정할 때만 약간의 학습이 필요합니다.

일단 익숙해지면 더 적은 코드로 더 안전한 상태 관리를 할 수 있습니다. 타입 오류도 컴파일 시점에 잡히고, IDE의 자동완성 지원도 훌륭합니다.

실전 팁

💡 - 개발 중에는 watch 모드를 항상 실행해두면 편리합니다

  • .g.dart 파일은 절대 직접 수정하지 마세요. 모든 변경은 원본 파일에서 해야 합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Flutter#Riverpod#CodeGeneration#StateManagement#Provider#Flutter,State Management

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스