• 개발목적
    • flutter, dart 연습용으로 앱을 만들어봄
  • 기능
    • naver webtoon을 가져다 보여줄 수 있는 app 개발
    • 당일 하루 webtoon만 뽑아서 보여주는 기능
  • code
    • 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,
                  ),
                ),
              ],
            ),
          );
        }
      }