티스토리 뷰

반응형

이제 파이어베이스 DB를 이용하는 플러터의 커뮤니티 앱을 만들어보겠습니다.

우선 최종 결과물인 [플러터 커뮤니티 앱] 실행모습 먼저 보여드리겠습니다.

플러터 커뮤니티 앱

 

위와 같이 동작하는 여러 유저들이 게시글을 쓰고 소통하는 간단한 커뮤니티 앱을 지금부터 만들어보도록 하겠습니다.

 

 

먼저 파이어베이스에서 DB기능을 담당하는 Firestore Database의 콜렉션을 생성하고 규칙을 변경해주세요.

 

1. Firestore Database 생성 - 파이어베이스 콘솔에서

 

생성한 파이어베이스 프로젝트로 이동 후 왼쪽 창에서 빌드 탭을 선택하고 Firesotre Database를 클릭합니다

 

데이터베이스 만들기를 클릭합니다

 

"테스트 모드에서 시작"을 선택하고 다음을 클릭합니다(figure8 참조).

지금은 사용하는데 문제가 없지만 추후에 allow read, write: if의 코드를 수정할 필요가 있습니다.

일단 이대로 사용을 하겠습니다.

 

Cloud Firestore 위치는 연동되는 앱이 실행될 국가의 도시를 선택합니다.

다른 나라의 도시를 선택해도 동작에는 상관없으나 속도가 느려질 수 있습니다.

 

데이터베이스를 사용할 준비가 끝났습니다.

이후에 앱에서 넣는 데이터를 여기서 확인할 수 있습니다

 

 

이렇게 만들어진 firestore database의 규칙 탭을 클릭해 다음과같이 규칙을 수정해줍니다.

 

 

 

2. 플러터 코드 작성 (커뮤니티 앱 만들기)

 

1) firebase initialize

 

main.dart의 main함수

 

우선 메인함수에서 내 Firebase를 initialize했을때 잘 빌드가 되는지부터 확인해야 합니다.

 

제 경우 파이어베이스에 등록한 패키지이름과 flutter 프로젝트 안에 등록된 패키지명이 일치하지 않아 다음과같은 에러가 발생하였습니다.

빌드 에러메세지

 

com.example.flutterfirebase_test1 이 패키지명은 플러터 프로젝트 생성 시 해당 프로젝트 폴더명으로 자동으로 생성됬던 이름입니다. 파이어베이스 콘솔로 이동하여 파이어베이스 프로젝트에 등록된 내 앱의 패키지명을 확인합니다.

실제 내 앱 패키지이름

 

이제 다음과같이 플러터 프로젝트 안에 있는 모든 com.example.flutterfirebase_test1 이 부분을 전부 파이어베이스 콘솔에 등록되있는 패키지명(com.bbangsang.flutterfirebase1)으로 바꿔줍니다.

패키지명 수정

 

 

패키지명 수정 후 빌드시 또다시 에러가 났는데 minSdk문제로 나는 에러입니다.

minSdk 버젼 수정

 

해당 플러터프로젝트 루트디렉토리에서 android > app > build.gradle파일에 있는 defaultConfig > minSdk변수를 23으로 바꿔주고 난 뒤 빌드를하니 실행이 되었습니다. --> firebase initialize 성공

 

2) 전체 코드작성

 

우선 해당 프로젝트의 코드폴더 구성은 다음과 같습니다.

프로젝트 폴더구성

 

플러터 프로젝트를 처음 생성하면 기본으로 생기는 main.dart파일과 전시간의 파이어베이스 연동으로 인해 생겨져있는 firebase_options.dart를 제외하고 커뮤니티 앱을 위해 작성해줘야하는 코드파일은 postlist_page.dart, viewpost_page.dart, postAddUpdateDelete_page.dart, firebase_database.dart 총 4가지입니다. 이제 각각의 코드들을 작성해보겠습니다.

 

먼저 firestore database에 있는 데이터들을 가져오기, 추가, 수정, 삭제 기능들을 담당하는 firebase_database.dart 코드는 다음과 같습니다.

 

[firebase_database.dart]

import 'package:cloud_firestore/cloud_firestore.dart';

class Database {
  Database() {
    setupcollectionref();
  }

  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  CollectionReference? _postscollection;

  //_postscollection변수에 내 파이어베이스 파이어스토어DB의 posts콜렉션 할당
  void setupcollectionref() {
    _postscollection = FirebaseFirestore.instance.collection('posts');
  }

  //CREATE: 데이터베이스에 새 post를 추가합니다
  Future<void> addPosts(
      String posttitle, String postcontent, String writer, String password) {
    return _postscollection!.add({
      'posttitle': posttitle,
      'postcontent': postcontent,
      'writer': writer,
      'timestamp': Timestamp.now(),
      'password': password
    });
  }

  //READ: 데이터베이스로부터 post들을 가져옵니다
  Stream<QuerySnapshot> getPostsStream() {
    final postsStream =
        _postscollection!.orderBy('timestamp', descending: true).snapshots();

    return postsStream;
  }

  //UPDATE: 데이터베이스에 있는 기존 post 내용을 수정합니다
  Future<void> updatePost(
      String docID, String newposttitle, String newpostcontent) {
    return _postscollection!.doc(docID).update({
      'posttitle': newposttitle,
      'postcontent': newpostcontent,
      'timestamp': Timestamp.now(),
    });
  }

  //DELETE: 데이터베이스에 있는 특정 post를 삭제합니다
  Future<void> deletePost(String docID) {
    return _postscollection!.doc(docID).delete();
  }
}

 

위 firebase_database.dart코드에 있는 함수들은 다른 _page.dart코드 안에서 전부 쓰입니다.

 

 

 

다음으로 처음 앱을 시작하면 실행되는 main.dart코드를 수정해주겠습니다.

 

 

[main.dart]

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutterfirebase_test1/firebase_options.dart';
import 'package:flutterfirebase_test1/postlist_page.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    name: 'flutterfirebasetest1',
    options: DefaultFirebaseOptions.currentPlatform,
  );

  //PostslistPage페이지로 바로 이동 (모든 게시글 리스트를 보여주는 페이지)
  runApp(const GetMaterialApp(
    home: PostslistPage(),
  ));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: '커뮤니티',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const PostslistPage(),
    );
  }
}

메인함수에서 데이터베이스에 들어있는 모든 글들을 가져와 리스트형태로 보여주는 PostslistPage 로 리디렉션합니다.

PostslistPage()가 있는 postlist_page.dart코드를 살펴보겠습니다.

 

 

[postlist_page.dart]

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutterfirebase_test1/firebase_database.dart';
import 'package:flutterfirebase_test1/postAddUpdateDelete_page.dart';
import 'package:flutterfirebase_test1/viewpost_page.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:intl/intl.dart';

// 모든 게시글 리스트를 보여주는 페이지
class PostslistPage extends StatefulWidget {
  const PostslistPage({super.key});

  @override
  State<PostslistPage> createState() => _PostslistPage();
}

class _PostslistPage extends State<PostslistPage> {
  bool isloading = true; //초기화 함수가 끝났을떄 isloading이 false가 됨
  final Database _database = Database(); //파이어베이스 DB 클래스를 객체로 가져옴

  Future<void> _initasyncfunc() async {
    setState(() {
      isloading = false; //초기화 함수에서 isloading false로 해줌
    });
  }

  // 페이지 첫 입장 시 제일먼저 실행되는 함수 - 주로 변수들 초기화를 해줌
  @override
  void initState() {
    super.initState();
    _initasyncfunc();
  }

  @override
  Widget build(BuildContext context) {
    if (isloading) {
      //로딩중일때: 아직 변수들이 초기화 안됬으므로 로딩창 띄움
      return const Scaffold(
          body: Center(
        child: CircularProgressIndicator(),
      ));
    } else {
      //로딩이 끝났을 때 화면에 표시할 것들
      return Scaffold(
        //AppBar부분: 상단 타이틀 및 메뉴바(필요하다면) 표시
        appBar: AppBar(
          backgroundColor: Colors.blue,
          centerTitle: true,
          title: const Text(
            "게시글 리스트",
            style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
          ),
          //actions는 앱바의 우측부분에 넣는 메뉴들
          actions: [
            //글 추가버튼
            IconButton(
                onPressed: () async {
                  Get.to(() => const PostAddUpdateDeletePage(
                        docID: "",
                        posttitle: "",
                        postcontent: "",
                        addtrue_updatefalse: true,
                      ));
                },
                icon: const Icon(
                  Icons.add,
                  color: Colors.white,
                ))
          ],
        ),

        //Body부분: 콘텐츠 표시
        body: StreamBuilder<QuerySnapshot>(
          stream: _database
              .getPostsStream(), //firebase데이터베이스에서 모튼 post리스트들 가져오기(실시간으로(stream))
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              //데이터베이스로부터 결과를 기다리는 중일때(로딩바 표시)
              return const Center(
                child: CircularProgressIndicator(),
              );
            } else {
              //데이터베이스로부터 결과를 가져왔을때

              //만약 post디비에 데이터가 존재하지 않는다면(데이터 없음 문구 표시)
              if (!snapshot.hasData) {
                return const Center(
                  child: Text("게시글이 없습니다"),
                );
              } else //post디비에 데이터가 한 개 이상 존재한다면
              {
                List postsList = snapshot.data!.docs;

                //데이터들의 각 값들을 list형태로 표시하기
                return ListView.builder(
                    itemCount: postsList.length,
                    itemBuilder: (context, index) {
                      //post컬렉션에있는 모든 post들을 가져오기(단위: document)
                      DocumentSnapshot document = postsList[index];
                      String docID = document.id;

                      //가져온 post별로 모든 컬럼값들 표시해주기
                      Map<String, dynamic> data =
                          document.data() as Map<String, dynamic>;

                      String posttitle = data['posttitle'];
                      String postcontent = data['postcontent'];
                      String writer = data['writer'];

                      Timestamp timestamp = data['timestamp'];
                      DateTime timestampdt = timestamp.toDate();
                      String stringformatedtime =
                          DateFormat('HH:mm').format(timestampdt);
                      //ㄴ참고: DateFormat 쓰려면 터미널에 flutter pub add intl 쳐서 패키지 설치해야함
                      //ㄴ설치 후 import 'package:intl/intl.dart';
                      String writerNdatetime = "$writer | $stringformatedtime";
                      String password = data['password'];

                      //각 컬럼값들을 ListTile에 넣어서 표시해주기
                      return ListTile(
                        title: Text(posttitle),
                        subtitle: Text(writerNdatetime),
                        onTap: () => {
                          //클릭(또는 터치) 시에 해당 포스트 자세히 보기 페이지로 이동
                          //ㄴ(이동 시에 인자로 DB에서 가져온 해당 포스트의 docID, posttitle, postcontent, writer,stringformatedtime, password 들을 넘겨줘야함)
                          Get.to(() => ViewpostPage(
                              docID: docID,
                              posttitle: posttitle,
                              postcontent: postcontent,
                              writer: writer,
                              datetimestr: stringformatedtime,
                              password: password))
                          //ㄴ참고: Get 패키지 없을 시에 flutter pub add get로 패키지 설치해야함
                          //ㄴ 설치 후 import 'package:get/get.dart';
                          //ㄴ 설치 후2 import 'package:get/get_core/src/get_main.dart';
                        },
                      );
                    });
              }
            }
          },
        ),
      );
    }
  }
}

위 포스트리스트 페이지에서 특정한 글을 클릭했을때 해당 글의 내용을 자세히 볼 수 있는 페이지인 ViewpostPage 로 이동합니다.

ViewpostsPage()가 있는 viewpost_page.dart코드를 살펴보겠습니다.

 

 

[viewpost_page.dart]

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutterfirebase_test1/firebase_database.dart';
import 'package:flutterfirebase_test1/postAddUpdateDelete_page.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';

//특정 게시글들의 내용을 자세히 보는 페이지
class ViewpostPage extends StatefulWidget {
  //이 페이지로 들어올때 받는 인자들
  final String docID;
  final String posttitle;
  final String postcontent;
  final String writer;
  final String datetimestr;
  final String password;
  const ViewpostPage(
      {super.key,
      required this.docID, //이 글의 파이어베이스DB에 등록된 다큐멘트ID
      required this.posttitle, //이 글의 파이어베이스DB에 등록된 글제목
      required this.postcontent, //이 글의 파이어베이스DB에 등록된 글내용
      required this.writer, //이 글의 파이어베이스DB에 등록된 작성자명
      required this.datetimestr, //이 글의 파이어베이스DB에 등록된 작성시간
      required this.password //이 글의 파이어베이스DB에 등록된 글 비밀번호
      });

  @override
  State<ViewpostPage> createState() => _ViewpostPage();
}

class _ViewpostPage extends State<ViewpostPage> {
  bool isloading = true; //초기화 함수가 끝났을떄 isloading이 false가 됨
  final Database _database = Database(); //파이어베이스 DB 클래스를 객체로 가져옴
  TextEditingController passwordcont =
      TextEditingController(); //글 수정 및 삭제를 위해 비밀번호 입력할 입력창변수

  Future<void> _initasyncfunc() async {
    setState(() {
      isloading = false; //초기화 함수에서 isloading false로 해줌
    });
  }

  // 페이지 첫 입장 시 제일먼저 실행되는 함수 - 주로 변수들 초기화를 해줌
  @override
  void initState() {
    super.initState();
    _initasyncfunc();
  }

  @override
  Widget build(BuildContext context) {
    if (isloading) {
      //로딩중일때: 아직 변수들이 초기화 안됬으므로 로딩창 띄움
      return const Scaffold(
          body: Center(
        child: CircularProgressIndicator(),
      ));
    } else {
      //로딩이 끝났을 때 화면에 표시할 것들
      return Scaffold(
        //AppBar부분: 상단 타이틀 및 메뉴바(필요하다면) 표시
        appBar: AppBar(
          //leading은 앱바의 좌측에 넣는 부분
          leading: IconButton(
            //뒤로가기 버튼 넣기
            icon: const Icon(
              Icons.arrow_back,
              color: Colors.white,
            ),
            onPressed: () {
              Navigator.pop(context);
            },
          ),
          backgroundColor: Colors.blue,
          centerTitle: true,
          title: const Text(
            "자세히 보기",
            style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
          ),
          actions: const [],
        ),
        backgroundColor: Colors.white,
        resizeToAvoidBottomInset: true,

        //Body부분: 콘텐츠 표시
        body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: SingleChildScrollView(
            //세로로 차례로 나열되기 때문에 Column으로 묶어줌
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisSize: MainAxisSize.max,
              children: [
                //세로 여백
                const SizedBox(
                  height: 40,
                ),

                //제목 표시
                Row(
                  children: [
                    const Text(
                      "제목 ",
                      style: TextStyle(
                          fontSize: 20, fontWeight: FontWeight.normal),
                    ),
                    const SizedBox(width: 5),
                    Expanded(
                      child: Text(
                        widget
                            .posttitle, //이 페이지 들어올때 인자로 받아온 posttitle을 텍스트로 표시
                        style: const TextStyle(
                            fontSize: 20, fontWeight: FontWeight.normal),
                      ),
                    ),
                  ],
                ),
                //세로 여백
                const SizedBox(
                  height: 5,
                ),

                //작성자 표시
                Row(
                  children: [
                    const Text(
                      "작성자 ",
                      style: TextStyle(
                          fontSize: 20, fontWeight: FontWeight.normal),
                    ),
                    const SizedBox(width: 5),
                    Expanded(
                      child: Text(
                        widget.writer, //이 페이지 들어올때 인자로 받아온 writer를 텍스트로 표시
                        style: const TextStyle(
                            fontSize: 20, fontWeight: FontWeight.normal),
                      ),
                    ),
                  ],
                ),
                //세로 여백
                const SizedBox(
                  height: 5,
                ),

                //작성일시 표시
                Row(
                  children: [
                    const Text(
                      "작성일시 ",
                      style: TextStyle(
                          fontSize: 20, fontWeight: FontWeight.normal),
                    ),
                    const SizedBox(width: 5),
                    Expanded(
                      child: Text(
                        widget
                            .datetimestr, //이 페이지 들어올때 인자로 받아온 datetimestr를 텍스트로 표시
                        style: const TextStyle(
                            fontSize: 20, fontWeight: FontWeight.normal),
                      ),
                    ),
                  ],
                ),
                //세로 여백
                const SizedBox(
                  height: 20,
                ),

                //내용 표시
                const Text(
                  "내용 ",
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.normal),
                ),
                //세로 여백
                const SizedBox(
                  height: 5,
                ),
                Text(
                  widget.postcontent, //이 페이지 들어올때 인자로 받아온 postcontent를 텍스트로 표시
                  style: const TextStyle(
                      fontSize: 20, fontWeight: FontWeight.normal, height: 15),
                ),
                //세로 여백
                const SizedBox(
                  height: 30,
                ),

                //비밀번호입력란 및 수정, 삭제버튼 (이 입력창과 버튼들은 가로로 일렬로 나열되기때문에 Row로 묶어줌)
                Row(
                  children: [
                    const SizedBox(
                      width: 30,
                    ),
                    Expanded(
                      child: TextFormField(
                        controller:
                            passwordcont, //비밀번호 입력창 변수(TextEditingController)
                        decoration: InputDecoration(
                            hintText: "비밀번호입력",
                            border: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(30))),
                        inputFormatters: [
                          FilteringTextInputFormatter.deny(" "),
                          LengthLimitingTextInputFormatter(10),
                        ],
                      ),
                    ),
                    const SizedBox(
                      width: 20,
                    ),
                    SizedBox(
                      width: 80,
                      child: MaterialButton(
                          //글 수정버튼 눌렀을때
                          onPressed: () {
                            //만약 입력된 비밀번호와 이 글의 비밀번호가 일치하면 글 수정페이지로 들어가기
                            if (passwordcont.text == widget.password) {
                              //비밀번호 일치. 글 수정페이지로 이동
                              //ㄴ(이동 시에 인자로 해당 포스트의 docID, posttitle, postcontent를 넘겨줘야하며,
                              //글 수정이니까 addtrue_updatefalse 변수는 false로 넘겨줘야함)
                              Get.to(() => PostAddUpdateDeletePage(
                                    docID: widget.docID,
                                    posttitle: widget.posttitle,
                                    postcontent: widget.postcontent,
                                    addtrue_updatefalse: false,
                                  ));
                            } else {
                              //비밀번호 불일치. 불일치메시지 스낵바 띄우기
                              Get.showSnackbar(const GetSnackBar(
                                duration: Duration(seconds: 2),
                                messageText: Text(
                                  "!![실패] 글 비밀번호가 일치하지 않습니다.",
                                  style: TextStyle(
                                      fontSize: 20,
                                      color: Colors.red,
                                      height: 3),
                                ),
                              ));
                            }
                          },
                          color: Colors.blue,
                          child: const Text("수정")),
                    ),
                    const SizedBox(
                      width: 10,
                    ),
                    SizedBox(
                      width: 80,
                      child: MaterialButton(
                          //글 삭제버튼 눌렀을때
                          onPressed: () async {
                            //만약 입력된 비밀번호와 이 글의 비밀번호가 일치하면 DB에서 지우기
                            if (passwordcont.text == widget.password) {
                              //비밀번호 일치. DB에서 삭제
                              await _database.deletePost(widget.docID);
                              //삭제완료메시지 스낵바띄우기
                              Get.showSnackbar(const GetSnackBar(
                                duration: Duration(seconds: 2),
                                messageText: Text(
                                  "[성공] 글이 삭제되었습니다.",
                                  style: TextStyle(
                                      fontSize: 20,
                                      color: Colors.white,
                                      height: 3),
                                ),
                              ));

                              //2초 기다림
                              await Future.delayed(const Duration(seconds: 2));

                              //뒤로가기
                              Navigator.pop(context);
                            } else {
                              //비밀번호 불일치. 불일치메시지 스낵바 띄우기
                              Get.showSnackbar(const GetSnackBar(
                                duration: Duration(seconds: 2),
                                messageText: Text(
                                  "!![실패] 글 비밀번호가 일치하지 않습니다.",
                                  style: TextStyle(
                                      fontSize: 20,
                                      color: Colors.red,
                                      height: 3),
                                ),
                              ));
                            }
                          },
                          color: Colors.blue,
                          child: const Text("삭제")),
                    ),
                    const SizedBox(
                      width: 30,
                    ),
                  ],
                )
              ],
            ),
          ),
        ),
      );
    }
  }
}

위 뷰포스트 페이지에서 해당 글을 수정 또는 삭제할 수 있는 페이지인 동시에 처음 포스트리스트 페이지에서 새 글을 추가할 수 있는 페이지인 PostAddUpdateDeletePage 페이지로 이동하겠습니다.  

PostAddUpdateDeletePage()가 있는 postAddUpdateDelete_page.dart코드를 살펴보겠습니다.

 

[postAddUpdateDelete_page.dart]

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutterfirebase_test1/firebase_database.dart';
import 'package:flutterfirebase_test1/postlist_page.dart';
import 'package:flutterfirebase_test1/viewpost_page.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';

class PostAddUpdateDeletePage extends StatefulWidget {
  final String docID;
  final String posttitle;
  final String postcontent;
  final bool addtrue_updatefalse;

  const PostAddUpdateDeletePage({
    super.key,
    required this.docID, //(글 수정일 경우) 이 글의 파이어베이스DB에 등록된 다큐멘트ID
    required this.posttitle, //(글 수정일 경우) 이 글의 파이어베이스DB에 등록된 글제목
    required this.postcontent, //(글 수정일 경우) 이 글의 파이어베이스DB에 등록된 글내용
    required this.addtrue_updatefalse, //이 글이 수정모드인지 추가모드인지 알려주는 변수
  });

  @override
  State<PostAddUpdateDeletePage> createState() => _PostAddUpdateDeletePage();
}

class _PostAddUpdateDeletePage extends State<PostAddUpdateDeletePage> {
  bool isloading = true; //초기화 함수가 끝났을떄 isloading이 false가 됨
  final Database _database = Database(); //파이어베이스 DB 클래스를 객체로 가져옴

  TextEditingController posttitlecont =
      TextEditingController(); //글 제목을 입력하는 입력창변수
  TextEditingController postcontentcont =
      TextEditingController(); //글 내용을 입력하는 입력창변수
  TextEditingController writercont =
      TextEditingController(); //글 작성자명을 입력하는 입력창변수
  TextEditingController passwordcont =
      TextEditingController(); //글 비밀번호를 입력하는 입력창변수

  Future<void> _initasyncfunc() async {
    setState(() {
      posttitlecont.text =
          widget.posttitle; //(글 수정일 경우) 이 글의 원래 글 제목을 제목입력창변수 안에 미리 써줌
      postcontentcont.text =
          widget.postcontent; //(글 수정일 경우) 이 글의 원래 글 내용을 내용입력창변수 안에 미리 써줌
      isloading = false; //초기화 함수에서 위에 초기화내용들이 다 끝난뒤 isloading false로 해줌
    });
  }

  // 페이지 첫 입장 시 제일먼저 실행되는 함수 - 주로 변수들 초기화를 해줌
  @override
  void initState() {
    super.initState();
    _initasyncfunc();
  }

  @override
  Widget build(BuildContext context) {
    if (isloading) {
      //로딩중일때: 아직 변수들이 초기화 안됬으므로 로딩창 띄움
      return const Scaffold(
          body: Center(
        child: CircularProgressIndicator(),
      ));
    } else {
      //로딩이 끝났을 때 화면에 표시할 것들
      return Scaffold(
        //AppBar부분: 상단 타이틀 및 메뉴바(필요하다면) 표시
        appBar: AppBar(
          //leading은 앱바의 좌측에 넣는 부분
          leading: IconButton(
            //뒤로가기 버튼 넣기
            icon: const Icon(
              Icons.arrow_back,
              color: Colors.white,
            ),
            onPressed: () {
              Navigator.pop(context);
            },
          ),
          backgroundColor: Colors.blue,
          centerTitle: true,
          title: Text(
            widget.addtrue_updatefalse
                ? "글 추가"
                : "글 수정", //앱바 타이틀은 수정모드인지 추가모드인지에 따라 다르게 입력
            style: const TextStyle(
                fontWeight: FontWeight.bold, color: Colors.white),
          ),
          actions: const [],
        ),
        backgroundColor: Colors.white,
        resizeToAvoidBottomInset: true,

        //Body부분: 콘텐츠 표시
        body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: SingleChildScrollView(
            //세로로 차례로 나열되기 때문에 Column으로 묶어줌
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisSize: MainAxisSize.max,
              children: [
                //세로 여백
                const SizedBox(
                  height: 40,
                ),

                //제목 입력또는 수정란 표시
                Row(
                  children: [
                    const Text(
                      "제목 ",
                      style: TextStyle(
                          fontSize: 20, fontWeight: FontWeight.normal),
                    ),
                    const SizedBox(width: 5),
                    Expanded(
                      child: TextFormField(
                        controller: posttitlecont,
                        decoration: InputDecoration(
                            hintText: "제목 입력",
                            border: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(30))),
                        inputFormatters: [
                          //FilteringTextInputFormatter.deny(" "),
                          LengthLimitingTextInputFormatter(20),
                        ],
                        style: const TextStyle(
                            fontSize: 20.0, height: 1.0, color: Colors.black),
                      ),
                    ),
                  ],
                ),

                //세로 여백
                const SizedBox(
                  height: 20,
                ),

                //내용 입력또는 수정란 표시
                const Text(
                  "내용 ",
                  textAlign: TextAlign.left,
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.normal),
                ),
                //세로 여백
                const SizedBox(
                  height: 5,
                ),

                TextFormField(
                  controller: postcontentcont,
                  decoration: InputDecoration(
                      hintText: "내용 입력",
                      border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(30))),
                  inputFormatters: [
                    //FilteringTextInputFormatter.deny(" "),
                    LengthLimitingTextInputFormatter(200),
                  ],
                  style: const TextStyle(
                      fontSize: 20.0, height: 15.0, color: Colors.black),
                ),

                //세로 여백
                const SizedBox(
                  height: 30,
                ),

                /////////////////////////////////////////////////////////////
                //만약 첫 입력이라면(새글 추가) 작성자와 비밀번호도 입력란도 표시
                if (widget.addtrue_updatefalse)
                  //작성자
                  Row(
                    children: [
                      const Text(
                        "작성자명 ",
                        style: TextStyle(
                            fontSize: 20, fontWeight: FontWeight.normal),
                      ),
                      const SizedBox(width: 5),
                      Expanded(
                        child: TextFormField(
                          controller: writercont,
                          decoration: InputDecoration(
                              hintText: "닉네임 입력",
                              border: OutlineInputBorder(
                                  borderRadius: BorderRadius.circular(30))),
                          inputFormatters: [
                            FilteringTextInputFormatter.deny(" "),
                            LengthLimitingTextInputFormatter(10),
                          ],
                          style: const TextStyle(
                              fontSize: 20.0, height: 1.0, color: Colors.black),
                        ),
                      ),
                    ],
                  ),

                if (widget.addtrue_updatefalse)
                  //세로 여백
                  const SizedBox(
                    height: 5,
                  ),

                if (widget.addtrue_updatefalse)
                  //비밀번호
                  Row(
                    children: [
                      const Text(
                        "비밀번호 ",
                        style: TextStyle(
                            fontSize: 20, fontWeight: FontWeight.normal),
                      ),
                      //const SizedBox(width: 5),
                      Expanded(
                        child: TextFormField(
                          controller: passwordcont,
                          decoration: InputDecoration(
                              hintText: "비밀번호 입력",
                              border: OutlineInputBorder(
                                  borderRadius: BorderRadius.circular(30))),
                          inputFormatters: [
                            FilteringTextInputFormatter.deny(" "),
                            LengthLimitingTextInputFormatter(10),
                          ],
                          style: const TextStyle(
                              fontSize: 20.0, height: 1.0, color: Colors.black),
                        ),
                      ),
                    ],
                  ),
                /////////////////////////////

                //글 제출버튼
                SizedBox(
                  width: 200,
                  child: MaterialButton(
                      //글 제출버튼 눌렀을때
                      onPressed: () async {
                        //제목과 내용이 비어있지 않다면 DB에 넣기
                        if (posttitlecont.text.isNotEmpty &&
                            postcontentcont.text.isNotEmpty) {
                          if (widget.addtrue_updatefalse) {
                            //글 새로추가의 경우 - 데이터베이스에 제목,내용,작성자,비번 등록함수 호출
                            await _database.addPosts(
                                posttitlecont.text,
                                postcontentcont.text,
                                writercont.text,
                                passwordcont.text);
                            //추가완료메세지 스낵바 띄우기
                            Get.showSnackbar(const GetSnackBar(
                              duration: Duration(seconds: 2),
                              messageText: Text(
                                "[성공] 글이 신규 등록되었습니다.",
                                style: TextStyle(
                                    fontSize: 20,
                                    color: Colors.white,
                                    height: 3),
                              ),
                            ));

                            //2초 기다림
                            await Future.delayed(const Duration(seconds: 2));

                            //뒤로가기
                            Navigator.pop(context);
                          } else {
                            //글 수정의 경우 - 해당 docID의 post에 수정된 제목,내용으로 업데이트함수 호출
                            await _database.updatePost(widget.docID,
                                posttitlecont.text, postcontentcont.text);
                            //추가완료메세지 스낵바 띄우기
                            Get.showSnackbar(const GetSnackBar(
                              duration: Duration(seconds: 2),
                              messageText: Text(
                                "[성공] 글이 수정되었습니다.",
                                style: TextStyle(
                                    fontSize: 20,
                                    color: Colors.white,
                                    height: 3),
                              ),
                            ));

                            //2초 기다림
                            await Future.delayed(const Duration(seconds: 2));

                            //홈 리스트로 가기
                            Get.off(const PostslistPage());
                          }
                        } else {
                          //제목이나 내용 둘중 하나 이상이 비어있다면 에러메시지 스낵바 띄우기
                          Get.showSnackbar(const GetSnackBar(
                            duration: Duration(seconds: 2),
                            messageText: Text(
                              "!![실패] 제목이나 내용중 비어있는 항목이 있습니다.",
                              style: TextStyle(
                                  fontSize: 20, color: Colors.red, height: 3),
                            ),
                          ));
                        }
                      },
                      color: Colors.blue,
                      child: const Text("제출")),
                ),
              ],
            ),
          ),
        ),
      );
    }
  }
}

 

 

이로서 모든 코드작성이 마무리 되었습니다.

해당 프로젝트를 안드로이드 스튜디오의 Pixel 8 Pro 에뮬레이터로 디버그 실행하면 다음과같이 실행됩니다.

 

플러터 커뮤니티 앱

반응형