[ Flutter 01 ] Flutter 시작하기
- 모든 플랫폼에 능하다.
- Dart 언어기반이다
예시 ) wonderous, flutter plasma, flokk
- 가상엔진위에서 작동해서 모든것을 그려낸다. dart 코드로 작성하면 그것을 해당플랫폼에 전달할때 해당플랫폼에 맞는 가상머신과 같이 컴파일한다.
[ Installation ]
…
위에 방식데로 해도 상관없으나 패키지 관리자로 설치하는것을 추천
window : chocolatey
macOS : homebrew
brew install --cask flutter

flutter 에관한 여러진단을 내려준다.
flutter doctor
쉽게 dart 언어를 테스트할 수 있다.
flutter create {프로젝트명}
visual studio code 로 진행 및 extension을 설치
- dart extension
- flutter extension
- Error lens extension
visual studio code 로 실뮬레이터 연다음 프로젝트 실행
libs/main.dart
[ Widget ]
- Class를 만들어서 레고블럭처럼 조합해서 사용한다. Widget
- StatelessWidget 을 상속받은 Class를 만든다.
- StatelessWidget은 build method를 반드시 작성해야한다.
- build method 는 Widget의 UI를 만드는것이다.
- build method 는 자동 작성이되는데, 기본 override 함수이다. ( 이미 Stateless Widget 에 작성되어있음 )
- root widget 은 두가지중 하나를 return 해야한다.
- material(구글스타일), cupertino(IOS스타일) 구글의 디자인 시스템
- Scaffold 를 가져야한다.
Main.dart
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget{
@override
Widget build(BuildContext context) {
return MaterialApp(
home:Scaffold(
appBar: AppBar(
title:Text("App Bar"),
),
body:Center(
child:Text("hello world!"),
),
)
);
}
}
[ class ]
- name parameter 형식으로 class들이 이루어져있다.
- class 가 안에 parameter가 많은경우

[ UI 예시 ]
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Color(0xFF181818),
body: Padding(
padding: EdgeInsets.symmetric(
horizontal: 40,
),
child: Column(
children: [
SizedBox(
height: 80,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Hey, Selena',
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.w800,
),
),
Text(
'Welcome back',
style: TextStyle(
color: Color.fromRGBO(255, 255, 255, 0.8),
fontSize: 18,
),
),
],
)
],
)
],
),
),
),
);
}
}
- Column, crossAxisAlignment : 수직축 / mainAxisAlignment : 수평
- Row, mainAxisAlignment : 수평축 / crossAxisAlignment : 수직축
- Sized Box, 공간을 만들기위해 사용
Row(
children: [
Container(
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(45),
),
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 20,
horizontal: 50,
),
child: Text(
'Transfer',
style: TextStyle(
fontSize: 20,
),
),
),
)
],
)- Box Container 로 버튼을 만드는 방식
[ Settings ]
- dart 는 const 상수를 쓰는경우 컴파일 효율을 높일 수 있어 추천된다. blue line 이 뜨는이유…
- visual studio code 에서 command + shift + p > open user settings(json) >
"editor.codeActionsOnSave":{
"source.fixAll":true
} - blue line이 사라진다. 자동으로 const 가설정된다. ( const 설정추천 )
"dart.previewFlutterUiGuides":true– 어떤 위젯이 부모/자식 관계인지 알려준다. ( 껐다 켜야 적용됨 )
"editor.formatOnSave":true
- 저장할때마다 코드를 이쁘게 처리한다.
[ Reusable Widget ]
import 'package:flutter/material.dart';
class Button extends StatelessWidget {
final String text;
final Color bgColor;
final Color textColor;
const Button({
super.key,
required this.text,
required this.bgColor,
required this.textColor,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(45),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 20,
horizontal: 50,
),
child: Text(
text,
style: TextStyle(
color: textColor,
fontSize: 20,
),
),
),
);
}
}
[ Transform.scale & translate ]

Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: const Color(0xFF1F2123),
borderRadius: BorderRadius.circular(25),
),
child: Padding(
padding: const EdgeInsets.all(30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Euro',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w600,
),
),
const SizedBox(
height: 10,
),
Row(
children: [
const Text(
'6 428',
style: TextStyle(
color: Colors.white,
fontSize: 20,
),
),
const SizedBox(
width: 5,
),
Text(
'EUR',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 20,
),
),
],
)
],
),
Transform.scale(
scale: 2.2,
child: Transform.translate(
offset: const Offset(-5, 15),
child: const Icon(
Icons.euro_rounded,
color: Colors.white,
size: 88,
),
),
)
],
),
),
),
[ State ]
- StatelessWidget : 단순히 UI를 빌드하는것
- StatefulWiget : 두부분으로 나뉜다.
- 첫번째 : widget. 그자체 , 적은양의 코드
- 두번째 : state , 데이터와 UI를 저장, 데이터가 변경되면 UI도변
- 주의점 : statefule.widget은 initState() 초기화 메서드가 있다. 상위.context 데이터를 초기화하기위하거나, 등등의 이유로 사용한다.
build 보다 먼저써야한다.
- dispose는 사라질때 사용한다. 위젯이 보여주거나.toggle할때 사용한다.
- 단축키 :st
- setState() 메서드를 호출해야 build 가 다시된다.

[ BuildContext ]
- Context 는 이전에 있는 모든 상위 요소들에 대한 정보 Widget 부모
- Theme.of(context)…. : 상위요소의 property에 접근가능하다.
[ Stateful Widget ]
import 'dart:async';
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
static const twentyFiveMinutes = 1500;
int totalSeconds = twentyFiveMinutes;
bool isRunning = false;
int totalPomodoros = 0;
late Timer timer;
void onTick(Timer timer) {
if (totalSeconds == 0) {
setState(() {
totalPomodoros = totalPomodoros + 1;
isRunning = false;
totalSeconds = twentyFiveMinutes;
});
timer.cancel();
} else {
setState(() {
totalSeconds = totalSeconds - 1;
});
}
}
void onStartPressed() {
timer = Timer.periodic(
const Duration(seconds: 1),
onTick,
);
setState(() {
isRunning = true;
});
}
void onPausePressed() {
timer.cancel();
setState(() {
isRunning = false;
});
}
String format(int seconds) {
var duration = Duration(seconds: seconds);
return duration.toString().split(".").first.substring(2, 7);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: Column(
children: [
Flexible(
flex: 1,
child: Container(
alignment: Alignment.bottomCenter,
child: Text(
format(totalSeconds),
style: TextStyle(
color: Theme.of(context).cardColor,
fontSize: 89,
fontWeight: FontWeight.w600,
),
),
),
),
Flexible(
flex: 3,
child: Center(
child: IconButton(
iconSize: 120,
color: Theme.of(context).cardColor,
onPressed: isRunning ? onPausePressed : onStartPressed,
icon: Icon(isRunning
? Icons.pause_circle_outline
: Icons.play_circle_outline),
),
),
),
Flexible(
flex: 1,
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(50),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Pomodoros',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color:
Theme.of(context).textTheme.displayLarge!.color,
),
),
Text(
'$totalPomodoros',
style: TextStyle(
fontSize: 58,
fontWeight: FontWeight.w600,
color:
Theme.of(context).textTheme.displayLarge!.color,
),
),
],
),
),
),
],
),
)
],
),
);
}
} [ key ]
- 위젯 식별자 key
[pub.dev]
flutter api 찾는곳
의존성 추가하는방법은 아래와같다.
dart pub add http
or
flutter pub add http
pubspec.yaml --> 복붙
그리고 visual studio code 에 버튼있음. 모든 패키지 다운로드
pubspec.yaml. 페이지열면,
[ data fetch ]
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:toonflix2/models/webtoon_model.dart';
class ApiService {
static const String baseUrl =
"https://webtoon-crawler.nomadcoders.workers.dev";
static const String today = "today";
static Future<List<WebtoonModel>> getTodaysToons() async {
List<WebtoonModel> webtoonInstances = [];
final url = Uri.parse('$baseUrl/$today');
final response = await http.get(url);
if (response.statusCode == 200) {
final webtoons = jsonDecode(response.body);
for (var webtoon in webtoons) {
final instance = WebtoonModel.fromJson(webtoon);
webtoonInstances.add(instance);
}
return webtoonInstances;
}
throw Error();
}
}
class WebtoonModel {
final String title, thumb, id;
WebtoonModel.fromJson(Map<String, dynamic> json)
: title = json['title'],
thumb = json['thumb'],
id = json['id'];
}
- http library를 설치하여 진행하였다.
- Future 형태의. return 값 함수이다. 통신이 미래에 이루어지기때문에
- WebtoonModel. 생성자를 만들고, Map<String, dynamic> 형태의 키-밸류 로 맵핑하여 사용하였다.
[ Future Builder ]
import 'package:flutter/material.dart';
import 'package:toonflix2/models/webtoon_model.dart';
import 'package:toonflix2/services/api_service.dart';
class HomeScreen extends StatelessWidget {
HomeScreen({super.key});
final Future<List<WebtoonModel>> webtoons = ApiService.getTodaysToons();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 2,
backgroundColor: Colors.white,
foregroundColor: Colors.green,
title: const Text(
"어늘의 웹툰",
style: TextStyle(
fontSize: 24,
),
),
),
body: FutureBuilder(
future: webtoons,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: [
const SizedBox(
height: 30,
),
Expanded(child: makeList(snapshot))
],
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
),
);
}
ListView makeList(AsyncSnapshot<List<WebtoonModel>> snapshot) {
print(snapshot.data!);
return ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: snapshot.data!.length,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
itemBuilder: (context, index) {
var webtoon = snapshot.data![index];
return Column(
children: [
Container(
width: 250,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
blurRadius: 15,
offset: const Offset(10, 10),
color: Colors.black.withOpacity(0.3),
)
],
),
child: Image.network(webtoon.thumb),
),
const SizedBox(
height: 10,
),
Text(
webtoon.title,
style: const TextStyle(
fontSize: 22,
),
),
],
);
},
separatorBuilder: (context, index) => const SizedBox(width: 40),
);
}
}
- StatefullWidget을 사용하지않고도 StatelessWidget에서도 통신할수있다.
- FutureBuild 를 사용하면 snapshopt에서 future데이터를 읽을수있다.
-
[ Routing and Heror ]
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(
title: title,
thumb: thumb,
id: id,
),
fullscreenDialog: true,
),
);
},- GestureDetector를 추가한다.
- Navigator.push. 로 stateless widget으로 페이지를 이동시킬수있다.
- MaterialPageRoute가 애니메이션효과를 주면서 이동시킨다.
- fullscreenDialog 옵션을 주게되면 밑에서 올라오고 , 전환된 페이지에서 x 로 변경된다.
Hero(
tag: id,
child: Container(
width: 250,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
blurRadius: 15,
offset: const Offset(10, 10),
color: Colors.black.withOpacity(0.3),
)
],
),
child: Image.network(thumb),
),
- Hero 위젯을 쓰는경우, 같은 태그 아이디를 가지고 있는것에 Navigator 될경우 floating animation 효과가 생긴다.
[ url_launcher ]
$ flutter pub add url_launcher
info.plist
/IOS/Runner/info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>sms</string>
<string>tel</string>
</array>
AndroidManifest.xml
/android/app/src/main/AndroidManifest.xml
<!-- Provide required visibility configuration for API level 30 and above -->
<queries>
<!-- If your app checks for SMS support -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="sms" />
</intent>
<!-- If your app checks for call support -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
<!-- If your application checks for inAppBrowserView launch mode support -->
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
onButtonTap() async {
await launchUrlString(
"https://comic.naver.com/webtoon/detail?titleId=$webtoonId&no=${episode.id}");
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onButtonTap,
child: Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.green.shade400,
boxShadow: [
BoxShadow(
blurRadius: 2,
offset: const Offset(2, 2),
color: Colors.black.withOpacity(0.1),
),
],
),
[ shared_preferences ]
class _DetailScreenState extends State<DetailScreen> {
late Future<WebtoonDetailModel> webtoon;
late Future<List<WebtoonEpisodeModel>> episodes;
late SharedPreferences prefs;
bool isLiked = false;
Future initPrefs() async {
prefs = await SharedPreferences.getInstance();
final likedToons = prefs.getStringList('likedToons');
if (likedToons != null) {
if (likedToons.contains(widget.id) == true) {
setState(() {
isLiked = true;
});
}
} else {
await prefs.setStringList('likedToons', []);
}
}
@override
void initState() {
super.initState();
webtoon = ApiService.getToonById(widget.id);
episodes = ApiService.getLatestEpisodesById(widget.id);
initPrefs();
}
onHeartTap() async {
final likedToons = prefs.getStringList('likedToons');
if (likedToons != null) {
if (isLiked) {
likedToons.remove(widget.id);
} else {
likedToons.add(widget.id);
}
await prefs.setStringList('likedToons', likedToons);
setState(() {
isLiked = !isLiked;
});
}
Tips
command + .
Widget 감싸기

