8. 플러터로 블로그 웹 앱 만들기: 웹뷰와 콜백 함수 활용하기
8.1. 사전 지식
8.1.1 콜백 함수
- 일정 작업이 완료되면 실행되는 함수
- 함수를 미리 정의해두고 특정 조건이 성립될 때 실행됨.
- ex) 웹뷰에서 페이지 로딩이 완료된 후 실행되는 onPageFinished() 콜백 함수
8.1.2 웹뷰 위젯
- 웹뷰 : 프레임워크에 내장된 브라우저를 앱의 네이티브 컴포넌트에 임베딩하는 기능(앱 내에서 웹 브라우저 기능을 구현)
- 단점 : 네이티브 컴포넌트에 비해 속도 느림 / 애니메이션이 부자연스러움
- 장점 : 기존 웹사이트를 손쉽게 앱으로 활용할 수 있음.
- 웹뷰 위젯 :: controller 파라미터에 WebViewController 객체를 입력해 제어 (주요 함수⤵️)
- setJavaScriptMode: 자바스크립트 실행 허용 여부 설정
- setBackgroundColor: 배경색 지정
- loadRequest: 새로운 URL로 이동
- setNavigationDelegate: 다양한 콜백 함수를 포함한 NavigationDelegate 객체 설정
8.1.3 안드로이드와 iOS 네이티브 설정
- 특정 기능을 사용하기 위해서는 각 플랫폼별 네이티브 설정이 필요할 수 있음.
- (ex. 웹뷰 사용 위해서 인터넷 권한과 HTTP/HTTPS 프로토콜 관련 설정 필요, 카메라, 사진첩, 푸시 알림 등 보안에 민감한 기능이나 하드웨어 접근 등에도 필요)
8.2 사전 준비
8.2.1 pubspec.yaml 설정하기
- 플러그인 설치하기
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
webview_flutter: 4.10.0
- 플러그인 설치 후 flutter pub get 명령어를 실행 -> 패키지 다운로드
- 그 외 다양한 pub 명령어:
- flutter pub add [플러그인 이름]: pubspec.yaml에 플러그인 추가
- flutter pub upgrade: 등록된 플러그인을 최신 버전으로 업데이트
- flutter pub run: 현재 프로젝트 실행
8.2.2 권한 및 네이티브 설정하기 (안드로이드)
- 추가할 코드들
- AndroidManifest.xml 파일 수정
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="blog_web_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<!-- 생략 -->
</application>
</manifest>
8.2.3 프로젝트 초기화하기
- lib 폴더 아래에 screen 폴더를 만들고, 그 안에 home_screen.dart 파일을 생성
import 'package:flutter/material.dart' ;
class HomeScreen extends StatelessWidget {
// @ const 생성자
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text('Home Screen’),
);
}
}
import 'package:blog_web_app/screen/home_screen.dart';
import 'package: flutter/material.dart';
void main() {
runApp(
Materia lApp(
home: HomeScreen(),
),
);
}
8.3 레이아웃 구상하기
- 앱바(AppBar): 제목과 홈 버튼을 표시
- 웹뷰(WebView): 지정한 URL의 웹페이지를 보여줌
8.4 구현하기
8.4.1 앱바 구현하기
- HomeScreen 위젯 => 앱바 추가하고, 제목 넣어주고, 배경색 설정:
import 'package: flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
// 1. 앱바 위젯 추가
appBar: AppBar(
// 2. 배경색 지정
backgroundColor: Colors.orange,
// 3. 앱 타이틀 설정
title: Text('Code Factory'),
// 4. 가운데 정렬
centerTitle: true,
),
body: Text('Home Screen’),
);
}
}
8.4.2 웹뷰 구현하기
- 필요한 패키지를 임포트, 위젯 추가
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.orange,
title: Text('Code Factory'),
centerTitle: true,
),
body: WebViewWidget(
controller: webViewController, // 아직 선언되지 않음
),
);
}
}
8.4.3. 웹뷰 컨트롤러 설정하기
- 웹뷰 컨트롤러 선언/설정:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class HomeScreen extends StatelessWidget {
// WebViewController 선언
WebViewController webViewController = WebViewController()
// loadRequest() 함수로 웹 페이지 로드
..loadRequest(Uri.parse('https://blog.codefactory.ai'))
// JavaScript 실행 허용
..setJavaScriptMode(JavaScriptMode.unrestricted);
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.orange,
title: Text('Code Factory'),
centerTitle: true,
),
body: WebViewWidget(
controller: webViewController,
),
);
}
}
8.4.4 main.dart 파일 수정하기
StatelessWidget에서 WebViewController를 프로퍼티로 직접 인스턴스화하기 위해 플러터 프레임워크가 앱을 실행할 준비가 됐는지 확인하는 ensureInitialized() 함수를 추가합니다:
import 'package:blog_web_app/screen/home_screen.dart';
import 'package:flutter/material.dart';
void main() {
// 플러터 프레임워크가 앱을 실행할 준비가 될 때까지 기다림
WidgetsFlutterBinding.ensureInitialized();
runApp(
MaterialApp(
home: HomeScreen(),
),
);
}
8.4.5 홈 버튼 구현하기
- 어느 페이지에서든 코드팩토리 홈페이지로 돌아올 수 있는 홈 버튼을 앱바에 추가:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.orange,
title: Text('Code Factory'),
centerTitle: true,
// AppBar에 액션 버튼 추가
actions: [
IconButton(
// 아이콘을 눌렀을 때 실행할 콜백 함수
onPressed: () {
webViewController.loadRequest(Uri.parse('https://blog.codefactory.ai'));
},
// 홈 버튼 아이콘 설정
icon: Icon(Icons.home),
),
],
),
body: WebViewWidget(
controller: webViewController,
),
);
}
9. 전자액자 : 위젯 생명주기, PageView, Timer, SystemChrome, StatefulWidget
9.1. 사전 지식
9.1.1 위젯 생명주기
- 위젯이 화면에 그려지는 순간부터 삭제되는 순간까지의 주기
- StatelessWidget & StatefulWidget 두 가지 유형 존재
StatelessWidget:
- 상태가 없는 위젯으로 한번 생성되면 화면 변경이 불가능
- 불변이기 때문에 생성된 인스턴스의 build() 함수는 재실행되지 않음
- 변경사항을 반영하려면 인스턴스를 완전히 새로 생성해야 함
StatefulWidget:
- 상태를 가지고 있어 위젯의 상태 변경 가능
- setState() 함수를 통해 build() 메소드를 다시 실행 가능
- 주요 생명주기 메소드:
- createState(): StatefulWidget과 연동되는 State를 생성
- initState(): State가 생성되는 순간 딱 한 번만 실행
- build(): UI를 구성하는 함수
- dispose(): 위젯이 제거될 때 호출
9.1.2 Timer
- 전자액자에서 이미지 자동 전환을 위해 사용
- Timer 클래스는 dart:async 패키지에서 제공함.
- Timer.periodic() 메소드 -> 정해진 시간마다 특정 함수를 반복 실행
Timer.periodic(
Duration(seconds: 3), // 주기
(Timer timer) {}, // 콜백 함수
);
9.2 사전 준비
9.2.1 이미지 추가하기
- 플러터 프로젝트 생성
- 이미지 준비 :: asset/img/ 폴더 생성 -> 이미지 5개를 복사 -> image_1 ~ image_5.jpeg 이미지 준비.
9.2.2 pubspec.yaml 설정하기
- pubspec.yaml :: assets 섹션에 이미지 경로 등록하는 파일
9.2.3 프로젝트 초기화하기
- home_screen.dart 파일, HomeScreen 생성.
9.3 레이아웃 구상하기
전자액자 앱의 레이아웃은 매우 단순합니다. 전체 화면을 채우는 PageView 하나로 구성되며, 이미지들을 좌우로 스와이프하여 넘길 수 있습니다8.
- 화면 전체를 차지하는 PageView 위젯
- 각 페이지는 하나의 이미지를 표시
- 이미지는 화면 크기에 맞게 조절 (BoxFit.cover 사용)
9.4 구현하기
9.4.1. 페이지뷰 구현하기
- PageView 추가하기
import 'package: flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView( // ① PageView 추가
children: [1, 2, 3, 4, 5] // ② 샘플 리스트 생성
.map(// ③ 위젯으로 매핑
(number) => Image.asset('asset/img/image_$number.jpeg'),
)
.toList(),
),
);
}
}
- 이미지 핏 조절하기
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
children: [1, 2, 3, 4, 5]
.map(
(number) => Image.asset(
'asset/img/image_$number.jpeg',
fit: BoxFit.cover,
),
)
.toList(),
),
);
}
)
- BoxFit.cover 사용 -> 이미지가 화면 전체를 채우도록 설정
9.4.2 상태바 색상 변경하기
- HomeScreen 위젯의 build() 메서드에 상태바 아이콘 색상 변경 코드를 추가
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 상태바가 이미 흰색이면 light 대신 dark를 주어 검정으로 바꾸기
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
return Scaffold(
... 생략 ...
);
}
}
9.4.2 타이머 구현
- initState() 메소드에서 타이머 설정
- 3초마다 다음 이미지로 자동 전환되도록 구현
- 3초마다 실행되며, 현재 페이지 번호 확인한 후 다음 페이지로 이동 / 마지막 페이지에 도달하면 첫 번째 페이지로 이동
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
// ❷ State 정의
class _HomeScreenState extends State<HomeScreen> {
final PageController pageController = PageController();
@override
void initState() {
super.initState(); // ➌ 부모 initState() 실행
Timer.periodic(
// ➍ Timer.periodic() 등록
Duration(seconds: 3),
(timer) {
print('실행!');
int? nextPage = pageController.page?.toInt();
// ➋
if (nextPage == null) {
return;
}
// ➌
if (nextPage == 4) {
nextPage = 0;
} else {
nextPage++;
}
pageController.animateToPage(
// ➍ 페이지 변경
nextPage,
duration: Duration(milliseconds: 500),
curve: Curves.ease,
);
},
);
}
@override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
return Scaffold(
body: PageView(
controller: pageController,
// ➊ PageView 추가
children: [1, 2, 3, 4, 5] // ➋ 샘플 리스트 생성
.map(
// ➌ 위젯으로 매핑
(number) => Image.asset(
'asset/img/image_$number.jpeg',
fit: BoxFit.cover,
),
)
.toList(),
),
);
}
}
9.5 테스트하기
- 이미지가 3초마다 자동으로 전환되는지 확인
- 마지막 이미지에서 자동으로 첫 번째 이미지로 돌아가는지 확인
- 사용자가 직접 이미지를 좌우로 스와이프하여 전환할 수 있는지 확인
- 이미지가 화면 크기에 맞게 잘 표시되는지 확인
10. 만난 지 며칠 U&I :: 상태 관리, CupertinoDatePicker, Dialog, Datetime
10.1 사전 지식
10.1.1 setState() 함수
10.1.2 showCupertinoDialog() 함수
import 'package:flutter/cupertino.dart';
showCupertinoDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return Text('Dialog');
},
);
10.2 사전 준비
10.2.3 프로젝트 초기화하기
import 'package: flutter/material.dart' ;
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text('Home Screen'),
);
}
}
import 'package:u_and_i/screen/home_screen.dart';
import 'package: flutter/material.dart' ;
void main() {
runApp(
MaterialApp(
home: HomeScreen(),
),
);
}
10.4 구현하기
10.4.1 홈 스크린 UI 구현하기
import 'package: flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text('Home Screen'),
);
}
}
class _CoupleImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('Couple Image Widget');
}
}
class _DDay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('DDay Widget');
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.pink[100],
... 생략 ...
);
}
}
class _CoupleImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Image.asset(
'asset/img/middle_image.png',
height: MediaQuery.of(context).size.height / 2,
),
);
}
}
...생략...
class _DDay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 16.0),
Text( // 최상단 U&I 글자
'U&I',
),
const SizedBox(height: 16.0),
Text( // 두번째 글자
'우리 처음 만난 날',
),
Text( // 임시로 지정한 만난 날짜
'2021..11.23',
),
const SizedBox(height: 16.0),
IconButton( // 하트 아이콘 버튼
iconSize: 60.0,
onPressed: onHeartPressed,
icon: Icon(
Icons.favorite,
),
),
const SizedBox(height: 16.0),
Text( // 만난 후 DDay
'D+365',
),
],
);
}
}
...생략...
- 위젯 만들기
- main.dart 파일에 텍스트 / IconButton 테마 정의
void main() {
runApp(
MaterialApp(
theme: ThemeData( // ➊ 테마를 지정할 수 있는 클래스
fontFamily: 'sunflower', // 기본 글씨체
textTheme: TextTheme( // ➋ 글짜 테마를 적용할 수 있는 클래스
displayLarge: TextStyle( // headline1 스타일 정의
color: Colors.white, // 글 색상
fontSize: 80.0, // 글 크기
fontWeight: FontWeight.w700, // 글 두께
fontFamily: 'parisienne', // 글씨체
),
displayMedium: TextStyle(
color: Colors.white,
fontSize: 50.0,
fontWeight: FontWeight.w700,
),
bodyLarge: TextStyle(
color: Colors.white,
fontSize: 30.0,
),
bodyMedium: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
)
),
home: HomeScreen(),
),
);
}
- Text 위젯에 스타일 적용
...생략...
class _DDay extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ➊ 테마 불러오기
final textTheme = Theme.of(context).textTheme;
return Column(
children: [
const SizedBox(height: 16.0),
Text(
'U&I',
style: textTheme.displayLarge, // headline1 스타일 적용
),
const SizedBox(height: 16.0),
Text(
'우리 처음 만난 날',
style: textTheme.bodyLarge, // bodyText1 스타일 적용
),
Text(
'2021.11.23',
style: textTheme.bodyMedium, // bodyText2 스타일 적용
),
const SizedBox(height: 16.0),
IconButton(
iconSize: 60.0,
onPressed: () {},
icon: Icon(
Icons.favorite,
color: Colors.red, // ➋ 색상 빨강으로 변경
),
),
const SizedBox(height: 16.0),
Text(
'D+365',
// headline2 스타일 적용
style: textTheme.displayMedium,
),
],
);
}
}
10.1 전체 코드
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
DateTime firstDay = DateTime.now();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.pink[100],
body: SafeArea(
// ➊ 시스템 UI 피해서 UI 그리기
top: true,
bottom: false,
child: Column(
// ➋ 위, 아래 끝에 위젯 배치
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// 반대 축 최대 크기로 늘리기
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_DDay(
// ➎ 하트 눌렀을때 실행할 함수 전달하기
onHeartPressed: onHeartPressed,
firstDay: firstDay,
),
_CoupleImage(),
],
),
),
);
}
void onHeartPressed(){ // ➍ 하트 눌렀을때 실행할 함수
showCupertinoDialog( // ➋ 쿠퍼티노 다이얼로그 실행
context: context,
builder: (BuildContext context) {
return Align( // ➊ 정렬을 지정하는 위젯
alignment: Alignment.bottomCenter, // ➋ 아래 중간으로 정렬
child: Container(
color: Colors.white, // 배경색 흰색 지정
height: 300, // 높이 300 지정
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime date) {
setState(() {
firstDay = date;
});
},
),
),
);
},
barrierDismissible: true,
);
}
}
class _DDay extends StatelessWidget {
final GestureTapCallback onHeartPressed;
final DateTime firstDay;
_DDay({
required this.onHeartPressed, // ➋ 상위에서 함수 입력받기
required this.firstDay,
});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final now = DateTime.now();
return Column(
children: [
const SizedBox(height: 16.0),
Text(
// 최상단 U&I 글자
'U&I',
style: textTheme.displayLarge,
),
const SizedBox(height: 16.0),
Text(
// 두번째 글자
'우리 처음 만난 날',
style: textTheme.bodyLarge,
),
Text(
// 임시로 지정한 만난 날짜
'${firstDay.year}.${firstDay.month}.${firstDay.day}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 16.0),
IconButton(
// 하트 아이콘 버튼
iconSize: 60.0,
onPressed: onHeartPressed,
icon: Icon(
Icons.favorite,
color: Colors.red,
),
),
const SizedBox(height: 16.0),
Text(
// 만난 후 DDay
'D+${DateTime(now.year, now.month, now.day).difference(firstDay).inDays + 1}',
style: textTheme.displayMedium,
),
],
);
}
}
class _CoupleImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Expanded(
// Expanded 추가
child: Center(
// ➊ 이미지 중앙정렬
child: Image.asset(
'asset/img/middle_image.png',
// ➋ 화면의 반만큼 높이 구현
height: MediaQuery.of(context).size.height / 2,
),
),
);
}
}
'EDOC > Flutter 스터디 (2025-1)' 카테고리의 다른 글
| [3.7 ~ 3.9] 오늘도 출첵 ~ AI 채팅봇, 소울챗 (1) | 2025.05.21 |
|---|---|
| [3.4 ~ 3.6] 디지털 주사위 ~ 영상 통화 (1) | 2025.05.21 |
| [2.6 ~ 2.7] 기본 위젯 알아보기 ~ 앱을 만들려면 알아야 하는 그 밖의 지식 (0) | 2025.04.12 |
| [1.4 ~ 2.5] 다트 3.0 신규 문법 / 플러터 입문하기 (0) | 2025.04.05 |
| [1.3] 다트 비동기 프로그래밍 (0) | 2025.03.29 |