이번 시간에는 firebase 데이터베이스와 연동하여 간단한 무서운이야기 어플을 만들도록 하겠습니다.
오늘 배워서 완성할 앱은 다음과 같습니다.
이 프로젝트를 만들기 위해선 파이어베이스에 프로젝트를 생성하고 해당 프로젝트를 플러터 프로젝트와 연동부터 해야합니다.
파이어베이스-플러터 연동방법은 ''Flutter&Firebase 커뮤니티 앱개발]"을 참조해주시기 바랍니다. (아래 링크)
https://eunbox8292.tistory.com/entry/FlutterFirebase-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EC%95%B1%EA%B0%9C%EB%B0%9C-02-firebasecli-%EC%84%A4%EC%B9%98
https://eunbox8292.tistory.com/entry/FlutterFirebase-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EC%95%B1%EA%B0%9C%EB%B0%9C-03-flutter-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%99%80-%EC%97%B0%EB%8F%99%ED%95%9C-firebase-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0
그럼 지금부터 만들어 봅시다.
main코드
코드폴더 구조는 위와 같습니다.
main.dart 코드입니다
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:horrorstory/firebase/firebase_alllistview.dart';
import 'firebase_options.dart';
import 'package:get/get.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const GetMaterialApp(
home: ReadListView(),
));
}
메인함수에서 ReadListView()페이지를 홈페이지로 호출합니다.
무서운이야기 리스트 홈 페이지
ReadListView는 firebase/firebase_alllistview.dart페이지 안에 있는 클래스입니다.
firebase_alllistview.dart코드를 보겠습니다.
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:horrorstory/firebase/firebase_readstory_content.dart';
class ReadListView extends StatefulWidget {
const ReadListView({super.key});
@override
State<ReadListView> createState() => _ReadListViewState();
}
class _ReadListViewState extends State<ReadListView> {
final _userStream =
FirebaseFirestore.instance.collection('horrorstories').snapshots();
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.black,
),
child: Scaffold(
backgroundColor: Colors.transparent, // 배경색을 투명으로 설정
appBar: AppBar(
backgroundColor: Colors.black38,
centerTitle: true,
title: const Text(
"무서운 이야기 전체글",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
),
actions: const <Widget>[],
),
body: StreamBuilder(
stream: _userStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Text("데이터베이스 연동 오류. 다시시도해주세요");
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("로딩중..");
}
var datadocs = snapshot.data!.docs;
return ListView.builder(
itemCount: datadocs.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListTile(
shape: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
tileColor: const Color.fromARGB(100, 0, 0, 0),
title: Text(
'${datadocs[index]["title"]}',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold),
),
onTap: () async {
await Get.to(() => ReadStoryContent(
documentId: datadocs[index].id,
storytitle: datadocs[index]["title"],
storycontent: datadocs[index]['content']));
},
),
);
});
},
),
),
);
}
}
위는 홈페이지의 전체 코드입니다.
위 전체 코드중 화면을 구성하는 body: 내에 핵심이 되는 listview.builder 부분을 보겠습니다.
return ListView.builder(
itemCount: datadocs.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListTile(
shape: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
tileColor: const Color.fromARGB(100, 0, 0, 0),
title: Text(
'${datadocs[index]["title"]}',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold),
),
onTap: () async {
await Get.to(() => ReadStoryContent(
documentId: datadocs[index].id,
storytitle: datadocs[index]["title"],
storycontent: datadocs[index]['content']));
},
),
);
});
위와같이 구글 파이어베이스 데이터스토어에 저장되있는 무서운이야기의 제목을 리스트형태로 쭉 나열합니다.
전체 리스트 갯수인 itemCount는 datadocs의 전체 length이며, ListTile을 이용해 datadocs의 구성 요소들(각 이야기들의 제목="title")을 텍스트위젯 형태로 index별로 쭉 나열하고있는 모습입니다.
해당 텍스트를 클릭할 시, onTap: 함수 안에서 ReadStoryContent라는 상세글 보기 페이지로 이동하게됩니다.
그렇다면 리스트뷰에서 나열되는 datadocs는 어디서 어떻게 가져온 내용들일까요?
final _userStream =
FirebaseFirestore.instance.collection('horrorstories').snapshots();
body: StreamBuilder(
stream: _userStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Text("데이터베이스 연동 오류. 다시시도해주세요");
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("로딩중..");
}
var datadocs = snapshot.data!.docs;
위와같이 body: 에서는 StreamBuilder를 이용하여 실시간으로 _userStream이라는 변수를 가져와 builder: 에서 snapshot으로 변환하고, 그 snapshot의 모든 데이터들을 datadocs라는 변수에 담습니다.
_userStream이라는 변수는 빌드위젯 밖에서 정의했던 변수이며 해당 변수는 firebasefirestore로부터 "horrostories"라는 디비컬렉션에 있는 내용들을 가져오는 변수입니다.
해당 "horrorstories" 디비컬렉션은 다음과같이 구성되 있습니다. (구글 파이어베이스 콘솔)
위와같이 "horrorstories"라는 이름의 콜렉션이 있고 해당 콜렉션엔 무서운 이야기 문서들이 하나씩 들어있습니다.
한개의 이야기 문서에는 제목과 문서id 및 내용이라는 필드들로 구성되어 있습니다.
해당 리스트뷰 홈페이지 화면은 다음과 같습니다.
(배경은 Colors.black으로, 글씨는 Colors.white로 설정해서 검은 배경에 흰글씨가 보이는 모습)
무서운이야기 한 글 상세페이지
무서운 이야기의 제목들만 나열되어있는 무서운이야기 제목 리스트페이지에서 읽고싶은 제목을 클릭하면 해당 글의 전체 내용을 볼 수 있어야 하겠죠?
지금부터 제목 클릭 시 조회되는 글 상세페이지 코드를 살펴보겠습니다.
글 상세페이지 firebase_readstory_content.dart 전체 코드입니다.
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
class ReadStoryContent extends StatefulWidget {
const ReadStoryContent(
{super.key,
required this.documentId,
required this.storytitle,
required this.storycontent});
final String documentId;
final String storytitle;
final String storycontent;
@override
_ReadStoryContentState createState() => _ReadStoryContentState();
}
class _ReadStoryContentState extends State<ReadStoryContent> {
bool isloading = true;
@override
void initState() {
super.initState();
isloading = false;
}
@override
Widget build(BuildContext context) {
if (isloading) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
));
} else {
return Container(
decoration: const BoxDecoration(
color: Colors.black,
),
child: Scaffold(
backgroundColor: Colors.transparent, // 배경색을 투명으로 설정
appBar: AppBar(
leading: BackButton(
color: Colors.white,
onPressed: () {
//추가함
Navigator.pop(context);
},
),
backgroundColor: Colors.black,
centerTitle: true,
title: Text(
widget.storytitle,
style: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.white),
)),
body: Padding(
padding: const EdgeInsets.all(30.0),
child: SingleChildScrollView(
scrollDirection: Axis.vertical, //.horizontal
child: Center(
child: Column(
children: [
Text(
widget.storycontent.replaceAll('\\n', '\n'),
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
),
],
),
)),
)
/// this is how you can use document id
),
);
}
}
}
먼저, 전체글 리스트 홈 페이지에서 읽고자 하는 글을 "클릭"했을 때 동작하는 함수에 대해 언급한적이 있습니다.
onTap: () async {
await Get.to(() => ReadStoryContent(
documentId: datadocs[index].id,
storytitle: datadocs[index]["title"],
storycontent: datadocs[index]['content']));
},
위와같이 firebase_readstory_content.dart 코드의 ReadStoryContent 클래스로 이동시키는데요.
이동시 documentId라는 파라미터에는 파이어베이스에서 가져온 해당 문서의 id와, storytitle이라는 파라미터에는 글의 title필드가, 마지막으로 storycontent라는 파라미터에는 글의 content필드가 담겨져 전달되게 됩니다.
(title과 content필드는 firebase의 datastore의 horrorstories콜렉션에 있던 개별 문서에 담긴 필드명인거 기억하시죠?)
전달된 documentId , storytitle , storycontent 들은 아래와같이
child: Scaffold(
backgroundColor: Colors.transparent, // 배경색을 투명으로 설정
appBar: AppBar(
leading: BackButton(
color: Colors.white,
onPressed: () {
//추가함
Navigator.pop(context);
},
),
backgroundColor: Colors.black,
centerTitle: true,
title: Text(
widget.storytitle,
style: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.white),
)),
body: Padding(
padding: const EdgeInsets.all(30.0),
child: SingleChildScrollView(
scrollDirection: Axis.vertical, //.horizontal
child: Center(
child: Column(
children: [
Text(
widget.storycontent.replaceAll('\\n', '\n'),
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
),
],
),
)),
)
/// this is how you can use document id
),
한 글 상세페이지의 빌드 위젯안에 widget.storytitle과 widget.storycontent로 쓰이는데요
앱바의 타이틀로는 넘겨진 storytitle변수가 써지고 (글의 제목)
body: 에서 storycontent들이 줄글로 표현되게 됩니다. (글의 내용)
아래와 같이 표시됩니다. (배경은 Colors.black으로, 글씨는 Colors.white로 설정해서 검은 배경에 흰글씨가 보이는 모습)
완성
최종 동작 화면입니다.