마지막 주차 스터디를 관통하는 것은 바로 Firebase SDK 이다.
이 Firebase는 유저인증, API, DB등 웹/앱 서비스를 만들면서 반복적으로 필요한 기능들을 한데 묶어서
서비스를 제공하는 것으로, Severlessr 개발에 활용된다.
L main.dart
L 📂screens
L auth.dart
L chat.dart
L splash.dart
L 📂 widgets
L chat_messages.dart
L new_messages.dart
L message_bubble.dart
L user_image_picker.dart
L firebase_options.dart
TextFromField 위젯을 사용하여 유저의 입력이 제약조건을 위반하면 바로바로 알려주도록 하였다.TextFormField(
decoration: const InputDecoration(
labelText: 'Email Address',
),
keyboardType: TextInputType.emailAddress,
autocorrect: false,
textCapitalization: TextCapitalization.none,
validator: (value) {
if (value == null ||
value.trim().isEmpty ||
!value.contains('@')) {
return 'Please enter a valid email address';
}
return null;
},
onSaved: (value) {
_enteredEmail = value!;
},
),
validator 옵션을 사용하여 이메일 주소를 받는 곳에는 값이 없지는 않는지, @가 없진 않은지, null 값인지 아닌지에 대한 검사를 하여, 본 옵션을 통과한 입력값들만 _enteredEmail 변수에 저장하게 된다.
ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.primaryContainer,
),
child: Text(_isLogin ? 'Login' : 'Sign Up'),
),
TextButton(
onPressed: () {
setState(() {
_isLogin = !_isLogin;
});
},
child: Text(_isLogin
? 'Create an Account'
: 'I already have an account. Login'),
),
하단에 있는 텍스트를 누르게 되면 true 로 설정된 _isLogin 변수를 false로 바꾸면서 로그인 form 에서 회원가입 form 으로 변경하게 된다.
로그인 폼과 마찬가지로 TextFormField 를 활용하였다.
ImagePicker 클래스를 활용하여 사용자가 기기의 카메라 혹은 갤러리에서 사진을 업로드 할 수 있도록 하였다.void _pickImage() async {
final pickedImage = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 50,
maxWidth: 150,
);
if (pickedImage == null) {
return;
}
setState(() {
_pickedImageFile = File(pickedImage.path);
});
widget.onPickImage(_pickedImageFile!);
}
PickImage 함수에 ImagePicker 클래스를 사용, source 에 ImageSource.camera로 설정하여 사용자가 바로 사진을 찍을 수 있도록 하였다.
(ImageSource.gallery로 설정하면 갤러리에서 사용자가 사진을 골라서 업로드 하는 형식으로 변경도 가능하다)
이후 사진 유효성 검사를 진행한후, File 클래스에 사용자가 고른 사진의 경로를 담아서 전달한다.
인증토큰을 생성한다final _firebase = FirebaseAuth.instance;
void _submit() async {
final isValid = _formKey.currentState!.validate();
if (!isValid || !_isLogin && _selectedImage == null) {
return;
}
_formKey.currentState!.save();
try {
setState(() {
_isAuthenticating = true;
});
if (_isLogin) {
final userCredentials = await _firebase.signInWithEmailAndPassword(
email: _enteredEmail, password: _enteredPassword);
// Log users in
} else {
//to create a new user by firebase
final userCredentials = await _firebase.createUserWithEmailAndPassword(
email: _enteredEmail, password: _enteredPassword);
final stroageRef = FirebaseStorage.instance
.ref()
.child('user_images')
.child('${userCredentials.user!.uid}.jpg');
await stroageRef.putFile(_selectedImage!);
final imageUrl = await stroageRef.getDownloadURL();
await FirebaseFirestore.instance
.collection('users')
.doc(userCredentials.user!.uid)
.set({
'username': _enteredUsername,
'email': _enteredEmail,
'image_url': imageUrl,
});
}
} on FirebaseAuthException catch (error) {
if (error.code == 'email-already-in-use') {
// ...
}
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error.message ?? 'Authentication Failed'),
),
);
setState(() {
_isAuthenticating = false;
});
}
}
유효성검사
일단 GloalKey 를 활용하여 하위 위젯인 TextFormField 의 값의 State에 대해 유효성 검사를 실시한다.
회원가입한 신규유저의 정보를 넘기는 것이기때문에 Login 모드가 아닌지, userImage가 없지 않은지를 검사한다.
유효성검사를 통과한 값은 GlobalKey의 하위메소드인 currentState의 save 옵션을 활용하여 저장한다.
Authenticating 시작
_isAuthenticating을 true로 바꾸어 유저 인증과정 진행을 시작,
만약 로그인 모드라면, signInWithEmailAndPassword에 입력된 값들을 넘기고,
그것이 아니라면 신규회원가입 이기 때문에 createUserWithEmailAndPassword에 입력된 값을 넘긴다.
Firebase의 편리함이 여기서 드러나는데, 위 두가지 모두 FirebaseAuth.instance 의 하위 메소드들이며,
쉽게 맞게 입력된 값을 넘기기만 하면 알아서 유저 인증 혹은 신규 유저 생성을 척척 해낸다.
2-1. 회원가입시, 입력값 저장하기
FirebaseStorage 클래스를 활용,ref 에 저장된 경로에 user_image와 각 이미지의 이름을 user 고유의 id 값으로 설정하여 저장하고
FirebaseFirestore.collection 을 사용하여 user 폴더를 데이터베이스 내에 생성하고, doc을 통해 단일 문서에 접근하여, username,email,image_url을 Firebase에 저장할수 있도록 하였다.
FirebaseFireStore 총정리
https://funncy.github.io/flutter/2021/03/06/firestore/
에러 캐치
FirebaseAuthException 을 활용하여 이미 존재하는 이메일 주소일 경우 에러처리를 할 수 있도록 설정하였다.
Snackar를 이용해서 사용자가 확인할 수 있도록 하였다.
Authenticating 종료
크게 두개의 로직으로 구분할 수 있다.
첫째는 왔던 메시지를 출력하는 ChatMessge 부분,
둘째는 메시지를 입력하는 NewMessage 부분,
셋째는 로그아웃 버튼 부분이다.
final authenticatedUser = FirebaseAuth.instance.currentUser!;
return StreamBuilder(
// listener 의 역할
stream: FirebaseFirestore.instance
.collection('chat')
.orderBy(
'createdAt',
descending: true,
)
.snapshots(),
builder: (ctx, chatSnapshots) {
if (chatSnapshots.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (!chatSnapshots.hasData || chatSnapshots.data!.docs.isEmpty) {
return const Center(
child: Text('No messages Found'),
);
}
if (chatSnapshots.hasError) {
return const Center(
child: Text('Something went wrong'),
);
}
final loadedMessages = chatSnapshots.data!.docs;
return ListView.builder(
padding: const EdgeInsets.only(
bottom: 40,
left: 13,
right: 13,
),
reverse: true,
itemCount: loadedMessages.length,
itemBuilder: (ctx, index) {
final chatMessage = loadedMessages[index].data();
final nextChatMessage = index + 1 < loadedMessages.length
? loadedMessages[index + 1].data()
: null;
final currentMessageUserId = chatMessage['userId'];
final nextMessageUserId =
nextChatMessage != null ? nextChatMessage['userId'] : null;
final nextUserIsSame = nextMessageUserId == currentMessageUserId;
if (nextUserIsSame) {
return MessageBubble.next(
message: chatMessage['text'],
isMe: authenticatedUser.uid == currentMessageUserId,
);
} else {
return MessageBubble.first(
userImage: chatMessage['userimage'],
username: chatMessage['username'],
message: chatMessage['text'],
isMe: authenticatedUser.uid == currentMessageUserId,
);
}
});
},
);
}
}
StreamBuilder 와 ListView로 두부분으로 나누어서 살펴보자
StreamBuilder 메소드는 비동기 방식으로 시간에 따라 변하는 값을 지속적으로 동기화하는 기능을 수행한다.stream 옵션에 FirebaseFirestore.instance를 설정, collection으로 chat 폴더에 접근하여 cratedAt에 담긴 시간 순서대로 마지막 메시지가 가장 아래 깔리도록 불러온다.
builder 옵션에 채팅 데이터를 불러오는 중이라면 원형 스피너를,
만약 데이터가 없으면 메시지가 없음을 출력한다.
데이터가 있다면 이를 loadedMessages 변수에 담아 ListView에서 출력하여 사용자가 볼 수 있도록 한다.
ListView는 채팅데이터가 있는경우 그 데이터를 출력하는 기능을 하는 위젯이다. void dispose() {
_messageController.dispose();
super.dispose();
}
void _submitMessage() async {
final enteredMessage = _messageController.text;
if (enteredMessage.trim().isEmpty) {
return;
}
FocusScope.of(context).unfocus();
_messageController.clear();
final user = FirebaseAuth.instance.currentUser!;
final userData = await FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.get();
FirebaseFirestore.instance.collection('chat').add({
'text': enteredMessage,
'createdAt': Timestamp.now(),
'userId': user.uid,
'username': userData.data()!['username'],
'userimage': userData.data()!['image_url'],
});
}
dispose
TextEditingController 클래스를 사용하여 입력값이 변하면 바로바로 업데이트 할 수 있도록 하였다.
(controller 의 값(입력된 채팅메시지)는 컨트롤러에 붙어있는 리스너에 의해 업데이트 된다)
다만 반드시 이 컨트롤러를 사용했을때는 dispose를 통해서 메모리의 낭비를 줄여주어야 한다.
_submitMessage
유저의 입력값을 controller 의 텍스트 값으로 넘겨준다,
또한 입력하는 사람이 누구인지를 판별하기 위해 역시나 FirebaseAuth 클래스를 활용,
userData에 접근하기위해 FirebaseFireStore클래스를 활용하여 DB내 user 데이터에 접근한다.
입력 유저 정보와 DB내 유저 정보를 활용하여 DB내 chat 폴더에 유저가 입력한 채팅메시지, 유저정보, 아이디, 유저이름, 유저사진 등을 Firebase로 넘겨주어 DB에 저장할 수 있도록 한다.
일단 Push 활용까지 가지 못했다.
MacOS 라서 xcode에서 애플 디벨로퍼 계정을 활용하여 인증을 받아야하는데 이게 Personal Team이라 그런지
넘어가질 않는다

다음주까지 에러를 해결해서 Push 활용까지 시도해보아야 겠다.