[Flutter] 스나이퍼팩토리 12일차 part2

KWANWOO·2023년 2월 9일
1
post-thumbnail

스나이퍼팩토리 플러터 12일차 part2

part1에 이어서 part2에서도 기존에 만들었던 앱을 업그레이드 할 것이다. part2에서는 1주차 주간평가로 진행했던 연락처 앱을 업그레이드 할 것이다.
[Flutter] 스나이퍼팩토리 1주차 주간평가 : 연락처 앱 기반 작성

학습한 내용

추가 내용 정리

BottomNavigationBar 페이지 연결

BottomNavigationBar에 위젯을 연결하여 아이템을 클릭했을 때 해당 위젯이 보이도록 할 수 있다. 하단 바의 선택된 아이템에 따라 화면이 다시 그려져야 하므로 StatefulWidget으로 작성해야 한다.

페이지를 연결하기 위해서는 우선 선택된 인덱스 번호인 selectedIndex 변수와 각 페이지 위젯을 담고 있는 리스트 widgetOptions를 선언한다.

추가로 setState를 사용하여 변경된 인덱스를 selectedIndex에 저장하는 함수인 onItemTapped()를 만든다.

BottomNavigationBar 위젯에서 currentIndex에 앞에서 만든 selectedIndex를 설정하고, onTap 이벤트에 onItemTapped()를 연결한다. 이렇게 하면 하단 바의 아이템을 클릭할 때마다 selectedIndex가 변경되고, 화면이 다시 그려질 수 있다.

본문의 내용에 widgetOptions.elementAt(selectedIndex)를 출력하면 원하는 위젯을 하단 바에 따라 다르게 띄울 수 있다.

아래는 하단 바에 따라 다른 Text 위젯을 출력하는 예시이다.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  static const String _title = 'Flutter Code Sample';

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: MyStatefulWidget(),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({super.key});

  
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _selectedIndex = 0;

  static const List<Widget> _widgetOptions = <Widget>[
    Text(
      'Index 0: Home',
      style: optionStyle,
    ),
    Text(
      'Index 1: Business',
      style: optionStyle,
    ),
    Text(
      'Index 2: School',
      style: optionStyle,
    ),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BottomNavigationBar Sample'),
      ),
      body: Center(
        child: _widgetOptions.elementAt(_selectedIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'Business',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.school),
            label: 'School',
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

하단 바에 따라 원하는 페이지를 각각 다른 파일로 생성하고 위의 예시에서 _widgetOptionsText위젯 대신 넣어주면 원하는 페이지를 연결할 수 있다.

더 많은 NavigationBottomBar의 페이지 연결 예시는 아래의 공식 문서를 참고
BottomNavigationBar class - Flutter

함수 이름 앞에 언더바(_)

바로 위의 BottomNavigationBar의 페이지 연결을 정리하다 보니 문득 궁금해진 것이 있다. 변수나 함수명은 여러 사람들이 알아보기 편하도록 정해놓은 코드 컨벤션이 존재하는데 주로 사용하는 Camel Case는 이해했지만 왜 함수나 변수명 맨 앞에 언더바를 작성할까...

우선 프로그래밍을 할 때 두 단어 이상을 조합할 때는 아래의 4가지 방법 중에 하나를 사용한다.(Dart는 보통 Camel Case 사용)

  1. 스네이크 케이스(Snake Case): snake_case
  2. 카멜 케이스(Camel Case): camelCase
  3. 케밥 케이스(Kebab Case): kebab-case
  4. 파스칼 케이스(Pascal Case: PascalCase

위의 내용 이외에 프로그래밍 언어에 따라 권장하는 코딩 컨벤션이 많이 존재하는데 일반적인 함수 이름에 관한 가이드는 아래와 같다.

함수 이름 스타일 가이드

  • 동사를 사용하여 작성한다. (ex. getElement(), setOption()...)
  • Private 메소드 인 경우 메서드 이름 앞에 언더스코어(_)를 사용한다.
  • 카멜 표기법을 준수한다. 복합어 이름은 첫 번째 단어를 소문자로 작성하고, 두 번째 이상의 단어 첫 글자를 대문자로 작성하여 단어를 구분한다.
  • 함수 이름의 첫 글자로 연속된 두 개의 언더스코어(__) 기호와 달러 기호($)는 사용하지 않는다.
  • Getter, Setter 메서드는 반드시 'get + 멤버 변수 이름', 'set + 멤버 변수 이름' 형식으로 작성한다. getElement(); isChecked(); setOption();
  • 이벤트 핸들러 메서드는 _on + 이벤트명으로 시작하도록 정의한다.

이러한 내용을 가지고 있는데 이를 통해서 위의 BottomNavigationBar 예시에서 Private 메소드나 변수이거나 이벤트 핸들러 앞에 언더바를 붙였다는 것을 알 수 있다.

아래는 Dart의 Style Guide 링크이다. 이를 참고해서 좀 더 이해하기 쉬운 작명을 해서 코드를 작성하도록 하자!!
Effective Dart: Style


12일차 과제 part2

  1. 연락처 앱 업그레이드하기
  2. 추가 사항

1. 연락처 앱 업그레이드하기

기존 연락처 앱 기반

기존에 작성했던 연락처 앱에 대한 내용은 아래의 링크를 통해 확인할 수 있다.
[Flutter] 스나이퍼팩토리 1주차 주간평가 : 연락처 앱 기반 작성

연락처 앱 업그레이드 사항

  • 스크린 제외 두 페이지가 존재합니다.

    • 연락처 상세보기 페이지, ContactDetailPage
    • 메인페이지, MainPage
  • MainPage는 3개의 스크린을 가집니다.

    • 연락처, ContactScreen
    • 통화기록, HistoryScreen
    • 설정, SettingScreen
  • BottomNavigationBar의 요소를 클릭시 해당 스크린으로 바뀌어 보여집니다.

  • ContactScreen의 커스텀위젯인 ContactTile을 누르면 연락처 상세보기 페이지로 이동됩니다.

  • ContactDetailPage 안에도 ContactTile 위젯이 포함되어 있습니다.

  • 결과물 예시

코드 작성

연락처 앱을 구성하기 위해 총 6개의 파일을 작성했으며 파일명은 아래와 같다.

  • main.dart
  • ContactTile.dart
  • PhoneDetailPage.dart
  • PhoneBookPage.dart
  • HistoryPage.dart
  • SettingsPage.dart
  • main.dart
import 'package:flutter/material.dart';
import 'package:my_app/HIstoryPage.dart';
import 'package:my_app/PhoneBookPage.dart';
import 'package:my_app/SettingsPage.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _selectedIndex = 0; // 하단 바 선택 인덱스

  //하단 바 연결 페이지리스트
  final List<Widget> _widgetOptions = [
    PhoneBookPage(),
    HistoryPage(),
    SettingsPage()
  ];

//하단 바 onTap 핸들러
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      // 앱바
      appBar: AppBar(
        title: Text('내 연락처'),
        actions: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Icon(Icons.search),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Icon(Icons.more_vert),
          ),
        ],
      ),
      body: SafeArea(
        child: _widgetOptions.elementAt(_selectedIndex), //본문 내용 연결
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: Icon(Icons.add),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex, // 선택된 인덱스 연결
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.contact_phone), label: '연락처'),
          BottomNavigationBarItem(icon: Icon(Icons.history), label: '통화기록'),
          BottomNavigationBarItem(icon: Icon(Icons.settings), label: '설정'),
        ],
        onTap: _onItemTapped, // 하단 바 아이템 선택 이벤트
      ),
    );
  }
}

하단 바를 페이지들과 연결하기 위해 _selectedIndex_widgetOptions를 선언하고, 하단 바의 onTap이벤트를 수행할 _onItemTapped를 작성했다. _onItemTapped에서는 setState를 사용해 다시 build를 할 수 있도록 했고, 선택된 인덱스를 _selectedIndex 변수에 넣어주었다.

앱바를 만들고, 본문의 내용에서 SafeArea 위젯 안에 child로 _widgetOptions.elementAt(_selectedIndex)를 통해 하단 바의 선택된 아이템에 따른 페이지가 출력되도록 했다. FAB도 기능은 없지만 만들어 주었다.

하단 바에서는 currentIndex_selectedIndex를 설정하고, onTap이벤트에 _onItemTapped를 연결해 주었다. 아이템은 페이지 수와 맞게 3개를 생성했다.

  • ContactTile.dart
import 'package:flutter/material.dart';
import 'package:my_app/PhoneDetailPage.dart';

class ContactTile extends StatelessWidget {
  const ContactTile(
      {super.key,
      required this.name,
      required this.phone,
      required this.imgUrl});

  final String name; // 이름
  final String phone; // 전화번호
  final String imgUrl; // 이미지 Url

  //페이지 이동 라우트 생성
  Route _createRoute(Widget page) {
    return PageRouteBuilder(
      pageBuilder: (context, animation, secondaryAnimation) => page,
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        var begin = Offset(1.0, 0.0); //오른쪽 위 시작 지점
        var end = Offset.zero; //왼쪽 위 끝 지점
        var curve = Curves.ease;

        var tween =
            Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

        return SlideTransition(
          position: animation.drive(tween),
          child: child,
        );
      },
    );
  }

  
  Widget build(BuildContext context) {
    return ListTile(
      //길게 누르는 이벤트(연락처 상세보기 페이지로 이동)
      onLongPress: () {
        Navigator.of(context).push(_createRoute(
            PhoneDetailPage(name: name, phone: phone, imgUrl: imgUrl)));
      },
      title: Text(name),
      subtitle: Text(phone),
      leading: CircleAvatar(
          //원형 이미지 생성
          radius: 24,
          backgroundImage: NetworkImage(imgUrl)),
      trailing: Icon(Icons.call),
    );
  }
}

ContactTile은 사람 한 명의 전화번호 정보로 사람의 이름인 name, 전화번호인 phone 그리고 이미지 Url인 imgUrl을 전달 받아 리스트 타일을 그려준다.

ListTile에서는 길게 눌렀을 때 연락처 상세보기 페이지로 이동할 수 있도록 onLongPress 이벤트에서 네이게이터를 작성했다. 페이지 이동은 12일차 part1에서 키오스크 앱과 마찬가지로 페이지가 오른쪽에서 왼쪽으로 슬라이딩되는 애니메이션을 적용하기 위해 같은 함수를 만들어 사용했다. 자세한 페이지 이동 애니메이션 아래의 12일차 part1의 링크를 참고
[Flutter] 스나이퍼팩토리 12일차 part1

title subtitle leading은 전달 받은 데이터를 사용해 작성했고, trailing은 전화기 아이콘을 그렸다.

  • PhoneDetailPage.dart
import 'package:flutter/material.dart';
import 'package:my_app/ContactTile.dart';

class PhoneDetailPage extends StatelessWidget {
  const PhoneDetailPage(
      {super.key,
      required this.name,
      required this.phone,
      required this.imgUrl});

  final String name; // 이름
  final String phone; // 전화번호
  final String imgUrl; //이미지 URL

  
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true, //본문 내용을 앱바까지 확장
      appBar: AppBar(
        backgroundColor: Colors.transparent, //배경 투명
        elevation: 0, // 그림자 없음
        title: Text('연락처 상세'),
        actions: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Icon(color: Colors.grey, Icons.more_vert), //더보기 아이콘
          ),
        ],
      ),
      body: Column(
        children: [
          Container(
            height: 220,
            decoration: BoxDecoration(
              image: DecorationImage(
                fit: BoxFit.fill,
                image: NetworkImage(imgUrl),
              ),
            ),
          ),
          ContactTile(name: name, phone: phone, imgUrl: imgUrl),
        ],
      ),
    );
  }
}

연락처 상세 페이지는 ContactTile을 길게 눌렀을 때 이동되는 페이지로 name phone imgUrl을 전달받는다.

리턴되는 위젯은 Scaffold에서 본문 내용이 앱바 까지 확장 되도록 설정하고, 앱바를 생성했다. 앱바는 투명하게하고 그림자를 제거한 뒤 titleactions를 추가했다.

본문에서는 선택된 사람의 이미지를 크게 띄우고 그 아래에 ContactTile이 출력되도록 Column으로 구성했다.

  • PhoneBookPage.dart
import 'package:flutter/material.dart';
import 'package:my_app/ContactTile.dart';

class PhoneBookPage extends StatelessWidget {
  PhoneBookPage({super.key});

  // 전화번호부 리스트
  var phoneList = [
    {
      'name': '이테디',
      'phone': '010-1000-2000',
      'imgUrl': 'https://picsum.photos/100/100'
    },
    {
      'name': '밀리',
      'phone': '010-1000-2100',
      'imgUrl': 'https://picsum.photos/100/101'
    },
    {
      'name': '크리스',
      'phone': '010-1000-2200',
      'imgUrl': 'https://picsum.photos/100/102'
    },
    {
      'name': '주노',
      'phone': '010-1000-2300',
      'imgUrl': 'https://picsum.photos/100/103'
    },
    {
      'name': '햬리',
      'phone': '010-1000-2400',
      'imgUrl': 'https://picsum.photos/100/104'
    },
    {
      'name': '벨라',
      'phone': '010-1000-2500',
      'imgUrl': 'https://picsum.photos/100/105'
    },
    {
      'name': '헬렌',
      'phone': '010-1000- 2600',
      'imgUrl': 'https://picsum.photos/100/106'
    },
  ];

  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: phoneList.length,
      itemBuilder: (context, index) {
        return ContactTile(
          name: phoneList[index]['name']!,
          phone: phoneList[index]['phone']!,
          imgUrl: phoneList[index]['imgUrl']!,
        );
      },
    );
  }
}

하단 바의 첫 번째 화면 위젯에서는 전화번호부 정보를 가진 리스트르 생성하고, 해당 리스트의 정보들을 활용해 ListView.builder로 화면을 구성했다.

각 전화번호 정보들은 앞에서 작성한 ConTactTile을 사용했다.

  • HistoryPage.dart
import 'package:flutter/material.dart';

class HistoryPage extends StatelessWidget {
  HistoryPage({super.key});

//통화 내역 리스트
  var historyList = [
    {'name': '이테디', 'icon': 'north_east'},
    {'name': '이테디', 'icon': 'call_missed'},
    {'name': '이테디', 'icon': 'call_missed'},
    {'name': '이테디', 'icon': 'north_east'},
    {'name': '이테디', 'icon': 'call_missed'},
    {'name': '이테디', 'icon': 'call_missed'},
    {'name': '이테디', 'icon': 'north_east'},
    {'name': '이테디', 'icon': 'call_missed'},
    {'name': '이테디', 'icon': 'call_missed'},
  ];

  
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.all(8.0),
      itemCount: historyList.length,
      itemBuilder: ((context, index) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(historyList[index]['name'].toString()), // 이름
              //발신인 경우 아이콘
              if (historyList[index]['icon'] == 'north_east')
                Icon(Icons.north_east),
              //부재중인 경우 아이콘
              if (historyList[index]['icon'] == 'call_missed')
                Icon(Icons.call_missed),
            ],
          ),
        );
      }),
    );
  }
}

하단 바의 두 번째 페이지로 통화 내역을 보여준다. 통화 내역은 리스트로 저장하고, 본문에서는 ListView.builder로 만들었다.

각 통화 내역은 Row를 사용해 사람의 이름과 유형을 나타내는 아이콘을 출력했다. 여기서 아이콘은 if문을 사용해 발신일 경우와 부재중일 경우를 다르게 그려주었다.

  • SettingsPage.dart
import 'package:flutter/material.dart';

class SettingsPage extends StatelessWidget {
  const SettingsPage({super.key});

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('차단목록'),
          SizedBox(height: 24),
          Text('벨소리설정'),
          SizedBox(height: 24),
          Text('전화통계'),
        ],
      ),
    );
  }
}

하단 바의 세 번째 페이지로 설정 내용들을 보여준다. 하지만 아직 특별한 기능을 수행하지는 않기 때문에 Column을 사용해 텍스트로 세 가지 내역을 출력해 주었다.

결과

2. 추가 사항

part1과 마찬가지로 강의 수강 후 아쉬웠던 점 몇 가지를 추가로 정리하고자 한다.

요구사항 확인

왜 그런지 모르겠지만 클래스 이름을 내 맘대로 만들었는데 요구사항에 정해진 것이 있었다. 크게 문제가 되는 것은 아니지만 요구사항을 제대로 확인하자!

아래와 같이 클래스를 변경하면 좋을 것 같다.

  • MyHomePage -> MainPage
  • PhoneBookPage -> ContactScreen
  • HistoryPage -> HistoryScreen
  • SettingsPage -> SettingScreen
  • PhoneDetailPage -> ContactDetailPage

파일 구조와 이름

part1에서와 마찬가지로 main.dartMyApp 위젯과 MyHomePage 위젯을 다른 파일로 나누면 좋을 것 같다.

또한 Scaffold로 구성된 화면은 Page로 관리하고 네비게이션 바의 아이템에 따른 위젯은 Screen으로 관리하면 좋다. 이에 맞게 파일 이름과 클래스 이름도 작명하는 것이 코드를 이해하기 더 쉽다. (파일명에서 페이지와 스크린은 스네이크 케이스로 작성하고, 소규모 위젯은 파스칼 케이스로 작성해 보자)

참고) 파스칼 케이스란 카멜 케이스와 비슷하지만 첫 시작을 대문자로 시작한다는 점이 다르다.

페이지와 스크린, 소규모 위젯을 따로 폴더를 생성해주면 파일 구조가 더욱 보기 좋을 것이다.

위의 방식을 모두 포함하면 이번에 진행한 과제는 lib폴더 안에서 아래와 같은 구조를 갖게 될 것이다.

lib
	ㄴ page
    	ㄴ main_page.dart
        ㄴ contact_detail_page.dart
        
    ㄴ screen
    	ㄴ contact_screen.dart
        ㄴ history_screen.dart
        ㄴ setting_screen.dart
        
    ㄴ widget
    	ㄴ ContactTile.dart
        
    ㄴ main.dart

오늘은 포스팅 두 개!!

후기가 이제 점점 비슷해 지는거 같다. ㅋㅋㅋㅋ 이번 과제는 지금까지 중에서 파일을 가장 많이 생성해 본 과제였다. 6개로 나눠서 프로그램을 만들었는데 파일을 나누니 코드도 짧아지고 정돈된 느낌이라 보기가 편한것 같다. 포스팅 방식도 원래 코드 넣고 결과 넣은 다음에 코드 설명을 썼는데 이번에는 파일 별로 코드 넣고 코드 설명 쓰고를 모든 파일에 대해 한 다음에 결과를 마지막에 넣어보았다. 이 방식이 더 깔끔하고 좋은것 같다. 진작 이렇게 할껄....😓

📄 Reference

profile
관우로그

0개의 댓글

관련 채용 정보