이제 파이어베이스 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 에뮬레이터로 디버그 실행하면 다음과같이 실행됩니다.
플러터 커뮤니티 앱