티스토리 뷰
들어가며
본 게시글은 노마드코더의 Flutter로 웹툰 만들기를 보고 정리한 글입니다.
코드 구조
코드
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:toonflix/screens/home_screen.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
API
// lib/services/api_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:toonflix/models/webtoon_detail_model.dart';
import 'package:toonflix/models/webtoon_episode_model.dart';
import 'package:toonflix/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 List<dynamic> webtoons = jsonDecode(response.body);
for (var webtoon in webtoons) {
webtoonInstances.add(WebtoonModel.fromJson(webtoon));
}
return webtoonInstances;
}
throw Error();
}
static Future<WebtoonDetailModel> getToonById(String id) async {
final url = Uri.parse("$baseURL/$id");
final response = await http.get(url);
if (response.statusCode == 200) {
final webtoon = jsonDecode(response.body);
return WebtoonDetailModel.fromJson(webtoon);
}
throw Error();
}
static Future<List<WebtoonEpisodeModel>> getLatestEpisodesById(
String id) async {
List<WebtoonEpisodeModel> episodesInstances = [];
final url = Uri.parse("$baseURL/$id/episodes");
final response = await http.get(url);
if (response.statusCode == 200) {
final episodes = jsonDecode(response.body);
for (var episode in episodes) {
episodesInstances.add(WebtoonEpisodeModel.fromJson(episode));
}
return episodesInstances;
}
throw Error();
}
}
Models
// lib/models/webtoon_model.dart
class WebtoonModel {
final String title, thumb, id;
WebtoonModel.fromJson(Map<String, dynamic> json)
: title = json['title'],
thumb = json['thumb'],
id = json['id'];
}
// lib/models/webtoon_episode_model.dart
class WebtoonEpisodeModel {
final String id, title, rating, date;
WebtoonEpisodeModel.fromJson(Map<String, dynamic> json)
: id = json['id'],
title = json['title'],
rating = json['rating'],
date = json['date'];
}
// lib/models/webtoon_detail_model.dart
class WebtoonDetailModel {
final String title, about, genre, age;
WebtoonDetailModel.fromJson(Map<String, dynamic> json)
: title = json['title'],
about = json['about'],
genre = json['genre'],
age = json['age'];
}
Widgets
// lib/widgets/webtoon_widget.dart
import 'package:flutter/material.dart';
import 'package:toonflix/screens/detail_screen.dart';
class Webtoon extends StatelessWidget {
final String title, thumb, id;
const Webtoon({
super.key,
required this.title,
required this.thumb,
required this.id,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailScreen(title: title, thumb: thumb, id: id),
fullscreenDialog: true,
));
},
child: Column(
children: [
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,
headers: const {
'Referer': 'https://comic.naver.com',
},
),
),
),
const SizedBox(
height: 10,
),
Text(
title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
// lib/widgets/episode_widget.dart
import 'package:flutter/material.dart';
import 'package:toonflix/models/webtoon_episode_model.dart';
import 'package:url_launcher/url_launcher_string.dart';
class Episode extends StatelessWidget {
const Episode({
super.key,
required this.episode,
required this.webtoonId,
});
final String webtoonId;
final WebtoonEpisodeModel episode;
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(
color: Colors.green.shade400,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 8,
offset: const Offset(8, 4),
color: Colors.black.withOpacity(0.3),
)
],
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
episode.title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
const Icon(
Icons.chevron_right_rounded,
color: Colors.white,
),
],
),
),
),
);
}
}
Screens
// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:toonflix/models/webtoon_model.dart';
import 'package:toonflix/services/api_service.dart';
import 'package:toonflix/widgets/webtoon_widget.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,
foregroundColor: Colors.green,
//backgroundColor: Colors.white,
title: const Text(
"오늘의 웹툰",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
),
body: FutureBuilder(
future: webtoons,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: [
const SizedBox(
height: 50,
),
Expanded(
child: makeList(snapshot),
),
],
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
),
);
}
ListView makeList(AsyncSnapshot<List<WebtoonModel>> snapshot) {
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 Webtoon(
title: webtoon.title,
thumb: webtoon.thumb,
id: webtoon.id,
);
},
separatorBuilder: (context, index) => const SizedBox(width: 40),
);
}
}
// lib/screens/detail_screen.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:toonflix/services/api_service.dart';
import 'package:toonflix/models/webtoon_detail_model.dart';
import 'package:toonflix/models/webtoon_episode_model.dart';
import 'package:toonflix/widgets/episode_widget.dart';
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();
}
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;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 2,
foregroundColor: Colors.green,
backgroundColor: Colors.white,
actions: [
IconButton(
onPressed: onHeartTap,
icon: Icon(isLiked
? Icons.favorite_rounded
: Icons.favorite_outline_rounded),
)
],
title: Text(
widget.title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 50,
vertical: 50,
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: widget.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(
widget.thumb,
headers: const {
'Referer': 'https://comic.naver.com',
},
),
),
),
],
),
const SizedBox(
height: 25,
),
FutureBuilder(
future: webtoon,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
snapshot.data!.about,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(
height: 15,
),
Text(
"${snapshot.data!.genre} / ${snapshot.data!.age}",
style: const TextStyle(
fontSize: 18,
),
),
],
);
}
return const Text("...");
},
),
const SizedBox(
height: 25,
),
FutureBuilder(
future: episodes,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: [
for (var episode in snapshot.data!)
Episode(
episode: episode,
webtoonId: widget.id,
),
],
);
}
return Container();
},
),
],
),
),
),
);
}
}
'공부한 내용 정리 > Flutter' 카테고리의 다른 글
Flutter 기초 공부 4: Pomodoro App (0) | 2024.07.22 |
---|---|
Flutter 기초 공부 3: 반응형 앱 (0) | 2024.07.17 |
Flutter 생산성 올리는 팁 (0) | 2024.07.05 |
Flutter 기초 공부 2: UI 만들어보기 (0) | 2024.06.23 |
Flutter 기초 공부 1: 작동방식 개요 (0) | 2024.01.14 |
댓글