정보
- 업무명 : [Flutter] 쉽게 배우는 플러터 앱 개발 실무 05강
- 작성자 : 이상호
- 작성일 : 2025.01.11
- 설 명 :
- 수정이력 :
Dart 언어 III
학습목표
- Python, Java, Javascript로 배운 프로그래밍의 기본 원리와 개념과 의미를 기억해 봅니다.
- 이미 알고 있는 프로그래밍 지식을 확장하여 Dart 언어를 이해합니다.
Dart 객체 지향 프로그래밍
- 객체지향이란?
- 클래스란?
- 클래스 만들기
- 멤버 변수(Member Variable)은 객체 내에 정의된 변수로, 객체의 상태나 데이터를 저장하는 데 사용되어 객체의 특징이나 특성을 나타내는 것으로 흔히 속성(Property)라고 부릅니다.
- 객체의 기능을 나타내는 메소드(Method)는 객체 내에 정의된 함수로, 멤버 함수(Member Function)로 볼 수 있습니다.
- 생성자(Constructor)는 객체의 데이터를 초기화하는 일을 해 줍니다. 변수에 값이 저장되어 있어야 프로그램이 수행되듯이 객체에도 변수에 값이 있어서 그 값들을 기반으로 특정한 기능을 수행할 수있습니다.
객체 만들기
- Person("Kildong", 10)와 같이 생성자 파라메터 순서에 맞게 값을 넣어 변수를 만들듯이 객체를 만듭니다. boy.name 혹은 boy.age와 같이 객체이름.멤버변수의 형태로 호출하여 사용할 수 있습니다.
메소드 호출
위치 파라메터 (Positional Parameter)
이름있는 파라메터(Named Parameter)
- 파라메터에 이름을 붙일 수는 없을까요? 생성자 정의시 Person({required this.name, required this.age})와 같이 인자들을 중괄호({})로 감싸고 인자의 앞에 required 키워드를 붙입니다. 그리고 객체를 만들때 마치 딕셔너리 혹은 맵을 만들때와 같이 Person(name: "Kildong", age: 10)의 형식으로 파라메터의 이름 뒤에 콜론(:)을 찍은 후 인자에 넘겨줄 값을 기술합니다.
생략가능한 파라메터(Optional Parameter)
- this.age=1과 같이 이름있는 파라메터에 required 키워드를 생략하고 초기값을 부여하면 생략가능한 파라메터를 만들수 있습니다. Person(name: "Kildong")와 같이 인자에 값을 넘기는 것을 생략하면 초기값이 대신 사용됩니다.
- int? age와 같이 파라메터를 Nullable 변수로 정의하고 생략가능한 파라메터에 초기값을 지정하지 않으면 초기값대신 null 값으로 지정됩니다.
- 예제. 위의 코드를 위치 파라메터와 이름있는 파라메터를 섞어서 코딩해 보세요.
캡슐화(Encapsulation)/정보은폐(Information Hiding)
외부 공개(public)와 외부 비공개(private)
- 일반적으로 외부 공개(public)와 외부 비공개(private)는 클래스 내부에서만 사용되는가 혹은 클래스 외부에서도 사용될 수 있는가를 말하는데 Dart는 같은 파일에서 사용이 가능한 것을 의미합니다. Dart는 다른 언어와 달리 private나 public과 같은 키워드를 제공하지 않으며 변수나 함수의 이름에 _(언더바)를 붙이면 외부 비공개(private)로 인식하고 그렇지 않으면 외부 공개(public)로 인식합니다. Python과 같이 접근 제어를 강제하지 않는 언어도 있지만, 특별히 외부에 공개하지 않을 변수나 함수를 외부 비공개로 만드는 것은 여전히 클래스간 그리고 파일간 변수의 간섭을 차단하기 때문에 안전한 코딩 습관이 됩니다.
상속(Inheritance)
믹스인(Mix In)
추상화(Abstraction)
- 클래스(Class)가 붕어빵틀이고 객체(Object)는 붕어빵이라는 비유에서 따온 추상화(Abstraction)가 미완의 붕어빵틀이라는 비유는 적절하지 않으며, 추상화(Abstraction)는 철학, 수학 및 과학분야에서 유사 이래로 꾸준히 사용되어온 "본질적인 특성을 도출하여 단순화”하는 것을 의미하는 것으로 개념화(Conceptualization)에 가까운 표현입니다.
- 예제. 위의 코드에서 클래스의 이름을 Circle로 변경하고, Shape 추상 클래스를 상속받아 Rectangle 클래스를 만들고 사각형의 면적을 구하는 코드로 수정하세요.
- 추상 클래스(Abstract Class)를 개념적인 비행기에 비유한다면, 클래스(Class)는 설계된 보잉 747에 비유할 수 있고, 객체(Object)는 제작된 보잉 747 비행기 자체에 비유할 수 있습니다.
- Instructor Note : 클래스와 객체에 대하여 배웠으니 이제 Flutter가 기본적으로 제공하는 코드에서 클래스가 정의된 형태를 살펴보며 이해해 볼 것
실습 - 음악 밴드 만들기
현대식 프로그래밍 언어의 공통적 특징
절차적 프로그래밍 (Procedural Programming)
- 프로그램 문장을 순서대로 배치하여 코드로 문제를 해결해가는 방식입니다. 변수의 값을 바꾸는 예제로 각 언어가 어떻게 공통적인 코드 형태를 가지는지 확인해 보겠습니다. 섭섭하겠지만 코드를 실행해 보기 어려운 JavaScript는 예를 들지 않겠습니다.
// Dart 코드
void main() {
String a = "첫번째 문자열";
String b = "두번째 문자열";
print("a : " + a + ", b : " + b);
String temp;
temp = a;
a = b;
b = temp;
print("a : " + a + ", b : " + b);
}
- Python은 변수의 자료형을 지정하지 않아도 변수에 값을 할당할 때 할당되는 값의 자료형에 맞게 자동으로 변수의 자료형이 결정됩니다. 이는 코딩의 생산성을 높여 주나 오류 해결을 어렵게 합니다.
# Python 코드
a = "첫번째 값"
b = "두번째 값"
print("a : " + a + ", b : " + b)
temp = a
a = b
b = temp
print("a : " + a + ", b : " + b)
// Java 코드
package com.example.demo_maven_mybatis;
public class Test {
public static void main(String[] args) {
String a = "첫번째 값";
String b = "두번째 값";
System.out.println("a : " + a + ", b : " + b);
String temp;
temp = a;
a = b;
b = temp;
System.out.println("a : " + a + ", b : " + b);
}
}
- 프로그램 문장의 순서를 논리에 맞추어 나열하는 것이 절차적 프로그래밍의 핵심입니다. 필요에 따라 if 문장으로 선택적 구조, for나 while 문장으로 반복적 구조를 구현할 수 있습니다.
객체지향 프로그래밍 (Object Oriented Programming)
- 객체지향프로그래밍은 변수와 메소드를 객체안에 독립적으로 배치함으로써 코드의 독립성과 재활용성을 높여 복잡도가 높은 프로그램을 개발할 수 있게 해 줍니다. 간단한 객체를 만들어 반복하여 사용해 보겠습니다.
// Dart 코드
class Car { // 클래스 정의
String brand; // 필드 (속성)
String model;
int year;
Car(this.brand, this.model, this.year); // 생성자
void displayInfo() { // 메서드 (동작)
print('Car: $brand $model, Year: $year');
}
}
void main() {
Car yourCar = Car('Tesla', 'Model S', 2023); // Car 객체 생성
yourCar.displayInfo(); // Car 객체의 메서드 호출
Car myCar = Car('기아', 'K5 Hybrid', 2020); // Car 객체 생성
myCar.displayInfo(); // Car 객체의 메서드 호출
}
# Python 코드
class Car: # 클래스 정의
def __init__(self, brand, model, year): # 생성자
self.brand = brand # 필드 (속성)
self.model = model
self.year = year
def display_info(self): # 메서드 (동작)
print(f'Car: {self.brand} {self.model}, Year: {self.year}')
your_car = Car('Tesla', 'Model S', 2023) # Car 객체 생성
your_car.display_info() # Car 객체의 메서드 호출
my_car = Car('기아', 'K5 Hybrid', 2020) # Car 객체 생성
my_car.display_info() # Car 객체의 메서드 호출
// Java 코드
package com.example.demo_maven_mybatis;
class Car { // 클래스 정의
private String brand; // 필드 (속성)
private String model;
private int year;
public Car(String brand, String model, int year) { // 생성자
this.brand = brand;
this.model = model;
this.year = year;
}
public void displayInfo() { // 메서드 (동작)
System.out.println("Car: " + brand + " " + model + ", Year: " + year);
}
}
public class Test { // 메인 클래스
public static void main(String[] args) {
Car yourCar = new Car("Tesla", "Model S", 2023); // Car 객체 생성
yourCar.displayInfo(); // Car 객체의 메서드 호출
Car myCar = new Car("기아", "K5 Hybrid", 2020); // Car 객체 생성
myCar.displayInfo(); // Car 객체의 메서드 호출
}
}
- 처음에는 객체를 만들어 사용하기보다 만들어져서 제공되는 객체의 활용에 초점을 맞추면 쉽게 객체지향프로그래밍에 적응해 갈 수 있습니다. 실제 업무 현장에서도 객체를 만들어 사용하는 것보다 만들어진 객체를 사용하는 경우가 더 많습니다. 제공되는 객체의 속성 변수와 메소드가 어떤 것들이 있고 어떤 역할을 하는지를 이해하고 사용하는 것이 객체지향 프로그래밍의 핵심입니다.
- 객체를 직접 만들어 사용하는 것은 쉽지 않은데 많은 초보자들이 처음부터 객체를 만드는 기술을 배우기 위하여 Java와 Spring을 어려워하는 이유가 됩니다.
함수형 프로그래밍 (Functional Programming)
- 함수형 프로그래밍은 코드의 가독성, 유지보수성, 병렬 처리 등을 향상시키는 다양한 장점을 제공합니다. 최근에는 이름이 없는 한줄 함수인 람다 함수를 사용하여 코딩의 편의성과 간결성을 크게 향상시키고 있습니다. 리스트 변수에 2개의 간단한 함수를 적용하는 예를 들어 보겠습니다.
- Dart언어는 JavaScript의 var이나 let의 경우와 같이 자료형을 명시적으로 지정하지 않으면 할당되는 값에 의해 자료형이 결정되는 var 키워드를 가지고 있습니다.
// Dart 코드
int addTen(int number) { // 일반 함수 정의
return number + 10;
}
void main() {
var numbers = [1, 2, 3, 4, 5];
var adds = numbers.map(addTen); // 일반 함수 적용
print(adds);
adds = numbers.map((num) { return num + 10; }); // 익명 함수 적용
print(adds);
adds = numbers.map((num) => num + 10); // 람다 함수 적용
print(adds);
}
# Python 코드
def add_ten(number): # 일반 함수 정의
return number + 10
numbers = [1, 2, 3, 4, 5]
adds = list(map(add_ten, numbers)) # 일반 함수 적용
print(adds)
squares = list(map(lambda num: num * num, numbers)) # 람다 함수 적용 / 익명 함수 적용
print(squares)
- 함수형 프로그래밍에서 가장 이해하기 어려운 것이 람다함수입니다. 기호도 익숙하지 않고 언듯 암호처럼 보이지만 현대의 프로그래밍 언어들은 대부분 함수형 프로그래밍과 람다함수를 제공하니 꼭 위의 코드 형태를 기억하고 있어야 합니다.
선언적 프로그래밍 (Declarative Programming)
- 앞에서 설명한 절차적 프로그래밍, 객체지향 프로그래밍 및 함수형 프로그래밍 기법을 모두 사용하여도 복잡도가 매우 높은 프로그래밍을 하는 경우에는 한계에 부딛힙니다. 이런 어려움을 간단하게 해결해 주는 프로그래밍 기법이 선언적 프로그래밍입니다.
- 어노테이션(Annotation)이나 데코레이터(Decorator)라는 메타 데이터 태그를 클래스, 함수, 또는 변수에 붙이면, 프레임워크나 라이브러리가 이를 읽어 자동으로 특정 기능을 수행하도록 설정할 수 있습니다. 이로 인해 프로그래머는 수동으로 많은 코드를 작성하지 않고도 원하는 기능을 구현할 수 있습니다.
- 어노테이션(Annotation)이나 데코레이터(Decorator)는 프로그래밍 언어마다 사용하는 프레임워크마다 많이 다르기 때문에 공통된 예를 들지 않고 사용의 형태만 간단히 예를 들겠습니다. 따라서 위에서 예를 든 코드들과 달리 실행해 볼 수 없습니다.
// Dart 코드
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
# Python 코드
from flask import Flask, request, json
app = Flask(__name__)
@app.route('/receive-data', methods=['POST'])
def receive_data():
received_data = request.get_json()
// Java 코드
package com.example.demo_maven_mybatis.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/sample")
public class SampleController {
@RequestMapping("/aaa")
public void aaa() {
System.out.println("@RequestMapping : /sample/aaa");
}
@RequestMapping("/bbb")
public void bbb() {
System.out.println("@RequestMapping : /sample/bbb");
}
}
- 선언적 프로그래밍은 매우 강력하지만 프로그래머의 통제 범위를 벗어나 있기 때문에 반드시 해당 프로그래밍 언어나 프레임워크의 지침대로 사용해야 합니다. 그리고 프레임워크나 라이브러리가 이를 읽어 자동으로 특정 기능을 수행하도록 설정하기 때문에 오류가 발생할 경우 원인을 찾기가 매우 어렵습니다. 그러나 이제는 선언적 프로그래밍을 모르면 코딩을 할 수 없는 시대가 되었습니다. 오류가 발생한다면 혹은 원하는대로 동작하지 않는다면 인터넷 검색과 생성형 AI의 도움 외에 이전에 오류를 경험하고 해결한 경험자의 도움이 필요할지도 모릅니다.
상태 관리
학습목표
- 프로그램의 상태(State)에 대하여 이해합니다.
- StatelessWidget과 StatefulWidget의 차이와 언제 어떤 Widget을 적용하는지 이해합니다.
- StatefulWidget에서 변수에 값이 변경될 때 상태 변경을 Widget에 알려 주는 이유와 방법을 이해합니다.
- StatefulWidget에서만 동작하는 BottomNavigationBar Widget을 사용하여 화면 이동을 구현해 봅니다.
State란?
- 프로그램의 상태는 프로그램의 실행 과정에서 달라지는 변수들의 현재 값들을 의미합니다. 상태는 프로그램의 동작과 연관되며, 프로그램의 목표는 특정 상태에 도달하는 것입니다. 예를 들어, 사용자의 로그인 여부, 장바구니에 담긴 상품 목록, 또는 게시글에 달린 댓글 수 등이 프로그램의 상태가 될 수 있습니다. 이러한 상태들은 프로그램의 흐름에 따라 변하며, 이를 효과적으로 관리하는 것이 중요한 과제입니다.
- 상태에 대한 논의를 Widget으로 확장해 보겠습니다. navigation 프로젝트에서 만들었던 MySecondPage와 MyThirdPage 화면 Widget은 상태가 있을까요? 없습니다. 항상 고정된 화면이 나타나니까요.
- 그러면 first_flutter 프로젝트에서 Flutter 프레임워크가 자동으로 만들었던 MyHomePage 화면 Widget은 상태가 있을까요? 있습니다. 우측 하단에 위치한 버튼을 누르면 숫자가 1씩 증가하니까요.
- 안드로이드 스튜디오 편집 화면에서 stless 코드 조각(Code Snippet)으로 Widget을 자동 생성하면
- 아래 화면과 같이 StatelessWidget 클래스를 상속한(extends) 객체가 1개로 구성된 Widget이 만들어 지는데 이 Widget이 상태가 없는 상수 형태의 Widget입니다.
- 안드로이드 스튜디오 편집 화면에서 stful 코드 조각(Code Snippet)으로 Widget을 자동 생성하면
- 아래 화면과 같이 StatefulWidget 클래스를 상속한(extends) 객체가 2개로 구성된 Widget이 만들어지는데 이것이 상태가 있는 변수 형태의 Widget입니다.
- Python은 상태가 없는 변수인 상수의 개념이 없고 상태가 있는 변수인 일반 변수만 있는데 Flutter도 상태가 있는 Widget 한가지 방식으로 통일하면 좋을텐데 도대체 왜 이러는걸까요??? 그 이유는 스마트폰이 성능이 비교적 낮은 장비라는데에 있습니다.
- const나 final 키워드를 사용해서 상태가 있는 변수가 아닌 상태가 없는 상수로 만들어 놓으면 스마트폰에 부하를 덜 걸게 되어 성능이 향상되는 것처럼, 상태가 있는 StatefulWidget을 상속하여 사용하지 않고 상태가 없는 StatelessWidget을 상속하여 사용해도 마찬가지로 앱의 성능이 향상됩니다.
StatefulWidget에서 상태를 바꾸는 방법
- 또 다시 Flutter 프레임워크가 기본적으로 제공했던 코드를 소환하겠습니다.
- 위의 코드를 보면 int _counter = 0;로 변수가 하나 정의되어 있습니다. 앱 프로그램이 아닌 일반적인 프로그램이라면 _counter++; 문장으로 프로그램의 상태가 변경될 것입니다. 그러나 우리는 Flutter 앱 프로그램을 사용하고 있습니다. Flutter 앱 프로그램은 변수의 값이 바뀌어도 화면을 그에 맞게 다시 그려주지 않으면 우리 눈에 바뀐 변수이 값을 보여 줄 수 없습니다. 그렇게 하려면 바뀐 변수의 값을 보여 주기 위하여 화면을 그려주는 아래 화면과 같은 build() 메소드가 다시 수행되어야 합니다. 변수가 변했으니 화면을 다시 그리라고 StatefulWidget에 신호를 보내주는 함수가 setState() 함수입니다.
- 이제는 그 동안 사용하기만 해 왔던 build() 메소드에 대하여 말할 수 있습니다. 사용할 Widget으로 이동하면 그 Widget의 build() 메소드가 수행되어 화면을 그립니다. 이때 화면에 대한 정보는 context에 들어 있습니다. 반환값은 Widget인데 그래서 build() 메소드는 return 문장을 사용하여 그리고 싶은 Widget 반환하는 것입니다.
- StatefulWidget의 경우 StatelessWidget에서 한번만 수행되던 build() 메소드가 Widget의 상태가 변할 때마다 다시 수행되는 것입니다.
- 예제. first_flutter 프로젝트의 build 함수의 하단에 print(”build 함수가 수행됩니다.”); 문장을 추가하기 바랍니다. 그리고 _incrementCounter() 함수에서 setState() 함수를 주석 처리한 후 _counter++; 문장만 남겨 놓은 뒤에 + 버튼을 클릭하며 화면상의 숫자가 변화하여 나타나는지와 안드로이드 스튜디오의 하단에 “build 함수가 수행됩니다.” 메세지가 출력되는지 관찰해 보기 바랍니다. 다시 주석 처리를 원복한 후 + 버튼을 클릭하며 같은 방법으로 관찰해 보기 바랍니다. 그러면 Flutter의 상태 관리의 의미를 이해하게 될 것 입니다.
- Widget의 상태가 변경될 때 반복수행하는 build() 메소드와 함께 Widget 생성시 한번만 수행되는 initState() 메소드와 Widget 소멸시 한번만 수행되는 dispose() 메소드의 존재도 알고 있어야 합니다. 필요시 build() 메소드를 @override하여 사용했듯이 initState() 메소드와 dispose() 메소드를 @override하여 사용하기 바랍니다.
- 엄밀하게 따지면 Flutter의 상태관리는 아래 화면과 같은 복잡한 구조를 가지는데 자주 사용되는 것만 떼어서 생각하면 위의 2개의 화면과 같이 단순화하여 이해하는 것으로 시작하면 좋습니다.
Flutter 프레임워크 코드 분석 II
- 이제 상태관리까지 이해하여 Dart와 Flutter의 전반적인 내역들을 알게 되었으니 Flutter 프레임워크가 만들어준 코드를 분석해 봅시다. 이번에는 큰 흐름에 맞추어 이해하는 단계를 넘어 세부적인 사항까지 집중하여 이해해 봅시다.
- Instructor Note : 아래의 코드를 second_flutter 프로젝트로 만들어 전체적으로 리뷰해 볼 것. TRY THIS로 표기된 것을 주석대로 바꾸며 프레임워크가 제공한 코드를 사용할 때의 이점을 느껴볼 것.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
BottomNavigationBar를 사용한 화면 이동
- 이제 Widget의 상태에 대하여 이해를 했으니 StatefulWidget으로 동작하는 몇가지 유용한 Widget들을 사용해 보겠습니다.
- 먼저 bottom_navigation_bar 프로젝트를 만듭시다.
- Flutter가 제공해준 main.dart 파일의 _MyHomePageState 클래스로 이동하여 Scaffold Widget에 floatingActionButton 속성을 지정하는 코드를 삭제하고, bottomNavigationBar 속성 코드를 추가합니다.
// 변경후 코드
... 생략 ...
class _MyHomePageState extends State<MyHomePage> {
... 중략 ...
/*
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
*/
bottomNavigationBar: BottomNavigationBar( // bottomNavigationBar 시작
items: [
BottomNavigationBarItem(
icon: Icon(Icons.looks_two, color: Colors.blue),
label: "2nd Page",
),
BottomNavigationBarItem(
icon: Icon(Icons.looks_3, color: Colors.blue),
label: "3rd Page",
),
],
), // bottomNavigationBar의 끝
);
- 프로젝트를 실행해 보면 왼쪽 화면과 같이 FloatingActionButton으로 나타나던 것이 오른쪽 화면과 같이 하단에 네비게이션(Navigation,화면 이동)을 위한 숫자 버튼과 하단에 레이블이 추가된 형태 즉, BottomNavigationBar로 나타나는 것을 확인할 수 있습니다. 프로그램의 변경이 Hot Reload할 수준을 넘어섰기 때문에 재시작해 주어야 합니다.
- 이제 화면 하단에 숫자로된 네비게이션 버튼이 눌릴 때마다 나타나는 변경을 화면에 표시하도록 수정해 봅시다. 아래의 코드에서 눈여겨 보아야 할 것은 void _incrementCounter()에는 인자가 없는데 새로 만들어진 _onBottomNavigationItemTapped(index)에는 index라는 인자가 되었다는 차이인데 이것은 Flutter 프레임워크의 BottomNavigationBar가 이벤트 처리기에 index 정보를 인자로 자동으로 넘겨 주게 되어 있기 때문에 생긴 변경입니다. Flutter 프레임워크 내부적으로 처리하는 동작이기 때문에 다른 프로그램의 코드를 보고 코딩하는 방법을 알아 내야 합니다.
// 변경후 코드
class _MyHomePageState extends State<MyHomePage> {
/* _counter 변수 정의와 이벤트 처리기 삭제
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
*/
int _selectedNaviIndex = 0; // _selectedNaviIndex 변수 정의와 이벤트 핸들러 시작
_onBottomNavigationItemTapped(index) {
setState(() {
_selectedNaviIndex = index;
});
} // _selectedNaviIndex 변수 정의와 이벤트 핸들러의 끝
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
//'You have pushed the button this many times:',
'선택한 페이지의 Index :', // 변경된 코드
),
Text(
//'$_counter',
'$_selectedNaviIndex', // 변경된 코드
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
onTap: _onBottomNavigationItemTapped, // 추가된 코드
items: [
BottomNavigationBarItem(
icon: Icon(Icons.looks_two, color: Colors.blue),
label: "2nd Page",
),
BottomNavigationBarItem(
icon: Icon(Icons.looks_3, color: Colors.blue),
label: "3rd Page",
],
),
);
}
}
- 이제 프로젝트를 실행하고 하단의 숫자 버튼을 클릭하면 클릭되는 항목에 따라 Index의 값이 0과 1로 변경되는 것을 확인할 수 있습니다. 0이 BottomNavigationBar 첫번째 항목의 Index이고, 1이 두번째 항목의 Index입니다. 그래서 아래의 왼쪽 화면이 숫자 2 버튼을 누른 경우의 실행화면이고, 오른쪽 화면이 숫자 3 버튼을 누른 경우의 실행화면인 것을 알 수 있습니다.
- 항목이 2개인 BottomNavigationBar가 만들어졌으니 body에 사용할 페이지 2개가 필요합니다. 따로 만들지 않고 navigation 프로젝트에서 만들어 놓은 MySecondPage와 MyThirdPage를 복사해 사용하도록 하겠습니다.
- navigation 프로젝트에서 my_second_page.dart 파일과 my_third_page.dart 파일을 복사(Copy)하여
- bottom_navigation_bar 프로젝트의 lib 폴더에 붙여넣습니다 (Paste).
- 다른 프로젝트에서 소스 코드를 복사해 놓았으니 혹시 오류가 발생하지는 않을까요?
- 와!!! my_second_page.dart 파일에서도 my_third_page.dart 파일에서도 오류인 것을 알려 주는 빨간색이 보이지 않습니다. 이유는 프로젝트 내부의 dart 파일을 import할 때에 import 'my_third_page.dart'; 문장과 같이 상대경로를 사용하여 import했기 때문입니다.
- 그럼 절대 경로를 사용한다면 어떤 일이 생길까요? navigation 프로젝트에 my_second_page.dart 파일을 복사한 후 my_fourth_page.dart 파일을 만들어
- 상대경로인 import 'my_third_page.dart'; 문장을 주석처리한 후 의도적으로 소스 코드에 import 'package:navigation/my_third_page.dart';와 같은 절대 경로 import 문장으로 변경한 후 동일한 방식으로 my_fourth_page.dart 파일을 bottom_navigation_bar 프로젝트의 lib 폴더로 복사해 보겠습니다.
- 그랬더니 이런 아래 화면과 같이 오류가 발생합니다. 당연하지요. 절대경로에는 package:navigation/와 같은 패키지 이름이 포함되어 있거든요.
- 상대 경로는 개발 환경과 운영 환경에 독립적이라는 것을 코드 조각과 실행 화면으로 보여 드려 보았습니다. 그러면 앞으로 불필요한 파일에 의하여 오류가 파생되는 것을 막기 위하여 두 프로젝트에서 my_fourth_page.dart 파일을 삭제합시다.
- 그러면 파일들을 복사해 온 후 프로그램이 정상적으로 동작하는지 실행하여 확인해 봅시다. 정상적으로 잘 동작합니다. my_second_page.dart 파일과 my_third_page.dart 파일을 복사해 놓았지만 main.dart 파일과 연결되는 코드가 없으니 기존 화면이 그대로 나타나게 됩니다.
- 이제 선택된 Index가 0이면 MySecondPage를 보여 주고, Index가 1이면 MyThirdPage를 보여 주도록 코드를 수정해 봅시다.
- 우선 main.dart 파일의 _MyHomePageState 클래스에서 아래 코드와 같이 변경해 봅시다.
// 변경후 코드
... 생략 ...
/*
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
//'You have pushed the button this many times:',
'선택한 페이지의 Index :',
),
Text(
//'$_counter',
'$_selectedNaviIndex',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
*/
body: MyThirdPage(),
... 생략 ...
- 그리고 실행해 보면 어떤 BottomNavigationBarItem을 누르면 항상 세번째 페이지가 나타납니다.
- 이 문제를 해결하기 위해서는 호출할 화면의 리스트를 만들고 앞에서 클릭한 BottomNavigationBarItem의 인덱스를 저장해 놓은 _selectedNaviIndex 변수를 사용하여 선택된 화면을 보여 주게 코드를 보완하여야 하겠습니다.
// 변경후 코드
class _MyHomePageState extends State<MyHomePage> {
var pages = [ MySecondPage(id: 320811, password: "mypassword"),
MyThirdPage()
]; // body에 사용할 페이지들의 리스트 정의
int _selectedNaviIndex = 0;
_onBottomNavigationItemTapped(index) {
setState(() {
_selectedNaviIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
//body: MyThirdPage(),
body: pages[_selectedNaviIndex], // pages 리스트에서 선택된 화면을 출력
bottomNavigationBar: BottomNavigationBar(
onTap: _onBottomNavigationItemTapped,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.looks_two, color: Colors.blue),
label: "2nd Page",
),
BottomNavigationBarItem(
icon: Icon(Icons.looks_3, color: Colors.blue),
label: "3rd Page",
),
],
),
);
}
}
- 실행시키면 화면의 형태가 각각 오른쪽과 같습니다.
- Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → 주석이 부여된 순서로 보여주면 됨
- BottomNavigationBar가 보이는 형태를 조금만 다듬어 보겠습니다.
// 변경후 코드
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: false, // Label 제거
showUnselectedLabels: false,
currentIndex: _selectedNaviIndex, // 선택된 Color와 선택되지 않은 Color 구분
selectedItemColor: Colors.blue, // Icon Widget에서 color 속성을 제거
unselectedItemColor: Colors.grey,
// 배경색을 AppBar에서 복사하여 AppBar와 통일
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
onTap: _onBottomNavigationItemTapped,
items: [
BottomNavigationBarItem(
//icon: Icon(Icons.looks_two, color: Colors.blue), // color 속성 삭제
icon: Icon(Icons.looks_two),
label: "2nd Page",
),
BottomNavigationBarItem(
//icon: Icon(Icons.looks_3, color: Colors.blue), // color 속성 삭제
icon: Icon(Icons.looks_3),
label: "3rd Page",
),
],
),
- 실행해 보니 오른쪽 화면과 같이 나타납니다.
- Scaffold Widget의 bottomNavigationBar 속성으로 지정할 수 있는 객체로는 BottomNavigationBar Widget 외에 TabBar Widget을 이용하는 화면 이동 기법도 있으니 필요할 때 필요한 만큼 찾아서 사용하기 바랍니다.
기본 Widget II
학습목표
- ListView Widget의 사용법과 사용시의 이점을 이해하고 구현해 봅니다.
- 그외 알아두면 유용한 Widget들이 어떤 것들이 있는지 알아봅니다.
- if 조건 표현식, 삼항 연산자 및 for 반복 표현식을 활용하여 Widget을 구현하는 방법을 이해하고 구현해 봅니다.
ListView Widget
Simple ListView
- ListView는 화면과 같이 데이터를 반복하여 보여주는 스마트폰 앱 개발에서 매우 빈번하게 사용되는 Widget입니다.
- 위와 같은 형태의 ListView를 만들어 볼텐데 우선 간단하게 Text로 구성된 SimpleListViewPage를 만들어 보겠습니다.
- simple_listview_page.dart 파일을 만들어 SimpleListViewPage Stateless Widget을 만듭시다.
// simple_listview_page.dart 파일의 SimpleListViewPage 클래스 코드
import 'package:flutter/material.dart'; // 코드 혹은 복사
class SimpleListviewPage extends StatelessWidget { // stl 코드 조각으로 생성
const SimpleListviewPage({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
- 그리고 main.dart 파일로 이동하여 아래 화면과 같이 SimpleListViewPage를 pages 리스트 변수에 등록합니다. 테스트하기 쉽도록 첫번째 페이지로 등록하겠습니다.
- 그리고 파일의 하단부로 이동하여 첫번째 BottomNavigationBarItem으로 등록합니다.
- 그리고 프로그램을 다시 실행해 보았습니다. 첫번째 페이지로 등록한 SimpleListViewPage가 잘 나타납니다. 프로그램의 변경이 Hot Reload할 수준을 넘어섰기 때문에 재시작해 주어야 합니다.
- 화면이 정상적으로 나타나는 것을 확인했으니 이제 Scaffold Widget과 ListView.builder() 생성자 메소드를 이용하여 ListView를 만들어 보겠습니다.
// simple_listview_page.dart 파일의 SimpleListViewPage 클래스 코드
import 'package:flutter/material.dart';
class SimpleListViewPage extends StatelessWidget {
//const SimpleListViewPage({super.key});
SimpleListViewPage({super.key}); // const 키워드 삭제
var strList = ["1번째 항목","2번째 항목","3번째 항목","4번째 항목","5번째 항목",
"6번째 항목","7번째 항목","8번째 항목","9번째 항목","10번째 항목",
"11번째 항목","12번째 항목","13번째 항목","14번째 항목","15번째 항목",
"16번째 항목","17번째 항목","18번째 항목","19번째 항목","20번째 항목"
]; // ListView에 보여줄 문자열 리스트 생성
@override
Widget build(BuildContext context) {
//return const Placeholder(); // 제공된 코드 삭제
return ListView.builder(
itemCount: strList.length, // ListView에 나타날 항목의 수 지정
itemBuilder: (context, index) {
return Text(strList[index], // ListView의 각 item에
style: TextStyle(fontSize: 40), // strList의 각 item을 Text Widget으로 추가
);
}
);
}
}
- 참고. 위의 코드 중 ListView.builder 생성자 메소드가 itemBuilder 속성에 의하여 return Text(…); 문장과 같이 Text Widget이라는 객체를 반환(return)하는 것을 눈여겨 보시기 바랍니다. 팩토리 메소드(Factory Method) 혹은 생성자 메소드(Constructor Method)는 이와 같이 객체를 만들어 반환하는 메소드를 말합니다.
- 그리고 실행해 보면 strList 리스트 변수에 들어 있던 항목들이 순서대로 화면에 나타나고 항목들이 화면의 크기를 초과하는 경우 스크롤이 되는 것을 확인할 수 있습니다.
- 그런데 항목들은 잘 나타나지만 조금 보기 좋게 다듬어야 하겠습니다.
- Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → 여기서는 Text, Center, Container,의 순으로 화면이 변하는 모습을 보여줄 것
// simple_listview_page.dart 파일의 SimpleListViewPage 클래스 코드
import 'package:flutter/material.dart';
class SimpleListViewPage extends StatelessWidget {
SimpleListViewPage({super.key});
var strList = ["1번째 항목","2번째 항목","3번째 항목","4번째 항목","5번째 항목",
"6번째 항목","7번째 항목","8번째 항목","9번째 항목","10번째 항목",
"11번째 항목","12번째 항목","13번째 항목","14번째 항목","15번째 항목",
"16번째 항목","17번째 항목","18번째 항목","19번째 항목","20번째 항목"
];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: strList.length,
itemBuilder: (context, index) {
return Container( // Container를 활용하여
margin: EdgeInsets.symmetric(vertical: 2), // 상하 마진과
padding: EdgeInsets.symmetric(vertical: 5), // 상하 패딩 지정
color: Colors.grey, // 항목별 배경색을 지정
child: Center( // 텍스트를 가운데 정렬
child: Text(strList[index],
style: TextStyle(fontSize: 40),
),
),
);
}
);
}
}
- 실행해 보니 오른쪽 화면과 같이 나타납니다.
- 혹시 ListView의 방향을 수직 방향에서 수평 방향으로 바꾸고 싶다면 ListView.builder()에 scrollDirection: Axis.horizontal 속성을 추가하고 마진(Margin)과 패딩(Padding)을 수직(Vertical) 방향에서 수평(Horizontal) 방향으로 수정하면 됩니다.
- ListView의 방향을 바꾸면 화면의 형태를 그에 맞게 다시 수정하여야 합니다. 우리가 만든 ListView는 수평방향와 맞지 않으니 scrollDirection: Axis.horizontal 속성을 삭제합시다.
- 그런데 Container의 마진과 패딩을 인위적으로 조정하고 색상을 조정하는 등의 작업이 번거로운데 위의 실행화면을 보면 만족할 만큼 예쁘지도 않습니다. 그래서 ListView의 Look and Feel을 부드럽게 해 주는 Card Widget을 Container Widget 대신 사용해 보겠습니다.
// itemBuilder의 반환 Widget을 Container에서 Card로 바꾼 코드
child: ListView.builder(
itemCount: strList.length,
itemBuilder: (context, index) {
//return Container( // Container Widget을
// margin: EdgeInsets.symmetric(vertical: 2),
// padding: EdgeInsets.symmetric(vertical: 5),
// color: Colors.grey,
return Card( // Card Widget으로 대체
child: Center(
child: Text(strList[index],
style: TextStyle(fontSize: 40),
),
),
);
},
),
- Container Widget을 Card Widget으로 바꾸었더니 ListView의 Look and Feel이 왼쪽 화면의 형태에서 오른쪽 화면의 형태로 부드러워졌습니다.
- 이번에는 ListView 항목을 Tab했을 때 해당 항목의 상세 화면으로 이동하는 기능을 구현해 보겠습니다. ListView Widget은 이벤트 처리 기능을 가지고 있지 않기 때문에 Tab이나 Click 등의 이벤트를 받아서 처리하려면 GestureDetector Widget을 사용해야 합니다. 상세화면은 아직 만들어지지 않아서 우선 my_second_page.dart 파일의 MySecondPage 클래스를 사용하겠습니다. 다행히 객체의 속성을 인자로 받아 들이는 기능이 이미 구현되어 있습니다.
// ListView에 Tap 이벤트 처리 로직을 추가한 SimpleListViewPage 클래스 코드
... 생략 ...
class SimpleListViewPage extends StatelessWidget {
... 중략 ...
itemBuilder: (context, index) {
return GestureDetector( // ListView의 항목에 이벤트 처리 기능 부여
onTap: () {
Navigator.of(context).push( // MySecondPage의 id 인자에 index를 넘기고
MaterialPageRoute(builder: (_) => MySecondPage(id: index, password: strList[index]))
); // password 인자에 strList에 저장된 항목 데이터를 넘김
},
child: Card(
... 생략 ...
- 이제 ListView의 특정 항목을 Tap하거나 Click하면 상세 페이지가 나타납니다.
- my_second_page.dart 파일의 이름이 simple_listview_detail_page.dart로 바뀌고 MySecondPage 클래스가 SimpleListViewDetailPage로 바뀌면 코드의 의미가 더 명확해질 것 같습니다.
- 파일 이름은 다른 파일의 import 문장에서 사용되고 있고, 클래스 이름도 화면 이동을 위한 코드나 Widget 생성을 위한 코드에 산재되어 있어 이름을 일일이 찾아가면서 바꾼다는 것은 쉬운 일이 아닙니다. 안드로이드 스튜디오는 이런 상황에 대비하여 Refactor 기능을 제공합니다.
- 먼저 파일 이름부터 바꾸어(Refactor) 보겠습니다. my_second_page.dart 파일 위에 마우스 커서를 두고 오른쪽 버튼을 눌러 팝업 메뉴가 나타나면 Refactor와 Rename 메뉴를 차례로 클릭합니다.
- 변경될 파일 이름으로 simple_listview_detail_page.dart를 입력한 후 Refactor 버튼을 누릅니다.
- 이번에는 클래스 이름을 바꾸어(Refactor) 보겠습니다. MySecondPage 클래스 이름을 마우스로 선택한 후 다시 마우스 오른쪽 클릭을 한후 Refactor와 Rename 메뉴를 차례로 선택합니다.
- 변경될 클래스 이름으로 SimpleListViewDetailPage를 입력한 후 Refactor 버튼을 누릅니다.
- 파일 이름과 클래스 이름의 바꾸는(Refactor) 작업이 완료되면 관련된 파일과 클래스 이름들이 모두 일관성 있게 변경됩니다. 이름을 일일이 찾아가 바꾸지 말고 안드로이드 스튜디오의 Refactor 기능을 활용합시다.
- 아무튼 덕분에 우리같은 프로그래머들은 Refactoring의 힘에 의하여 보다 손쉽게 다른 프로젝트의 소스 코드들을 가져다 사용할 수 있습니다. 처음부터 다시 개발하는 것보다 매우 생산성이 있는 일입니다.
- Refactor 작업으로 코드들이 많이 변경되었으니 정상적으로 동작하는지 확인하고 갑시다.
- 현재까지 작성된 코드를 공유합니다.
// SimpleListViewPage 클래스의 코드
import 'package:bottom_navigation_bar/simple_listview_detail_page.dart';
import 'package:flutter/material.dart';
class SimpleListViewPage extends StatelessWidget {
SimpleListViewPage({super.key});
var strList = ["1번째 항목","2번째 항목","3번째 항목","4번째 항목","5번째 항목",
"6번째 항목","7번째 항목","8번째 항목","9번째 항목","10번째 항목",
"11번째 항목","12번째 항목","13번째 항목","14번째 항목","15번째 항목",
"16번째 항목","17번째 항목","18번째 항목","19번째 항목","20번째 항목"
];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: strList.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => SimpleListViewDetailPage(id: index, password: strList[index]))
);
},
child: Card(
child: Center(
child: Text(strList[index],
style: TextStyle(fontSize: 40),
),
),
),
);
}
);
}
}
- simple_listview_detail_page.dart와 같은 형식의 이름은 뱀처럼 연결되어 ****Snake 표기법이라고 부르고, SimpleListViewDetailPage와 같은 형식의 이름은 프로그래밍 언어 Pascal에서 사용된 방식이어서 Pascal 표기법이라고 부르는데, 그중 bottomNavigationBar와 같이 첫글자가 소문자이면 Camel 표기법이라고 부릅니다. 최근의 이름을 부여하는 경향은 파일 이름은 Snake 표기법, 클래스의 이름은 Pascal 표기법, 변수와 상수의 이름은 Carmel 표기법을 사용하는 경향이 있습니다. Python은 예외적으로 클래스의 이름에만 Pascal 표기법을 사용하고 파일 이름과 변수에 모두 Snake 표기법을 사용합니다.
표기법 | 사용예 | Dart | Java | Python |
Snake 표기법 | simple_listview_detail_page.dart | 파일/폴더 이름 | 폴더이름 | 파일/폴더/변수/속성/함수 이름 |
Pascal 표기법 | SimpleListViewDetailPage | 클래스(객체) 이름 | 파일(클래스)/클래스(객체) 이름 | 클래스(객체) 이름 |
Camel 표기법 | bottomNavigationBar | 변수/상수/속성/함수 이름 | 변수/상수/속성/함수 이름 | |
모두 대문자 (C언어 Macro) | EPOCH, BATCH_SIZE | 상수 이름 |
- ListView Widget과 유사하게 GridView Widget을 사용하면 인스타그램에서 가져온 아래 화면과 같은 형태를 만들 수 있습니다.
Image ListView
- Text 중심의 SimpleListView를 만들어 보며 ListView의 기본적인 원리와 개념과 의미를 배웠습니다. 이제 배운 것을 활용하여 Image가 추가된 형태의 ImageListView를 만들어 보겠습니다.
- simple_listview_page.dart 파일을 복사하여 image_listview_page.dart 파일을 만듭니다. 아래 화면과 같이 복사한 파일에서 SimpleListViewPage 클래스 이름을 ImageListViewPage로 클래스 정의와 생성자 문장에서 2군데 모두 수정하세요.
- 그리고 main.dart 파일에서 아래와 같이 새로 만든 ImageListViewPage가 BottomNavigationBar의 첫번째 버튼을 눌렀을 때 실행되도록 코드를 수정합니다.
// main.dart 파일의 _MyHomePageState 클래스 코드
... 생략 ...
class _MyHomePageState extends State<MyHomePage> {
var pages = [ ImageListViewPage(), // 첫번째 페이지로 추가
SimpleListViewPage(),
// Detail Page로 전환된 SimpleListViewDetailPage 페이지 제거
//SimpleListViewDetailPage(id: 320811, password: "mypassword"),
MyThirdPage(),
];
... 중략 ...
items: [
BottomNavigationBarItem( // ImageListViewPage 추가
icon: Icon(Icons.photo_library),
label: "Image List View",
),
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: "Simple List View",
),
// Detail Page로 전환된 SimpleListViewDetailPage 페이지 제거
//BottomNavigationBarItem(
// icon: Icon(Icons.looks_two),
// label: "2nd Page",
//),
BottomNavigationBarItem(
icon: Icon(Icons.looks_3),
label: "3rd Page",
),
],
),
);
}
}
- 실행해서 정상적으로 동작하는지 확인해 봅시다. SimpleListViewPage를 복사하여 ImageListViewPage를 만들었기 때문에 아래 화면에서 첫번째 화면과 두번째 화면이 동일합니다. 프로그램의 변경이 Hot Reload할 수준을 넘어섰기 때문에 재시작해 주어야 합니다.
- 이제 ImageListViewPage를 수정하여 원하는 화면을 만들어 봅시다.
// Text 형태의 LisView의 콘텐츠를 Image 형태로 변경한 image_listview_page.dart 파일 코드
import 'package:flutter/material.dart';
class ImageListViewPage extends StatelessWidget {
ImageListViewPage({super.key});
/* Text 중심의 strList 삭제
var strList = ["1번째 항목","2번째 항목","3번째 항목","4번째 항목","5번째 항목",
"6번째 항목","7번째 항목","8번째 항목","9번째 항목","10번째 항목",
"11번째 항목","12번째 항목","13번째 항목","14번째 항목","15번째 항목",
"16번째 항목","17번째 항목","18번째 항목","19번째 항목","20번째 항목"
]; */
// 이미지 URL로 구성된 imgList 추가
var imgList = [ "https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg",
"https://image.aladin.co.kr/product/31794/10/cover200/k362833219_1.jpg",
"https://image.aladin.co.kr/product/3422/90/cover200/8966260993_1.jpg",
"https://image.aladin.co.kr/product/57/79/cover200/8991268072_2.jpg",
"https://image.aladin.co.kr/product/34274/43/coversum/scm9462999557200.jpg",
"https://image.aladin.co.kr/product/26031/38/coversum/k702737950_1.jpg",
];
@override
Widget build(BuildContext context) {
return ListView.builder(
//itemCount: strList.length // ListView의 항목의 수를
itemCount: imgList.length, // strList.length에서 imgList의 수로 변경
itemBuilder: (context, index) {
return GestureDetector(
onTap: () { // 이벤트 처리기 주석 처리 -> 향후 구현
// Navigator.of(context).push(
// MaterialPageRoute(builder: (_) => SimpleListViewDetailPage(id: index, password: strList[index]))
// );
},
child: Card(
child: Center(
//child: Text(strList[index], // Text Widget을
// style: TextStyle(fontSize: 40),
//),
child: Image.network(imgList[index]), // Image Widget으로 변경
),
),
);
}
);
}
}
- 실행해 보니 첫번째 페이지에 서적들의 이미지의 ListView가 나타납니다.
- 이제 Image와 Text가 공존하는 ListView를 만들어 보겠습니다.
// LisView의 콘텐츠에 Image와 Text가 공존하는 형태로 변경한 image_listview_page.dart 파일 코드
import 'package:flutter/material.dart';
class ImageListViewPage extends StatelessWidget {
ImageListViewPage({super.key});
var imgList = [ "https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg",
"https://image.aladin.co.kr/product/31794/10/cover200/k362833219_1.jpg",
"https://image.aladin.co.kr/product/3422/90/cover200/8966260993_1.jpg",
"https://image.aladin.co.kr/product/57/79/cover200/8991268072_2.jpg",
"https://image.aladin.co.kr/product/34274/43/coversum/scm9462999557200.jpg",
"https://image.aladin.co.kr/product/26031/38/coversum/k702737950_1.jpg",
];
// 이미지 이름 리스트 추가
var imgNameList = [ "파이썬 + AI", "점프 투 파이썬", "생각하는 프로그래밍",
"실용주의 프로그래머", "프로그래밍 심리학", "UWP 퀵스타트",
];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: imgList.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
// Navigator.of(context).push(
// MaterialPageRoute(builder: (_) => SimpleListViewDetailPage(id: index, password: strList[index]))
// );
},
child: Card(
child: Center(
child: Row( // Image와 Text를 Row로 배치
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Image.network(imgList[index]),
Column( // Text들을 Column으로 배치
children: [
Text("Index - $index", // index를 Text로 출력
style: TextStyle(fontSize: 30),
),
Text(imgNameList[index], // Image의 이름을 Text로 출력
style: TextStyle(fontSize: 25),
),
],
)
],
),
),
),
);
}
);
}
}
- 실행시켜 보았습니다. 이미지의 크기가 들쭉날쭉하고, 이미지가 작은 것은 너무 작습니다.
- 혹시 Image의 파일이 큰 것이 들어오거나 Text의 길이가 긴 것이 들어 온다면 어떤 일이 생기는지 확인해 보았습니다.
- 먼저 Image의 파일이 크게 들어오는 경우입니다.
// image_listview_page.dart 파일 코드
... 생략 ...
Image.network(imgList[index],
width: 400, // 이미지의 너비를 지정함
),
... 생략 ...
- 이미지가 커지면서 코딩된 화면이 장치의 화면의 크기를 넘어서 빗금으로 깨지는 현상이 생깁니다.
// image_listview_page.dart 파일 코드
... 생략 ...
children: [
Image.network(imgList[index],
// width: 400,
),
Column(
children: [
Text("Index - $index",
style: TextStyle(fontSize: 30),
),
Text(imgNameList[index],
style: TextStyle(fontSize: 25),
),
// 길이가 긴 Text를 추가함
Text("리스트 항목 중 사진이 크거나 텍스트가 길어지면 어떤 일이 생길까?",
style: TextStyle(fontSize: 30),
),
],
)
],
... 생략 ...
- 마찬가지로 Text가 길어지면서 코딩된 화면이 장치의 화면의 크기를 넘어서 빗금으로 깨지는 현상이 생깁니다.
- 그럴때를 대비하여 코드를 아래와 같이 수정해 보겠습니다.
// image_listview_page.dart 파일 코드
... 생략 ...
children: [
// Expanded Widget으로 감싸면 화면 크기의 비율을 자동으로 조정해 줍니다.
Expanded(
child: Image.network(imgList[index],
width: 400,
),
),
// Expanded Widget으로 감싸면 화면 크기의 비율을 자동으로 조정해 줍니다.
Expanded(
child: Column(
children: [
Text("Index - $index",
style: TextStyle(fontSize: 30),
),
Text(imgNameList[index],
style: TextStyle(fontSize: 25),
),
// 길이가 긴 Text를 추가함
Text("리스트 항목 중 사진이 크거나 텍스트가 길어지면 어떤 일이 생길까?",
style: TextStyle(fontSize: 30),
),
],
),
)
],
),
),
),
);
}
);
}
}
- 실행해 보니 Expanded Widget의 도움으로 화면이 넘치는 현상이 해결이 되었는데, 이미지가 너무 작은 문제는 아직 남아 있습니다.
- 이 문제는 노션의 “Flutter FAQ와 Tips” 페이지에서 “주변의 Image와 넓이와 높이를 맞출때 Image에 빈공간이 나타나거나 Image가 왜곡되어 나타나는 경우가 있습니다. 어떻게 해결하여야 할까요?” 를 보고 해결해 봅시다.
// 완성된 코드
... 생략 ...
Expanded(
child: Image.network(imgList[index],
width: 400,
fit: BoxFit.fill, // BoxFit.fiil이 적합하다고 판단함
),
),
... 생략 ...
- 실행해 보니 이미지가 작았던 문제도 해결되었습니다.
- 이제 ImageListViewDetailPage 클래스를 만든 후 ImageListViewPage 클래스와 연결할 차례입니다. 먼저 image_listview_detail_page.dart 파일을 만든 후 아래와 같이 코딩합시다.
// ImageListViewDetailPage 클래스의 코드
import 'package:flutter/material.dart';
class ImageListViewDetailPage extends StatelessWidget {
const ImageListViewDetailPage({super.key, required this.index, required this.name, required this.imgUrl});
final int index;
final String name;
final String imgUrl;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text("ImageListView Detail"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
"Index - $index",
style: TextStyle(fontSize: 30),
),
Image.network(imgUrl),
Text(
name,
style: TextStyle(fontSize: 40),
),
],
),
)
);
}
}
- 그리고 image_listview_page.dart 파일에 ImageListViewDetailPage 페이지로 이동하는 코드를 추가합시다.
// Detail 페이지로의 이동 로직이 추가된 ImageListViewPage 클래스의 코드
import 'package:bottom_navigation_bar/image_listview_detail.page.dart';
import 'package:flutter/material.dart';
class ImageListViewPage extends StatelessWidget {
ImageListViewPage({super.key});
var imgList = [ "https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg",
"https://image.aladin.co.kr/product/31794/10/cover200/k362833219_1.jpg",
"https://image.aladin.co.kr/product/3422/90/cover200/8966260993_1.jpg",
"https://image.aladin.co.kr/product/57/79/cover200/8991268072_2.jpg",
"https://image.aladin.co.kr/product/34274/43/coversum/scm9462999557200.jpg",
"https://image.aladin.co.kr/product/26031/38/coversum/k702737950_1.jpg",
];
var imgNameList = [ "파이썬 + AI", "점프 투 파이썬", "생각하는 프로그래밍",
"실용주의 프로그래머", "프로그래밍 심리학", "UWP 퀵스타트",
];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: imgList.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.of(context).push( // Detai 페이지로 이동
MaterialPageRoute(builder: (_) => ImageListViewDetailPage(index: index, name: imgNameList[index], imgUrl: imgList[index]))
);
},
child: Card(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Image.network(imgList[index],
width: 400,
fit: BoxFit.fill,
),
),
Expanded(
child: Column(
children: [
Text("Index - $index",
style: TextStyle(fontSize: 30),
),
Text(imgNameList[index],
style: TextStyle(fontSize: 25),
),
Text("리스트 항목 중 사진이 크거나 텍스트가 길어지면 어떤 일이 생길까?",
style: TextStyle(fontSize: 30),
),
],
),
)
],
),
),
),
);
}
);
}
}
- 프로그램을 실행한 후 ImageListView에서 특정 항목을 클릭하면 상세 화면으로 이동합니다.
- **Instructor Note : “**Flutter FAQ와 Tips”에서”Flutter 프로젝트 이름 변경/Flutter 프로젝트 복사”를 설명할 것
[실습] 자신의 가족을 소개하는 family_intro 앱을 개선해 보세요.
- family_intro 프로젝트를 참조하여 family_intro2 프로젝트로 다시 구성해 보세요. 반드시 하단에 BottomNavigationBar가 나타나야 합니다. family_intro 프로젝트의 화면 구조를 유지하되 하나의 화면은 Image ListView이어야 하며 ListView 항목을 클릭하면 Detail 페이지로 이동해야 합니다. ListView는 가족 구성원들의 목록이어야 하고 Detail 페이지는 family_intro에서 개발해 두었던 가족 구성원을 소개하는 화면으로 연결하기 바랍니다.
TextField Widget
- TextField Widget은 문자열의 입력을 받는 Widget입니다.
- first_flutter 프로젝트를 다시 연후 아래 코드를 화면의 최상단 Widget으로 추가해 봅시다.
// main.dart 함수
... 생략 ...
TextField( // 추가된 TextField Widget
decoration: InputDecoration(
labelText: "필드이름",
border: OutlineInputBorder(),
),
),
Container(
width: double.infinity,
alignment: Alignment.center,
color: Colors.yellow,
child: const Text(
'You have pushed the button this many times:',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.pink,
fontSize: 40,
fontStyle: FontStyle.italic,
),
),
),
... 생략 ...
- 앱을 실행해 보면 화면의 상단에 입력을 위한 필드가 추가되어 있는 것을 알 수 있습니다. 그런데 입력 필드가 화면의 행을 모두 채우고 있습니다.
- 입력 필드의 길이는 SizedBox Widget을 사용하여 통제할 수 있습니다.
// main.dart 함수
... 생략 ...
SizedBox( // SizedBox Widget으로
width: 200, // 입력필드의 길이를 200 논리적 픽셀로 지정
child: TextField(
decoration: InputDecoration(
labelText: "필드이름",
border: OutlineInputBorder(),
),
),
),
... 생략 ...
- 앱을 다시 실행해 보면 입력 필드의 길이가 지정한 만큼 축소되는 것을 확인할 수 있습니다.
- 텍스트 필드에 입력한 문자열이 화면에만 보이지 않고 변수에서 값을 받아 오거나 변수에 값을 저장하는 방법을 알아 보겠습니다. TextField Widget과 연결하여 사용할 변수는 TextEditingController 객체 변수로 정의한 후 TextField Widget의 controller 속성으로 지정해 줍니다.
// main.dart 함수
... 생략 ...
class _MyHomePageState extends State<MyHomePage> {
TextEditingController textEditingController = TextEditingController(); // 콘트롤러 추가
@override
void initState() {
// TODO: implement initState
super.initState();
textEditingController.text = "입력 필드의 초기값";
}
... 중략 ...
SizedBox(
width: 200,
child: TextField(
controller: textEditingController, // TextField와 Controller 연결
decoration: InputDecoration(
labelText: "필드이름",
border: OutlineInputBorder(),
),
),
),
... 생략 ...
- 그후 프로그램의 코드에서 textEditingController.text = "입력 필드의 초기값";와 같은 할당문을 사용하면 텍스트 필드에 값이 나타납니다.
- 화면에 입력한 값을 가져다 사용할 때에는 TextField Widget과 연결된 변수의 값을 textEditingController.text와 같은 형식으로 코드에서 가져다 사용하면 됩니다.
// main.dart 함수
... 생략 ...
SizedBox(
width: 200,
child: TextField(
// 값 변경 후 Enter를 치면 textEditingController.text에 저장된 값을 print
onSubmitted: (_) {print(textEditingController.text);},
controller: textEditingController,
decoration: InputDecoration(
labelText: "필드이름",
border: OutlineInputBorder(),
),
),
),
... 생략 ...
- TextField Widget에 TextField Changed라고 입력한 후 Enter를 치면 안드로이드 스튜디오의 하단 창에 입력한 값이 출력되는 것을 확인할 수 있습니다.
- 이와 같이 화면에서 TextField 입력을 받아 사용하기 위해서는 화면의 TextField와 연결되어 변수 역할과 제어 역할을 해 주는 제어기(TextEditingController)의 중재가 필요합니다. 아래의 그림을 참조바랍니다. TextEditingController 외에도 이와 유사한 역할을 해 주는 다양한 (TextEditingController)들이 존재하는데 기본적인 원리와 개념과 의미는 동일하며 사용법은 약간의 차이가 있을 뿐 유사합니다.
표현식과 연산자를 활용한 Widget
- Flutter는 if나 for와 같은 표현식(Expression)이나 삼항 연산자(Operatr)를 사용하여 Widget를 구성하는 방법을 제공합니다.
if 조건 표현식
// main.dart 함수
... 생략 ...
class _MyHomePageState extends State<MyHomePage> {
bool contentLoaded = false; // 콘텐츠가 로드되었는지 여부를 나타내는 변수
TextEditingController textEditingController = TextEditingController();
... 중략 ...
if (contentLoaded ) Text("콘텐츠가 로드되었습니다.") // 콘텐츠 로드된 경우
else CircularProgressIndicator(), // 콘텐츠 로드 중인 경우
SizedBox(
width: 200,
child: TextField(
onSubmitted: (_) {print(textEditingController.text);},
controller: textEditingController,
decoration: InputDecoration(
labelText: "필드이름",
border: OutlineInputBorder(),
),
),
),
... 생략 ...
- 앱을 실행해 보면 contentLoaded 변수에 false가 저장되어 있어서 로딩 중이라는 의미로 원형 프로그래스 바가 출력되는 것을 확인할 수 있습니다.
- 플러스(+) 버튼을 누를 때 _count 변수를 증가시킴과 동시에 contentLoaded 변수의 값을 true로 변경시켜 보겠습니다.
// main.dart 함수
... 생략 ...
void _incrementCounter() {
setState(() {
_counter++;
contentLoaded = true; // 콘텐츠가 로드되었다고 변수 설정
});
}
... 생략 ...
- 앱을 다시 실행해 보면 플러스(+) 버튼을 누를 때 원형 프로그래스 바가 “콘텐츠가 로드되었습니다.”라는 텍스트로 출력되는 것을 확인할 수 있습니다.
삼항 연산자
- 이번에는 if 표현식(Expression)을 삼항 연산자(Operator)로 간결하게 표현해 보겠습니다.
// main.dart 함수
... 생략 ...
// if (contentLoaded ) Text("콘텐츠가 로드되었습니다.")
// else CircularProgressIndicator(),
// 삼항 연산자로 변경
contentLoaded ? Text("콘텐츠가 로드되었습니다.") : CircularProgressIndicator(),
SizedBox(
width: 200,
child: TextField(
onSubmitted: (_) {print(textEditingController.text);},
controller: textEditingController,
decoration: InputDecoration(
labelText: "필드이름",
border: OutlineInputBorder(),
),
),
),
... 생략 ...
- 앱의 실행결과는 동일합니다.
for 반복 표현식
- ListView Widget에서 사용했던 SimpleListView 예제를 화면의 상단에 추가해 보겠습니다.
// main.dart 함수
... 생략 ...
class _MyHomePageState extends State<MyHomePage> {
// ListView Widget에 반복하여 보여줄 리스트 변수
var strList = ["1번째 항목","2번째 항목","3번째 항목","4번째 항목","5번째 항목",
"6번째 항목","7번째 항목","8번째 항목","9번째 항목","10번째 항목",
"11번째 항목","12번째 항목","13번째 항목","14번째 항목","15번째 항목",
"16번째 항목","17번째 항목","18번째 항목","19번째 항목","20번째 항목"
];
bool contentLoaded = false;
... 중략 ...
// ListView Widget의 크기를 SizedBox의 크기로 제한
SizedBox(
height: 200,
child: ListView.builder(
itemCount: strList.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
// 오류 발생을 예방하기 위하여 주석 처리
// Navigator.of(context).push(
// MaterialPageRoute(builder: (_) => SimpleListViewDetailPage(id: index, password: strList[index]))
// );
},
child: Card(
child: Center(
child: Text(strList[index],
style: TextStyle(fontSize: 40),
),
),
),
);
}
),
),
// if (contentLoaded ) Text("콘텐츠가 로드되었습니다.")
// else CircularProgressIndicator(),
contentLoaded ? Text("콘텐츠가 로드되었습니다.") : CircularProgressIndicator(),
... 생략 ...
- 앱을 실행해 보면 오른쪽 화면과 같이 나타납니다.
- 이 코드를 for 반복 표현식을 사용하여 코딩해 보겠습니다. 아래 코드와 같이 리스트가 나타나게 하는 것까지는 단순한 형식으로 구현이 가능한데, itemBuilder의 index 인자를 사용할 수 없는 한계가 있습니다.
// main.dart 함수
... 생략 ...
// ListView Widget의 크기를 SizedBox의 크기로 제한
SizedBox(
height: 200,
// child: ListView.builder(
// itemCount: strList.length,
// itemBuilder: (context, index) {
// return GestureDetector(
// onTap: () {
// // Navigator.of(context).push(
// // MaterialPageRoute(builder: (_) => SimpleListViewDetailPage(id: index, password: strList[index]))
// // );
// },
// child: Card(
// child: Center(
// child: Text(strList[index],
// style: TextStyle(fontSize: 40),
// ),
// ),
// ),
// );
// }
// ),
child: ListView( // ListView.builder가 ListView로 변경됨
children: [ // itemCount와 itemBuilder 속성대신 children 속성 사용
for (var element in strList) // for 반복 표현식 사용
Card( // Card Widget이 strList 변수의 요소마다 생성됨
child: Center(
child: Text(element,
style: TextStyle(fontSize: 40),
),
),
),
],
),
),
... 생략 ...
- 그래서 Dart 언어의 문장(Statement)으로 이 문제를 해결하려고 하면 아래 화면과 같이 오류가 발생합니다. 여기서 사용하는 if와 for 표현식(Expression)과 삼항 연산자(Operator)는 return 문장안에서 사용되어 Dart 언어의 문장과 달리 약식의 제한적인 문법만 허용하기 때문입니다. Dart의 표현식 기반 문법은 간결하면서도 강력한 기능을 제공하여, 웹 프로그래밍에서 사용되는 ASP, JSP, Thymeleaf와 유사한 동적 콘텐츠 처리를 구현할 수 있습니다.
DropdownButton Widget
- DropdownButton Widget은 사전에 정해진 값들 중에서 선택하여 입력을 받는 Widget입니다. 아래 코드를 화면의 최상단 Widget으로 추가해 봅시다.
// main.dart 함수
... 생략 ...
class _MyHomePageState extends State<MyHomePage> {
var selectedElement = "요소2"; // DropdownButton 요소의 초기값
var strList = ["1번째 항목","2번째 항목","3번째 항목","4번째 항목","5번째 항목",
"6번째 항목","7번째 항목","8번째 항목","9번째 항목","10번째 항목",
"11번째 항목","12번째 항목","13번째 항목","14번째 항목","15번째 항목",
"16번째 항목","17번째 항목","18번째 항목","19번째 항목","20번째 항목"
];
... 중략 ...
SizedBox(
width: 200,
child: Center(
child: DropdownButton<String>( // DropdownButton Widget 추가
value: selectedElement, // DropdownButton의 선택과 변수를 연결
onChanged: (newValue) { // DropdownButton 선택시 이벤트 처리기
print(String? newValue); // 인자가 Null로 넘어올 수 있어 ?로 허용
setState(() { // DropdownButton에 연결된 변수값 상태 변경
selectedElement = newValue!; // 값이 Null 아닌 것을 !로 확인후 사용
});
},
items: [
for (var element in ["요소1","요소2","요소3"]) // for 반복 표현식
DropdownMenuItem<String>( // DropdownButton의 요소 Widget 추가
value: element,
child: Text(element),
),
],
),
),
),
... 생략 ...
- 앱을 실행하면 화면 상단에 DropdownButton Widget이 나타나는 것을 확인할 수 있으며 value 속성으로 지정한 selectedElement 변수의 값이 초기값으로 나타나는 것을 확인할 수 있습니다.
- DropdownButton Widget을 클릭하거나 탭하면 for 표현식으로 추가한 DropdownMenuItem<String> Widget들이 선택 가능한 목록으로 나타나는 것을 확인할 수 있습니다.
- 요소를 선택하면 선택한 요소가 DropdownButton Widget에 나타나고, 실행창에도 print되는 것을 확인할 수 있습니다.
- TextField와 DropdownButton Widget 외에 Radio<T>와 RichText 등의 입력을 위한 Widget들이 다양하게 존재하니 필요할 때 찾아서 사용하기 바랍니다.
SingleChildScrollView Widget
- SingleChildScrollView Widget은 화면 하나로 제한된 크기를 Scroll하여 확장하게 만들어 줍니다.
// main.dart 함수
... 생략 ...
@override
Widget build(BuildContext context) {
double imageWidth = (MediaQuery.of(context).size.width - 30) / 3;
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: SingleChildScrollView( // Center Widget을 SingleChildScrollView으로 감싸줌
child: Center(
child: Column(
// mainAxisAlignment 변경 : spaceAround -> start
//mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
... 생략 ...
- 앱을 실행하면 화면 상단에 하단에 화면이 깨지는 현상이 사라지고, 아래로 Scroll할 수 있는 것을 확인할 수 있습니다.
[실습] 계산기
- 프로젝트 이름 : calculator
- 설명 : BottomNavigationBar를 사용하여 4개의 주화면을 구성한 후 단계별로 계산기를 만듭니다.
import 'package:flutter/material.dart';
import 'calculator_page1.dart';
import 'calculator_page2.dart';
import 'calculator_page3.dart';
import 'calculator_page4.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var pages = [
CalculatorPage1(),
CalculatorPage2(),
CalculatorPage3(),
CalculatorPage4(),
];
int _selectedNaviIndex = 0;
_onBottomNavigationItemTapped(index) {
setState(() {
_selectedNaviIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: pages[_selectedNaviIndex],
bottomNavigationBar: BottomNavigationBar(
//showSelectedLabels: false,
//showUnselectedLabels: false,
currentIndex: _selectedNaviIndex,
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
onTap: _onBottomNavigationItemTapped,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.looks_one),
label: "Calculator1",
),
BottomNavigationBarItem(
icon: Icon(Icons.looks_two),
label: "Calculator2",
),
BottomNavigationBarItem(
icon: Icon(Icons.looks_3),
label: "Calculator3",
),
BottomNavigationBarItem(
icon: Icon(Icons.looks_4),
label: "Calculator4",
),
],
),
);
}
}
- 1단계 : 사칙연산(/,*,-,+)을 수행하는데 연산자와 숫자의 입력이 적절한지 확인하지 않고 우선 계산되는 기능을 구현해 봅니다. 연산자가 틀린 경우 오류가 발생하거나 동작하지 않는 한계가 있습니다.
- 2단계 : 연산자가 사칙연산(/,*,-,+)을 벗어나는 경우 하단에 오류 메세지를 출력하도록 개선합니다. 여전히 입력된 숫자가 틀린 경우 오류가 발생하거나 동작하지 않는 한계가 있습니다.
- 3단계 : 사칙연산(/,*,-,+)의 입력이 틀리지 않도록 드롭 다운 버튼(DropdownButton Widget)으로 구현하여 실수를 방지함으로써 입력 검증을 하지 않습니다. 입력한 숫자가 적절하지 않은 오류 메세지를 보여 주고 다시 입력하게 합니다.
- 4단계 : 숫자와 사칙연산(/,*,-,+)의 입력이 틀리지 않도록 버튼으로 구현하여 입력하는 수식을 상단에 나타나게 한 후 = 버튼을 누르면 계산하게 합니다. 입력한 수식이 적절하지 않은 C(Clear) 버튼을 눌러 다시 입력하게 합니다. 수작업 입력을 배제하여 실수가 방지되나 여전히 수식이 잘못될 위험이 남아 있어서 try~catch 예외처리로 수식의 오류를 잡아 냅니다.
expressions: ^0.2.5 패키지를 사용합니다. pubspec.yaml에 아래와 같이 등록한 후 Pub Get 버튼을 누릅시다.
코드 팩토리의 플러터 프로그래밍 - 10장 만난지 며칠 U&I (240페이지)
- 프로젝트 이름 : u_and_i
- 설명 : 하트를 눌러서 처음 만난 날을 설정하면 오늘이 D+몇일인지를 보여 줍니다. 처음으로 iOS의 쿠퍼티노 디자인을 사용해 봅니다.
- 이미지와 폰트를 저자의 깃허브(https://github.com/codefactory-co/flutter-golden-rabbit-novice-v2.git)에서 다운받아 images와 fonts 폴더에 복사해 넣은 후 pubspec.yaml에 등록 후 Pub Get
- 이 예제에 폰트를 등록하는 전형적인 pubspec.yaml 설정이 나오고 있음. Regular와 Light 폰트는 weight을 지정하지 않고 있고, Medium과 Bold 폰트는 weight를 각각 500과 700으로 지정하고 있음
# pubspec.yaml의 코드
assets:
- images/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# <https://flutter.dev/to/resolution-aware-images>
# For details regarding adding assets from package dependencies, see
# <https://flutter.dev/to/asset-from-package>
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
fonts:
- family: parisienne
fonts:
- asset: fonts/Parisienne-Regular.ttf
- family: sunflower
fonts:
- asset: fonts/Sunflower-Light.ttf
- asset: fonts/Sunflower-Medium.ttf
weight: 500
- asset: fonts/Sunflower-Bold.ttf
weight: 700
- 화면을 만들어 봅시다.
- Hear Icon을 누르면 쿠퍼티노 날짜 다이어로그가 화면의 하단에 나오도록 해 봅시다. import 'package:flutter/cupertino.dart';로 import하는 showCupertinoDialog() 쿠퍼티노 함수를 사용하여 쉽게 구현할 수 있습니다.
참고 문헌
[논문]
- 없음
[보고서]
- 없음
[URL]
- 없음
문의사항
[기상학/프로그래밍 언어]
- sangho.lee.1990@gmail.com
[해양학/천문학/빅데이터]
- saimang0804@gmail.com
'프로그래밍 언어 > Flutter' 카테고리의 다른 글
[Flutter] 쉽게 배우는 플러터 앱 개발 실무 04강 (0) | 2025.01.04 |
---|---|
[Flutter] 쉽게 배우는 플러터 앱 개발 실무 03강 (2) | 2024.12.28 |
[Flutter] 쉽게 배우는 플러터 앱 개발 실무 FAQ 및 Tips (0) | 2024.12.24 |
[Flutter] 쉽게 배우는 플러터 앱 개발 실무 02강 (0) | 2024.12.21 |
[Flutter] 쉽게 배우는 플러터 앱 개발 실무 01강 (2) | 2024.12.21 |
최근댓글