상세 컨텐츠

본문 제목

[Donfo] 초안 작업 完

projects

by 서울의볼 2024. 5. 16. 20:14

본문

- Draft 구현의 전개는 아래의 순서로 진행함.

 

순서:

  1. main.dart 파일 셋업, 테마 파일 설정
  2. web platform 고려하여 responsive layout widget 파일(mobile & web) 생성, 플랫폼에 맞춰 개발 진행
  3. Firebase 연동 및 관련 환경설정
  4. 로그인 스크린 UI 작업. text_field_input 위젯을 분리하여 필요한 파일에 쓸 수 있도록 설정 (비밀번호 별도 설정)
  5. 회원가입 스크린 UI 작업. (위의 로그인과 유사)
  6. Firebase Auth 사용하여 서버 연동, image picker로 라이브러리 엑세스 설정 (utils파일에), Firebase Storage에 저장
  7. Firebase Auth에 로그인 내역 연동
  8. Auth state 유지 설정, StreamBuilder로 authStateChanges method 활용 (snapshot의 connectionState active 여부 확인)
  9. user model 분리하여 만듦
  10. Provider 패키지로 상태관리 작업
  11. PageController 활용하여 Bottom Navigation Bar 구현
  12. Add Post UI 작업
  13. Selecting Image 기능 구현
  14. Post Data Firebase에 저장
  15. Feed Post UI 작업
  16. Feed Post Data 가져와서 보여주는 작업
  17. 좋아요 애니메이션
  18. 좋아요 누적해서 db에 저장
  19. Comments UI 작업
  20. Comments도 DB에 저장
  21. 저장된 Comments data 불러와서 보여줌
  22. Post 삭제 기능
  23. 유저 Search 기능
  24. Search 스크린에 포스트 띄우기
  25. Profile UI
  26. 프로필 데이터 가져와서 띄우기
  27. 팔로워 기능
  28. 로그아웃 기능

 

 

 

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());
    }
  }
}

이런식.

- 이외의 기능들은 다 비슷하게 진행돼서 생략하고 테마 및 커스텀 시작할 예정.

- 이후 클린아키텍처에 맞춰 코드 리팩토링 할 것임.

 

'projects' 카테고리의 다른 글

[reactNative] 환경설정  (0) 2024.04.13

관련글 더보기