앱 프로그래밍/플러터(Flutter)

TabBar와 리스트뷰

0_TLS 2025. 1. 17. 16:25

TabBar를 이용한 페이지 분기처리


1. lib/sub/firstPage.dart 파일 생성

 

TabBar_example이라는 이름으로 새로운 플러터 프로젝트 생성 -> lib 폴더 아래에 sub 폴더를 추가한 후 firstPage.dart파일을 만듦

 

2. lib/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('첫 번째 페이지'),
        ),
      ),
    );
  }
}

Stateless Widget을 상속받는 FirstApp 클래스를 생성한다.

그 다음 스캐폴드 위젯 안에 body Text에 '첫 번째 페이지' 라고 입력.

 

3. lib/sub/secondPage.dart 파일 작성

 

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('두 번째 페이지'),
        ),
      ),
    );
  }
}

 

4. lib/main.dart 탭 컨드롤러 선언

 

TabBar 위젯을 사용하려면 탭 컨드롤러가 필요하다

=> main.dart 파일에서 작업

class _MyHomePageState extends State<MyHomePage> {

  TabController controller;
  @override
  void initState() {
    super.initState();
    controller = TabController(length: 2, vsync: this);

TabController에서 length에 몇 개의 탭을 만들지 정하고,

vsync에는 탭이 이동했을 때 호출되는 콜백 함수를 어디서 처리할지 지정하도록 한다.

 

현재 코드에서 vsync:this 코드에서는 오류가 발생하는데 탭 컨트롤러는 여러 화면을 이동하는 역할을 하기 때문에 기본적으로 애니메이션을 사용한다. 그런데 this가 가리키는 _MyHomePageState 클래스에 이와 관련된 코드가 빠졌기 때문에 오류가 발생하는 것이다.

=> _MyHomePageState 클래스에 with 키워드를 추가하고 SingleTickerProviderStateMixin 클래스를 지정해준다.

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{

  TabController controller;
  @override
  void initState() {
    super.initState();
    controller = TabController(length: 2, vsync: this);
    controller.addListener(() {
      if(!controller.indexIsChanging){
        print("이전 index, ${controller.previousIndex}");
        print("현재 index, ${controller.index}");
        print("전체 탭 길이, ${controller.length}");        
      }
    });
  }

 

with를 이용해 SingleTickerProviderStateMixin 클래스를 추가로 상속

=> 탭을 눌렀을 때 _MyHomePageState 클래스에서 애니메이션 동작을 처리할 수 있게 함

 

만약, SingleTickerProviderStateMixin 클래스를 상속에 포함하지 않으면 _MyHomePageState 클래스에서 TabController를 만들 수 없기 때문에 주의해야 한다.


TabBar를 이용한 페이지 분기처리 실습

 

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: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{

  TabController controller;
  @override
  void initState() {
    super.initState();
    controller = TabController(length: 2, vsync: this);
    controller.addListener(() {
      if(!controller.indexIsChanging){
        print("이전 index, ${controller.previousIndex}");
        print("현재 index, ${controller.index}");
        print("전체 탭 길이, ${controller.length}");        
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('TabBar Example'),
        ),
        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();
  }
}

 

 

이런식으로 length에 설정한 길이랑 TabBar에 있는 children의 페이지의 개수가 맞는게 중요! 

맞지 않으면 에러가 발생할 수 있다.


리스트뷰를 이용한 앱 구현


1. 리스트 뷰를 이용한 앱 구현 절차

1. 이미지 추가

2. Animal 클래스 만들기

=> lib 폴더에 animalItem.dart 파일을 만듦

import 'package:flutter/material.dart';

class Animal{
  String imagePath;
  String animalName;
  String kind;
  bool flyExist = false;

  Animal({  @required this.animalName,@required this.kind,@required this.imagePath, this.flyExist});

}
flyExist 논리형 변수 : false를 기본값으로 해놓고 새로운 Animal 객체를 선언할 때 이를 입력 받음.

매개변수 앞에 붙은 애너테이션 @required는 함수를 호출할 때 꼭 전달해야 하는 값이라는 뜻.

 

3. List 선언

List를 선언할 때 처음에는 빈값이므로 List.empty, 그리고 growable:true로 선언한다.

growable : 리스트가 가변적으로 증가할 수 있다는 것을 의미

 

4. Animal 객체 생성

=> 각 동물의 정보를 입력해 Animal 객체를 생성하고 동물목록 animalList에 추가한다.

@override
  void initState() {
    super.initState();
    controller = TabController(length: 2, vsync: this);
    
    animalList.add(Animal(animalName: "벌", kind: "곤충",
        imagePath: "repo/images/bee.png"));
    animalList.add(Animal(animalName: "고양이", kind: "포유류",
        imagePath: "repo/images/cat.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/fox.png"));
    animalList.add(Animal(animalName: "원숭이", kind: "영장류",
        imagePath: "repo/images/monkey.png"));
    animalList.add(Animal(animalName: "돼지", kind: "포유류",
        imagePath: "repo/images/pig.png"));
    animalList.add(Animal(animalName: "늑대", kind: "포유류",
        imagePath: "repo/images/wolf.png"));

 

5. ListView.builder 사용

itemBuilder : BuildContext와 int를 반환
BuildContext : 위젯 트리에서 위젯의 위치를 알려줌
Int : 아이템의 순번을 의미

리스트뷰를 이용한 앱 구현 실습

 

lib/sub/firstPage.dart

import 'package:flutter/material.dart';

import '../animalItem.dart';

class FirstApp extends StatelessWidget {
  final List<Animal> list; // Animal List 선언
  FirstApp({Key key, this.list}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Center(
          child: ListView.builder(
              itemBuilder: (context, position) {
                return GestureDetector(
                  child:  Card(
                    child: Row(
                      children: <Widget>[
                        Image.asset(list[position].imagePath , height: 100, width: 100, fit: BoxFit.contain,),
                        Text(list[position].animalName),
                      ],
                    ),
                  ),
                  onTap: (){
                    AlertDialog dialog = AlertDialog(
                      content: Text('이 동물은 ${list[position].kind} 입니다' , style: TextStyle(fontSize: 30.0),),
                    );
                    showDialog(
                        context: context, builder: (BuildContext context) => dialog);
                  },
                  onLongPress: (){
                    list.removeAt(position);
                  },
                );
              },
              itemCount: list.length),
        ),
      ),
    );
  }
}

 

 

lib/sub/secondPage.dart

import 'package:flutter/material.dart';
import '../animalItem.dart';

class SecondApp extends StatefulWidget {
  List<Animal> list;

  SecondApp({Key key, @required this.list}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _SecondApp();
}

class _SecondApp extends State<SecondApp> {
  int _radioValue = 0;
  final nameController = TextEditingController();
  bool flyExist = false;
  var _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(children: <Widget>[
                Radio(
                    value: 0, groupValue: _radioValue, onChanged: _radioChange),
                Text('양서류'),
                Radio(
                    value: 1, groupValue: _radioValue, onChanged: _radioChange),
                Text('포충류'),
                Radio(
                    value: 2, groupValue: _radioValue, onChanged: _radioChange),
                Text('포유류'),
              ], mainAxisAlignment: MainAxisAlignment.spaceAround),
              Row(children: <Widget>[
                Text('날수 있나요?'),
                Checkbox(
                    value: flyExist,
                    onChanged: (check) {
                      setState(() {
                        flyExist = check;
                      });
                    })
              ], mainAxisAlignment: MainAxisAlignment.spaceAround),
              Container(
                height: 100,
                child: ListView(
                  scrollDirection: Axis.horizontal,
                  children: <Widget>[
                    GestureDetector(
                      child: Image.asset('repo/images/cow.png', width: 80),
                      onTap: () {
                        _imagePath = 'repo/images/cow.png';
                      },
                    ),
                    GestureDetector(
                      child: Image.asset('repo/images/pig.png', width: 80),
                      onTap: () {
                        _imagePath = 'repo/images/pig.png';
                      },
                    ),
                    GestureDetector(
                      child: Image.asset('repo/images/bee.png', width: 80),
                      onTap: () {
                        _imagePath = 'repo/images/bee.png';
                      },
                    ),
                    GestureDetector(
                      child: Image.asset('repo/images/cat.png', width: 80),
                      onTap: () {
                        _imagePath = 'repo/images/cat.png';
                      },
                    ),
                    GestureDetector(
                      child: Image.asset('repo/images/fox.png', width: 80),
                      onTap: () {
                        _imagePath = 'repo/images/fox.png';
                      },
                    ),
                    GestureDetector(
                      child: Image.asset('repo/images/monkey.png', width: 80),
                      onTap: () {
                        _imagePath = 'repo/images/monkey.png';
                      },
                    ),
                  ],
                ),
              ),
              RaisedButton(
                  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: [
                        RaisedButton(onPressed: (){
                          widget.list.add(animal);
                          Navigator.of(context).pop();
                        } , child: Text('예'),),
                        RaisedButton(onPressed: (){
                          Navigator.of(context).pop();
                        } , child: Text('아니요'),),
                      ],
                    );
                    showDialog(
                        context: context,
                        builder: (BuildContext context) => dialog);

                  })
            ],
          ),
        ),
      ),
    );
  }

  _radioChange(int value) {
    setState(() {
      _radioValue = value;
    });
  }

  getKind(int radioValue) {
    switch (radioValue) {
      case 0:
        return "양서류";
      case 1:
        return "파충류";
      case 2:
        return "포유류";
    }
  }

}

 

lib/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: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  
  final String title;

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

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{
  TabController controller;
  List<Animal> animalList = List();

  @override
  void initState() {
    super.initState();
    controller = TabController(length: 2, vsync: this);
    animalList.add(Animal(animalName: "벌", kind: "곤충",
        imagePath: "repo/images/bee.png"));
    animalList.add(Animal(animalName: "고양이", kind: "포유류",
        imagePath: "repo/images/cat.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/fox.png"));
    animalList.add(Animal(animalName: "원숭이", kind: "영장류",
        imagePath: "repo/images/monkey.png"));
    animalList.add(Animal(animalName: "돼지", kind: "포유류",
        imagePath: "repo/images/pig.png"));
    animalList.add(Animal(animalName: "늑대", kind: "포유류",
        imagePath: "repo/images/wolf.png"));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Listview Example'),
        ),
        body: TabBarView(
          children: <Widget>[
            FirstApp(list: animalList),
            SecondApp(list: animalList)
          ],
          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();
  }
}