Flutter

[Flutter] Instagram Clone Coding (인스타그램 클론코딩) 4 (Responsive 화면)

모리선생 2023. 7. 31. 23:24
728x90

Clone coding 2,3에서 보면 이런 부분이 있다.

Navigator.of(context).pushReplacement(
        MaterialPageRoute(
          builder: (context) => const ResponsiveLayout(
            mobileScreenLayout: MobileScreenLayout(),
            webScreenLayout: WebScreenLayout(),
          ),
        ),
      );
      
      (...)

혹시라도 Clone coding 2, 3번을 보시지 않은 분은 다음의 링크를 통해 확인 가능하다.

[Flutter] Instagram Clone Coding (인스타그램 클론코딩) 2: https://riris01.tistory.com/76

[Flutter] Instagram Clone Coding (인스타그램 클론코딩) 3: https://riris01.tistory.com/77

 

여튼 해당 내용은 로그인이 성공하였을때 혹은 등록에 성공하였을때, mobileScreenLayout이건 webScreenLayout이건 플랫폼의 사이즈만 확인이 되면 해당 규격으로 화면을 불러오라는 것이다. 그래서 아마, 이 부분에 대한 내용이 없어서 매우 당황을 했을 수도 있다.

 

자, 우리는 바햐으로 다양한 화면 속에서 살고 있다.

 

직장에서는 노트북을 사용하고 있고 넷플릭스를 보기 위해서는 태블릿이 필요하다 (설마 태블릿을 공부용으로 쓴다는 변명은 하지 않겠지).

그리고 매일 출근하는 길에 들여다보고 있는 휴대폰 화면 이 3가지로 크게 분류할 수 있을것이다. 그런데 말이다. 이게 참, 화면이라는게 디스플레이 크기대로 자기가 알아서 늘었다 줄었다 아니면 아이콘을 배치하면 참 좋은데 그렇지가 않다.

 

코딩은!!!! 겉으로는 멋있지만 사실상 한땀한땀 코드를 적어주는 수.작.업의 극치이기 때문이다.

그러게 왜 이런 험한길을 택했니.

자 그럼, 화면이 큰지 작은지 구분하는 로직하나와 그럼 화면이 클때는 어떤 배치를 보여줄지 아니라면 작을때는 어떤 배치를 보여줄지를 생각해보면 된다.

 

자, 그럼 일단 폴더를 만들자.

우리는 폴더 만드는 장인이 되어야한다. 폴더 하나를 빚어보자.

 

일단 lib/responsive 폴더를 하나 만들고, 그안에 다음의 3개를 만들자

mobile_screen_layout.dart

responsive_layout_screen.dart

web_screen_layout.dart

 

딱 제목을 보면 감이 오지 않나? responsive_layout_screen.dart는 화면의 사이즈에 따라 어떻게 화면 분류를 할지 알려주는 파일이고 그 외의 두개의 파일은 실제 화면이 어떻게 반영이 될지 레이아웃을 나타내는 것이다.

 

자, 그럼 lib/responsive/responsive_layout_screen.dart에는 다음의 코드를 집어넣자.

import 'package:flutter/material.dart';
import 'package:instagram_flutter/providers/user_provider.dart';

import 'package:provider/provider.dart';
import 'package:instagram_flutter/utils/global_variables.dart';

class ResponsiveLayout extends StatefulWidget {
  final Widget webScreenLayout;
  final Widget mobileScreenLayout;
  const ResponsiveLayout(
      {Key? key,
      required this.webScreenLayout,
      required this.mobileScreenLayout})
      : super(key: key);

  @override
  State<ResponsiveLayout> createState() => _ResponsiveLayoutState();
}

class _ResponsiveLayoutState extends State<ResponsiveLayout> {
  @override
  void initState() {
    super.initState();
    addData();
  }

  addData() async {
    UserProvider _userProvider = Provider.of(context, listen: false);
    await _userProvider.refreshUser();
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth > webScreenSize) {
          return widget.webScreenLayout;
        }
        return widget.mobileScreenLayout;
      },
    );
  }
}

자 차근히 보면 ResponsiveLayout이라는 클래스를 StatefulWidget으로 정의해준다. 왜? 정보를 받아서 분배를 하는거니까. 자 여기서 유저의 정보를 받아주기 위해서는 addData()를 통해 user의 정보를 refresh할때마다 받아오도록 하고!

addData() async {
    UserProvider _userProvider = Provider.of(context, listen: false);
    await _userProvider.refreshUser();
  }

webScreenSize보다 크거나 작을때를 기준으로 어떤 화면을 반환할지 조건을 적어주면 된다.

Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth > webScreenSize) {
          return widget.webScreenLayout;
        }
        return widget.mobileScreenLayout;
      },
    );
  }

이게 전부이다. 생각보다 쉽지 않은가? 이게 나도 코린이 이지만, 코딩이라는건 생각을 얼만큼 잘게 쪼개어서 표현할 수 있는가가 매우 중요한 부분이라는게 요즘 절실히 느껴진다.

 

(근데 왜 코딩을 계속 잘 못하는걸까...)

누가 내 자서전을 적었냐

자 그럼, 이제 기준을 나누는 방법을 잡아놨으니, 기준대로 구분이 되었을때 어떤 화면이 나오면 되는지 레이아웃을 잡아 주자.

 

lib/responsive/mobile_screen_layout.dart에 다음의 코드를 넣어주자.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:instagram_flutter/utils/colors.dart';
import 'package:instagram_flutter/utils/global_variables.dart';

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;

  @override
  void initState() {
    super.initState();
    pageController = PageController();
  }

  @override
  void dispose() {
    super.dispose();
    pageController.dispose();
  }

  void navigationTapped(int page) {
    pageController.jumpToPage(page);
  }

  void onPageChanged(int page) {
    setState(() {
      _page = page;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        children: homeScreenItems,
        physics: NeverScrollableScrollPhysics(),
        controller: pageController,
        onPageChanged: onPageChanged,
      ),
      bottomNavigationBar: CupertinoTabBar(
        backgroundColor: mobileBackgroundColor,
        items: [
          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,
      ),
    );
  }
}

인스타그램은 Bottom 부분에 4-5개의 탭을 가지고 있었다(예전엔 말이다). 그러다 보니 그 탭바를 구현할 필요가 있어서 이렇게 탭을 만들 수 있는 Layout을 잡아 주었다. navigationTapped와 onPageChnaged 메서드를 만들어서 각 탭을 누르면 해당 페이지로 이동하는 탭바를 만들었다. 

 

그리고 color는 평소에는 primaryColor였다가, 탭을 누르면 secondaryColor로 나타나도록 하였다. 그럼 websize는 무엇이 다른가? 웹버전에는 앱바를 넣었다. 바로 이렇게! (사진 엑박은 이해해주길 바란다. 링크 문제를 아직 해결안했을때이다.)

기존의 앱버전에서는 앱바가 bottom에 있지만 이제는 위에 있다. 그렇다면 그말인 즉슨 앱바를 추가하면 된다는 것이다.

 

자 그럼 앱바를 추가한 web버전은 다음과 같다.

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:instagram_flutter/utils/colors.dart';
import 'package:instagram_flutter/utils/global_variables.dart';

class WebScreenLayout extends StatefulWidget {
  const WebScreenLayout({Key? key}) : super(key: key);

  @override
  State<WebScreenLayout> createState() => _WebScreenLayoutState();
}

class _WebScreenLayoutState extends State<WebScreenLayout> {
  int _page = 0;
  late PageController pageController;

  @override
  void initState() {
    super.initState();
    pageController = PageController();
  }

  @override
  void dispose() {
    super.dispose();
    pageController.dispose();
  }

  void navigationTapped(int page) {
    pageController.jumpToPage(page);
    setState(() {
      _page = page;
    });
  }

  void onPageChanged(int page) {
    setState(() {
      _page = page;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: mobileBackgroundColor,
        centerTitle: false,
        title: SvgPicture.asset(
          'assets/ic_instagram.svg',
          color: primaryColor,
          height: 32,
        ),
        actions: [
          IconButton(
            onPressed: () => navigationTapped(0),
            icon: Icon(
              Icons.home,
              color: _page == 0 ? primaryColor : secondaryColor,
            ),
          ),
          IconButton(
            onPressed: () => navigationTapped(1),
            icon: Icon(
              Icons.search,
              color: _page == 1 ? primaryColor : secondaryColor,
            ),
          ),
          IconButton(
            onPressed: () => navigationTapped(2),
            icon: Icon(
              Icons.photo,
              color: _page == 2 ? primaryColor : secondaryColor,
            ),
          ),
          IconButton(
            onPressed: () => navigationTapped(3),
            icon: Icon(
              Icons.favorite,
              color: _page == 3 ? primaryColor : secondaryColor,
            ),
          ),
          IconButton(
            onPressed: () => navigationTapped(4),
            icon: Icon(
              Icons.person,
              color: _page == 4 ? primaryColor : secondaryColor,
            ),
          ),
        ],
      ),
      body: PageView(
        physics: const NeverScrollableScrollPhysics(),
        children: homeScreenItems,
        controller: pageController,
        onPageChanged: onPageChanged,
      ),
    );
  }
}

지금까지는 그렇게 어렵지 않았을 것이라 믿는다. 사실 이게, 생각한 것을 구현해내는 과정이라 아이디어와 코드 정렬하는 것이 매우 중요하다. 자 지금까지 Responsive layout 분리 및 화면 레이아웃 구성을 알아보았다.

 

그럼 급작스럽게 빠이

728x90