Dandy Now!
  • [Flutter] "Do it! 플러터 앱 프로그래밍" - 탭바와 리스트 만들기 | 탭바, 리스트뷰
    2022년 02월 13일 01시 50분 04초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    플러터 위젯 사용법

    "조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 5장을 실습하였다. 탭바와 리스트뷰는 스마트폰 화면에 정보를 효과적으로 보여주는 방법이다.

     

    Do it! 플러터 앱 프로그래밍

    플러터 기본 & 고급 위젯은 물론오픈 API와 파이어베이스를 이용한 앱 개발부터 배포까지!플러터 SDK 2.x 버전을 반영한 개정판!이 책은 플러터의 기초부터 고급 활용법까지 다루어 다양한 영역에

    book.naver.com

     

    탭바

    탭바는 화면 위나 아래에 놓을 수 있다. 모든 내용과 기능을 한 화면에 보여줄 수 없을 때 탭(tab)으로 연결한 탭바를 이용하면 관련 있는 내용끼리 묶을 수 있다.

    // main.dart
    import 'package:flutter/material.dart';
    import 'sub/firstPage.dart';
    import 'sub/secondPage.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'TabBar Example',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return _MyHomePageState();
      }
    }
    
    // SingleTickerProviderStateMixin는 _MyHomePageState 클래스에서 애니메이션 동작을 처리할 수 있게 한다. vsync: this 코드의 오류가 사라진다.
    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      TabController? controller;
    
      @override
      // initState함수를 이용해 TabController를 선언한다.
      void initState() {
        super.initState();
        // length는 탭수, vsync는 탭이 이동했을 때 호출되는 콜백 함수를 어디서 처리할지 지정한다.
        controller = TabController(length: 2, vsync: this);
      }
    
      @override
      Widget build(BuildContext context) {
        // Scaffold로 appBar, body, bottomNavigation를 선언한다.
        return Scaffold(
          appBar: AppBar(
            title: Text('TabBar Example'),
          ),
          // body에 TabBarView을 bottomNavigationBar에 TabBar 위젯을 넣는다.
          body: TabBarView(
            children: <Widget>[
              FirstApp(),
              SecondApp(),
            ],
            controller: controller,
          ),
          bottomNavigationBar: TabBar(
            tabs: <Tab>[
              Tab(
                icon: Icon(Icons.looks_one, color: Colors.blue),
              ),
              Tab(
                icon: Icon(Icons.looks_two, color: Colors.blue),
              )
            ],
            controller: controller,
          ),
        );
      }
    
      @override
      // 스테이이트풀 생명주기에서 위젯의 상태를 완전히 끝내도록 하여 메모리 누수를 막는다.
      void dispose() {
        controller!.dispose();
        super.dispose();
      }
    }

     

    // sub/firstPage.dart
    import 'package:flutter/material.dart';
    
    class FirstApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            child: Center(
              child: Text('첫 번째 페이지'),
            ),
          ),
        );
      }
    }

     

    // sub/secondPage.dart
    import 'package:flutter/material.dart';
    
    class SecondApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            child: Center(
              child: Text('두 번째 페이지'),
            ),
          ),
        );
      }
    }

     

    [그림 1] 탭바

     


     

    리스트뷰

    대부분의 앱이 세로로 긴 목록으로 화면을 구성한다. 이때 리스트뷰를 이용해 많은 정보를 보여 줄 수 있다.

    // main.dart
    import 'package:flutter/material.dart';
    import './animalItem.dart';
    import 'sub/firstPage.dart';
    import 'sub/secondPage.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Listview Example',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return _MyHomePageState();
      }
    }
    
    // SingleTickerProviderStateMixin는 _MyHomePageState 클래스에서 애니메이션 동작을 처리할 수 있게 한다. vsync: this 코드의 오류가 사라진다.
    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      TabController? controller;
      List<Animal> animalList = new List.empty(
          growable: true); // growable 이 리스트가 가변적으로 증가할 수 있다는 것을 의미한다.
    
      // animalList를 추가한 initState() 함수
      @override
      void initState() {
        super.initState();
        // length는 탭수, vsync는 탭이 이동했을 때 호출되는 콜백 함수를 어디서 처리할지 지정한다.
        controller = TabController(length: 2, vsync: this);
    
        animalList.add(Animal(
            animalName: "고양이", kind: "포유류", imagePath: "repo/images/cat.png"));
        animalList.add(Animal(
            animalName: "병아리", kind: "조류", imagePath: "repo/images/chicken.png"));
        animalList.add(
            Animal(animalName: "소", kind: "포유류", imagePath: "repo/images/cow.png"));
        animalList.add(
            Animal(animalName: "개", kind: "포유류", imagePath: "repo/images/dog.png"));
        animalList.add(Animal(
            animalName: "오리", kind: "조류", imagePath: "repo/images/duck.png"));
        animalList.add(Animal(
            animalName: "개구리", kind: "양서류", imagePath: "repo/images/frog.png"));
        animalList.add(Animal(
            animalName: "팬더", kind: "포유류", imagePath: "repo/images/panda.png"));
        animalList.add(Animal(
            animalName: "돼지", kind: "포유류", imagePath: "repo/images/pig.png"));
        animalList.add(Animal(
            animalName: "토끼", kind: "포유류", imagePath: "repo/images/rabbit.png"));
      }
    
      @override
      Widget build(BuildContext context) {
        // Scaffold로 appBar, body, bottomNavigation를 선언한다.
        return Scaffold(
          appBar: AppBar(
            title: Text('Listview Example'),
          ),
          // 탭바 처리
          body: TabBarView(
            children: <Widget>[
              FirstApp(list: animalList),
              SecondApp(),
            ],
            controller: controller,
          ),
          bottomNavigationBar: TabBar(
            tabs: <Tab>[
              Tab(
                icon: Icon(Icons.looks_one, color: Colors.blue),
              ),
              Tab(
                icon: Icon(Icons.looks_two, color: Colors.blue),
              )
            ],
            controller: controller,
          ),
        );
      }
    
      @override
      // 스테이이트풀 생명주기에서 위젯의 상태를 완전히 끝내도록 하여 메모리 누수를 막는다.
      void dispose() {
        controller!.dispose();
        super.dispose();
      }
    }

     

    // animalItem.dart
    import 'package:flutter/material.dart';
    
    // 동물 추가 클래스
    class Animal {
      String? imagePath;
      String? animalName;
      String? kind;
      bool? flyExist = false; // false를 기본값을 지정
    
      // Animal 생성자, required는 함수 호출시 꼭 전달해야 하는 값을 의미한다.
      Animal(
          {required this.animalName,
          required this.kind,
          required this.imagePath,
          this.flyExist});
    }

     

    // sub/firstPage.dart
    import 'package:flutter/material.dart';
    import '../animalItem.dart'; // Animal 클래스를 사용하기 위해 import
    
    class FirstApp extends StatelessWidget {
      final List<Animal>? list;
      FirstApp({Key? key, this.list})
          : super(key: key); // 이 생성자가 없으면 final의 list에 오류가 표시된다.
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            child: Center(
              // 전달 받은 list를 통해서 리스트뷰를 만든다. ListView.builder를 사용하려면 itemBuilder가 필요하다.
              child: ListView.builder(
                itemBuilder: (context, position) {
                  // GestureDetector로 터치 이벤트 처리
                  return GestureDetector(
                    child: Card(
                      child: Row(children: <Widget>[
                        Image.asset(
                          // position은 리스트에서 아이템의 위치를 나타낸다.
                          list![position].imagePath!,
                          height: 100,
                          width: 100,
                          fit: BoxFit.contain, // 이미지 안 잘리는 최대 크기, 비율 유지
                        ),
                        Text(list![position].animalName!)
                      ]),
                    ),
                    // GestureDetector 위젯의 onTap으로 한 번 터치했을 때 알림 창(showDialog)을 띄우도록 한다.
                    onTap: () {
                      AlertDialog dialog = AlertDialog(
                        content: Text(
                          '이 동물은 ${list![position].kind}입니다.',
                          style: TextStyle(fontSize: 30.0),
                        ),
                      );
                      showDialog(
                          context: context,
                          builder: (BuildContext context) => dialog);
                    },
                  );
                },
                // 아이템 개수만큼만 스크롤할 수 있게 제한
                itemCount: list!.length,
              ),
            ),
          ),
        );
      }
    }

     

    // sub/secondPage.dart
    import 'package:flutter/material.dart';
    import '../animalItem.dart'; // Animal 클래스를 사용하기 위해 import
    
    class SecondApp extends StatefulWidget {
      @override
      State<SecondApp> createState() => _SecondApp();
      List<Animal>? list;
      SecondApp({Key? key, @required this.list}) : super(key: key);
    }
    
    class _SecondApp extends State<SecondApp> {
      final nameController = TextEditingController();
      int? _radioValue = 0;
      bool? flyExist = false;
      String? _imagePath;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  // 사용자가 동물 이름을 입력할 텍스트필드를 하나 추가한다.
                  TextField(
                    controller: nameController,
                    keyboardType: TextInputType.text,
                    maxLines: 1,
                  ),
                  // 동물 종류 선택 라디오 버튼
                  Row(
                    mainAxisAlignment:
                        MainAxisAlignment.spaceAround, // 위젯들을 양쪽 여백 사이에 균일하게 배치
    
                    children: <Widget>[
                      // 라디오 버튼을 만들려면 value(인덱스 값), groupValue(그룹화: 라디오 버튼은 값은 그룹에서 하나만 선택할 수 있다), onChanged(이벤트 처리) 값을 지정해야 한다.
                      Radio(
                          value: 0,
                          groupValue: _radioValue,
                          onChanged: _radioChange),
                      Text('양서류'),
                      Radio(
                          value: 1,
                          groupValue: _radioValue,
                          onChanged: _radioChange),
                      Text('조류'),
                      Radio(
                          value: 2,
                          groupValue: _radioValue,
                          onChanged: _radioChange),
                      Text('포유류'),
                    ],
                  ),
                  // 날 수 있는지 선택 체크박스
                  Row(
                    mainAxisAlignment:
                        MainAxisAlignment.spaceAround, // 위젯들을 양쪽 여백 사이에 균일하게 배치
                    children: <Widget>[
                      Text('날 수 있나요?'),
                      Checkbox(
                          value: flyExist,
                          onChanged: (bool? check) {
                            setState(
                              () {
                                flyExist = check;
                              },
                            );
                          }),
                    ],
                  ),
                  // 기존 Row를 Container로 교체한다. 스크롤 달아 overflow 해결하기 위함이다.
                  // Row(
                  // mainAxisAlignment: MainAxisAlignment.spaceAround, // 위젯들을 양쪽 여백 사이에 균일하게 배치
                  Container(
                    height: 100,
                    child: ListView(
                      // 수평 스크롤
                      scrollDirection: Axis.horizontal,
                      children: <Widget>[
                        GestureDetector(
                          child: Image.asset(
                            'repo/images/cat.png',
                            width: 80,
                          ),
                          onTap: () {
                            _imagePath = 'repo/images/cat.png';
                          },
                        ),
                        GestureDetector(
                          child: Image.asset(
                            'repo/images/cow.png',
                            width: 80,
                          ),
                          onTap: () {
                            _imagePath = 'repo/images/cow.png';
                          },
                        ),
                        GestureDetector(
                          child: Image.asset(
                            'repo/images/dog.png',
                            width: 80,
                          ),
                          onTap: () {
                            _imagePath = 'repo/images/dog.png';
                          },
                        ),
                        GestureDetector(
                          child: Image.asset(
                            'repo/images/duck.png',
                            width: 80,
                          ),
                          onTap: () {
                            _imagePath = 'repo/images/duck.png';
                          },
                        ),
                        GestureDetector(
                          child: Image.asset(
                            'repo/images/frog.png',
                            width: 80,
                          ),
                          onTap: () {
                            _imagePath = 'repo/images/frog.png';
                          },
                        ),
                        GestureDetector(
                          child: Image.asset(
                            'repo/images/panda.png',
                            width: 80,
                          ),
                          onTap: () {
                            _imagePath = 'repo/images/panda.png';
                          },
                        ),
                        GestureDetector(
                          child: Image.asset(
                            'repo/images/pig.png',
                            width: 80,
                          ),
                          onTap: () {
                            _imagePath = 'repo/images/pig.png';
                          },
                        ),
                        GestureDetector(
                          child: Image.asset(
                            'repo/images/rabbit.png',
                            width: 80,
                          ),
                          onTap: () {
                            _imagePath = 'repo/images/rabbit.png';
                          },
                        ),
                      ],
                    ),
                  ),
                  ElevatedButton(
                      child: Text('동물 추가하기'),
                      onPressed: () {
                        var animal = Animal(
                            animalName: nameController.value.text,
                            kind: getKind(_radioValue),
                            imagePath: _imagePath,
                            flyExist: flyExist);
                        AlertDialog dialog = AlertDialog(
                          title: Text('동물 추가하기'),
                          content: Text(
                            '이 동물은 ${animal.animalName} 입니다.'
                            ' 또 동물의 종류는 ${animal.kind} 입니다.\n이 동물을 추가하시겠습니까?',
                            style: TextStyle(fontSize: 30.0),
                          ),
                          actions: [
                            ElevatedButton(
                              onPressed: () {
                                widget.list?.add(animal);
                                Navigator.of(context).pop();
                              },
                              child: Text('예'),
                            ),
                            ElevatedButton(
                              onPressed: () {
                                Navigator.of(context).pop();
                              },
                              child: Text('아니오'),
                            ),
                          ],
                        );
                        showDialog(
                            context: context,
                            builder: (BuildContext context) => dialog);
                      }),
                ],
              ),
            ),
          ),
        );
      }
    
      // Radio의 onChanged에서 사용하는 함수
      _radioChange(int? value) {
        setState(
          () {
            _radioValue = value;
          },
        );
      }
    
      getKind(int? radioValue) {
        switch (radioValue) {
          case 0:
            return "양서류";
          case 1:
            return "조류";
          case 2:
            return "포유류";
        }
      }
    }

     

    [그림 2] 동물 목록 화면(탭 1)

     

    [그림 3] 동물 추가 화면(탭 2)

    728x90
    반응형
    댓글