코딩하는 제리

[Flutter/Project](Instagram Clone) Stream과 MultiProvider를 이용한 유저 상태 표시 본문

Flutter/Project_InstaClone(완)

[Flutter/Project](Instagram Clone) Stream과 MultiProvider를 이용한 유저 상태 표시

JerryCho 2021. 2. 17. 13:02


Stream 메서드 생성 후 with Transformers
repo/helper/transformers.dart
main.dart 변경점. MultiProvider로 변경
firebase_auth_state.dart 변경점. User 데이터를 가져오기 위함.
profile_body.dart 파일 username 변경

 

Stream을 통해서 데이터가 바뀔때마다 불러옴.

파이어스토어의 username을 변경하는 즉시 앱에서도 변경됨.


소스코드 및 pubspec.yaml

// repo/helper/transformers.dart

import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_project_IJ/models/firestore/user_model.dart';

class Transformers {
  // DocumentSnapshot을 UserModel로 바꿈.
  // fromHandlers -> 생성자
  final toUser = StreamTransformer<DocumentSnapshot, UserModel>.fromHandlers(
      handleData: (snapshot, sink) async {
    // 변화된 snapshot을 sink에 넣어서 데이터를 보냄.
    sink.add(UserModel.fromSnapshot(snapshot));
  });
}
// main.dart

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_project_IJ/models/firebase_auth_state.dart';
import 'package:flutter_project_IJ/models/user_model_state.dart';
import 'package:flutter_project_IJ/repo/user_network_repository.dart';
import 'package:flutter_project_IJ/screens/auth_screen.dart';
import 'package:flutter_project_IJ/widgets/my_gallery.dart';
import 'package:flutter_project_IJ/widgets/my_progress_indicator.dart';
import 'package:provider/provider.dart';
import 'home_page.dart';
import 'constants/colors.dart';

void main() async {
  // 파이어베이스 사용을 위한 호출
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  FirebaseAuthState _firebaseAuthState = FirebaseAuthState();
  Widget _currentWidget;

  @override
  Widget build(BuildContext context) {
    _firebaseAuthState.watchAuthChange();
    return MultiProvider(
      providers: [
        // vallue() -> 기존에 생성되어 있는 인스턴스를 사용.
        ChangeNotifierProvider<FirebaseAuthState>.value(
          value: _firebaseAuthState,
        ),
        // (create:) -> 인스턴스 생성과 동시에 사용.
        ChangeNotifierProvider<UserModelState>(
          create: (_) => UserModelState(),
        )
      ],
      child: MaterialApp(
        home: Consumer<FirebaseAuthState>(
          builder: (BuildContext context, FirebaseAuthState firebaseAuthState,
              Widget child) {
            switch (firebaseAuthState.firebaseAuthStatus) {
              case FirebaseAuthStatus.signout:
                _currentWidget = AuthScreen();
                break;
              case FirebaseAuthStatus.singin:
                // 로그인이 되었을 때 UserModel 업데이트
                // 유저의 uid를 가져오려면 firebase_auth_state.dart 파일에서 User를 가져와야함.
                userNetworkRepository
                    .getUserModelStreams(firebaseAuthState.firebaseUser.uid)
                    .listen((userModel) {
                  Provider.of<UserModelState>(context, listen: false)
                      .userModel = userModel;
                  // notifyListeners()로 받아온 데이터라면, listen: false를 입력.
                });
                _currentWidget = HomePage();
                break;
              default:
                _currentWidget = MyProgressIndicator();
            }
            return AnimatedSwitcher(
              duration: Duration(milliseconds: 300),
              child: _currentWidget,
            );
          },
          child: HomePage(),
        ),
        theme: ThemeData(primarySwatch: white),
      ),
    );
  }
}
// widgets/profile_body.dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_project_IJ/constants/common_size.dart';
import 'package:flutter_project_IJ/constants/screen_size.dart';
import 'package:flutter_project_IJ/models/user_model_state.dart';
import 'package:flutter_project_IJ/screens/profile_screen.dart';
import 'package:flutter_project_IJ/widgets/rounded_avatar.dart';
import 'package:provider/provider.dart';

// 가독성을 위해서 사용.
enum SelectedTab { left, right }

class ProfileBody extends StatefulWidget {
  // 함수로 받아옴
  final Function onMenuChanged;

  const ProfileBody({Key key, this.onMenuChanged}) : super(key: key);

  @override
  _ProfileBodyState createState() => _ProfileBodyState();
}

class _ProfileBodyState extends State<ProfileBody>
    with TickerProviderStateMixin /* AnimationController를 사용하기 위해 추가 */ {
  // bool selectedLeft = true;
  SelectedTab _selectedTab = SelectedTab.left;

  double _leftImagesPageMargin = 0;
  double _rightImagesPageMargin = size.width;

  // AnimatedIcon을 사용하기위한 AnimationController
  AnimationController _iconAnimationController;

  @override
  void initState() {
    // 해당 state가 새로 생성되었을 때 실행.
    // this는 _ProfileBodyState 클래스를 가르킴
    // AnimationController를 사용하기 위해서 TickerProvider가 필요.
    // 해당 클래스에 with 키워드로 TickerProviderStateMixin를 사용.
    _iconAnimationController =
        AnimationController(vsync: this, duration: duration);
    super.initState();
  }

  @override
  void dispose() {
    // State가 제거될 때 실행.
    _iconAnimationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      // SafeArea() 디바이스 상단바와 거리를 띄움
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _appbar(),
          Expanded(
            // Expanded()로 CustomScrollView()가 빈공간을 차지하도록 해야 표시됨.
            child: CustomScrollView(
              // CustomScrollView() 스크롤 가능한 뷰들을 섞어서 사용할 때.
              slivers: <Widget>[
                SliverList(
                  delegate: SliverChildListDelegate([
                    Row(
                      children: [
                        Padding(
                          padding: const EdgeInsets.all(common_gap),
                          child: RoundedAvatar(
                            size: 80,
                          ),
                        ),
                        Expanded(
                          child: Padding(
                            padding: const EdgeInsets.only(right: common_gap),
                            child: Table(
                              children: [
                                TableRow(children: [
                                  _valueText('123'),
                                  _valueText('456'),
                                  _valueText('789'),
                                ]),
                                TableRow(children: [
                                  _labelText('Post'),
                                  _labelText('Followers'),
                                  _labelText('Following'),
                                ])
                              ],
                            ),
                          ),
                        ),
                      ],
                    ),
                    _username(context),
                    _userBio(),
                    _editProfileBtn(),
                    _tabButtons(),
                    _selectedIndicator(),
                  ]),
                ),
                _imagesPager(),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Row _appbar() {
    return Row(
      children: [
        SizedBox(width: 44),
        Expanded(
          child: Text(
            'jerrycho',
            textAlign: TextAlign.center,
          ),
        ),
        IconButton(
          // menu_close 메뉴 아이콘에서 X 모양 아이콘으로 변경하는 애니메이션 아이콘
          // AnimatedIcon을 사용하기 위해서는 AnimationController가 필요.
          icon: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _iconAnimationController,
          ),
          onPressed: () {
            // 아이콘이 선택될 때 함수를 실행.
            // widget. 다른 위젯 클래스의 데이터를 사용할 때.
            widget.onMenuChanged();

            /*
            애니메이션 상태가
            끝난 상태로 멈춰 있으면(completed)
            다시 돌아오게(reverse) 하고,
            시작전 상태로 멈춰 있으면(dismissed)
            시작하게(forward) 한다.
            */
            _iconAnimationController.status == AnimationStatus.completed
                ? _iconAnimationController.reverse()
                : _iconAnimationController.forward();
          },
        )
      ],
    );
  }

  Text _valueText(String value) => Text(
        value,
        style: TextStyle(fontWeight: FontWeight.bold),
        textAlign: TextAlign.center,
      );

  Text _labelText(String label) => Text(
        label,
        style: TextStyle(fontWeight: FontWeight.w300, fontSize: 12),
        textAlign: TextAlign.center,
      );

  SliverToBoxAdapter _imagesPager() {
    return SliverToBoxAdapter(
      // SliverToBoxAdapter() 일반적인 뷰를 Sliver 뷰로 변환.
      child: Stack(
        children: <Widget>[
          AnimatedContainer(
            // 애니메이션 길이 0.3초
            duration: duration,
            //transform: Matrix4.translationValues(x, y, z),
            transform: Matrix4.translationValues(_leftImagesPageMargin, 0, 0),
            curve: Curves.fastOutSlowIn,
            child: _images(),
          ),
          AnimatedContainer(
            // 애니메이션 길이 0.3초
            duration: duration,
            //transform: Matrix4.translationValues(x, y, z),
            transform: Matrix4.translationValues(_rightImagesPageMargin, 0, 0),
            curve: Curves.fastOutSlowIn,
            child: _images(),
          ),
        ],
      ),
    );
  }

  GridView _images() {
    return GridView.count(
      crossAxisCount: 3,
      physics: NeverScrollableScrollPhysics() /* GridView의 스크롤을 사용하지 않음 */,
      shrinkWrap: true /* 데이터의 양 만큼만 공간을 차지하게함 */,
      childAspectRatio: 1 /* 이미지 비율 */,
      children: List.generate(
        // List.generate() 리스트 생성
        30 /* 30개의 아이템 */,
        (index) => CachedNetworkImage(
          fit: BoxFit.cover,
          imageUrl: "https://picsum.photos/id/$index/100/100",
        ),
      ),
    );
  }

  Widget _selectedIndicator() {
    return AnimatedContainer(
      duration: duration,
      alignment: _selectedTab == SelectedTab.left
          ? Alignment.centerLeft
          : Alignment.centerRight,
      curve: Curves.fastOutSlowIn,
      child: Container(
        height: 5,
        width: size.width / 2,
        color: Colors.black87,
      ),
    );
  }

  Row _tabButtons() {
    return Row(
      children: [
        Expanded(
          child: IconButton(
            color: _selectedTab == SelectedTab.left
                ? Colors.black
                : Colors.black38,
            icon: ImageIcon(AssetImage('assets/images/grid.png')),
            onPressed: () {
              setState(() {
                // StatefulWidget의 상태변화 적용.
                _tabSelected(SelectedTab.left);
              });
            },
          ),
        ),
        Expanded(
          child: IconButton(
            color: _selectedTab == SelectedTab.left
                ? Colors.black38
                : Colors.black,
            icon: ImageIcon(AssetImage('assets/images/saved.png')),
            onPressed: () {
              setState(() {
                // StatefulWidget의 상태변화 적용.
                _tabSelected(SelectedTab.right);
              });
            },
          ),
        ),
      ],
    );
  }

  _tabSelected(SelectedTab selectedTab) {
    switch (selectedTab) {
      case SelectedTab.left:
        _selectedTab = SelectedTab.left;
        _leftImagesPageMargin = 0;
        _rightImagesPageMargin = size.width;
        break;
      case SelectedTab.right:
        _selectedTab = SelectedTab.right;
        _leftImagesPageMargin = -size.width;
        _rightImagesPageMargin = 0;
        break;
    }
  }

  Widget _editProfileBtn() {
    return Padding(
      padding: const EdgeInsets.symmetric(
        horizontal: common_gap,
        vertical: common_xxs_gap,
      ),
      child: SizedBox(
        height: 24,
        child: OutlineButton(
          onPressed: () {},
          borderSide: BorderSide(color: Colors.black45),
          shape: RoundedRectangleBorder(
            // RoundedRectangleBorder() 둥근 모서리 직사각형.
            borderRadius: BorderRadius.circular(6),
          ),
          child: Text(
            'Edit Profile',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        ),
      ),
    );
  }

  Widget _username(BuildContext context) {
    UserModelState userModelState = Provider.of<UserModelState>(context);
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: common_gap),
      child: Text(
        userModelState == null || userModelState.userModel == null
            ? ""
            : userModelState.userModel.username,
        style: TextStyle(fontWeight: FontWeight.bold),
      ),
    );
  }

  Widget _userBio() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: common_gap),
      child: Text(
        'username',
        style: TextStyle(fontWeight: FontWeight.w400),
      ),
    );
  }
}
// repo/user_network_repository.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_project_IJ/constants/firestore_keys.dart';
import 'package:flutter_project_IJ/models/firestore/user_model.dart';
import 'package:flutter_project_IJ/repo/helper/transformers.dart';

// with -> 해당 클래스의 모든 기능을 사용.
class UserNetworkRepository with Transformers {
  // 유저가 생성되어 있는지 확인
  Future<void> attempCreateUser({String userKey, String email}) async {
    final DocumentReference userRef =
        FirebaseFirestore.instance.collection(COLLECTION_USERS).doc(userKey);

    // 해당 레퍼런스의 데이터를 가져옴
    DocumentSnapshot snapshot = await userRef.get();

    // 해당 데이터가 존재하지 않으면 생성
    if (!snapshot.exists) {
      return await userRef.set(UserModel.getMapForeCreateUser(email));
    }
  }

  Stream<UserModel> getUserModelStreams(String userKey) {
    return FirebaseFirestore.instance
        .collection(COLLECTION_USERS)
        .doc(userKey)
        .snapshots()
        .transform(toUser);
    // transform -> 필요한 데이터 타입으로 변경시켜줌.
    /*
    get() -> 유저 데이터를 Future로 한 번만 가져옴.
    snapshots() -> 유저 데이터를 Stream으로 필요할 때 계속 가져옴.
    */
  }
}

UserNetworkRepository userNetworkRepository = UserNetworkRepository();
Comments