Flutter-Blog 글쓰기,삭제

홍윤's avatar
Oct 14, 2024
Flutter-Blog 글쓰기,삭제
 

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("글쓰기")), ], ), ); } }
💡

코드설명

PostWriteBodyConsumerWidget을 상속받은 위젯으로, Riverpod의 상태 관리를 사용합니다.

1.TextEditingController 사용

final _title = TextEditingController(); final _content = TextEditingController();
  • _title_contentTextEditingController로, 각각 제목과 내용의 텍스트 필드 값을 관리합니다.
  • 사용자가 입력한 텍스트는 컨트롤러를 통해 접근할 수 있습니다.

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: trueListView의 높이를 필요한 만큼만 줄이도록 설정하는 속성입니다. 즉, 전체 화면을 채우지 않고 자식 요소에 맞는 크기로 ListView가 줄어듭니다.
  • 제목과 내용을 입력하는 두 개의 TextFormField가 각각 _title, _content 컨트롤러에 연결되어 있습니다.

4. 글쓰기" 버튼과 비동기 처리

TextButton( onPressed: () { ref.read(postListProvider.notifier).notifySave(_title.text, _content.text); }, child: Text("글쓰기"), )
  • "글쓰기" 버튼을 눌렀을 때, notifySave 메서드가 호출되어 사용자가 입력한 제목과 내용이 저장됩니다.
  • 여기서 중요한 점은 네비게이션 pop을 바로 호출하지 않는 것입니다. 주석에서 설명한 것처럼, 저장 로직이 비동기로 동작할 경우 화면이 먼저 사라지면(팝되면) 데이터 저장이 제대로 이루어지지 않을 수 있습니다. 따라서 두 가지 해결책
      1. asyncawait를 사용하여 비동기 처리를 동기적으로 처리한 후 화면을 pop합니다.
      1. 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으로 묶는게 좋다.
notion image
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번 방법
notion image
2번 방법
notion image
  • Riverpod 사용하여 글 쓰기:
    • 상태 관리를 위해 Riverpod을 사용.
    • StateNotifier를 통해 제목과 내용을 관리하고, 저장 버튼을 눌렀을 때 상태를 업데이트.
    • 비동기적으로 서버에 데이터를 저장하고, 완료 후 UI가 자동으로 갱신.
  • TextEditingController 사용:
    • TextEditingController로 텍스트 필드 값을 관리.
    • 사용자가 입력한 내용을 쉽게 가져와 저장할 수 있고, 저장 후 필드 값을 초기화.
    • 간단한 텍스트 입력 처리에 적합.
    • Riverpod 글 쓰기 예제:

      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 사용법

💡
notion image

체크박스 예제

💡
체크박스와 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 위젯과 함께 사용되며, 둘 사이에 몇 가지 차이점이 있습니다.

FlexibleExpanded 차이점:

  • Flexible은 자식이 필요한 만큼만 공간을 차지하게 하되, 남은 공간도 유연하게 사용할 수 있습니다.
  • Expanded는 가능한 모든 남은 공간을 자식에게 채워주도록 만듭니다. 즉, Flexible은 더 세밀한 제어를 제공한다고 볼 수 있습니다.

Flexible의 주요 속성:

  • fit: FlexFit.tight 또는 FlexFit.loose를 사용해 자식 위젯이 남은 공간을 어떻게 차지할지 결정합니다.
    • FlexFit.tight: 가능한 모든 남은 공간을 채웁니다 (즉, Expanded와 동일).
    • FlexFit.loose: 자식이 필요한 만큼만 공간을 차지하게 하며, 남은 공간을 채우지 않습니다.

글쓰기 너무 밑에 있다
notion image
Flexible 사용하기
notion image
 
 

8.

💡
Flutter에서 화면 추적을 위해 Navigator에 접근하는 방법 중 하나는 GlobalKey를 사용하는 것입니다. 이를 통해 네비게이션 상태를 추적하고, 화면 전환을 제어할 수 있습니다. 특히, GlobalKey<NavigatorState>를 사용하면 어디서든지 네비게이션을 관리할 수 있게 됩니다

1. GlobalKey를 사용한 네비게이션키 설정

먼저, GlobalKey<NavigatorState>를 생성하여 네비게이션 상태를 추적할 수 있도록 해야 합니다.

코드 예제:

notion image
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

Uni