[Flutter] ํ๋ฌํฐ ๊ธฐ์ด ๋ด์ฉ ์ ๋ฆฌ - Part 2
๐ก ์๋๋ Part 1์์ ์ด์ด์ง๋ ๋ด์ฉ
Data Fetching
๊ฐ์ฒด์งํฅ ์ธ์ด์ธ Dart๋ ๊ฑฐ์ ๋๋ถ๋ถ์ด ํด๋์ค ๊ธฐ๋ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ค. API ํธ์ถ, ์๋ต ๋ฐ์ดํฐ ํ์ ์ ์, ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ญ์ ๋ชจ๋ ํด๋์ค ๋ด๋ถ์์ ์ด๋ฃจ์ด์ง๋ค.
๋จผ์ ์๋ต ๋ฐ์ดํฐ๋ฅผ ๋ํ๋ด๋ ๋ชจ๋ธ ํด๋์ค๋ฅผ ์ ์ํ๋ค. ์ผ๋ฐ์ ์ผ๋ก fromJson
์ด๋ผ๋ ์ด๋ฆ ์๋ ์์ฑ์๋ฅผ ์ฌ์ฉํ์ฌ JSON ๋ฐ์ดํฐ๋ฅผ ์ธ์คํด์คํ ์ํจ๋ค. ์๋ ์์์์ JSON ๊ฐ์ฒด๋ฅผ ์ธ์๋ก ๋ฐ๊ธฐ ๋๋ฌธ์ ์ด๊ธฐํ ๋ฆฌ์คํธ๋ฅผ ์ฌ์ฉํ์ฌ ํด๋์ค ํ๋์ ํ ๋นํ๊ณ ์๋ค.
// lib/models/webtoon_model.dart
class WebtoonModel {
final String id, title, thumb;
WebtoonModel.fromJson(Map<String, dynamic> json)
: title = json['title'],
thumb = json['thumb'],
id = json['id'];
}
Dart์ ๋คํธ์ํฌ ํธ์ถ์ ์ฃผ๋ก http ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ค. ๋ธ๋ผ์ฐ์ , Node.js์์ ์ฌ์ฉํ ์ ์๋ fetch ํจ์์ ๋น์ทํ๋ค๊ณ ๋ณด๋ฉด ๋๋ค. pubspec.yaml ํ์ผ dependencies ๋ชฉ๋ก์ http: ๋ฒ์
์ ์ถ๊ฐํ๋ฉด, IDE๊ฐ ์๋์ผ๋ก ์ธ์ํ์ฌ ์์กด์ฑ ์ถ๊ฐ ์ฌ๋ถ ํ์
์ด ํ์๋๋ค. ํน์ Pub get ๋ฒํผ์ ๋๋ฌ์ ๋ฐ๋ก ์ถ๊ฐํ ์๋ ์๋ค. ํจํค์ง๋ฅผ ์ ๊ฑฐํ ๋๋ dependencies ๋ชฉ๋ก์์ ์ง์ด ํ Pub get ๋ฒํผ์ ๋๋ฅด๋ฉด ๋๋ค.
pub get
: ํจํค์ง ๋ค์ด๋ก๋/์ค์น (์ต์ด ํ๋ก์ ํธ ์ค์ ํน์ ํจํค์ง ์ถ๊ฐ ํ ์ฌ์ฉ)pub upgrade
: ์ฌ์ฉ ์ค์ธ ํจํค์ง๋ค์ ๊ฐ๋ฅํ ์ต์ ๋ฒ์ ์ผ๋ก ์ ๊ทธ๋ ์ด๋pub outdated
: ์ฌ์ฉ ์ค์ธ ํจํค์ง๋ค์ ํ์ฌ ๋ฒ์ ๊ณผ ์ ๊ทธ๋ ์ด๋ ๊ฐ๋ฅํ ๋ฒ์ ์ ๋น๊ตํด์ ํ์
๊ทธ ํ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ณ , ๋ถ๋ฌ์จ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ API ์๋น์ค ํด๋์ค๋ฅผ ์ ์ํ๋ค. BaseUrl๊ณผ ๊ฐ์ ๊ฐ์ ๋ณ๊ฒฝ๋์ง ์๊ธฐ ๋๋ฌธ์ const
ํค์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ปดํ์ผ ์์๋ก ์ ์ธํ๋ค. ์ด๋ฌํ ๊ฐ์ ์ฌ๋ฌ ์ธ์คํด์ค์์ ๋์ผํ๊ฒ ์ฌ์ฉํ๋ฏ๋ก ํด๋์ค์ ์ ์ (static) ํ๋๋ก ์ ์ํ๋๊ฒ ์ ํฉํ๋ค. ๋ฐ์ดํฐ ํจ์นญ ๋ก์ง์ ๋ณ๋์ ๋ฉ์๋์ ์์ฑํ๋ค.
// services/api_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/webtoon_model.dart';
class ApiService {
static const String baseUrl = 'webtoon-crawler.nomadcoders.workers.dev';
static const String today = "today";
// ์ค๋์ ์นํฐ ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋ ๋ฉ์๋. async ํค์๋๋ฅผ ๋ถ์๊ธฐ ๋๋ฌธ์ Future๋ฅผ ๋ฐํํ๋ค.
static Future<List<WebtoonModel>> getTodayToons() async {
try {
// baseUrl๊ณผ today๋ฅผ ์ฌ์ฉํด HTTPS URI ์์ฑ
// Uri.parse(...)๋ก๋ ๊ฐ๋ฅ(์ด๋ ์ฃผ์์ ํ๋กํ ์ฝ๊น์ง ๋ชจ๋ ๋ช
์)
final uri = Uri.https(baseUrl, '/$today');
// ์์ฑํ URI๋ก GET ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต ๋๊ธฐ
final response = await http.get(uri);
if (response.statusCode == 200) {
// String์ผ๋ก ๋ฐ์ ์๋ต ๋ฐ์ดํฐ๋ฅผ JSON์ผ๋ก ๋์ฝ๋ฉ. json.decode(...)๋ก๋ ๊ฐ๋ฅ
final List<dynamic> webtoons = jsonDecode(response.body);
// ๋์ฝ๋ฉ๋ ๊ฐ JSON ๋ฐ์ดํฐ๋ฅผ WebtoonModel ์ธ์คํด์ค๋ก ๋งคํํ์ฌ ๋ฆฌ์คํธ๋ก ๋ฐํ
return webtoons.map((data) => WebtoonModel.fromJson(data)).toList();
} else {
// ์ํ ์ฝ๋๊ฐ 200์ด ์๋ ๊ฒฝ์ฐ, ์์ธ ๋ฐ์
throw Exception('Failed to load webtoons: ${response.statusCode}');
}
} on http.ClientException catch (e) {
// HTTP ํด๋ผ์ด์ธํธ ์์ธ ๋ฐ์ ์
throw Exception('Failed to fetch webtoons: $e');
} on Exception catch (e) {
// ๊ธฐํ ์์ธ ๋ฐ์ ์
throw Exception('An unknown error occurred: $e');
}
}
}
// ์ฌ์ฉ ์์. ์ฝ์ ์ถ๋ ฅ: [Instance of 'WebtoonModel', ...]
ApiService.getTodayToons().then(print);
Uri.https
์์ฑ์๋ฅผ ์ฌ์ฉํ๋ฉด ํ๋กํ ์ฝ์ด ํฌํจ๋ URI ์ฃผ์๋ฅผ ์์ฑํ๋ค. ์ฒซ ๋ฒ์งธ ์ธ์๋ ๋ณดํต ๋๋ฉ์ธ ์ฃผ์๋ฅผ, ๋๋ฒ์งธ ์ธ์๋ ๋ฆฌ์์ค์ ๋ํ ๊ฒฝ๋ก(path)๋ฅผ ์ง์ ํ๋ค. ์ด๋ ๊ฒฝ๋ก ๊ฐ์ฅ ์์๋ /
์ฌ๋์๋ฅผ ๋ถ์ฌ์ผ ํ๊ณ , ๋ด๋ถ์ ์ผ๋ก ์๋ ์ธ์ฝ๋ฉ ๋๊ธฐ ๋๋ฌธ์, ์ธ์ฝ๋ฉ๋์ง ์์ ์ฃผ์๋ฅผ ๋๊ฒจ์ผ ํ๋ค.
on ์์ธ์ ํ
๊ตฌ๋ฌธ์ ํน์ ์์ธ ์ ํ์ ์ง์ ํ ๋ ์ฌ์ฉํ๋ค. ์์ธ ๊ฐ์ฒด๊ฐ ํ์ํ ๋ on ์์ธ์ ํ catch
๊ตฌ๋ฌธ์ ์ฌ์ฉํ๋ค. ์ผ์นํ๋ ์์ธ ์ ํ์ด ์์ผ๋ฉด ๋ค์ ๋ธ๋ก์ผ๋ก ๋์ด๊ฐ๊ณ , ์์ธ๋ฅผ ์ฒ๋ฆฌํ ๋ธ๋ก์ด ์์ ๊ฒฝ์ฐ์ ํ๋ก๊ทธ๋จ์ด ์ข
๋ฃ๋๊ธฐ ๋๋ฌธ์ ์ผ๋ฐ catch
๋ธ๋ก๋ ํจ๊ป ์ง์ ํด์ฃผ๋ ๊ฒ์ด ์ข๋ค.
๋ฐ์ดํฐ ํจ์นญ/์ฒ๋ฆฌ ๊ณผ์ ์ ์ ๋ฆฌํ๋ฉด ์๋์ ๊ฐ๋ค.
- URI ์์ฑ :
Uri.https()
- ๋คํธ์ํฌ ์์ฒญ :
http.get()
- ๋ฌธ์์ด์ JSON์ผ๋ก ๋ณํ :
jsonDecode()
- JSON ๊ฐ ํญ๋ชฉ์ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ก ๋งตํ ํ ๋ฆฌ์คํธ๋ก ๋ฐํ
FutureBuilder
FutureBuilder๋ ๋น๋๊ธฐ ์์
์ ์ํ๋ฅผ ์ถ์ ํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ UI๋ฅผ ์๋์ผ๋ก ์
๋ฐ์ดํธํ ์ ์๋ ์์ ฏ์ด๋ค. FutureBuilder๋ Future ๊ฐ์ฒด๋ฅผ ์ธ์๋ก ๋ฐ์ ํด๋น ์์
์ด ์๋ฃ๋๋ฉด builder
ํจ์๋ฅผ ํตํด UI๋ฅผ ๋ค์ ๋ ๋๋งํ๋ค. then
, await
๋ฑ์ ์ฌ์ฉํ์ง ์๊ณ ๋ ๋น๋๊ธฐ ์์
์ํ์ ๋ฐ๋ผ UI๋ฅผ ๊ด๋ฆฌํ ์ ์๋ ์ฅ์ ์ด ์๋ค. React 18๋ฒ์ ๋ถํฐ ์ง์ํ๋ Suspense์ ์ ์ฌํ๋ค๊ณ ๋ณผ ์ ์๋ค.
FutureBuilder๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฉด isLoading
๋ด๋ถ ์ํ๋ฅผ ์ถ๊ฐํ๊ณ , initState
๊ฐ์ ๋ผ์ดํ์ฌ์ดํด ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด StatefulWidget์ผ๋ก ์์ฑํด์ผํ๋ค. ์ด๋ก์ธํด ๊ฑฐ์ถ์ฅ์ค๋ฌ์ด ๋ณด์ผ๋ฌํ๋ ์ดํธ๊ฐ ๋ง์์ง๋ค .
// FutureBuilder๋ฅผ ์ฌ์ฉํ์ง ์์์ ๋
class HomeScreen extends StatefulWidget {
// ...
}
class _HomeScreenState extends State<HomeScreen> {
List<WebtoonModel> webtoons = [];
bool isLoading = true;
void waitForWebToons() async {
webtoons = await ApiService.getTodayToons();
isLoading = false;
setState(() => {});
}
@override
void initState() {
super.initState();
waitForWebToons();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(/* ... */),
);
}
}
FutureBuilder๋ฅผ ์ฌ์ฉํ๋ฉด StatefulWidget์ ์ฌ์ฉํ์ง ์๊ณ ๋ ์ ์ธ์ ์ธ ๋ฐฉ์์ผ๋ก ๋น๋๊ธฐ ์์
์ ์ฒ๋ฆฌํ ์ ์๋ค. FutureBuilder์ future
์์ฑ์ ๋น๋๊ธฐ ์์
์ ๋ฐํํ๋ Future ๊ฐ์ฒด๋ฅผ ๋ฃ์ด์ฃผ๋ฉด, ๋น๋๊ธฐ ์์
์ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค builder
ํจ์๊ฐ ์คํ๋ผ์ UI๋ฅผ ์
๋ฐ์ดํธํ๋ค.
// FutureBuilder๋ฅผ ์ฌ์ฉํ์ ๋
// ...
import 'package:practice_flutter/services/api_service.dart';
class HomeScreen extends StatelessWidget {
HomeScreen({super.key});
late final Future<List<WebtoonModel>> webtoons = ApiService.getTodayToons();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(/* ... */),
body: FutureBuilder(
future: webtoons,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// ๋ก๋ฉ ์ค์ผ ๋
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
// ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
// ๋ฐ์ดํฐ ์์ ๋ (์๋ต)
}
// ๋ฐ์ดํฐ ์์ ๋
return const Center(child: Text('No data available'));
},
),
);
}
}
builder
ํจ์์ ๋ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ snapshot
์ Future์ ์ํ๋ฅผ ๋ํ๋ด๋ ๊ฐ์ฒด๋ก ๋ค์ํ ์์ฑ์ ์ ๊ณตํ๋ค. ์ด๋ฅผ ์ด์ฉํด ํ์ฌ ์ํ์ ๋ง๋ UI๋ฅผ ์ ์ ํ๊ฒ ํ์ํ ์ ์๋ค.
- connectionState: ๋น๋๊ธฐ ์์
์ ํ์ฌ ์ํ
- none: Future๊ฐ ์์๋์ง ์์
- waiting: Future๊ฐ ์คํ ์ค์ด๋ฉฐ, ๊ฒฐ๊ณผ ๋๊ธฐ ์ค
- active: Future ์งํ ์ค (์ฃผ๋ก StreamBuilder์์ ์ฌ์ฉ)
- done: Future ์๋ฃ
- hasData: ์ฑ๊ณต์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ผ๋ฉด
true
๋ฐํ - hasError: ์๋ฌ ๋ฐ์ ์
true
๋ฐํ - data: ์๋ฃ ํ ๋ฐํ๋ ๋ฐ์ดํฐ
- error: ๋ฐ์ํ ์๋ฌ ๋ฉ์์ง ๋๋ ๊ฐ์ฒด
ListView
์คํฌ๋กค ํ ์ ์๋ ๋ชฉ๋ก์ ํ์ํ ๋ ListView
์์ ฏ์ ์ฌ์ฉํ๋ค. ListView
๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์คํฌ๋กค ๊ธฐ๋ฅ์ ์ง์ํ์ฌ ๋ฐ์ดํฐ๊ฐ ํ๋ฉด์ ์ด๊ณผํ ๊ฒฝ์ฐ ์คํฌ๋กคํ ์ ์๋ค. ๋ฐ์ดํฐ ์์ด ๋ง๋ค๋ฉด ListView.builder
์์ฑ์๋ฅผ ์ฌ์ฉํด์ ์ง์ฐ ๋ก๋ฉ(Lazy Loading)์ ์ ์ฉํ ์๋ ์๋ค.
// ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.map), // ์นด๋ ๊ฐ์ฅ ์ผ์ชฝ์ ๋ํ๋๋ ์์
title: Text('Map'), // ์นด๋ ์ฐ์ธก ์๋จ ์ ๋ชฉ
),
ListTile(
leading: Icon(Icons.photo_album),
title: Text('Album'),
),
ListTile(
leading: Icon(Icons.phone),
title: Text('Phone'),
),
],
)
ListView.builder
์์ฑ์๋ ํ๋ฉด์ ๋
ธ์ถ๋ ํญ๋ชฉ๋ค๋ง ๋น๋ํ๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ ์ต์ ํ๊ฐ ํ์ํ ์ํฉ์ด๋ ๋ฌดํ ์คํฌ๋กค ๋ฑ์ ๊ตฌํํ ๋ ์ ์ฉํ๊ฒ ์ฐ์ธ๋ค. ํนํ, ๋ฐ์ดํฐ๊ฐ ๋ง๊ฑฐ๋ ๋์ ์ผ๋ก ์ถ๊ฐ๋ ๋ ์ด์ ์ ๋
ธ์ถ๋ ํญ๋ชฉ์ ์ฌ์ฌ์ฉํ์ฌ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ ์ฝํ ์ ์๋ค.
class HomeScreen extends StatelessWidget {
HomeScreen({super.key});
late final Future<List<WebtoonModel>> webtoons = ApiService.getTodayToons();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(/* ... */),
body: FutureBuilder(
future: webtoons,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
// ์คํฌ๋กค ๋ฐฉํฅ ์ง์
scrollDirection: Axis.horizontal,
// ์์ดํ
์ด ๊ฐ์ ์ง์
itemCount: snapshot.data!.length,
// ํ๋ฉด์ ๋
ธ์ถ๋ ์์ดํ
์ ์์ฑํ ๋ ํธ์ถ๋๋ ํจ์
itemBuilder: (BuildContext context, int index) {
var webtoon = snapshot.data![index];
return Text(webtoon.title);
},
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
),
);
}
}
itemCount
์์ฑ์ผ๋ก ์์ดํ
์ ์ด ๊ฐ์๋ฅผ ์ง์ ํ๋ฉด ๋น๋๊ฐ ํธ์ถ๋ ํ์๋ฅผ ๋ฏธ๋ฆฌ ๊ณ์ฐํด์ ๋ฆฌ์คํธ๋ฅผ ๋ ํจ์จ์ ์ผ๋ก ์์ฑํ๋ค. itemBuilder
๋ ๊ฐ ์์ดํ
์ ์์ฑํ ๋ ํธ์ถ๋๋ ํจ์๋ก ์์ดํ
์ด ํ๋ฉด์ ๋
ธ์ถ๋ ๋๋ง๋ค ํด๋น ์์ดํ
์ ์ธ๋ฑ์ค ์ ๋ณด๊ฐ itemBuilder
๋ ๋ฒ์งธ ์ธ์๋ก ์ ๋ฌ๋๋ค.
๋ฆฌ์คํธ ํญ๋ชฉ ์ฌ์ด์ ๊ตฌ๋ถ์ ์ ๋ฃ์ ์ ์๋ ListView.separated
์์ฑ์๋ ์๋ค. ๊ฐ ์์ดํ
์ฌ์ด์ ๊ฐ๊ฒฉ์ ์ถ๊ฐํ๊ฑฐ๋ ํน์ ์์ ฏ์ ํ์ํ ๋ ์ฌ์ฉํ๋ค. separatorBuilder
๋ ๋ฆฌ์คํธ ํญ๋ชฉ ์ฌ์ด์ ๋ค์ด๊ฐ ๊ตฌ๋ถ์ ์ ์์ฑํ๋ ํจ์๋ก, ๊ตฌ๋ถ์ ์ด ํ๋ฉด์ ๋
ธ์ถ๋ ๋๋ง๋ค ํธ์ถ๋๊ณ , ๋ ๋ฒ์งธ ์ธ์๋ก ๋
ธ์ถ ์ค์ธ ํญ๋ชฉ์ ์ธ๋ฑ์ค๊ฐ ์ ๋ฌ๋๋ค.
class HomeScreen extends StatelessWidget {
HomeScreen({super.key});
late final Future<List<WebtoonModel>> webtoons = ApiService.getTodayToons();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(/* ... */),
body: FutureBuilder(
future: webtoons,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: snapshot.data!.length,
itemBuilder: (BuildContext context, int index) {
var webtoon = snapshot.data![index];
return Text(webtoon.title);
},
// ํ๋ฉด์ ๋
ธ์ถ๋ ์์ดํ
์ฌ์ด์ ๊ตฌ๋ถ์ ์ ์์ฑํ ๋ ํธ์ถ
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(width: 10);
},
);
}
return const Center(child: CircularProgressIndicator());
},
));
}
}
Image.network
๋คํธ์ํฌ ์์ ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ฌ ๋ Image.network
์์ฑ์๋ฅผ ์ฌ์ฉํ๋ค. Image.network
๋ ๋ก๋ฉ ์ฒ๋ฆฌ๋ฅผ ์ํ loadingBuilder
, ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ํ errorBuilder
๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ฉฐ, ์ด๋ฏธ ๋ค์ด๋ก๋ํ ์ด๋ฏธ์ง๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ธฐ๊ธฐ์ ์ ์ฅ๋ ์บ์๋ฅผ ์ฌ์ฌ์ฉํ๋ค.
Image.network(
// ๋ถ๋ฌ์ฌ ์ด๋ฏธ์ง URL
'https://example.com/image.png',
// ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๋ ๋์ ํ์ํ ์์ ฏ ์ ์
loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child; // ๋ก๋ ์๋ฃ ์ ์ด๋ฏธ์ง(child) ๋ฐํ
} else {
return Center(
// ์งํ๋ฅ ์ ํ์ํ๋ ์ํ ์ธ๋์ผ์ดํฐ. value๋ 0.0~1.0 ์ฌ์ด์ ๊ฐ. null์ด๋ฉด ๋ฌดํ ๋ก๋ฉ ์ธ๋์ผ์ดํฐ๋ก ๋์
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: null, // ์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ์ ์ ์๋ ๊ฒฝ์ฐ null ๋ฐํ
),
);
}
},
// ์ด๋ฏธ์ง ๋ก๋์ ์คํจํ์ ๋ ํ์ํ ์์ ฏ ์ ์
errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
return Text('์ด๋ฏธ์ง ๋ก๋ ์คํจ');
},
// ๋คํธ์ํฌ ์์ฒญ์ ํ์ํ HTTP ํค๋ ์ง์
headers: const {
'Referer': 'https://example.com',
},
// ์ด ์ธ์๋ width, height, fit ๋ฑ์ ๋งค๊ฐ๋ณ์๋ฅผ ํตํด ์ด๋ฏธ์ง ํ์ ๋ฐฉ์์ ์กฐ์ ํ ์ ์๋ค
)
loadingBuilder
์ฝ๋ฐฑ์ ์๋ 3๊ฐ์ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ๋๋ค.
- context: ํ์ฌ ์์ ฏ ํธ๋ฆฌ์์ Image.network๊ฐ ์์นํ ๋น๋ ์ปจํ ์คํธ.
- child: ์ด๋ฏธ์ง ๋ก๋๋ฅผ ์๋ฃํ์ ๋ ํ์ํ ์ค์ ์ด๋ฏธ์ง.
- loadingProgress: ์ด๋ฏธ์ง ๋ก๋ ์ํ๋ฅผ ๋ํ๋ด๋ ImageChunkEvent ๊ฐ์ฒด. ๋ก๋ ์๋ฃ ์ null ๋ฐํ. ์ด ๊ฐ์ฒด๋ ์๋ ๋ ์์ฑ์ ๊ฐ์ง๋ค.
- cumulativeBytesLoaded: ํ์ฌ๊น์ง ๋ค์ด๋ก๋ํ ๋ฐ์ดํธ ์ (int).
- expectedTotalBytes: ์ด๋ฏธ์ง ํ์ผ์ ์์ ์ด ๋ฐ์ดํธ ์ (int?). ์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ์ ์ ์์ผ๋ฉด null ๋ฐํ.
GestureDetector
์ฌ์ฉ์์ ํฐ์น ๋์์ ๊ฐ์งํ๊ณ ์ฒ๋ฆฌํ ๋ GestureDetector
์์ ฏ์ ์ฌ์ฉํ๋ค. ํญ, ๋๋ธํญ, ๋๋๊ทธ, ์ค์์ดํ ๋ฑ ๋ค์ํ ์ ์ค์ฒ๋ฅผ ๊ฐ์งํ ์ ์๋ค. ์น๊ฐ๋ฐ์์ ์์ฃผ ์ฌ์ฉํ๋ onClick
, onChange
์ด๋ฒคํธ์ ๋น์ทํ๊ฒ ์๋ํ๋ค๊ณ ๋ณด๋ฉด ๋๋ค.
GestureDetector
ํด๋์ค์ child
์์ฑ์ ํฐ์น ์ด๋ฒคํธ๋ฅผ ๊ฐ์งํ ์์ ฏ์ ์ถ๊ฐํ๊ณ , ์ํ๋ ์ ์ค์ฒ์ ๋ํ ์ฝ๋ฐฑ์ ์ ์ํ๋ฉด ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋๋ง๋ค ๋์ํ๋ ์ฝ๋ฐฑ์ด ์คํ๋๋ค.
GestureDetector(
onTap: () {
print('ํญ ๊ฐ์ง');
},
onDoubleTap: () {
print('๋๋ธํญ ๊ฐ์ง');
},
onLongPress: () {
print('๋กฑํ๋ ์ค ๊ฐ์ง');
},
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(
child: Text('ํฐ์น'),
),
),
);
Page Transition
Navigator.push
์ MaterialPageRoute
๋ฅผ ์ฌ์ฉํ๋ฉด ์๋ก์ด ํ์ด์ง๋ฅผ ์คํ(stack)์ ์ถ๊ฐํ์ฌ ๋ค๋ฅธ ํ๋ฉด์ผ๋ก ์ด๋ํ ์ ์๋ค. ์น ๋ธ๋ผ์ฐ์ ์ ํ์ด์ง ๋ด๋น๊ฒ์ด์
์ฒ๋ผ(์์ผ๋ก/๋ค๋ก๊ฐ๊ธฐ) ํ๋ฌํฐ๋ ํ๋ฉด ์ ํ์ ์คํ ๊ตฌ์กฐ๋ก ๊ด๋ฆฌํ๋ค. ์๋ก์ด ํ๋ฉด์ผ๋ก ์ด๋ํ ๋ ํ์ด์ง๊ฐ ์คํ์ ์์ด๊ณ , ๋ค๋ก๊ฐ๊ธฐ ์์ ์คํ์์ ์ ๊ฑฐ๋๋ค.
Navigator.push
๋ ์๋ก์ด ํ๋ฉด(ํ์ด์ง)์ ์คํ์ ์ถ๊ฐํ๋ ์ญํ ์ ํ๋ฉฐ, MaterialPageRoute
๋ ์๋ก์ด ํ์ด์ง๋ฅผ ์์ฑํ๊ณ Material Design ๊ฐ์ด๋๋ฅผ ๋ฐ๋ฅธ ํ๋ฉด ์ ํ ์ ๋๋ฉ์ด์
์ ์ ๊ณตํ๋ค.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(),
fullscreenDialog: true,
),
// ...
);
๊ธฐ๋ณธ์ ์ผ๋ก ํ๋ฉด ์ ํ ์ ๋๋ฉ์ด์
์ ์ค๋ฅธ์ชฝ์์ ์ผ์ชฝ์ผ๋ก ์ฌ๋ผ์ด๋ ๋๋ฉด์ ์๋ก์ด ํ์ด์ง๊ฐ ๋ํ๋๊ณ , ์๋จ์ ๋ค๋ก ๊ฐ๊ธฐ ๋ฒํผ์ด ํ์๋๋ค. fullscreenDialog
์์ฑ์ true
๋ก ์ค์ ํ๋ฉด, ์๋ก์ด ํ์ด์ง๊ฐ ์๋์์ ์๋ก ์ฌ๋ผ์ด๋ ๋๋ฉด์ ์ ์ฒด ํ๋ฉด ๋ชจ๋๋ก ์ด๋ฆฌ๋ฉฐ, ๋ค๋ก ๊ฐ๊ธฐ ๋ฒํผ ๋์ ๋ซ๊ธฐ ๋ฒํผ์ด ํ์๋๋ค. ์ด ๋ฐฉ์์ ์ฃผ๋ก ์ค์ ํ์ด์ง๋ ๋ณ๋์ ํ์ธ ์ฐฝ์ ์ด ๋ ์ ์ฉํ๋ค. ์ฐธ๊ณ ๋ก fullscreenDialog
์ ์ ๋๋ฉ์ด์
๋์์ ํ๋ซํผ๋ง๋ค ๋ค๋ฅผ ์ ์๋ค.
์๋ก์ด ํ์ด์ง๋ ์์ ํ ๋
๋ฆฝ์ ์ธ ํ๋ฉด์ด๊ธฐ ๋๋ฌธ์ Scaffold
์์ ฏ์ ์ฌ์ฉํ์ฌ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ์ ์ํด์ผ ํ๋ค. ์ด๋ฅผ ํตํด ์ฑ๋ฐ(appBar), ๋ณธ๋ฌธ(body), ํ๋กํ
์ก์
๋ฒํผ๊ณผ ๊ฐ์ ๋ ์ด์์ ์์๋ฅผ ์๋กญ๊ฒ ๊ตฌ์ฑํ ์ ์๋ค.
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: Text('Welcome to Second Page'),
),
);
}
}
Hero
Hero ์์ ฏ์ ๋ ํ๋ฉด(ํ์ด์ง) ๊ฐ์ ์์ฐ์ค๋ฌ์ด ์ ํ ์ ๋๋ฉ์ด์ ์ ์ ๊ณตํ์ฌ, ํน์ UI ์์๊ฐ ๋ง์น ํ๋ฉด์ ๊ฐ๋ก์ง๋ฌ ์ด๋ํ๋ ๋ฏํ ์๊ฐ์ ํจ๊ณผ๋ฅผ ๋ง๋ ๋ค. ์๋ฅผ ๋ค์ด Navigator๋ฅผ ํตํด A ํ๋ฉด์์ B ํ๋ฉด์ผ๋ก ์ด๋ํ ๋ A์ B ํ๋ฉด์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ ์์๋ฅผ Hero ์์ ฏ์ผ๋ก ๊ฐ์ธ๋ฉด, ํด๋น ์์๊ฐ ๋ ํ๋ฉด ์ฌ์ด๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ์ด๋ํ๋ ๋ฏํ ์ ๋๋ฉ์ด์ ์ด ์ ์ฉ๋๋ค. ์ด๋ฅผ ํตํด ์ฌ์ฉ์๋ ํ๋ฉด ์ ํ ์ ์ผ๊ด์ฑ ์๊ณ ๋ถ๋๋ฌ์ด ์ฌ์ฉ์ ๊ฒฝํ์ ์ป์ ์ ์์ผ๋ฉฐ, ์ฑ์ ํ๋ฆ์ ๋ ์ฝ๊ฒ ์ดํดํ ์ ์๋ค.
Hero ์์ ฏ์ ์ฌ์ฉ ๋ฐฉ๋ฒ๋ ๊ฐ๋จํ๋ค. Hero ์ ํ ํจ๊ณผ๋ฅผ ์ฃผ๊ณ ์ถ์ A, B ํ๋ฉด์ ์์๋ฅผ ๊ฐ๊ฐ Hero ์์ ฏ์ผ๋ก ๊ฐ์ผ ํ ๊ณ ์ ํ tag ๊ฐ์ ์ง์ ํ๋ฉด ๋๋ค. Hero ์์ ฏ์ ์ด๋ฏธ์ง ๊ฐค๋ฌ๋ฆฌ, ์นด๋ ๋ฆฌ์คํธ ๋๋ ํ๋ฉด ์ ํ ์ ์๊ฐ์ ์ฐ๊ฒฐ๊ฐ์ ๊ฐ์กฐํ๊ณ ์ ํ ๋ ์ ์ฉํ๋ค.
// ...
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Screen'),
),
body: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(),
),
);
},
child: Hero(
// ๋ ํ๋ฉด์ Hero ์์ ฏ tag ๊ฐ์ ๋์ผํ๊ฒ ์ง์ ํ๋ค
tag: 'hero-tag',
child: Image.asset(
'assets/my_image.png',
width: 100,
height: 100,
),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Screen'),
),
body: Center(
child: Hero(
// ๋ ํ๋ฉด์ Hero ์์ ฏ tag ๊ฐ์ ๋์ผํ๊ฒ ์ง์ ํ๋ค
tag: 'hero-tag',
child: Image.asset(
'assets/my_image.png',
width: 300,
height: 300,
),
),
),
);
}
}
Refer to Parent Widget
Stateful ์์ ฏ์์ ๋ถ๋ชจ ์์ ฏ์ ๋ฐ์ดํฐ๋ ์ค์ ์ ์ฐธ์กฐํ ๋ widget
์ ์ฌ์ฉํ๋ค.
Stateful ์์ ฏ์ ์ ์ํ ๋, _MyWidget extends State<MyWidget>
ํ์์ผ๋ก ์ด ์์ ฏ์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๋ณ๋์ State
ํด๋์ค๋ฅผ ์์ฑํ๋ค. State
ํด๋์ค ๋ด๋ถ์์ widget
์ ๊ทผ์ ์์ฑ์ ์ ๊ณตํ๋๋ฐ, ์ด ์์ฑ์ ํตํด ๋ถ๋ชจ ํ๋กํผํฐ๋ ๋ฐ์ดํฐ์ ์ ๊ทผํ ์ ์๋ค.
// Stateful ์์ ฏ
class DetailScreen extends StatefulWidget {
final String title, thumb, id;
const DetailScreen({
super.key,
required this.title,
required this.thumb,
required this.id,
});
@override
State<DetailScreen> createState() => _DetailScreenState();
}
// State ํด๋์ค
class _DetailScreenState extends State<DetailScreen> {
late Future<WebtoonDetailModel> webtoon;
late Future<List<WebtoonEpisodeModel>> episodes;
@override
void initState() {
super.initState();
// widget ์ ๊ทผ์ ์์ฑ์ ํตํด ๋ถ๋ชจ ์์ ฏ์ ํ๋กํผํฐ ์ ๊ทผ
webtoon = ApiService.getToonById(widget.id);
episodes = ApiService.getLatestEpisodesById(widget.id);
}
@override
Widget build(BuildContext context) {
return Scaffold(/* ... */);
}
}
์ ์ฝ๋์์ getToonById
, getLatestEpisodesById
๋ id
๋ฅผ ๋ฐ์์ผ ํ๋ ํจ์๋ค์ด๋ค. ์ด id
๋ ๋ถ๋ชจ ์์ ฏ์ธ DetailScreen
์ด ์์ฑ๋ ๋ ์ ๋ฌ๋์ง๋ง, State
ํด๋์ค๊ฐ ์ด๊ธฐํ๋๊ธฐ ์ ๊น์ง ๋ถ๋ชจ ์์ ฏ์ ํ๋กํผํฐ์ ์์ ํ๊ฒ ์ ๊ทผํ ์ ์๋ค.
๋๋ฌธ์ ๋ถ๋ชจ ์์ ฏ์ ์ ๋ณด๋ฅผ ํ์๋ก ํ๋ ํ๋กํผํฐ๋ค์ late
ํค์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ธํ๊ณ (null ์์ ์ฑ ๋ณด์ฅ), ์ค์ ๋ฐ์ดํฐ ํ ๋น์ initState()
์คํ ์์ ์ผ๋ก ์ง์ฐ์์ผ์ผ ํ๋ค. initState()
๋ ์ํ ํด๋์ค์ ์ด๊ธฐํ๊ฐ ์๋ฃ๋ ํ ํธ์ถ๋๋ฏ๋ก, ์ด ์์ ์ widget
ํ๋กํผํฐ์ ์ ๊ทผํ๋ ๊ฒ์ ์์ ํ๋ค.
SingleChildScrollView
ํ๋ฉด์ ๋ฒ์ด๋ ์ฝํ
์ธ ๋ฅผ ์คํฌ๋กค ๊ฐ๋ฅํ๋๋ก ๋ง๋ค ๋ SingleChildScrollView
ํด๋์ค(์์ ฏ)๋ฅผ ์ฌ์ฉํ ์ ์๋ค. ์ด ํด๋์ค๋ ํ๋์ ์์ ์์ ฏ๋ง ํฌํจํ ์ ์์ผ๋ฉฐ, scrollDirection
์์ฑ์ ํตํด ์คํฌ๋กค ๋ฐฉํฅ(๊ฐ๋ก/์ธ๋ก)์ ์ค์ ํ ์ ์๋ค. ๊ธฐ๋ณธ๊ฐ์ ์ธ๋ก ์คํฌ๋กค์ด๋ค.
SingleChildScrollView(
scrollDirection: Axis.horizontal, // ๊ฐ๋ก ์คํฌ๋กค ์ค์
child: Row(
children: [
// ...
],
),
)
ListView
๊ฐ์ ๋ค๋ฅธ ์คํฌ๋กค ๊ฐ๋ฅํ ์์ ฏ๊ณผ ํจ๊ป ์ฌ์ฉํ๋ฉด ์ถฉ๋์ด ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก ์ฃผ์ํ์. SingleChildScrollView
๋ ์ฃผ๋ก ์ฝํ
์ธ ์์ด ์ ์ ๋ ์ฌ์ฉํ๊ธฐ ์ข๋ค. ๋ฐ์ดํฐ์์ด ๋ง๋ค๋ฉด ์ง์ฐ ๋ก๋ฉ์ ์ง์ํ๋ ListView
๋ฅผ ์ฌ์ฉํ๋๊ฒ ๋ ์ ์ ํ ์ ์๋ค.
URL Launcher
URL Launcher๋ ์ฑ์์ ์ธ๋ถ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ ์ ์๊ฒ ํด์ฃผ๋ ํจํค์ง๋ค. ์น ํ์ด์ง๋ฅผ ๋ธ๋ผ์ฐ์ ์์ ์ด๊ฑฐ๋, ์ ํ ๊ฑธ๊ธฐ, ์ด๋ฉ์ผ/๋ฉ์์ง ์ ์ก ๋ฑ์ ์์ ์ ์ํํด์ผ ํ ๋ ์ฌ์ฉํ๋ค. ์๋ฅผ๋ค์ด ์ฌ์ฉ์๊ฐ ์ฑ ๋ด์์ ๋งํฌ๋ฅผ ํด๋ฆญํ๋ฉด ์ฌํ๋ฆฌ ๊ฐ์ ์ธ๋ถ ์ฑ์ ํตํด ํด๋น ํ์ด์ง๋ฅผ ์ด๋๋ก ํ ์ ์๋ค.
๊ตฌ๋ถ | ์คํค๋ง |
์น ํ์ด์ง ์ด๊ธฐ | https:<URL> |
์ด๋ฉ์ผ ๋ณด๋ด๊ธฐ | mailto:<email address>?subject=<subject>&body=<body> |
์ ํ ๊ฑธ๊ธฐ | tel:<phone number> |
๋ฌธ์ ๋ณด๋ด๊ธฐ | sms:<phone number> |
ํ์ผ ์ด๊ธฐ | file:<path> |
URL ์คํค๋ง๋ ํด๋น ์คํค๋ง๋ฅผ ์ฒ๋ฆฌํ ์ ์๋ ์ฑ์ด ๊ธฐ๊ธฐ์ ์ค์น๋์ด ์์ ๋๋ง ์๋ํ๋ค. ์๋ฅผ๋ค์ด iOS ์๋ฎฌ๋ ์ดํฐ์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฉ์ผ, ์ ํ ์ฑ์ด ์ค์น๋์ด ์์ง ์๊ธฐ ๋๋ฌธ์ tel:
, mailto:
์คํค๋ง๋ฅผ ์คํํ ์ ์๋ค.
URL ์คํค๋ง๋ฅผ ์คํํ๋ ค๋ฉด ํ์ฉํ ์คํค๋ง ๋ชฉ๋ก์ ๋ฏธ๋ฆฌ ๋ช ์ํด์ผ ํ๋ค. iOS๋ Info.plist ํ์ผ์ ์์ ํด์ผํ๊ณ , ์๋๋ก์ด๋๋ AndroidManifest.xml ํ์ผ์ ์์ ํด์ผ ํ๋ค. ์๋๋ URL์ ๋ธ๋ผ์ฐ์ ์์ ์ด๊ธฐ ์ํด https ์คํค๋ง๋ฅผ ํ์ฉํ๋๋ก ์ค์ ํ ์์.
ios/Runner/Info.plist โผ
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- ์ฑ์์ ํน์ URL ์คํด์ ์ฌ์ฉํ ์ ์๋๋ก ํ์ฉ -->
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
</dict>
</plist>
๐ก Info.plist, AndroidManifest.xml ์ฒ๋ผ ๋ค์ดํฐ๋ธ ๊ตฌ์ฑ ์์๋ฅผ ์ ์ํ๋ ํ์ผ์ Hot Reload, Hot Restart๋ง์ผ๋ก๋ ๋ณ๊ฒฝ์ฌํญ์ด ๋ฐ์๋์ง ์๊ณ , ์ฑ์ ์์ ํ ์ข ๋ฃํ ํ ๋ค์ ๋น๋ ๋ฐ ์คํ ํด์ผ ํ๋ค.
- Hot Reload : ์ํ๋ ์ ์งํ๋ฉด์ ์ฝ๋ ๋ณ๊ฒฝ ์ฌํญ ๋ฐ์.
main()
,initState()
๋ค์ ์คํ ์ํจ. - Hot Restart : ๋ชจ๋ ์ํ๋ฅผ ์ด๊ธฐํํ์ฌ ์ฑ ์ฌ์์.
main()
๋ค์ ์คํ.
URL Launcher ํจํค์ง๋ ํน์ URL์ ์ด ๋ ์ฌ์ฉํ๋ launchUrl(Uri)
, ๋ฌธ์์ด URL์ ์ง์ ์ ๋ฌํ์ฌ ์คํํ ์ ์๋ launchUrlString(String)
, ํน์ URL์ ์คํ ์ฌ๋ถ๋ฅผ ํ์ธํ ์ ์๋ canLaunchUrl(Uri)
ํจ์๋ฅผ ์ ๊ณตํ๋ค.
import 'package:url_launcher/url_launcher.dart';
class Episode extends StatelessWidget {
// ...
static const String baseUrl = 'https://comic.naver.com/webtoon/detail';
// ...
onEpisodeTab() async {
final uri = Uri.parse(baseUrl).replace(queryParameters: {
'titleId': webtoonId,
'no': episode.id,
});
// Uri.https ์์ฑ์๋ฅผ ์ฌ์ฉํ ์๋ ์์
// final uri = Uri.https('comic.naver.com', 'webtoon/detail', {
// 'titleId': webtoonId,
// 'no': episode.id,
// });
// launchUrl์ ๋น๋๊ธฐ ํจ์๋ก Future๋ฅผ ๋ฐํํจ.
// ์ํผ์๋๋ฅผ ํญํ์ ๋, ์ง์ ๋ ์ฃผ์๋ก ์ด๋.
// ์: https://comic.naver.com/webtoon/detail?titleId=...&no=...
await launchUrl(uri);
}
// ...
}
๋ฌธ์์ด ๋ณด๊ฐ๋ฒ(${...}
์ผ๋ก ๋ฌธ์์ด์ ๋ณ์ ์ฝ์
)์ผ๋ก ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ์ง์ ์
๋ ฅํ๋ฉด ์คํ๋ฅผ ๋ด๊ธฐ ์ฌ์ฐ๋ฏ๋ก ์์ฒ๋ผ replace()
๋ฉ์๋๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ Uri()
์์ฑ์๋ฅผ ์ด์ฉํ๋ ๋ฐฉ์์ด ๋ ๊ตฌ์กฐ์ ์ด๊ณ ์์ ํ๋ค.
SharedPreferences
SharedPreferences๋ ํค-๊ฐ ์์ผ๋ก ์ด๋ฃจ์ด์ง ๋ฐ์ดํฐ๋ฅผ ์๊ตฌ์ ์ผ๋ก ์ ์ฅํ ๋ ์ฌ์ฉํ๋ ํ๋ฌ๊ทธ์ธ์ด๋ค(ํ๋ฌํฐ ํ์์ ๊ฐ๋ฐ). ์ด ํ๋ฌ๊ทธ์ธ์ ์ฌ์ฉํ๋ฉด ์ฌ์ฉ์ ์ฅ์น์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ธฐ ๋๋ฌธ์ ์ฑ์ ์ข ๋ฃํ๊ณ ๋ค์ ์คํํด๋ ๋ฐ์ดํฐ๋ ์ ์ง๋๋ค. ์น ๋ธ๋ผ์ฐ์ ์ ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ๋น์ทํ๋ค๊ณ ๋ณด๋ฉด ๋๋ค.
๋ฐ์ดํฐ๋ ๋์คํฌ์ ๋น๋๊ธฐ์ ์ผ๋ก ์ ์ฅ๋๋ฉฐ, ์ฐ๊ธฐ ์์ ์ ํธ์ถํ ํ ๋ฐ์ดํฐ๊ฐ ์ฆ์ ์ ์ฅ๋๋ค๊ณ ๋ณด์ฅํ ์ ์๊ธฐ ๋๋ฌธ์(๋ฐ์ดํฐ์ ์ผ๊ด์ฑ ๋ณด์ฅ ์ํจ) ์ค์ํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๊ฒ์ ๊ถ์ฅํ์ง ์๋๋ค. ์ฃผ๋ก ์ฑ ์ค์ , ์ฌ์ฉ์ ์ ํธ๋, ๊ฐ๋จํ ์ํ ์ ๋ณด ๊ฐ์ ์์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋ ์ ํฉํ๋ค.
SharedPreferences๋ int
, bool
, double
, String
, List<String>
์ด 5๊ฐ์ง ์๋ฃํ์ผ๋ก ์ ์ฅํ ์ ์์ผ๋ฉฐ, ๊ฐ ์๋ฃํ์ ๋ง๋ ๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
์ฐ๊ธฐ ๋ฉ์๋ โผ
// SharedPreferences ์ธ์คํด์ค๋ฅผ ๋ถ๋ฌ์จ ํ prefs ๋ณ์์ ํ ๋น
final SharedPreferences prefs = await SharedPreferences.getInstance();
// 'counter' ํค์ ์ ์ ๊ฐ ์ ์ฅ
await prefs.setInt('counter', 10);
// 'repeat' ํค์ ๋ถ๋ฆฌ์ธ ๊ฐ ์ ์ฅ
await prefs.setBool('repeat', true);
// 'decimal' ํค์ ์ค์ ๊ฐ ์ ์ฅ
await prefs.setDouble('decimal', 1.5);
// 'action' ํค์ ๋ฌธ์์ด ๊ฐ ์ ์ฅ
await prefs.setString('action', 'Start');
// 'items' ํค์ ๋ฌธ์์ด ๋ฆฌ์คํธ ์ ์ฅ
await prefs.setStringList('items', <String>['Earth', 'Moon', 'Sun']);
์ฝ๊ธฐ ๋ฉ์๋ โผ
// 'counter' ํค ๋ฐ์ดํฐ ์กฐํ. ์กด์ฌํ์ง ์์ผ๋ฉด null ๋ฐํ
final int? counter = prefs.getInt('counter');
// 'repeat' ํค ๋ฐ์ดํฐ ์กฐํ. ์กด์ฌํ์ง ์์ผ๋ฉด null ๋ฐํ
final bool? repeat = prefs.getBool('repeat');
// 'decimal' ํค ๋ฐ์ดํฐ ์กฐํ. ์กด์ฌํ์ง ์์ผ๋ฉด null ๋ฐํ
final double? decimal = prefs.getDouble('decimal');
// 'action' ํค ๋ฐ์ดํฐ ์กฐํ. ์กด์ฌํ์ง ์์ผ๋ฉด null ๋ฐํ
final String? action = prefs.getString('action');
// 'items' ํค ๋ฐ์ดํฐ ์กฐํ. ์กด์ฌํ์ง ์์ผ๋ฉด null ๋ฐํ
final List<String>? items = prefs.getStringList('items');
์ ๊ฑฐ ๋ฉ์๋ โผ
// 'counter' ํค์ ๋ฐ์ดํฐ ์ญ์
await prefs.remove('counter');
์๋๋ ์นํฐ์ ์ข์์ ์ํ๋ฅผ ์คํ ๋ฆฌ์ง์ ์ ์ฅํ๊ณ ๋ถ๋ฌ์ค๋ ์์. ์์ ฏ์ด ์ด๊ธฐํ๋๋ฉด initPrefs
๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ SharedPreferences ์ธ์คํด์ค๋ฅผ ๋ถ๋ฌ์ค๊ณ , ์ ์ฅ๋ ์ข์์ ๋ชฉ๋ก์ ๊ธฐ๋ฐ์ผ๋ก isLiked
์ ์ด๊ธฐ ์ํ๋ฅผ ์ค์ ํ๋ค.
class _DetailScreenState extends State<DetailScreen> {
// SharedPreferences ์ธ์คํด์ค๋ฅผ ์ ์ฅํ ๋ฉค๋ฒ ๋ณ์
late SharedPreferences prefs;
// ์ข์์ํ ์นํฐ ID ๋ชฉ๋ก์ ์ ์ฅํ ๋ ์ฌ์ฉํ๋ SharedPreferences ํค
static const likedToonsPrefsKey = 'likedToons';
// ํ์ฌ ์นํฐ์ ์ข์์ ์ฌ๋ถ๋ฅผ ๋ํ๋ด๋ ์ํ ๋ณ์
bool isLiked = false;
// SharedPreferences ์ด๊ธฐํ ๋ฐ isLiked ์ด๊ธฐ ์ํ ์ค์
Future<void> initPrefs() async {
// SharedPreferences ์ธ์คํด์ค๋ฅผ ๋ถ๋ฌ์จ ํ prefs ๋ณ์์ ํ ๋น
prefs = await SharedPreferences.getInstance();
// ์ข์์ํ ์นํฐ ID ๋ชฉ๋ก ๋ถ๋ฌ์ค๊ธฐ
final likedToons = prefs.getStringList(likedToonsPrefsKey);
// ์ฑ์ ์ฒ์ ์คํํด์ ์ข์์ ๋ชฉ๋ก์ด ์๋ ๊ฒฝ์ฐ ๋น ๋ฆฌ์คํธ๋ก ์ด๊ธฐํ
if (likedToons == null) {
prefs.setStringList(likedToonsPrefsKey, []);
} else {
// ์ข์์ ๋ชฉ๋ก์ ํ์ฌ ์นํฐ ID๊ฐ ํฌํจ๋์ด ์๋์ง ํ์ธ ํ isLiked ์ํ ์ค์
setState(() {
isLiked = likedToons.contains(widget.id);
});
}
}
@override
void initState() {
super.initState();
initPrefs();
}
// ์ข์์ ์ํ ๋ณ๊ฒฝ ๋ฉ์๋ (AppBar ์ฐ์ธก ์ข์์ ๋ฒํผ ํด๋ฆญ ์ ํธ์ถ)
void onHeartTab() async {
final likedToons = prefs.getStringList(likedToonsPrefsKey);
if (likedToons == null) return;
// ํ์ฌ isLiked ์ํ์ ๋ฐ๋ผ ์นํฐ ID๋ฅผ ๋ชฉ๋ก์ ์ถ๊ฐํ๊ฑฐ๋ ์ ๊ฑฐ
if (isLiked) {
likedToons.remove(widget.id);
} else {
likedToons.add(widget.id);
}
// ์
๋ฐ์ดํธํ ์ข์์ ๋ชฉ๋ก์ SharedPreferences์ ์ ์ฅ
prefs.setStringList(likedToonsPrefsKey, likedToons);
// isLiked ์ํ ํ ๊ธ ๋ฐ UI ์
๋ฐ์ดํธ
setState(() {
isLiked = !isLiked;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
// AppBar ์ฐ์ธก์ ํ์๋ ์ก์
์์ ฏ ๋ชฉ๋ก (์์ด์ฝ ๋ฒํผ ๋ฑ)
actions: [
IconButton(
onPressed: onHeartTab,
icon: Icon(isLiked ? Icons.favorite : Icons.favorite_outline),
),
],
// ...
),
body: SingleChildScrollView(
// ...
),
);
}
}
๊ทธ ํ AppBar์ ์ข์์ ๋ฒํผ์ ํด๋ฆญ ํ ๋๋ง๋ค onHeartTab
๋ฉ์๋๊ฐ ํธ์ถ๋ผ์ โ ํ์ฌ isLiked
์ํ์ ๋ฐ๋ผ ์ข์์ ๋ชฉ๋ก์ ์
๋ฐ์ดํธํ๊ณ , โก์
๋ฐ์ดํธํ ์ข์์ ๋ชฉ๋ก์ SharedPreferences์ ์ ์ฅํ ํ, โขisLiked
์ํ ํ ๊ธ ๋ฐ setState
๋ฅผ ํธ์ถํด์ UI๋ฅผ ์
๋ฐ์ดํธํ๋ค.
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Algorithm] ์ฌ๋ผ์ด๋ฉ ์๋์ฐ Sliding Window ์๊ณ ๋ฆฌ์ฆ ํบ์๋ณด๊ธฐ (2) | 2024.11.11 |
---|---|
[React] ๋ฆฌ์กํธ ์ฝ๋๋ฅผ ๊ฐ์ ํ ์ ์๋ 4๊ฐ์ง ํ (0) | 2024.10.28 |
[Flutter] ํ๋ฌํฐ ๊ธฐ์ด ๋ด์ฉ ์ ๋ฆฌ - Part 1 (0) | 2024.10.05 |
[TS] ํ์ ์คํฌ๋ฆฝํธ ๋ธ๋๋๋ ํ์ (0) | 2024.09.26 |
๋์ปค(Docker)์ ์ฟ ๋ฒ๋คํฐ์ค(Kubernetes) ๊ธฐ๋ณธ ๊ฐ๋ (0) | 2024.09.17 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[Algorithm] ์ฌ๋ผ์ด๋ฉ ์๋์ฐ Sliding Window ์๊ณ ๋ฆฌ์ฆ ํบ์๋ณด๊ธฐ
[Algorithm] ์ฌ๋ผ์ด๋ฉ ์๋์ฐ Sliding Window ์๊ณ ๋ฆฌ์ฆ ํบ์๋ณด๊ธฐ
2024.11.11 -
[React] ๋ฆฌ์กํธ ์ฝ๋๋ฅผ ๊ฐ์ ํ ์ ์๋ 4๊ฐ์ง ํ
[React] ๋ฆฌ์กํธ ์ฝ๋๋ฅผ ๊ฐ์ ํ ์ ์๋ 4๊ฐ์ง ํ
2024.10.28 -
[Flutter] ํ๋ฌํฐ ๊ธฐ์ด ๋ด์ฉ ์ ๋ฆฌ - Part 1
[Flutter] ํ๋ฌํฐ ๊ธฐ์ด ๋ด์ฉ ์ ๋ฆฌ - Part 1
2024.10.05 -
[TS] ํ์ ์คํฌ๋ฆฝํธ ๋ธ๋๋๋ ํ์
[TS] ํ์ ์คํฌ๋ฆฝํธ ๋ธ๋๋๋ ํ์
2024.09.26