1. Flutter와 Riverpod을 사용해 글쓰기 화면 구현
1. post_write_body.dart
import 'package:blog/ui/list/post_list_vm.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PostWriteBody extends ConsumerWidget {
final _title = TextEditingController();
final _content = TextEditingController();
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Flexible(
fit: FlexFit.loose,
child: ListView(
shrinkWrap: true, //확장했다.
children: [
Container(
color: Colors.deepPurple[100],
height: 400,
width: double.infinity,
child: Icon(CupertinoIcons.airplane),
),
SizedBox(height: 10),
TextFormField(
controller: _title,
),
TextFormField(
controller: _content,
),
// Checkbox(
// value: true,
// onChanged: (value) {
// print(value);
// },
// ),
],
),
),
// TextButton을 children 리스트에 포함시킴
TextButton(
onPressed: () {
ref
.read(postListProvider.notifier)
.notifySave(_title.text, _content.text);
//여기에 네비게이션 pop을 쓰면 위험하다.. 화면이 먼저 팝 될수있다.
//Navigator.pop(context);
//창고에 통신 코드가 있는데 비동기로 돌고있는데 네이게이션팝을 하면 창고가 무너진다.
//하나의 비즈니스 로직은 트렉젝션으로 묶어주는 게 좋다.
// 방법이 2가지 있다. 1. async , awiat 걸어주기
// 2. notifySave에서 save와 post list vm에서 화면받는 걸 만들어준다
},
child: Text("글쓰기")),
],
),
);
}
}
코드설명
PostWriteBody
는 ConsumerWidget
을 상속받은 위젯으로, Riverpod의 상태 관리를 사용합니다.1.TextEditingController 사용
final _title = TextEditingController();
final _content = TextEditingController();
_title
과_content
는TextEditingController
로, 각각 제목과 내용의 텍스트 필드 값을 관리합니다.
- 사용자가 입력한 텍스트는 컨트롤러를 통해 접근할 수 있습니다.
2. ConsumerWidget과 상태 관리
ConsumerWidget
은 Riverpod을 사용해 상태 관리를 쉽게 할 수 있게 해주는 위젯입니다. WidgetRef
(여기서는 ref
)를 사용해 상태나 프로바이더에 접근합니다.ref.read(postListProvider.notifier).notifySave(_title.text, _content.text);
postListProvider
는 글 목록을 관리하는 Riverpod 프로바이더입니다. 사용자가 "글쓰기" 버튼을 눌렀을 때,notifySave
메서드를 호출해_title
과_content
의 값을 전달합니다.
ref.read()
를 통해postListProvider
의 상태를 읽고,notifier
를 통해 상태 변경을 알립니다.
3. UI 구성
Flexible
위젯을 사용하여 공간을 유연하게 차지하도록 구성했습니다. 여기서fit: FlexFit.loose
는 자식이 필요한 만큼만 공간을 차지하게 합니다.
ListView
내부에는TextFormField
두 개가 있으며, 각각 제목과 내용을 입력할 수 있는 필드입니다.
shrinkWrap: true
는ListView
의 높이를 필요한 만큼만 줄이도록 설정하는 속성입니다. 즉, 전체 화면을 채우지 않고 자식 요소에 맞는 크기로ListView
가 줄어듭니다.
- 제목과 내용을 입력하는 두 개의
TextFormField
가 각각_title
,_content
컨트롤러에 연결되어 있습니다.
4. 글쓰기" 버튼과 비동기 처리
TextButton(
onPressed: () {
ref.read(postListProvider.notifier).notifySave(_title.text, _content.text);
},
child: Text("글쓰기"),
)
- "글쓰기" 버튼을 눌렀을 때,
notifySave
메서드가 호출되어 사용자가 입력한 제목과 내용이 저장됩니다.
- 여기서 중요한 점은 네비게이션 pop을 바로 호출하지 않는 것입니다. 주석에서 설명한 것처럼, 저장 로직이 비동기로 동작할 경우 화면이 먼저 사라지면(팝되면) 데이터 저장이 제대로 이루어지지 않을 수 있습니다. 따라서 두 가지 해결책
async
와await
를 사용하여 비동기 처리를 동기적으로 처리한 후 화면을 pop합니다.notifySave
메서드 내에서 저장과 관련된 로직을 모두 처리하고, 성공 후에 화면을 pop하도록 로직을 구성합니다.
2. post_list_vm.dart (수정한 부분)
//1. 창고
class PostLiveVM extends StateNotifier<PostListModel?> {
// ! 넣어야 하는 이유는 화면(화면을 무조건 띄운다)이 없을수 없어서 붙인다.
final mContext = navigatorKey.currentState!.context;
PostLiveVM(super.state);
//삭제 불러오기
Future<void> notifyDelete(int id) async {
await PostRepository().deleteById(id);
PostListModel model = state!;
//where은 필터다! 검색로직(이걸 넣으면 e.id == id)
List<_Post> newPosts = model.posts.where((e) => e.id != id).toList();
state = PostListModel(newPosts);
Navigator.pop(mContext);
}
//트랙젝션(일의 최소 단위) 통신해서 파싱하고 데이터 화면에 뿌리기(벨리게이트)
Future<void> notifySave(String title, String content) async {
//통신으로 세이브 요청(글쓰기가 완성)
//상태만 변경하면 await PostRepository().save(title, content); 이렇게 쓰면 된다.
Map<String, dynamic> one = await PostRepository().save(title, content);
//Map타입으로 받음
_Post newPost = _Post.fromMap(one);
PostListModel model = state!;
//model.posts.add(newPost);
//깊은 복사, _Post newPost = _Post.fromMap(one); 이거 만들고 나서 앞에 newPost 붙인다.
List<_Post> newPosts = [newPost, ...model.posts];
// 중요함!!!!!! 상태는 새로 객체를 만들어서 줘야한다.
state = PostListModel(newPosts);
Navigator.pop(mContext);
}
코드설명
1. mContext 사용
final mContext = navigatorKey.currentState!.context;
navigatorKey
를 사용해 현재의 네비게이션 상태에 접근하고context
를 가져옵니다.currentState!
에서!
를 붙인 이유는context
가 반드시 존재한다고 보장하기 위함입니다. 이context
는 화면을 pop하는 등의 네비게이션 작업에 사용됩니다.
2 notifyDelete(int id)
Future<void> notifyDelete(int id) async {
await PostRepository().deleteById(id);
PostListModel model = state!;
List<_Post> newPosts = model.posts.where((e) => e.id != id).toList();
state = PostListModel(newPosts);
Navigator.pop(mContext);
}
- 삭제 요청:
PostRepository().deleteById(id)
를 통해 서버나 데이터베이스에서 해당 ID의 게시글을 삭제합니다.
- 상태 업데이트: 삭제된 게시글을 제외한 나머지 게시글을
where
필터를 사용해 새 리스트로 만든 후, 새로운PostListModel
객체를 생성해 상태를 업데이트합니다.
- 화면 종료: 삭제 작업이 완료되면
Navigator.pop(mContext)
를 통해 현재 화면을 종료합니다.
3. notifySave(String title, String content)
Future<void> notifySave(String title, String content) async {
Map<String, dynamic> one = await PostRepository().save(title, content);
_Post newPost = _Post.fromMap(one);
PostListModel model = state!;
List<_Post> newPosts = [newPost, ...model.posts];
state = PostListModel(newPosts);
Navigator.pop(mContext);
}
- 저장 요청:
PostRepository().save(title, content)
를 통해 서버에 새 게시글을 저장합니다. 이 작업은 비동기로 처리되며, 결과로 서버에서 새롭게 저장된 게시글의 정보를Map<String, dynamic>
형태로 반환받습니다.
- 새로운 Post 생성:
fromMap
메서드를 사용하여 반환받은Map
데이터를 새로운_Post
객체로 변환합니다.
- 상태 업데이트: 기존 게시글 리스트에 새 게시글을 추가한 뒤, 새로운
PostListModel
객체를 만들어 상태를 업데이트합니다. 상태는 새 객체로 갱신되어야 하므로, 리스트를 직접 수정하지 않고newPosts
라는 새로운 리스트를 생성합니다.
- 화면 종료: 저장이 완료되면
Navigator.pop(mContext)
를 통해 화면을 종료합니다.
4. 트랜잭션(일의 최소 단위)
- 코드에서는 트랜잭션 개념을 언급하고 있는데, 트랜잭션은 하나의 작업 단위로, 모든 작업이 성공적으로 완료되어야 최종적으로 반영됩니다. 만약 중간에 실패가 발생하면 모든 변경 사항을 롤백할 수 있는 구조가 필요할 수 있습니다.
- 예를 들어,
notifySave
메서드에서 서버에 저장이 성공해야만 상태가 업데이트되고 화면이 종료됩니다. 이렇게 함으로써 데이터가 안전하게 처리되도록 보장할 수 있습니다.
5. Navigator.pop의 위험성
주석에서 설명된 것처럼,
Navigator.pop
을 사용해 화면을 닫는 시점을 신중하게 관리해야 합니다. 비동기 통신이 끝나기 전에 화면이 닫히면 아직 완료되지 않은 작업이 중단될 수 있습니다. 해결책- 비동기 작업이 완료된 후에만
pop
을 호출하거나,
- 트랜잭션이 성공적으로 끝났는지 확인한 후에 화면을 종료해야 합니다.
3. post_detail_body.dart(수정한 부분)
child: Icon(CupertinoIcons.trash_fill),
onPressed: () {
ref.read(postListProvider.notifier).notifyDelete(model.id);
},
코드설명
1. onPressed 콜백 함수
onPressed: () { ref.read(postListProvider.notifier).notifyDelete(model.id); },
onPressed
는 사용자가 버튼을 클릭했을 때 실행되는 함수입니다.
ref.read(postListProvider.notifier)
를 사용해postListProvider
의 상태를 관리하는StateNotifier
에 접근합니다.
notifyDelete(model.id)
는 해당 게시글의 ID를 인자로 받아, 게시글을 삭제하는 역할을 수행합니다. 이 ID는model.id
로 전달되며,notifyDelete
메서드에서 삭제 요청이 처리됩니다.
2. notifyDelete 설명
notifyDelete(model.id)
는 서버나 데이터베이스에서 게시글을 삭제하고, 상태를 갱신하여 UI에서 해당 게시글이 사라지도록 합니다. 또한, 작업이 끝난 후 화면을 종료하는 역할도 수행할 수 있습니다.
이 코드에서 버튼을 누르면
postListProvider
에 연결된 PostLiveVM
클래스의 notifyDelete
메서드가 호출되어, 해당 model.id
를 가진 게시글이 삭제됩니다.4. app화면 구성할 때 주의 할 점
Button 빼고 Column으로 묶는게 좋다.

child: Column(
children: [
Flexible(
fit: FlexFit.loose,
child: ListView(
shrinkWrap: true, //확장했다.
children: [
Container(
color: Colors.deepPurple[100],
height: 400,
width: double.infinity,
child: Icon(CupertinoIcons.airplane),
),
SizedBox(height: 10),
TextFormField(
controller: _title,
),
TextFormField(
controller: _content,
),
// Checkbox(
// value: true,
// onChanged: (value) {
// print(value);
// },
// ),
],
),
),
// TextButton을 children 리스트에 포함시킴
TextButton(
onPressed: () {
ref
.read(postListProvider.notifier)
.notifySave(_title.text, _content.text);
//여기세 네비게이션 pop을 쓰면 위험하다.. 화면이 먼저 팝 될수있다.
//Navigator.pop(context);
//창고에 통신 코드가 있는데 비동기로 돌고있는데 네이게이션팝을 하면 창고가 무너진다.
//하나의 비즈니스 로직은 트렉젝션으로 묶어주는 게 좋다.
// 방법이 2가지 있다. 1. async , awiat 걸어주기
// 2. notifySave에서 save와 post list vm에서 화면받는 걸 만들어준다
},
child: Text("글쓰기")),
],
),
);
5. Flutter 글쓰는 방법
선생님께서 알려주신 예제코드
1번 방법

2번 방법

- Riverpod 사용하여 글 쓰기:
- 상태 관리를 위해 Riverpod을 사용.
StateNotifier
를 통해 제목과 내용을 관리하고, 저장 버튼을 눌렀을 때 상태를 업데이트.- 비동기적으로 서버에 데이터를 저장하고, 완료 후 UI가 자동으로 갱신.
- TextEditingController 사용:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 상태 관리용 StateNotifier 정의
class PostNotifier extends StateNotifier<String> {
PostNotifier() : super('');
void updatePost(String value) {
state = value;
}
}
final postProvider = StateNotifierProvider<PostNotifier, String>((ref) {
return PostNotifier();
});
class PostWriteWithRiverpod extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final post = ref.watch(postProvider);
final postNotifier = ref.read(postProvider.notifier);
return Scaffold(
appBar: AppBar(title: Text('글쓰기')),
body: Column(
children: [
TextFormField(
onChanged: (value) => postNotifier.updatePost(value),
decoration: InputDecoration(hintText: '글을 입력하세요'),
),
ElevatedButton(
onPressed: () {
// 저장 로직 추가
print('작성된 글: $post');
},
child: Text('글쓰기'),
),
SizedBox(height: 10),
Text('작성된 글: $post'),
],
),
);
}
}
2. TextEditingController 글 쓰기 예제:
import 'package:flutter/material.dart';
class PostWriteWithController extends StatefulWidget {
@override
_PostWriteWithControllerState createState() => _PostWriteWithControllerState();
}
class _PostWriteWithControllerState extends State<PostWriteWithController> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('글쓰기')),
body: Column(
children: [
TextFormField(
controller: _controller,
decoration: InputDecoration(hintText: '글을 입력하세요'),
),
ElevatedButton(
onPressed: () {
print('작성된 글: ${_controller.text}');
_controller.clear(); // 저장 후 초기화
},
child: Text('글쓰기'),
),
],
),
);
}
}
6. Flutter checkbox 사용법

체크박스 예제
체크박스와 TextEditingController는 같이 사용 할 수 없다.
1. StatefulWidget을 사용한 체크박스 예제
Checkbox(
value: true,
onChanged: (value) {
print(value);
},
),
import 'package:flutter/material.dart';
class CheckBoxExample extends StatefulWidget {
@override
_CheckBoxExampleState createState() => _CheckBoxExampleState();
}
class _CheckBoxExampleState extends State<CheckBoxExample> {
bool isChecked = false; // 체크박스 상태를 위한 변수
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('체크박스 예제')),
body: Column(
children: [
Checkbox(
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value!;
});
},
),
Text(isChecked ? '선택됨' : '선택 안됨'), // 체크박스 상태에 따라 텍스트 변경
],
),
);
}
}
2. Riverpod을 사용한 체크박스 예제
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 체크박스 상태 관리를 위한 Riverpod Provider
final checkBoxProvider = StateProvider<bool>((ref) => false);
class CheckBoxWithRiverpod extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isChecked = ref.watch(checkBoxProvider); // 체크박스 상태 읽기
final checkBoxNotifier = ref.read(checkBoxProvider.notifier); // 상태 변경용
return Scaffold(
appBar: AppBar(title: Text('체크박스 예제')),
body: Column(
children: [
Checkbox(
value: isChecked,
onChanged: (bool? value) {
checkBoxNotifier.state = value!;
},
),
Text(isChecked ? '선택됨' : '선택 안됨'), // 상태에 따른 텍스트
],
),
);
}
}
요약:
- TextEditingController는 주로 텍스트 필드에서 입력된 텍스트를 관리하는 데 사용됩니다.
- 체크박스의 상태는 Boolean 값을 사용하여 관리해야 하며, 이를 위해 State 또는 상태 관리 라이브러리를 사용해 선택 여부를 처리해야 합니다.
- 체크박스와 TextEditingController는 다른 종류의 입력을 처리하기 때문에 직접적으로 함께 사용되지는 않습니다.
7. Flexible
Flexible
은 Flutter에서 유연한 레이아웃을 만들 때 사용되는 위젯으로, Row
, Column
같은 부모 위젯 안에서 자식 위젯이 가질 수 있는 공간을 유연하게 설정하는 데 사용됩니다. Flexible
을 사용하면 자식 위젯이 필요에 따라 공간을 차지하거나, 남은 공간을 비율로 나눠 가질 수 있습니다. Flexible
은 주로 Expanded
위젯과 함께 사용되며, 둘 사이에 몇 가지 차이점이 있습니다.Flexible
과 Expanded
차이점:
Flexible
은 자식이 필요한 만큼만 공간을 차지하게 하되, 남은 공간도 유연하게 사용할 수 있습니다.
Expanded
는 가능한 모든 남은 공간을 자식에게 채워주도록 만듭니다. 즉,Flexible
은 더 세밀한 제어를 제공한다고 볼 수 있습니다.
Flexible
의 주요 속성:
fit
:FlexFit.tight
또는FlexFit.loose
를 사용해 자식 위젯이 남은 공간을 어떻게 차지할지 결정합니다.FlexFit.tight
: 가능한 모든 남은 공간을 채웁니다 (즉,Expanded
와 동일).FlexFit.loose
: 자식이 필요한 만큼만 공간을 차지하게 하며, 남은 공간을 채우지 않습니다.
글쓰기 너무 밑에 있다

Flexible 사용하기

8.
Flutter에서 화면 추적을 위해 Navigator에 접근하는 방법 중 하나는 GlobalKey를 사용하는 것입니다. 이를 통해 네비게이션 상태를 추적하고, 화면 전환을 제어할 수 있습니다. 특히, GlobalKey<NavigatorState>를 사용하면 어디서든지 네비게이션을 관리할 수 있게 됩니다
1. GlobalKey를 사용한 네비게이션키 설정
먼저,
GlobalKey<NavigatorState>
를 생성하여 네비게이션 상태를 추적할 수 있도록 해야 합니다.코드 예제:

import 'package:flutter/material.dart';
// 네비게이션 상태를 추적하는 GlobalKey 생성
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
navigatorKey: navigatorKey, // navigatorKey 연결
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('홈 화면'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
// GlobalKey를 사용해 네비게이션 상태 추적
navigatorKey.currentState?.push(
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
child: Text('다음 화면으로 이동'),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('두 번째 화면'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
// GlobalKey를 사용해 화면 종료
navigatorKey.currentState?.pop();
},
child: Text('뒤로 가기'),
),
),
);
}
}
2. 설명:
- GlobalKey<NavigatorState> 생성:
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
- 이 키를 사용하면 애플리케이션 어디서나
navigatorKey.currentState
를 통해 네비게이션에 접근할 수 있습니다.
- MaterialApp의 navigatorKey:
MaterialApp
에서navigatorKey
속성에 위에서 생성한navigatorKey
를 할당합니다.- 이렇게 하면
navigatorKey.currentState
를 통해 네비게이션 상태를 제어할 수 있습니다.
- navigatorKey로 화면 전환:
- 버튼을 클릭하면
navigatorKey.currentState?.push(...)
를 사용해 다른 화면으로 이동합니다. navigatorKey.currentState?.pop()
을 사용해 이전 화면으로 돌아올 수 있습니다.
3. 장점:
- 이 방법은 네비게이션 상태를 전역에서 관리할 수 있어 어디서나 접근이 가능하며, 특히
context
가 없는 곳에서 화면을 이동하거나 제어할 때 유용합니다.
- 네비게이션을 중앙에서 관리하는 앱 구조를 만들기 좋습니다.
Share article