조금 매운맛 25 스트림과 파이어베이스
Stream
: 한번에 원하는 데이터를 받고 끝나는 것이 아니라 지속적으로 들어오는 데이터를 기다렸다 받아야할때 사용
(int를 예시로 사용) | 즉시 사용가능 | 기다려야 사용가능 | 예시 | 기다려야 사용가능 설명 |
단일 데이터 | int | Future<int> | int, string 등 일반 데이터 | - async 방식 - 전달되면 즉시 사용 가능 |
복수 데이터 | List<int> | Stream<int> | List | - 언제라도 int 데이터가 Stream에 전달 가능 - Stream을 subscribe(구독)하고 있다면 Stream에 데이터가 전달될때마다 즉시 알수 있음 |
> 채팅앱에서는 상대방이 언제 채팅을 칠지 모름
> 그래서 stream 사용해야 함
> StreamBuilder 위젯
: stream으로 전달되는 데이터 즉, 이벤트 구독 가능케 함
이벤트가 전달될때마다 리빌드되며 최신데이터 반영
스트림 예제
//main.dart
import 'package:flutter/material.dart';
import 'package:stream_builder/counter.dart';
void main(){
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Stream Builder',
theme: ThemeData(
primarySwatch: Colors.lightBlue
),
home: Counter(),
);
}
}
//counter.dart
import 'package:flutter/material.dart';
class Counter extends StatefulWidget {
const Counter({Key? key}) : super(key: key);
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
final int price = 2000;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Stream builder'),
),
body: StreamBuilder<int>(
initialData: price, //최초의 데이터
stream: addStreamValue(),
//snapshot = stream의 결과물. 스트림빌더에게 이 데이터(context)를 상용하라고 지정해주는 역할
builder: (context, snapshot){ //지속적으로 들어오는 데이터 결과 반영
final priceNumber = snapshot.data.toString();
return Center(
child: Text(priceNumber,
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.blue
),
),
);
},
),
);
}
Stream<int> addStreamValue(){ //1초씩 지속적으로 값 전달
return Stream<int>.periodic(
Duration(seconds: 1),
(count) => price + count
);
}
}
> StreamBuilder : 자체적으로 스트림을 통해서 들어오는 데이터를 구독하기 위한 기능
> addStreamValue 메소드로부터 새로운 데이터 들어올 때마다 데이터를 snapshot에 저장
> StreamBuilder 위젯으로 화면에 출력
채팅 앱에 Stream Data 랜더링하기 위해 Firestore Database 생성
1. 파이어베이스 콘솔 > 프로젝트
2. build - firestore database > 데이터베이스 만들기
3. 테스트 모드에서 시작 > 디폴트로 사용설정
4. 데이터베이스 생성 완료 후, 컬렉션 > ID를 chats로 설정 > 문서 ID 자동설정 > 저장
> 상위 컬렉션 chats
> 이 안의 여러 chat 문서
> chat 문서들은 다시 컬렉션을 가질 수 있는데 개별 메세지(데이터)가 들어간 문서
>> 파이어베이스는 '컬렉션>문서'의 패턴을 가짐
5. 컬렉션 생성 > 컬렉션 id > 문서 id와 필드 채워주기
> 데이터베이스에 헬로월드 저장된 것 확인
6. 데이터베이스 사용을 위한 임포트
//chat_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
7. body argument에서 streambuilder 위젯 불러오기
body: StreamBuilder(
stream: FirebaseFirestore.instance
//스냅샷메소드는 스트림 반환. 즉 데이터가 바뀔때마다 새로운 value 전달
.collection('chats/문서ID/컬렉션ID').snapshots(),
//이 빌더는 스트림에서 가장 최신 스냅샷을 가져오기 위한 AsyncSnapshot클래스
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
final docs = snapshot.data!.docs;
return ListView.builder( //새로운 value값이 전달될때마다 작동하기 위해 순차적으로 나열해주는 리스트뷰 빌더 리턴
itemCount: docs.length,
itemBuilder: (context, index){
return Container( //스트림 화면 출력
padding: EdgeInsets.all(8.0),
child: Text(docs[index]['text'],
style: TextStyle(fontSize: 20.0),
),
);
},
);
},
)
이러고 실행하면
에러가 뜬다
왜냐하면 바로 데이터가 불러와지는게 아니라 파이어베이스에 접속한 후 데이터를 가져오기 전까진 Null이기 떄문
스냅샷에 로딩 indicator 추가
if(snapshot.connectionState==ConnectionState.waiting){ //데이터를 기다려야할 경우
return Center(
child: CircularProgressIndicator(),
);
}
하지만 이래도 null check operator used on a null value 에러가 발생
> 파이어스토어 데이터베이스 > 규칙 > false를 true로 변경
파이어베이스 접속로딩이 길어질 시 등을 대비해 사용자 경험을 높이기 위해
8. 로그인 버튼 클릭시 로딩 인디케이터가 돌고 로그인 성공시 사라지는 기능 생성
1) 모달 패키지 다운
https://pub.dev/packages/modal_progress_hud_nsn
2) 스피너가 언제 돌아가고 멈춰야할지 정해주기
//main_screen.dart
bool showSpinner = false; //변수 생성
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Palette.backgroundColor,
body: ModalProgressHUD(
inAsyncCall: showSpinner, //처음엔 false이다 사인업, 사인인했을때 true
child: GestureDetector(
onTap: (){
//전송버튼 positioned
child: GestureDetector(
onTap: () async {
setState(() {
showSpinner = true;
});
//전송버튼 positioned
if (newUser.user != null) { // 회원 등록/로그인 성공
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen()
)
);// 바로 채팅화면으로 넘어가기
setState(() { //회원등록이 완료되면 스피터 끝내기
showSpinner = false;
}
);
}
조금 매운맛 26 파이어베이스 규칙 및 로그아웃 기능 수정
사인업과 로그인 인증에 필요한 데이터 외, 아이디 같은 엑스트라 데이터 데이터베이스에 등록하는 방법
//main_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
> 기본인증 관련해서 firebase auth패키지가 담당하지만 엑스트라 데이터는 cloud_firestore 패키지가 담당
await FirebaseFirestore.instance.collection('user').doc(
newUser.user!.uid).set({
'userName' : userName,
'email' : userEmail
});
> user는 즉성에서 생성가능하니 미리 생성 필요 ㄴㄴ(null이면 안되니 !)
> doc 메서드로 user id 전달
데이터베이스의 무분별한 접근을 막기 위해 인증받은 사용자만이 접근 가능하고, 누가 접근했는지 기록을 남겨야함
파이어베이스 룰(규칙)으로 가서 수정
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /user /{uid}{ //채팅방장만 접근가능
allow read, write: if request.auth != null && request.auth.uid == uid;
}
match /user /{uid} { //일반유저
allow read:if request.auth != null;
}
match /chats/{document=**} {
allow read, write: if request.auth != null;
}
}
}
> line3 match 문자열 : 전체 데이터베이스에 포괄적으로 적용되는 리퀘스트(걍 두는게 추천)
> line4 match 키워드 : 어떤 리퀘스트를 전달하고 어떤 룰이 리퀘스트에 적용될지 지정
> document=** : 모든 doc 접근 가능
> write 권한 안엔 creat/update/delete 포함
로그아웃 시 로그인 창에 정보가 그대로 남아 있는 문제 해결
> 텍스트폼필드에 정보가 그대로 남음
> firebaseAuth패키지가 토큰(=권한)을 관리해줌
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
backgroundColor: Colors.white,
),
home: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges() ,
builder: (context, snapshot){
//인증받은 토큰을 가지고 있다면 채팅화면으로
if(snapshot.hasData){
return ChatScreen();
}
return LoginSignupScreen();
},
),
);
}
}
> authStateChanges() : String 전달해줌
> stream은 authentication state가 변할때마다 새로운 value값 전달. 즉, 로긘/로그아웃시 authentication state가 변함
> 이때 파이어베이스가 발행해준 토큰을 firebaseAuth패키지가 관리해주기때문에 authStateChanges메소드 구독을 통해서 편하게 사용가능
실행하면 chatScreen이 두번 발생
> 왜냐면 main.dart에서 챗스크린 불러온 후, main_screen.dart 파일에서 navigator 방식으로 챗스크린을 또 불러오는데 이때 스택 방식으로 페이지가 쌓이면서 이동을 구현하기 때문
> 2번째 chatScreen 나가면 1번째 chatScreen 화면이 뜸
2번째 chatScreen을 나가서 1번째 chatScreen에서 로그아웃 하면 화면이 블랙되며 오류 뜸
> 왜냐면 1번째 chatScreen에서 로그아웃할 때, authentication state는 바뀌지만 동시에 스트림빌더가 로그인 스크린 리턴하기 전에 chatScreen이 위젯트리에서 삭제되며 화면 블랙만 뜸
이걸 해결해보자
1) main_screen.dart > 전송버튼 AnimatedPositioned에서 네비게이터 삭제
2) chat_screen.dart > pop 메서드 삭제
해당 과정 정리
1. 자동으로 firebaseAuth패키지가 사용자가 발급 받았던 토큰 삭제함으로써 authentication state가 바뀌는 이벤트를 authStateChanged 스트림에 전달
2. 더이상 스냅샷이 토큰을 가지지 않으므로 chat_screen으로 이동 하지 않음
3. 바로 loginsignupscreen을 리턴
이러면 다시 에러남
이것저것 해보니 파이어베이스 룰 셋팅 때문인듯 하나 아직 원인을 못 찾음
======== Exception caught by widgets library =======================================================
The following _TypeError was thrown building StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(dirty, state: _StreamBuilderBaseState<QuerySnapshot<Map<String, dynamic>>, AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>>>#a02ce):
Null check operator used on a null value
The relevant error-causing widget was:
StreamBuilder<QuerySnapshot<Map<String, dynamic>>> StreamBuilder:file:///D:/KJin/flutter%20project/yami_chat/lib/screens/chat_screen.dart:51:15
When the exception was thrown, this was the stack:
#0 _ChatScreenState.build.<anonymous closure> (package:yami_chat/screens/chat_screen.dart:62:37)
#1 StreamBuilder.build (package:flutter/src/widgets/async.dart:439:81)
#2 _StreamBuilderBaseState.build (package:flutter/src/widgets/async.dart:120:48)
#3 StatefulElement.build (package:flutter/src/widgets/framework.dart:5064:27)
#4 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4952:15)
#5 StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5117:11)
#6 Element.rebuild (package:flutter/src/widgets/framework.dart:4672:7)
#7 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2749:19)
#8 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:866:21)
#9 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:378:5)
#10 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1286:15)
#11 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1216:9)
#12 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1074:5)
#13 _invoke (dart:ui/hooks.dart:142:13)
#14 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:338:5)
#15 _drawFrame (dart:ui/hooks.dart:112:31)
====================================================================================================
W/Firestore( 9672): (24.4.3) [WatchStream]: (59f8e8b) Stream closed with status: Status{code=CANCELLED, description=Disconnecting idle stream. Timed out waiting for new targets., cause=null}.
W/ample.yami_cha( 9672): Accessing hidden method Ldalvik/system/CloseGuard;->close()V (light greylist, linking)
강의 댓글에 나랑 같은 오류로 고생하신분이 보이는데 강사님의 답댓이 없다...
기다린다... 머리를 식히고 다시 리프레쉬한 머리로 삽질해봐야겠다
> 하... 친구 덕에 오류를 찾았는데 진짜 바보였다.
chats인데 철자를 빠트려서 생긴 오류였다.
바보바보... 이걸로 몇시간 삽질한 나는 바보....
전체코드
//main.dart
//main.dart
import 'package:flutter/material.dart';
import 'package:yami_chat/screens/chat_screen.dart';
import 'package:yami_chat/screens/main_screen.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
void main() async{
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chatting app',
theme: ThemeData(
backgroundColor: Colors.white,
),
home: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges() ,
builder: (context, snapshot){
//인증받은 토큰을 가지고 있다면 채팅화면으로
if(snapshot.hasData){
return ChatScreen();
}
return LoginSignupScreen();
},
),
);
}
}
//main_screen.dart
//main_screen.dart
import 'package:flutter/material.dart';
import 'package:yami_chat/config/palette.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:yami_chat/screens/chat_screen.dart';
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class LoginSignupScreen extends StatefulWidget {
const LoginSignupScreen({Key? key}) : super(key: key);
@override
State<LoginSignupScreen> createState() => _LoginSignupScreenState();
}
class _LoginSignupScreenState extends State<LoginSignupScreen> {
final _authentication = FirebaseAuth.instance;
bool isSignupScreen = true;// 로그인가입화면 state 관리를 위해서
bool showSpinner = false;
final _formKey = GlobalKey<FormState>();
String userName = '';
String userEmail = '';
String userPassword = '';
void _tryValidation(){
final isValid = _formKey.currentState!.validate();
if(isValid){
_formKey.currentState!.save();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Palette.backgroundColor,
body: ModalProgressHUD(
inAsyncCall: showSpinner, //처음엔 false이다 사인업, 사인인했을때 true
child: GestureDetector(
onTap: (){
FocusScope.of(context).unfocus();
},
child: Stack(
children: [
//배경
Positioned(
top: 0,
left: 0, // 스크린 상단 시작점 맞추기위해서 좌우 0
right: 0,
child: Container(
height: 300,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('image/red.jpg'),
fit: BoxFit.fill //스크린 상단 끝까지 채우기
),
),
child: Container(
padding: EdgeInsets.only(top: 90, left: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(text: TextSpan( //문단등 구성할 수 있게 해주는 위젯
text: 'Welcome',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.white,
),
children: [
TextSpan( //문단등 구성할 수 있게 해주는 위젯
text: isSignupScreen ? ' To Yummy chat!' : ' back',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(
height: 5.0,
),
Text(
isSignupScreen ? 'Signup to continue' : 'Signin to continue',
style: TextStyle(
letterSpacing: 1.0,
color: Colors.white,
),
),
],
),
),
),),
//텍스트폼필드
AnimatedPositioned(//
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
top: 180.0,
child:
Container(
padding: EdgeInsets.all(20.0),
height: isSignupScreen ? 280.0 : 200.0,
width: MediaQuery.of(context).size.width-40, // MediaQuery 이용해서 디바이스의 크기에 맞춰 -40
margin: EdgeInsets.symmetric(horizontal: 20.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 15,
spreadRadius: 5,
)
]
),
child: SingleChildScrollView(
padding: EdgeInsets.only(bottom: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
GestureDetector(
onTap: () {
setState(() {
isSignupScreen = false;
});
},
child: Column(
children: [
Text(
'LOGIN',
style: TextStyle(
fontSize: 16,
color: !isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
if (!isSignupScreen)
Container(
height: 2,
width: 55,
color: Colors.orange,
)
],
),
),
GestureDetector(
onTap: () {
setState(() {
isSignupScreen = true;
});
},
child: Column(
children: [
Text(
'SIGNUP',
style: TextStyle(
fontSize: 16,
color: isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
if (isSignupScreen)
Container(
margin: EdgeInsets.only(top: 3),
height: 2,
width: 55,
color: Colors.orange,
)
],
),
)
],
),
if(isSignupScreen)
Container(
margin: EdgeInsets.only(top: 20),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
key: ValueKey(1),
validator: (value){
if(value!.isEmpty || value.length < 4) {
return 'Please enter at least 4 characters';
} return null;
},
onSaved: (value){
userName = value!;
},
onChanged: (value){
userName = value;
},
decoration: InputDecoration(
prefixIcon: Icon(Icons.account_circle,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'User name',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
),
SizedBox(
height: 8,
),
TextFormField(
keyboardType: TextInputType.emailAddress,
key: ValueKey(2),
validator: (value){
if(value!.isEmpty || value.contains('@')) {
return 'Please enter a valid email address';
} return null;
},
onSaved: (value){
userEmail = value!;
},
onChanged: (value){
userEmail = value;
},
decoration: InputDecoration(
prefixIcon: Icon(Icons.email_sharp,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'email',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
),
SizedBox(
height: 8,
),
TextFormField(
obscureText: true,
key: ValueKey(3),
validator: (value){
if(value!.isEmpty || value.length < 6) {
return 'Password must be at least 7 characters long';
} return null;
},
onSaved: (value){
userPassword = value!;
},
onChanged: (value){
userPassword = value;
},
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'password',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
)
],
),
),
),
if(!isSignupScreen)
Container(
margin: EdgeInsets.only(top: 20),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
key: ValueKey(4),
validator: (value){
if(value!.isEmpty || value.contains('@')) {
return 'Please enter a valid email address';
} return null;
},
onSaved: (value){
userEmail = value!;
},
onChanged: (value){
userEmail = value;
},
decoration: InputDecoration(
prefixIcon: Icon(Icons.account_circle,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'User name',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
),
SizedBox(
height: 8.0,
),
TextFormField(
obscureText: true,
key: ValueKey(5),
validator: (value){
if(value!.isEmpty || value.length < 6) {
return 'Password must be at least 7 characters long';
} return null;
},
onSaved: (value){
userPassword = value!;
},
onChanged: (value){
userPassword = value;
},
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'Password',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
),
],
),
),
)
],
),
),
),
),
//전송버튼
AnimatedPositioned(
duration: Duration(milliseconds: 150),
curve: Curves.easeIn,
top: isSignupScreen ? 430 : 350,
right:0,
left: 0,
child: Center(
child: Container(
padding: EdgeInsets.all(15),
height: 90,
width: 90,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(50),
),
child: GestureDetector(
onTap: () async {
setState(() { //스피너 돌기
showSpinner = true;
});
if (isSignupScreen) { //회원가입
_tryValidation();
try {
final newUser = await _authentication
.createUserWithEmailAndPassword(
email: userEmail,
password: userPassword);
await FirebaseFirestore.instance.collection('user').doc(
newUser.user!.uid).set({
'userName' : userName,
'email' : userEmail
});
if (newUser.user != null) { // 회원 등록 성공
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => ChatScreen()
// )
// );
setState(() { //회원등록이 완료되면 스피터 끝내기
showSpinner = false;
});
}
} catch (e) {
print(e);
//사용자에게 시각적으로 에러 표시
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content:
Text('Please check your email and password'),
backgroundColor: Colors.blue,
));
}
}
if (!isSignupScreen) { //로그인
try {
_tryValidation();
final newUser =
await _authentication.signInWithEmailAndPassword(
email: userEmail, password: userPassword);
if (newUser.user != null) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen()
),
);
setState(() {
showSpinner = false;
});
}
} catch (e) {
print(e);
}
}
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.orange,
Colors.red
],
begin: Alignment.topLeft,
end: Alignment.bottomRight
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 1,
offset: Offset(0,1), //버튼으로부터 수직수평 거리
),
],
),
child: Icon(
Icons.arrow_forward,
color: Colors.white,
),
),
),
),
)),
//signUp과 구글버튼
AnimatedPositioned(
duration: Duration(milliseconds: 200),
top: isSignupScreen ? MediaQuery.of(context).size.height-125 : MediaQuery.of(context).size.height-185,
right: 0,
left: 0,
child: Column(
children: [
Text(isSignupScreen ? 'or Signup with' : 'or Sign with'),
SizedBox(
height: 10,
),
TextButton.icon(
style: TextButton.styleFrom(
primary: Colors.white,
minimumSize: Size(155, 40),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)
),
backgroundColor: Palette.googleColor
),
icon: Icon(Icons.add),
onPressed: (){},
label: Text('Google'),
)
],
)
,),
],
),
),
),
);
}
}
//chat_screen.dart
//chat_screen.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({Key? key}) : super(key: key);
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _authentication = FirebaseAuth.instance;
User? loggedUser; //회원가입을 해서 정보가 들어올때 초기화되므로 널세이프티
@override
void initState() { //초기화 될때 getCurrentUser() 진행
// TODO: implement initState
super.initState();
getCurrentUser();
}
void getCurrentUser() { //유저 정보 확인
try {
final user = _authentication.currentUser;
if (user != null) {
loggedUser = user;
print(loggedUser!.email);
}
} catch (e) {
print(e);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Chat screen'),
actions: [
IconButton(onPressed: () {
_authentication.signOut();
//Navigator.pop(context);
},
icon: Icon(
Icons.exit_to_app_sharp,
color: Colors.white,)
)
],
),
body: StreamBuilder(
stream: FirebaseFirestore.instance
//스냅샷메소드는 스트림 반환. 즉 데이터가 바뀔때마다 새로운 value 전달
.collection('chats/xNH5bypXNZw1MqpMyOh5/message').snapshots(),
//이 빌더는 스트림에서 가장 최신 스냅샷을 가져오기 위한 AsyncSnapshot클래스
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if(snapshot.connectionState==ConnectionState.waiting){ //데이터를 기다려야할 경우
return Center(
child: CircularProgressIndicator(),
);
}
final docs = snapshot.data!.docs;
return ListView.builder( //새로운 value값이 전달될때마다 작동하기 위해 순차적으로 나열해주는 리스트뷰 빌더 리턴
itemCount: docs.length,
itemBuilder: (context, index){
return Container( //스트림 화면 출력
padding: EdgeInsets.all(8.0),
child: Text(docs[index]['text'],
style: TextStyle(fontSize: 20.0),
),
);
},
);
},
));
}
}
'공부 > Flutter' 카테고리의 다른 글
Flutter 스터디 29 Provider - ChangeNotifierProvider와 MultiProvider (0) | 2023.03.02 |
---|---|
Flutter 스터디 28 채팅 기능 (0) | 2023.02.27 |
Flutter 스터디 26 Firebase 연동 및 SignUp/SignIn 기능 구현 (0) | 2023.02.23 |
Flutter 스터디 25 채팅 앱 UI 디자인, TextFormField validation 구현 (0) | 2023.02.20 |
Flutter 스터디 24 Flutter Key 이해하기(Value Key, Global Key) (0) | 2023.02.20 |