- Draft 구현의 전개는 아래의 순서로 진행함.
순서:
Sign-up/Log-in
- FirebaseAuth에서 제공하는 auth state method로 idtokenchange, userchange, authstatechanges가 있는데 이중 나는 마지막껄로 진행함. 각자 서비스와 목적이 다름.
- 웹에서도 파일 첨부 가능하도록 file 타입이 아닌 Uint8List를 사용함. --> dart io의 file은 호환(?)이 아직 안좋다 뭐 이런 말 한 듯함 (근데 꽤 outdated된 얘기라 검증 안됨).
- authentication 자체는 email과 password로 이루어지는데, 나머지 정보들(ie. username, bio 등)은 Firestore에 저장되는 것임.
- 스낵바는 스크린 하단에 간단한 메세지를 띄우는 기능이라함. 해당 기능은 utils파일에 포함시킴.
(참고: https://velog.io/@realryankim/Flutter-%EC%8A%A4%EB%82%B5%EB%B0%94Snack-bar%EC%99%80-BuildContext)
- context는 BuildContext class의 instance라고 이해하면 됨.
- stream builder로 auth state 유지 --> 백엔드 한창 배울 땐 access & refresh token으로 로그인 상태 관리하였던 것으로 기억:
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => UserProvider(),),
],
child: MaterialApp(
// debugShowCheckedModeBanner: false,
title: 'Donfo',
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: mobileBackgroundColor,
),
home: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
// Checking if the snapshot has any data or not
if (snapshot.hasData) {
// if snapshot has data which means user is logged in then we check the width of screen and accordingly display the screen layout
return const ResponsiveLayout(
mobileScreenLayout: MobileScreenLayout(),
webScreenLayout: WebScreenLayout(),
);
} else if (snapshot.hasError) {
return Center(
child: Text('${snapshot.error}'),
);
}
}
// means connection to future hasnt been made yet
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
return const LoginScreen();
},
),
),
);
}
- user model로 db model 분리:
class User {
final String email;
final String uid;
final String photoUrl;
final String username;
final String bio;
final List followers;
final List following;
const User(
{required this.username,
required this.uid,
required this.photoUrl,
required this.email,
required this.bio,
required this.followers,
required this.following});
Map<String, dynamic> toJson() => {
"username": username,
"uid": uid,
"email": email,
"photoUrl": photoUrl,
"bio": bio,
"followers": followers,
"following": following,
};
}
상태관리
- 우선 user model에서 snapshot을 지정해줌:
static User fromSnap(DocumentSnapshot snap) {
var snapshot = snap.data() as Map<String, dynamic>;
return User(
username: snapshot["username"],
uid: snapshot["uid"],
email: snapshot["email"],
photoUrl: snapshot["photoUrl"],
bio: snapshot["bio"],
followers: snapshot["followers"],
following: snapshot["following"],
);
}
- auth methods에서 가지고 올 정보(uid)를 설정:
Future<model.User> getUserDetails() async {
User currentUser = _auth.currentUser!;
DocumentSnapshot documentSnapshot =
await _firestore.collection('users').doc(currentUser.uid).get();
return model.User.fromSnap(documentSnapshot);
}
- 가지고 온 정보를 middleware 쓰는거 마냥 user provider 파일에 등록함:
class UserProvider with ChangeNotifier {
User? _user;
final AuthMethods _authMethods = AuthMethods();
User get getUser => _user!;
Future<void> refreshUser() async {
User user = await _authMethods.getUserDetails();
_user = user;
notifyListeners();
}
}
- main.dart에 multiprovider를 이용해서 해당 정보를 상태 등록함:
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => UserProvider(),),
],
Bottom Navigation Bar
- mobile screen에 page number를 할당하여 pageController로 페이지가 넘어가게끔 해줌. Indexed stack과 비슷한 원리로 보임:
class MobileScreenLayout extends StatefulWidget {
const MobileScreenLayout({Key? key}) : super(key: key);
@override
State<MobileScreenLayout> createState() => _MobileScreenLayoutState();
}
class _MobileScreenLayoutState extends State<MobileScreenLayout> {
int _page = 0;
late PageController pageController; // for tabs animation
@override
void initState() {
super.initState();
pageController = PageController();
}
@override
void dispose() {
super.dispose();
pageController.dispose();
}
void onPageChanged(int page) {
setState(() {
_page = page;
});
}
void navigationTapped(int page) {
//Animating Page
pageController.jumpToPage(page);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: pageController,
onPageChanged: onPageChanged,
children: homeScreenItems,
),
bottomNavigationBar: CupertinoTabBar(
backgroundColor: mobileBackgroundColor,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(
Icons.home,
color: (_page == 0) ? primaryColor : secondaryColor,
),
label: '',
backgroundColor: primaryColor,
),
BottomNavigationBarItem(
icon: Icon(
Icons.search,
color: (_page == 1) ? primaryColor : secondaryColor,
),
label: '',
backgroundColor: primaryColor),
BottomNavigationBarItem(
icon: Icon(
Icons.add_circle,
color: (_page == 2) ? primaryColor : secondaryColor,
),
label: '',
backgroundColor: primaryColor),
BottomNavigationBarItem(
icon: Icon(
Icons.favorite,
color: (_page == 3) ? primaryColor : secondaryColor,
),
label: '',
backgroundColor: primaryColor,
),
BottomNavigationBarItem(
icon: Icon(
Icons.person,
color: (_page == 4) ? primaryColor : secondaryColor,
),
label: '',
backgroundColor: primaryColor,
),
],
onTap: navigationTapped,
currentIndex: _page,
),
);
}
}
페이지별 UI 및 기능 구현 후 Firebase와 연동하여 CRUD 작업
- Feed screen의 경우 새로운 피드가 생길 경우를 고려하여 StreamBuilder로 구성함.
- 좋아요 기능 구현시 Firebase 포스트에 좋아요 한 유저의 uid를 함께 넣어줌.
- Comments도 post db 내 별도의 collection으로 구성함. 최종 Firestore methods:
class FireStoreMethods {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Future<String> uploadPost(String description, Uint8List file, String uid,
String username, String profImage) async {
// asking uid here because we dont want to make extra calls to firebase auth when we can just get from our state management
String res = "Some error occurred";
try {
String photoUrl =
await StorageMethods().uploadImageToStorage('posts', file, true);
String postId = const Uuid().v1(); // creates unique id based on time
Post post = Post(
description: description,
uid: uid,
username: username,
likes: [],
postId: postId,
datePublished: DateTime.now(),
postUrl: photoUrl,
profImage: profImage,
);
_firestore.collection('posts').doc(postId).set(post.toJson());
res = "success";
} catch (err) {
res = err.toString();
}
return res;
}
Future<String> likePost(String postId, String uid, List likes) async {
String res = "Some error occurred";
try {
if (likes.contains(uid)) {
// if the likes list contains the user uid, we need to remove it
_firestore.collection('posts').doc(postId).update({
'likes': FieldValue.arrayRemove([uid])
});
} else {
// else we need to add uid to the likes array
_firestore.collection('posts').doc(postId).update({
'likes': FieldValue.arrayUnion([uid])
});
}
res = 'success';
} catch (err) {
res = err.toString();
}
return res;
}
// Post comment
Future<String> postComment(String postId, String text, String uid,
String name, String profilePic) async {
String res = "Some error occurred";
try {
if (text.isNotEmpty) {
// if the likes list contains the user uid, we need to remove it
String commentId = const Uuid().v1();
_firestore
.collection('posts')
.doc(postId)
.collection('comments')
.doc(commentId)
.set({
'profilePic': profilePic,
'name': name,
'uid': uid,
'text': text,
'commentId': commentId,
'datePublished': DateTime.now(),
});
res = 'success';
} else {
res = "Please enter text";
}
} catch (err) {
res = err.toString();
}
return res;
}
// Delete Post
Future<String> deletePost(String postId) async {
String res = "Some error occurred";
try {
await _firestore.collection('posts').doc(postId).delete();
res = 'success';
} catch (err) {
res = err.toString();
}
return res;
}
Future<void> followUser(String uid, String followId) async {
try {
DocumentSnapshot snap =
await _firestore.collection('users').doc(uid).get();
List following = (snap.data()! as dynamic)['following'];
if (following.contains(followId)) {
await _firestore.collection('users').doc(followId).update({
'followers': FieldValue.arrayRemove([uid])
});
await _firestore.collection('users').doc(uid).update({
'following': FieldValue.arrayRemove([followId])
});
} else {
await _firestore.collection('users').doc(followId).update({
'followers': FieldValue.arrayUnion([uid])
});
await _firestore.collection('users').doc(uid).update({
'following': FieldValue.arrayUnion([followId])
});
}
} catch (e) {
if (kDebugMode) print(e.toString());
}
}
}
- 이외의 기능들은 다 비슷하게 진행돼서 생략하고 테마 및 커스텀 시작할 예정.
- 이후 클린아키텍처에 맞춰 코드 리팩토링 할 것임.
[reactNative] 환경설정 (0) | 2024.04.13 |
---|