스마트시대

25.0 USER PROFILE Introduction 25.1 FireStore Intro 25.2 UserProfileModel 25.3 createProfile 25.4 findProfile 25.5 AvatarViewModel 25.6 onAvatarUpload 25.7 Recap 본문

Programing/Flutter

25.0 USER PROFILE Introduction 25.1 FireStore Intro 25.2 UserProfileModel 25.3 createProfile 25.4 findProfile 25.5 AvatarViewModel 25.6 onAvatarUpload 25.7 Recap

스마트시대 2023. 5. 31. 20:03
728x90

https://console.firebase.google.com/u/1/project/tiktok-clone-b524f/firestore?hl=ko

 

firebase cloud firestore(database), storage(Nosql)활성화하기

 

25.1 FireStore Intro

users안에 또 만들려면 컬렉션 시작 누르기(likes)

25.2 UserProfileModel 

 

 

 

25.3 createProfile

 

 

import 'dart:async';

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tiktok_clone/features/users/models/user_profile_model.dart';
import 'package:tiktok_clone/features/users/repos/user_repo.dart';

//(25.2)
class UsersViewModel extends AsyncNotifier<UserProfileModel> {
  //Json처리 방법(25.3)
  late final UserRepository _repository;

  @override
  FutureOr<UserProfileModel> build() {
    //Json처리 방법(25.3)
    _repository = ref.read(userRepo);

    return UserProfileModel.empty();
  }

  Future<void> createProfile(UserCredential credential) async {
    //!쓰기 싫으면 이렇게 해도됨
    if (credential.user == null) {
      throw Exception("Account not created");
    }

    //(25.3)profile만들어지는 동안 로딩페이지 만들기
    state = const AsyncValue.loading();
    //이 데이터를 firestore에 넣기 위한 처리방법, profile함수화(25.3)
    final profile = UserProfileModel(
      bio: "undefined",
      link: "undefined",
      email: credential.user!.email ?? "anon@anon.com",
      uid: credential.user!.uid,
      name: credential.user!.displayName ?? "Anon",
    );
    //Json처리 방법(25.3)
    await _repository.createProfile(profile);

    state = AsyncValue.data(profile);
  }
}

//(25.3)
final usersProvider = AsyncNotifierProvider<UsersViewModel, UserProfileModel>(
  () => UsersViewModel(),
);

 

25.4 findProfile

------------
//(25.4)? 있을수도 없을수도 있음
  Future<Map<String, dynamic>?> findProfile(String uid) async {
    final doc = await _db.collection("users").doc(uid).get();
    return doc.data();
  }
  ------------

 

//(25.2)
class UsersViewModel extends AsyncNotifier<UserProfileModel> {
  //Json처리 방법(25.3)
  
  --------------------
  late final UserRepository _usersRepository;
  //(25.4)
  late final AuthenticationRepository _authenticationRepository;
--------------------



  @override
  FutureOr<UserProfileModel> build() {
    //Json처리 방법(25.3)
    _usersRepository = ref.read(userRepo);
    
    --------------------
    //(25.4)
  FutureOr<UserProfileModel> build() async {
    _usersRepository = ref.read(userRepo);
    _authenticationRepository = ref.read(authRepo);

    if (_authenticationRepository.isLoggedIn) {
      final profile = await _usersRepository
          .findProfile(_authenticationRepository.user!.uid);
      if (profile != null) {
        return UserProfileModel.fromJson(profile);
      }
    }
--------------------


    return UserProfileModel.empty();
  }

  Future<void> createProfile(UserCredential credential) async {
    //!쓰기 싫으면 이렇게 해도됨
    if (credential.user == null) {
      throw Exception("Account not created");
    }

      link = "";
---------------
//(25.4)
  UserProfileModel.fromJson(Map<String, dynamic> json)
      : uid = json["uid"],
        email = json["email"],
        name = json["name"],
        bio = json["bio"],
        link = json["link"];
---------------


  Map<String, String> toJson() {

 

25.5 AvatarViewModel

  //(25.5)
class UserProfileScreen extends ConsumerStatefulWidget {
  //웹 설정(URL에 어떤 이름 넣어도 출력되는)
  final String username;
  //프로파일 스크린 랜더링할 때 post 탭이 아닌 likes탭이 보이게 하는 설정
  final String tab;

  const UserProfileScreen({
    super.key,
    required this.username,
    required this.tab,
  });

  @override
  //(25.5)
  ConsumerState<UserProfileScreen> createState() => _UserProfileScreenState();
}

//(25.5)
class _UserProfileScreenState extends ConsumerState<UserProfileScreen> {
  //_onGearPressed설정창 누르는 메소드
  void _onGearPressed() {
  
  
  
  Widget build(BuildContext context) {
  
  
  
  -----------------
    //(25.5)
    return ref.watch(usersProvider).when(
        error: (error, stackTrace) => Center(
              child: Text(error.toString()),
            ),
        loading: () => const Center(
              child: CircularProgressIndicator.adaptive(),
            ),
            //(25.5)Scaffold여기로 넣기
        data: (data) => Scaffold(
          -----------------
              backgroundColor:

 

  @override
  FutureOr<UserProfileModel>build() async {
  
  -------------
  //(25.5)iphone에서 로딩화면 뜨고 프로파일이 뜨게 한다.없어도 됨
    await Future.delayed(const Duration(seconds: 10));
  -------------
  
  
    _usersRepository = ref.read(userRepo);
    _authenticationRepository = ref.read(authRepo);

 

iphone에서 로딩화면 뜨고 프로파일이 뜨게 한다.

 

lib/features/users/views/widgets/avatar.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';

//(25.5)
class Avatar extends ConsumerWidget {
  final String name;

  const Avatar({
    super.key,
    required this.name,
  });

  Future<void> _onAvatarTap() async {
    final xfile = await ImagePicker().pickImage(
      source: ImageSource.gallery,
      imageQuality: 40,
      maxHeight: 150,
      maxWidth: 150,
    );
    if (xfile != null) {
      final file = File(xfile.path);
    }
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return GestureDetector(
      onTap: _onAvatarTap,
      child: CircleAvatar(
        radius: 50,
        foregroundImage: const NetworkImage(
        //NetworkImage는 한번만 fetch하고 캐싱됨
            "https://media.licdn.com/dms/image/C4D03AQEfaXwimUdjmw/profile-displayphoto-shrink_100_100/0/1517469163893?e=1689811200&v=beta&t=0i37nt-0uT5JvkQGUUzQlZtA5fUFFYUVinTFDTyo0iQ"),
        child: Text(name),
      ),
    );
  }
}

import 'dart:async';
import 'dart:io';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tiktok_clone/features/authentication/repos/authentication_repo.dart';
import 'package:tiktok_clone/features/users/repos/user_repo.dart';

//(25.5)
class AvatarViewModel extends AsyncNotifier<void> {
  late final UserRepository _repository;

  @override
  FutureOr<void> build() {
    _repository = ref.read(userRepo);
  }

  Future<void> uploadAvatar(File file) async {
    state = const AsyncValue.loading();
    // 현재 유저가 누군지 알게 함
    final fileName = ref.read(authRepo).user!.uid;
    state = await AsyncValue.guard(
      () async => await _repository.uploadAvatar(file, fileName),
    );
  }
}

 

    final doc = await _db.collection("users").doc(uid).get();
    return doc.data();
  }

class UserRepository {
  final FirebaseFirestore _db = FirebaseFirestore.instance;
  
  
  --------------
  final FirebaseStorage _storage = FirebaseStorage.instance;
--------------


--------------
//(25.5)
  Future<void> uploadAvatar(File file, String fileName) async {
    final fileRef = _storage.ref().child("avatars/$fileName");
    await fileRef.putFile(file);
  }
}
--------------


final userRepo = Provider(

 

25.6 onAvatarUpload

 

    state = const AsyncValue.loading();
    final fileName = ref.read(authRepo).user!.uid;
    state = await AsyncValue.guard(


----------------
//(25.6)
      () async {
        await _repository.uploadAvatar(file, fileName);
        //usersProvider에 우리가 업로드 했다는 걸 알림
        await ref.read(usersProvider.notifier).onAvatarUpload();
      },
    );
  }
}

final avatarProvider = AsyncNotifierProvider<AvatarViewModel, void>(
  () => AvatarViewModel(),
);
----------------

 

class Avatar extends ConsumerWidget {
  final String name;
  ---------------------
//(25.6)
  final bool hasAvatar;
  final String uid;
    ---------------------
    
    
  const Avatar({
    super.key,
    ---------------------
//(25.6)
    required this.uid,
    required this.hasAvatar,
        ---------------------
        
        
    required this.name,
  });


---------------------
//(25.6)
  Future<void> _onAvatarTap(WidgetRef ref) async {
  ---------------------
    final xfile = await ImagePicker().pickImage(
      source: ImageSource.gallery,
      imageQuality: 40,
      
      
      
          if (xfile != null) {
      final file = File(xfile.path);
      ---------------------
//(25.6)
      await ref.read(avatarProvider.notifier).uploadAvatar(file);
            ---------------------
    }
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
  ---------------------
//(25.6)
    final isLoading = ref.watch(avatarProvider).isLoading;

    return GestureDetector(
      onTap: isLoading ? null : () => _onAvatarTap(ref),
      child: isLoading
          ? Container(
              width: 50,
              height: 50,
              alignment: Alignment.center,
              decoration: const BoxDecoration(
                shape: BoxShape.circle,
              ),
              child: const CircularProgressIndicator(),
            )
          : CircleAvatar(
              radius: 50,
              foregroundImage: hasAvatar
                  ? NetworkImage(
                  //(25.6)s%2F$    uid?  만들기
                      "https://firebasestorage.googleapis.com/v0/b/tiktok-abc-xyz.appspot.com/o/avatars%2F$uid?alt=media")
                  : null,
              child: Text(name),
            ),
                      ---------------------

여기도 추가하기

커서까지만 복사하기

 

alt=media부터 빼면 이렇게 정보도 얻을 수 있음

 

아바타에서 에뮬레이터에 있는 샘플 사진을 올리면 이런식으로 사진이 업로드 된다.

 

이제 에뮬레이터에 있는 아바타를 위 사진으로 바꿔주는 작업

 

                        child: Column(
                          children: [
                            Gaps.v20,
                            //Avatar(name: data.name),
                            
                            //(25.6)
                            ----------------------------
                            Avatar(
                              uid: data.uid,
                              name: data.name,
                              hasAvatar: data.hasAvatar,
                            ),
                                                        ----------------------------
                                                        
                            Gaps.v20,
                            Row(
                              mainAxisAlignment: MainAxisAlignment.center,

 

    state = const AsyncValue.loading();
    final profile = UserProfileModel(
    
    --------------------
    //(25.6)
      hasAvatar: false,
          --------------------
          
          
      bio: "undefined",
      link: "undefined",
      email: credential.user!.email ?? "anon@anon.com",
      
      
      
    await _usersRepository.createProfile(profile);
    state = AsyncValue.data(profile);
  }



          --------------------
              //(25.6)여기에 아바타가 있다는 걸 알려줘야 이미지 처리를 해줌
  Future<void> onAvatarUpload() async {
    if (state.value == null) return;
    state = AsyncValue.data(state.value!.copyWith(hasAvatar: true));
    await _usersRepository.updateUser(state.value!.uid, {"hasAvatar": true});
  }
            --------------------
            
}

final usersProvider = AsyncNotifierProvider<UsersViewModel, UserProfileModel>(

copyWith는 원본 데이터는 그대로 냅두고 hasAvatar 값만 바꾸기 위함 입니다.

'copyWith'는 Flutter에서 사용하는 메소드 중 하나로, 기존 객체를 복제하고 일부 속성을 수정하여 새로운 객체를 생성하는 기능을 합니다. 일반적으로 객체의 속성을 변경할 때, 해당 객체를 직접 수정하는 방식으로 처리할 수 있습니다. 그러나, 이 방식은 객체의 불변성(immutability)을 보장하지 않기 때문에 예상치 못한 결과를 초래할 수 있습니다. 'copyWith'는 이러한 문제를 해결하기 위한 방법으로, 불변성을 보장하면서 객체의 일부 속성을 변경할 수 있도록 해줍니다. 이는 객체 지향 프로그래밍에서 매우 중요한 개념 중 하나로, 객체의 불변성을 유지하면서 객체를 다루는 것이 코드의 안정성과 가독성을 향상시키는 데 도움이 됩니다.

 

 

  final String name;
  final String bio;
  final String link;
  
  -------------
  //(25.6)
  final bool hasAvatar;
  -------------
  
  
  
  UserProfileModel({
    required this.uid,
    required this.email,
    required this.name,
    required this.bio,
    required this.link,
    
    
      -------------
        //(25.6)
    required this.hasAvatar,
      -------------
  });

  UserProfileModel.empty()
      : uid = "",
        email = "",
        name = "",
        bio = "",
        
              -------------
                //(25.6)
        link = "",
        hasAvatar = false;
              -------------
        
          UserProfileModel.fromJson(Map<String, dynamic> json)
      : uid = json["uid"],
        email = json["email"],
        name = json["name"],
        bio = json["bio"],
        
              -------------
                //(25.6)
        hasAvatar = json["hasAvatar"],
              -------------
              
              
        link = json["link"];

  Map<String, String> toJson() {
  
  
        "link": link,
    };
  }




      -------------
        //(25.6)
  UserProfileModel copyWith({
  //? nullable
    String? uid,
    String? email,
    String? name,
    String? bio,
    String? link,
    bool? hasAvatar,
  }) {
    return UserProfileModel(
    //?? or
      uid: uid ?? this.uid,
      email: email ?? this.email,
      name: name ?? this.name,
      bio: bio ?? this.bio,
      link: link ?? this.link,
      hasAvatar: hasAvatar ?? this.hasAvatar,
    );
  }
}
      -------------

 

    final fileRef = _storage.ref().child("avatars/$fileName");
    await fileRef.putFile(file);
  }


------------------
//(25.6)
  Future<void> updateUser(String uid, Map<String, dynamic> data) async {
    await _db.collection("users").doc(uid).update(data);
  }
}
------------------


final userRepo = Provider(

 

UID 위의 일련의 과정을 거쳐

주소창에서 이렇게 처리됨

파이어 베이스에는 이미지는 하나만 올라가고 오버라이드 된다.

 

25.7 Recap

 

버그픽싱

    );
    if (xfile != null) {
      final file = File(xfile.path);
      //await ref.read(avatarProvider.notifier).uploadAvatar(file);
      
      -------------------
      //(25.7)
      ref.read(avatarProvider.notifier).uploadAvatar(file);
            -------------------
    }
  }


              radius: 50,
              foregroundImage: hasAvatar
                  ? NetworkImage(
                                                      -------------------
                 
                 //(25.6)s%2F$    uid?  만들기
                      "https://media.licdn.com/dms/image/C4D03AQEfaXwimUdjmw/profile-displayphoto-shrink_100_100/0/1517469163893?e=1689811200&v=beta&t=0i37nt-0uT5JvkQGUUzQlZtA5fUFFYUVinTFDTyo0iQ")
                  // "https://firebasestorage.googleapis.com/v0/b/tiktok-clone-b524f.appspot.com/o/avatars%2F$uid?alt=media")
                              
                                             -------------------                  
                                                               -------------------
                                 //(25.7)
                      //"https://firebasestorage.googleapis.com/v0/b/tiktok-clone-b524f.appspot.com/o/avatars%2F$uid?alt=media")

                      "https://firebasestorage.googleapis.com/v0/b/tiktok-clone-b524f.appspot.com/o/avatars%2F$uid?alt=media&haha=${DateTime.now().toString()}")
                              -------------------
                              
                              
                  : null,
              child: Text(name),
            ),

?alt=media 뒤에

&haha=${DateTime.now().toString()} 추가

27.1에서 수정한대로 하면 될지도??

728x90
반응형
Comments