목표
Firebase의 Storage 등록법을 알아보고 이를 활용해 todolist를 만든다. 이를통해, Authentication와 Storage를 모두 사용한 앱을 만드는 방법을 구상하여본다.
목표 완료 후 구현 가능한 어플의 모습
해당 문구들은 내가 너무 좋아하는 'Papa Swolio'의 Go to FXXX gym에 영감을 받아 적었다. :)
오늘은 Authentication을 이전 과정에서 공부를 하였으니 이제 어떻게 하면 Firebase내 Storage를 사용해서 나의 정보를 저장을 하고 불러올 수 있는지 등을 공부할 예정이다. 이를 할 수 있기 위해서는 다음의 과정을 통해서 Firebase내 Firestore를 등록해야한다.
자 그럼 시작.
다운로드 가능 github
https://github.com/riris01/firebase_todo
GitHub - riris01/firebase_todo
Contribute to riris01/firebase_todo development by creating an account on GitHub.
github.com
1. Firestore를 클릭한다.
2. 데이터베이스를 클릭한다.
3. 데이터 베이스를 클릭하기를 나오면 이러한 팝업창이 뜰텐데 여기서 테스트 모드에서 시작하기를 누른다. 사실 단순 용량 저장이라던지 간단한 내용 읽기는 이러한 '테스트 모드'로도 충분하다.
4. 그 다음에는 다음과 같은 데이터베이스 만들기 탭으로 넘어가게 되는데 여기서는 기본으로 설정되어있는 미국 서버로 지정을 해두자. 뭐 다른 서버를 선택해도 상관은없다.
5. 그럼 이렇게 최종적으로 해당 프로젝트를 위한 Cloud Firestore페이지가 생성이 된다. 이제 그럼 여기서 데이터를 입력할 수 있는 일종의 문서 파일 그리고 문서 '집'같은 개념의 것들을 만들어야 한다. 자 예를 들어보자.
학교라던지 회사에서 보면 서류함이라는 것이 있다. 그럼 그 안에는
대분류: (영업)서류함 혹은 (수출입)서류함
중분류: 파일집 (예를들어, 미국 - 영업 파일) 등의 묶음 파일이 있을것이다.
소분류 (혹은 내용): 영업 내용 (대화목록 혹은 요청 목록)
등이 이렇게 포함되어 있을 것이다. Cloud Firestore도 같은 방식으로 구성이 되어있다.
(추가) 아, 그 전에 일단은 우리가 만든 프로젝트 안의 pubspec.yaml파일안에 dependencies를 추가해주자. 혹시 이것들이 무엇인지 모르시는 분들은 'Firebase-login 구현하기'에 설명을 적어 놓았으니 확인해두자 (링크 걸어두었다).
cloud_firestore: ^4.5.3
firebase_core: ^2.10.0
자 그럼 계속해서 '대분류'인 todo를 만들어준다.
그다음에는 문서 ID를 만들어주는데, 다음의 윈도우에서 보이지 않지만 '자동'이라고 하는 버튼이 있을테니 클릭하면 이렇게 자동으로 문서 ID를 생성해준다. 그럼 이렇게 난해한 랜덤 숫자와 알파벳 조합이 나오는데, 이걸로 그냥 사용하면 된다. 이게바로 '중분류'이다.
자, 그럼 소분류 안에 어떤 내용이 있는지 적어줘야겠다. 보면 소분류(내용)에 제목은 title로 지정을 해두어서 값을 string으로 반환하도록 만들어 두었고, 다른 필드는 isDone으로 명명해두어 Yes/No를 판단하는 boolean의 값을 반환하도록 하였다. (기본 설정은 일단 false)로 해두었다. 우리가 하는 일들은 보통 false로 '일을 아직 하지 않은 상태로 저장을 하는 거니까'.
그럼 이렇게 대분류 - 중분류 - 소분류의 형태로 자동으로 생성이 된다. 축하한다. 만약 이렇게 해보는게 처음이라면 Google Firestore에 당신의 첫 데이터를 입력한 것이다 (물론, 카테고리를 직접 만들면서 만든 데이터로는 처음으로...?).
그럼 이제 화면을 구성하고 우리가 어떤 데이터를 받아올지 생각을 해보자.
1. 파일구성
main.dart
2. 플러그인 임포트
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
3. Firebase의 Initialization()
Firebase를 Initalize하기 위해서는 main()에 Firebase의 initalization을 앱 실행시 진행해준다는 일종의 선언문을 적어야 한다. widgetFlutterBiding, 일단 이 작업은 비동기 작업이므로 main 메소드에서 비동기 작업을 진행한 후 runApp()을 실행,을 적어두어 flutter engine과 상호작용을 할 수 있도록 해준다. 그리고 Firebase.initializeApp()을 통해 initialization이 필요한 앱임을 알려준다.
Future<void> main() async {
//Initialize Firebase
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
4. Todo 클래스 생성
Firestore에서 선언했던 대분류-중분류-소분류를 불러오기 위한 클래스를 선언한다.
class Todo {
String title;
bool isDone;
Todo(this.title, {this.isDone = false});
}
5. 기능들의 생성
- 위젯 생성 _buildItem
- Todo 리스트를 렌더링 하는데 사용이 된다. DocumentSnapshot 인스턴스를 인자로 받아 Todo 객체를 만들고 이를 ListTile로 반환을 한다. Todo 객체들은 snapshot에서 추출한 title과 isDone을 사용하여 생성이 된다. isDone이 true이라면 Todo항목의 글꼴 크기를 10으로 정하도록 하였다. 그리고 trailing이라는 것이 있는데 삭제 아이콘을 포함하는 IconButton 위젯을 구성하기 위해서 넣었다. onTap은 해당 항목을 탭을 하였을때 _toggleTodo() 메서드를 호출하여 snapshot에 연결하기 위함이다. 이와 마찬가지로 삭제 아이콘을 누르면 _deleteTodo가 호출이 된다.
Widget _buildItem(DocumentSnapshot snapshot) {
final todo = Todo(snapshot['title'], isDone: snapshot['isDone']);
return ListTile(
title: Text(
todo.title,
style: todo.isDone ? TextStyle(fontSize: 10) : null,
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteTodo(snapshot),
),
onTap: () => _toggleTodo(snapshot),
);
}
- 메서드의 생성
- _deleteTodo
- 삭제를 할 수 있는 기능을 호출한다. 이 버튼을 누르면 Firebase의 Firestore에 있는 '소분류'의 내용이 삭제가 된다.
- _toggleTodo
- todo가 완료되었을때 해당항목을 삭제를 할 수도 있겠지만 단순하게 눌러서 '상태변경 (예를 들자면 글자가 작아진다거나 색이 바뀐다거나'를 할 수 있을 것이다. 그렇다면 '상태가 변경이 되었다'라는 것을 Firebase측에 알려야할 것이다. 이 기능이 바로 _toggleTodo이다. 거창하게 말했지만 뭐... 특별한게 없다. 원래 False로 우리가 각 메시지의 상태를 저장했는 것을 기억하는가? 이 기능을 사용하면 글자의 크기가 10으로 작아지면서 isDone이 true가 되고 글씨는 작아지게 된다.
- _deleteTodo
메서드를 하나 더 추가해야한다. Add your day를 하려면? 그렇다 Add 기능이 필요하다.
- _addTodo
- 해당 기능은 말그대로 add기능이다. Firestore에 들어가서 일일이 내용을 다시 적을 필요가 없이, 앱 내에서 내용을 적으면 된다.
void _addTodo(Todo todo) {
setState(() {
FirebaseFirestore.instance
.collection('todo')
.add({'title': todo.title, 'isDone': todo.isDone});
});
}
6. 화면 구성 (UI)
기능들은 모두 넣었으니 이제 화면을 구성해보자.
- MaterialApp()
- Scaffold()
를 각각 만든다.
Scaffold()내에는 이제 Text Editing 기능을 넣을 것이니, TextEditingController()를 넣어준다.
그 다음은 우리가 일반적으로 내용을 구성하는 형태로 column과 row등을 섞어 내용이 들어갈 부분을 적고 난다음에, 이제 우리가 지금껏 넣었던 기능들과 내용 반환 등을 할 수 있는 Streambuilder 위젯을 사용한다.
Streambuilder를 사용함으로써 Firestore 데이터베이스에서 Todo 리스트를 가져와서 UI를 렌더링 하는데 사용할 수 있도록 한다. 즉, snapshots() 스트림을 구독하여 Todo 리스트에 대한 업데이트를 수신한다. builder는 스트림에서 새로운 데이터가 제공될때마다 호출이 되며, snapshot.hasData가 false이면 진행중인 작업을 나타나내는 CircularProgressIndicator() 위젯을 반환하도록 하였다 (아니면 No data is available이라는 문구를 반환하도록 설정해도 된다.) 만약 Data가 존재한다면 documents 변수를 사용해서 Listview 위젯을 빌드하도록 한다.
Listview의 children 프로퍼티는 documents 리스트에서 _buildItem(doc) 메서드를 호출한 결과를 반환하도록 한다. 그리고 Expanded는 Listview를 확장하여 StreamBuilder 위젯이 차지하는 영역을 최대화한다. 이게 없다면 아마 오류가 발생할 것이다.
즉, 이 부분은 Firebase Firestore와 StreamBuilder를 사용하여 데이터베이스에서 Todo 리스트를 동적으로 렌더링 하도록 만들었다.
StreamBuilder(
stream: FirebaseFirestore.instance.collection('todo').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
final documents = snapshot.data?.docs;
return Expanded(
child: ListView(
children: documents!.map((doc) => _buildItem(doc)).toList(),
),
);
},
),
방금 설명한것을 모두 합치면 이렇게 UI를 그린다.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'todo',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
TextEditingController _todoController = TextEditingController();
@override
void initState() {
super.initState();
}
@override
void dispose() {
_todoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('What is your next step?'),
),
body: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _todoController,
),
),
ElevatedButton(
onPressed: () => _addTodo(Todo(_todoController.text)),
child: Text('Add your day'),
),
],
),
StreamBuilder(
stream: FirebaseFirestore.instance.collection('todo').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
final documents = snapshot.data?.docs;
return Expanded(
child: ListView(
children: documents!.map((doc) => _buildItem(doc)).toList(),
),
);
},
),
],
),
);
}
그리고 이렇게 하면 전체 코드가 만들어진다.
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
Future<void> main() async {
//Initialize Firebase
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
//firebase에 입력해둔 값을 class로 선언한다.
class Todo {
String title;
bool isDone;
Todo(this.title, {this.isDone = false});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'todo',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
TextEditingController _todoController = TextEditingController();
@override
void initState() {
super.initState();
}
@override
void dispose() {
_todoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('What is your next step?'),
),
body: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _todoController,
),
),
ElevatedButton(
onPressed: () => _addTodo(Todo(_todoController.text)),
child: Text('Add your day'),
),
],
),
StreamBuilder(
stream: FirebaseFirestore.instance.collection('todo').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
final documents = snapshot.data?.docs;
return Expanded(
child: ListView(
children: documents!.map((doc) => _buildItem(doc)).toList(),
),
);
},
),
],
),
);
}
Widget _buildItem(DocumentSnapshot snapshot) {
final todo = Todo(snapshot['title'], isDone: snapshot['isDone']);
return ListTile(
title: Text(
todo.title,
style: todo.isDone ? TextStyle(fontSize: 10) : null,
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteTodo(snapshot),
),
onTap: () => _toggleTodo(snapshot),
);
}
void _addTodo(Todo todo) {
setState(() {
FirebaseFirestore.instance
.collection('todo')
.add({'title': todo.title, 'isDone': todo.isDone});
});
}
void _deleteTodo(DocumentSnapshot snapshot) {
setState(() {
FirebaseFirestore.instance.collection('todo').doc(snapshot.id).delete();
});
}
void _toggleTodo(DocumentSnapshot snapshot) {
FirebaseFirestore.instance
.collection('todo')
.doc(snapshot.id)
.update({'isDone': !snapshot['isDone']});
}
}
인증을 공부하는 것보다는 훨씬 코드가 짧다. 역시 인증관련 내용이 가장 어렵다고 하더니 정말이다.
다음 앱은 뭘만들어 볼까?
1. Tic Tac Toe
2. Reminder App (Sticky Note)
3. Quiz App
4. Note App (Google Keep style)
5. Stopwatch App
6. Calculator App
참조
1. ICODEBROKER(https://icodebroker.tistory.com/10122)
2. cloud_firestore (https://pub.dev/packages/cloud_firestore)
3. fire_basecore (https://pub.dev/packages/firebase_core)
'Flutter' 카테고리의 다른 글
[이론] MVC, MVP 패턴 + MVVM 패턴과 비교 (0) | 2023.05.02 |
---|---|
[이론] MVVM 패턴 (0) | 2023.05.02 |
[Android]Firebase-Signup (0) | 2023.04.27 |
[Android]Firebase-Logout 구현 + 로그인한 사람 표시 (0) | 2023.04.26 |
[Android] Firebase-Login 구현하기 (0) | 2023.04.25 |