main.dart
import 'package:flutter/material.dart';
import 'package:webtoon/screens/home_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
//모든 위젯은 key를 가지고 있고, 이 key는 flutter가 웨젯을 찾을 때 사용하는 ID 이다.
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
api_service.dart
//url로 뭔가를 가져오려면 http package를 설치해야 함
///flutter, dart의 추가 패키지를 설치하려면 pub.dev로 가서 필요한 패키지를 검색하면 됩
///검색으로 나온 패키지를 클릭하고 installing을 선택하면 필요한 방법들이 적혀 있음
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:webtoon/models/webtoon_model.dart';
class ApiService {
static const String baseUrl =
"<https://webtoon-crawler.nomadcoders.workers.dev>";
static const String today = "today";
//async keyword가 붙은 함수는 비동기 처리를 하겠다는 것이고 함수 안에 await keyword가 사용된다.
//async를 쓸 경우 반환값에 Future<>를 감싸줘야 함 void일 때는 노상관
static Future<List<WebtoonModel>> getTodaysToons() async {
//get은 http패키지에 있는 애인데 이름이 너무 범용적이어서 namefspace를 지정하여 http를 붙여줌
//get 요청을 보낼 때는 Uri type으로 가져와야 하므로… (get 속성을 확인)
final url = Uri.parse('$baseUrl/$today');
///http.get 동작은 실행 즉시 결과를 반환하는 것이 아니라 시간이 좀 걸림(네트워크 장애 등.. )
///http.get 속성을 보면 return type이 response이고 앞에 future가 붙는데, 이는 바로 결과를 반환하는 것이 아니라 나중에 반환한다는 것
///await keyword를 사용하지 않으면 이 함수를 실행하고 그냥 다음 스텝으로 가는데,
///현재는 get으로 오는 결과를 바로 다음 스텝에서 사용해야 하기 때문에 await keyword를 사용(결과를 기다리라는 의미)
final response = await http.get(url);
//http.get() 실행할 때 macos에서 "Unhandled Exception: Connection failed" 이런 에러가 뜨면 터미널에서 아래 명령어 수행
///flutter build macos
///open macos/Runner.xcworkspace
///실행하면 창이 뜨는 데, 거기서
///Runner/DebugProfile, Runner/Release 두 항목을 찾아 com.apple.security.network.client 를 추가하고 true 설정을 해주면 됨. 맥은 보안 절차가 까다로워서 그런듯
List<WebtoonModel> webtoonInstances = [];
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();
}
}
home_screen.dart
import 'package:flutter/material.dart';
import 'package:webtoon/models/webtoon_model.dart';
import 'package:webtoon/services/api_service.dart';
import 'package:webtoon/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: 3, //elevation is a shadow func
backgroundColor: Colors.white,
foregroundColor: Colors.green,
title: const Text(
"오늘의 웹툰s",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22),
),
),
///statefullwidgetd을 사용하면 initstate을 사용하여 초기화 시켜주고
///future data를 받아오는 동안 setstate을 돌려주면서 refresh 시켜주는데,
///stateless를 사용하면 FutureBuilder라는 심박한 widget을 사용하여 future 맞춤 로직을 짤 수 있음
body: FutureBuilder(
future: webtoons,
///shapshot은 http로 받아오는 response 내용을 들고 있음
///snapshot dot 하고 나오는 option들 체크하여 사용. snapshot이라는 변수명은 마음대로 바꿀 수 있음
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: [
const SizedBox(
height: 50,
),
///ListView는 높이가 무한대이기 때문에 column안에 그대로 넣으면 에러남 unbounded error
///expanded로 감싸면, expanded는 남는 공간을 다 차지하는 위젯이기 때문에 알아서 공간이 할당됨
Expanded(
///만들어진 ListView에서 code action을 누르고 extra method를 클릭하면 원하는 이름의 method가 만들어짐
///해당 method는 코드 아래에 생성
child: makeList(snapshot),
),
],
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
),
);
}
///데이터가 너무 많을 때는 column이나 row를 쓰지 않음. ListView를 사용하면 알아서 scrollview가 됨
///But, ListView는 모든 아이템을 한꺼번에 보여주기 때문에 이런식으로 쓰면 메모리가 터짐
/*
return ListView(
children: [
for (var webtoon in snapshot.data!) Text(webtoon.title)
],
);
*/
///ListView.builder는 ListView의 upgrade 버전
///한번에 얼마만큼 보여줄지 선택하려면 ListView.builder widget사용
///itemBuilder는 ListView.builder가 아이템을 빌드할때 호출하는 함수. index는 현재 사용자가 보는 아이템의 index number.
///사용자가 현재 보지 않는 아이템은 메모리에서 날림
///builder대신에 separated를 쓰면 builder에 separatorBuilder라는 게 추가됨
ListView makeList(AsyncSnapshot<List<WebtoonModel>> snapshot) {
return ListView.separated(
scrollDirection: Axis.horizontal,
//한번에 얼마만큼 보여줄지 선택
//여기로 들어올 때는 데이터 요청을 보내고 결과를 받는 순간이기 때문에 전체 데이터가 아닌 일부 데이터임
//그 일부만 보여주는 거
itemCount: snapshot.data!.length,
padding: const EdgeInsets.symmetric(vertical: 5),
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,
),
);
}
}
detail_screen.dart
import 'package:flutter/material.dart';
class DetailScreen extends StatelessWidget {
final String title, thumb, id;
const DetailScreen({
super.key,
required this.title,
required this.thumb,
required this.id,
});
@override
Widget build(BuildContext context) {
///navigation으로 build하면 home screen이 사라지고 새로운 screen이 생성되므로 scaffold를 새로 만들어줌
///AppBar까지 리턴해주는 이유는 웹툰을 클릭했을 때 웹툰 타이틀을 AppBar에서 보여주고 싶어서
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 3, //elevation is a shadow func
backgroundColor: Colors.white,
foregroundColor: Colors.green,
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22),
),
),
body: Column(
children: [
const SizedBox(
height: 50,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 250,
///edge를 등굴게 하려고 borderRadius를 설정하는데 clipBehavior에서 부모의 침범여부?를 설정해 주어야 적용됨
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: const [
BoxShadow(
blurRadius: 5,
offset: Offset(5, 5),
color: Colors.black45,
),
],
),
child: Image.network(
thumb,
///http에서 User-Agent는 서버 또는 클라이언트의 소프트웨어 버전이나 OS 버전을 나타내는 헤더인데, 얘를 넣어 줘야 이미지를 제대로 불러옴
///<https://gist.github.com/preinpost/941efd33dff90d9f8c7a208da40c18a9>
headers: const {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
},
),
),
],
),
],
),
);
}
}
webtoon_widget.dart
import 'package:flutter/material.dart';
import 'package:webtoon/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: () {
///route은 웨젯을 애니메이션 효과로 감싸서 스크린처럼 보이게 하는 것
///사실은 또 다른 위젯을 실행시켰을 뿐인데 새로운 화면으로 들어가는 듯한 효과를 줌
///보통 navigator로 route을 push한다고 함
///builder is a function to create route
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(
title: title,
thumb: thumb,
id: id,
),
//fullscreenDialog: true,
),
);
},
child: Column(
children: [
Container(
width: 250,
///edge를 등굴게 하려고 borderRadius를 설정하는데 clipBehavior에서 부모의 침범여부?를 설정해 주어야 적용됨
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: const [
BoxShadow(
blurRadius: 5,
offset: Offset(5, 5),
color: Colors.black45,
),
],
),
child: Image.network(
thumb,
///http에서 User-Agent는 서버 또는 클라이언트의 소프트웨어 버전이나 OS 버전을 나타내는 헤더인데, 얘를 넣어 줘야 이미지를 제대로 불러옴
///<https://gist.github.com/preinpost/941efd33dff90d9f8c7a208da40c18a9>
headers: const {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
},
),
),
const SizedBox(
height: 10,
),
Text(
title,
style: const TextStyle(
fontSize: 20,
),
),
],
),
);
}
}