위 글에서 StateNotifier를 이론적으로 배웠다면 실습을 Riverpod을 이용해서 해 보겠습니다.
이글에서 StateNotifer 와 Riverpod을 이용해서 만들어 볼 앱 입니다.
Start
앞서 앱 파일 구조는 이렇게 됩니다.
미리 파일만 만들어 주셔도 됩니다.
프로젝트 생성
플러터 프로젝트를 생성해주세요.
그리고 main.dart에 있는 모든 코드는 지우고 시작하겠습니다.
Riverpod 추가
Riverpod을 사용하기 위해서는 riverpod 패키지를 설치해야 합니다.
https://pub.dev/packages/flutter_riverpod
pubspec.yaml 에 추가한뒤 pub get
main.dart
비워둔 main.dart에 코드를 추가해 보겠습니다.
void main() {
runApp(ProviderScope(child: MyApp())); //리버팟을 사용하기 위해 최상위루트 위젯을 ProviderScope 로 감쌈.
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.dark, //다크모드설정
darkTheme: ThemeData.dark(), //다크모드설정
home: BookScreen(),
);
}
}
작성하거나 복붙하셔도 됩니다.
뜨는 빨간줄은 import 해주시면 됩니다. (BookScreen() 빨간 줄은 좀 있다가 없애겠습니다.)
기존에 하던 프로젝트랑 다른 거는 Riverpod을 사용하기 위해서 최상위 루트 위젯을 "ProviderScope"로 감싼 겁니다.
book_state_notifier.dart
book_state_notifier.dart 파일을 생성하고 코드 입력 & 복붙
final booksProvider = StateNotifierProvider<BookStateNotifier, List<Book>>((ref) {
return BookStateNotifier();
});
class BookStateNotifier extends StateNotifier<List<Book>> {
BookStateNotifier() : super([]);
void addBook(Book newBook) {
state = [...state, newBook];
}
void removeBook(Book removeBook) {
state = state.where((book) => book != removeBook).toList();
}
}
class Book {
final String title;
final String description;
Book({required this.title, required this.description});
}
코드설명
class Book {
final String title;
final String description;
Book({required this.title, required this.description});
}
Book 클래스 생성
class BookStateNotifier extends StateNotifier<List<Book>> {
BookStateNotifier() : super([]);
void addBook(Book newBook) {
state = [...state, newBook];
}
void removeBook(Book removeBook) {
state = state.where((book) => book != removeBook).toList();
}
}
Book 상태를 관리하는 BookStateNotifier 클래스 생성
StateNotifier를 상속받고 제네릭엔 어떠한 상태를 관리할 것인지 적어준다.
List형식으로 위에서만든 Book클래스의 인스턴스를 관리할 것 이므로 <List<Book>>
BookStateNotifier() : super([]);
빈 list를 넣어서 초기화 해주겠습니다.
void addBook(Book newBook) {
state = [...state, newBook];
}
void removeBook(Book removeBook) {
state = state.where((book) => book != removeBook).toList();
}
그리고 이전 글에서 봤다시피 StateNotifier는 notifier 안에서만 값을 수정할 수 있습니다.
그래서 값을 수정할 수 있는 함수 2개를 만들어 줍니다.
state는 StateNorifier가 관리하고 있는 상태값이 들어 있습니다.
즉 여기에서는 state = List<Book> 입니다.
그러면 state는 list를 관리하고 있는데 왜. add 나. remove를 사용해서 값을 수정하지 않는 건가요?
이유는 state를 변경하기 위해서 입니다. StateNorifier의 사용 이유가 state가 변경되면 알려주기 위해서인데 .add나 .remove를 사용하면 해당 list객체 안에서 값을 추가하거나 , 값을 지우기 때문에 state가 변경되지 않습니다.이러한 이유 때문에 addBook(), removeBook()을 이용해 새로운 list 객체를 넣어서 state가 변경되게 합니다.
final booksProvider = StateNotifierProvider<BookStateNotifier, List<Book>>((ref) {
return BookStateNotifier();
});
Rivierpod을 사용해 앱전체에서 데이터를 관리할 수 있는 provider를 생성하는 코드입니다.
Riverpod은 전역으로 final provider를 생성하여도 아무 문제가 없습니다.
StateNotifierProvider는 StateNotifier를 관리하는 전용 프로바이더 입니다.
제네릭에 관리할 StateNotifier 와 해당 Notifier가 관리하는 값의 타입을 넣어줍니다.
그리고 해당 StateNotifier 클래스의 객체를 생성해 return 해주면 됩니다.
그러면 booksProvider는 앱 어디서든 사용할 수 있는 BookStateNotifier의 인스턴스가 됩니다.
book_widget.dart
book_widget.dart 파일을 생성하고 코드 입력 & 복붙
리스트뷰에 보여줄 아이템 위젯입니다.
재사용하기 위해 만들어준거니 그냥 복붙하셔도 무방합니다.
class BookWidget extends StatelessWidget {
final String title;
final String description;
const BookWidget({
required this.title,
required this.description,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: Colors.white),
borderRadius: BorderRadius.circular(30),
),
child: Row(
children: [
Icon(Icons.book_rounded),
SizedBox(
width: 16,
),
RichText(
text: TextSpan(
children: [
TextSpan(
text: "${title}\n",
style:
TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),
TextSpan(
text: description,
style: TextStyle(fontSize: 12),
),
],
),
),
],
),
);
}
}
book_screen.dart
book_screen.dart 파일을 생성하고 코드 입력 & 복붙
class BookScreen extends ConsumerWidget {
const BookScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final bookList = ref.watch(booksProvider);
return Scaffold(
appBar: AppBar(
title: Text("책 소개"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
bookList.isEmpty
? Text("소개할 책이 없습니다.")
: Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
return GestureDetector(
onDoubleTap: (){
ref.read(booksProvider.notifier).removeBook(bookList[index]);
},
child: BookWidget(
title: bookList[index].title,
description: bookList[index].description),
);
},
itemCount: bookList.length,
),
),
const Spacer(),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BookAddScreen(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
child: const Text("책 추가 페이지 이동"),
),
],
),
),
);
}
}
빨간줄은 import 해주시면 됩니다. (BookAddScreen() 빨간 줄은 좀 있다가 없애겠습니다.)
그리고 main.dart 파일로 돌아가 BookScreen()의 빨간 줄을 import해서 없애주세요.
이 코드에서 기존에 사용하던 코드랑 다른 점은 Riverpod을 사용하기 위해 StatelessWidget을 상속받는 대신 ConsumerWidget을 상속받았습니다. 그리고 build메소드 파라미터에 WidgetRef ref가 추가 됐습니다.
자세한 설명은 여기서 볼 수 있습니다.
코드설명
final bookList = ref.watch(booksProvider);
ref.watch(booksProvider) 를 하게 되면 booksProvider가 관리하는? 값을 가져오게 됩니다.
초기화를 빈 리스트로 했기때문에 처음에는 bookList에는 빈 리스트를 가져옵니다.
이 부분이 헷갈릴 수도 있을 거 같아서 추가 설명을 하자면
booksProvider의 return은 BooksStateNotifier() 입니다. BooksStateNotifier() 객체를 생성하면 빈 리스트로 초기화 한걸 위에서 설명했습니다.
bookList를 통해 리스트에 값이 없다면 "소개할 책이 없습니다." 문구 출력
있다면 리스트뷰로 해당 책을 보여줍니다.
ref.read(booksProvider.notifier).removeBook(bookList[index]);
제스처 위젯으로 두 번 클릭하면 삭제되게 만들었습니다.
버튼같이 사용자 입력으로 이벤트가 처리되는 곳에서는 .read 로 접근해야합니다.
그리고 값을 수정하고 싶을때에는 .notifier로 추가 접근합니다.
해당 코드로 접근한 곳이 여기입니다. 그래서 removeBook 함수도 사용할 수 있는 겁니다.
book_add_screen.dart
book_add_screen.dart 파일을 생성하고 코드 입력 & 복붙
class BookAddScreen extends ConsumerWidget {
const BookAddScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
var title = "";
var description = "";
return Scaffold(
appBar: AppBar(title: const Text("책 추가하기"),),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
hintText: "책 제목을 입력해 주세요."
),
onChanged: (text) {
title = text;
},
),
TextFormField(
decoration: const InputDecoration(
hintText: "책 소개를 입력해 주세요."
),
onChanged: (text) {
description = text;
},
),
const SizedBox(height: 16,),
ElevatedButton(
onPressed: () {
final book = Book(title: title, description: description);
ref.read(booksProvider.notifier).addBook(book);
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
child: const Text("책 추가하기"),
),
],
),
),
);
}
}
코드를 입력하고 book_screen.dart 파일로 돌아가서 BookAddScreen() 을 import해서 빨간 줄을 없애줍니다.
위에서 다 설명했기에 해당코드는 추가로 설명하지 않겠습니다.
차근차근 따라 하다 보면 이해되실 겁니다. riverpod에서 이해가 되지 않는다면 해당 글부터 추가로 읽어보시면 이해가 되실 수도 있습니다.
그래도 이해가 되지 않는다면 오픈 카톡으로 질문 주시면 아는 부분에서 최대한 알려드리겠습니다.
https://open.kakao.com/o/sCxg8Hze
긴 글 봐주셔서 감사합니다. 틀린 부분 있으면 언제든지 말씀해 주세요.
'Flutter > Riverpod' 카테고리의 다른 글
Flutter) Riverpod refresh / invalidate - 6 (0) | 2023.01.22 |
---|---|
Flutter) Riverpod Consumer 사용하기 - 5 (0) | 2023.01.22 |
Flutter) Riverpod watch,listen,read 사용하기 - 4 (0) | 2023.01.21 |
Flutter) Riverpod "ref" 얻기 - 3 (0) | 2023.01.21 |
Flutter) Riverpod 프로바이더란? - 2 (0) | 2023.01.21 |