[flutter] ๐Ÿ“š5,6์ฃผ์ฐจ ์Šคํ„ฐ๋”” - Webtoon App

seunghyoยท2023๋…„ 12์›” 13์ผ
0

Flutter

๋ชฉ๋ก ๋ณด๊ธฐ
5/5
post-thumbnail

๐Ÿ“ Week 05-06 Webtoon App

1. HTTP ํ†ต์‹  ์„ธํŒ…(pub.dev)


https://pub.dev/packages/http

ํ•ด๋‹น ์‚ฌ์ดํŠธ์—์„œ ์„ค์น˜ ๊ฐ€๋Šฅํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

flutter pub add (๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ช…)
  • ์„ค์น˜ํ•˜๊ธฐ
//http ์„ค์น˜
flutter pub add http

2 . API ๋ฐ์ดํ„ฐ ๋ฐ›์•„์˜ค๊ธฐ


API ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ json ํ˜•ํƒœ์˜ ๊ตฌ์กฐ๋ฅผ ์•ฑ์—์„œ ์‚ฌ์šฉํ•  ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•œ๋‹ค. webtoon app์—์„œ ๊ฐ€์ ธ์˜ฌ ๋ฐ์ดํ„ฐ๋Š” title, thumb, id ์ด๋‹ค.

// API ๋ฐ์ดํ„ฐ
[
    {
    "id": "654774",
    "title": "์†Œ๋…€์˜ ์„ธ๊ณ„",
    "thumb": "https://image-comic.pstatic.net/webtoon/654774/thumbnail/thumbnail_IMAG21_4048794550434817075.jpg"
    },
    .
    .
    .

๐Ÿ“ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ์ฝ”๋“œ

class WebtoonModel {
  final String title, thumb, id;

  WebtoonModel.fromJson(Map<String, dynamic> json)
      : title = json['title'],
        thumb = json['thumb'],
        id = json['id'];
}

๐Ÿ“ api_service.dart

class ApiService {
  static const String baseUrl =
      "https://webtoon-crawler.nomadcoders.workers.dev";
  static String today = "today";


  static Future<List<WebtoonModel>> getTodaysToons() async {
    List<WebtoonModel> webtoonInstances = []; // webtoonModel ํƒ€์ž…์˜ ์ธ์Šคํ„ด์Šค๋“ค์ด ๋“ค์–ด๊ฐ€๊ฒŒ ๋  ๊ณณ!
    final url = Uri.parse('$baseUrl/$today');
    // ์ ‘์†ํ•  url
    final response = await http.get(url);
    if (response.statusCode == 200) { //์ •์ƒ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ›์•„์™€์ง„ ๊ฒฝ์šฐ
      final List<dynamic> webtoons = jsonDecode(response.body);
      for (var webtoon in webtoons) { //for๋ฌธ์„ ๋Œ๋ฉด์„œ ๋ฐ์ดํ„ฐ๋“ค์„ json ํ˜•์‹์—์„œ ๊ฐ์ฒด ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ๊ณ  ๋ฐฐ์—ด์— add ํ•ด์ค€๋‹ค.
        final instance = WebtoonModel.fromJson(webtoon);
        webtoonInstances.add(instance);
      }
      return webtoonInstances; //์›นํˆฐ ์ธ์Šคํ„ด์Šค๋“ค(์šฐ๋ฆฌ๊ฐ€ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ)๋ฅผ ์ตœ์ข… ๋ฐ˜ํ™˜
    }
    throw Error();
  }

๐Ÿ“ main.dart

final Future<List<WebtoonModel>> webtoons = ApiService.getTodaysToons();
//webtoons : ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•  ์›นํˆฐ ์ธ์Šคํ„ด์Šค๋“ค (ํƒ€์ž…์€ webtoonModel)

3. Futurebuilder


์œ„์—์„œ ๋ฐ›์•„์˜จ api ๋ฐ์ดํ„ฐ(webtoons)๋ฅผ ์ด์ œ ํ™”๋ฉด์— ๋ณด์—ฌ์ค˜์•ผ ํ•œ๋‹ค. ์ด๋•Œ futureBuilder๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. Future์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค ๋ฐ›์•„์™€์ง€๊ธฐ ์ „ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒƒ์„ ๋ง‰์•„์ค€๋‹ค. FutureBuilder๊ฐ€ ์—†๋‹ค๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค ๋ฐ›์•„์ง€๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ ํ›„ ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ์˜ ๋ณ€ํ™”์„ setState()๋ฅผ ํ†ตํ•ด ๋ฐ”๊ฟ”์•ผ ์ค˜์•ผํ•œ๋‹ค. FutureBuilder๋Š” ๋Œ€๋ถ€๋ถ„ ์•จ๋ฒ”์—์„œ ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ, ํ˜„์žฌ ๋ฐฐํ„ฐ๋ฆฌ ํ‘œ์‹œ, ํŒŒ์ผ ๊ฐ€์ ธ์˜ค๊ธฐ, http ์š”์ฒญ ๋“ฑ ์ผํšŒ์„ฑ ์‘๋‹ต์— ์‚ฌ์šฉํ•œ๋‹ค.

FutureBuilder(
        future: webtoons,
        builder: (context, snapshot) {
          if (snapshot.hasData) { //๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ
            return Column(
              children: [
                const SizedBox(
                  height: 50,
                ),
                Expanded(
                  child: makeList(snapshot),
                  //๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ํ•จ์ˆ˜ ์‹คํ–‰. makeList๋Š” ๋”ฐ๋กœ ์ž‘์„ฑํ•œ ์ฝ”๋“œ(ListView์—์„œ ์„ค๋ช…ํ•  ์˜ˆ์ •)
                ),
              ],
            );
          }
          return const Center( //future๊ฐ€ ๋ถˆ๋Ÿฌ์™€์ง€๊ธฐ ์ „ data๊ฐ€ ์—†์œผ๋ฏ€๋กœ ๋กœ๋”ฉ์•ก์…˜์ด ํ™”๋ฉด์— display๋œ๋‹ค.
            child: CircularProgressIndicator(),
          );
        },
      ),

4. ListViewBuilder


ListView.builder์— ๋ช‡ ๊ฐœ์˜ ํ•ญ๋ชฉ์„ ๋งŒ๋“ค ๊ฒƒ์ด๊ณ  ๋ช‡ ๋ฒˆ์งธ ํ•ญ๋ชฉ์—๋Š” ์–ด๋–ค View๋ฅผ ๊ทธ๋ ค์ฃผ์ž๋ผ๋Š” ๊ฒƒ์„ ์•Œ๋ ค์ฃผ์–ด์•ผ ํ•œ๋‹ค. itemCount๊ฐ€ ์ด ๋ช‡ ๊ฐœ์— ํ•ด๋‹นํ•˜๊ณ , itemBuilder๊ฐ€ ์–ด๋–ค View๋ฅผ ๊ทธ๋ ค์ฃผ์ž ๋ผ๋Š” ๊ฒƒ์— ํ•ด๋‹นํ•œ๋‹ค.

  • itemCount : int๊ฐ’์ด๋ฉฐ ListView ํ•ญ๋ชฉ๋“ค์˜ ์ด๊ฐœ์ˆ˜์— ํ•ด๋‹นํ•œ๋‹ค. ๋‹จ, ์ฃผ์–ด์ง€์ง€ ์•Š์œผ๋ฉด ๋ฌดํ•œํžˆ ํ•ญ๋ชฉ์„ ๋งŒ๋“ ๋‹ค.
  • itemBuilder(BuildContext ctx, int idx) : idx๋ฒˆ์งธ์— ํ•ด๋‹นํ•˜๋Š” ํ•ญ๋ชฉ์— ๊ทธ๋ ค์งˆ View๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค. idx๋Š” 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•œ๋‹ค.
  ListView makeList(AsyncSnapshot<List<WebtoonModel>> snapshot) {
    return ListView.separated(
      scrollDirection: Axis.horizontal,
      //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: 20,
      ),
    );

5. Hero

Hero๋ฅผ ์ด์šฉํ•ด์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค. tag์— ๊ฐ™์€ id๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๊ฐ™์€ ๊ฐ์ฒด๋กœ ํŒ๋‹จํ•ด ์ด๋ฏธ์ง€๊ฐ€ ํŒ์—…๋˜๋Š” ๊ฒƒ ๊ฐ™์€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๊ฐ€ display ๋œ๋‹ค.

//detail_screen.dart
  Hero(
                  tag: widget.id,
                  child: Container(
                    width: 250,
                    clipBehavior: Clip.hardEdge,
                    decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(10),
                        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',
                      },
                    ),
                  ),
                ),
//webtoon_widget.dart
Hero(
            tag: id,
            child: Container(
              width: 250,
              clipBehavior: Clip.hardEdge,
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10),
                  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',
                },
              ),
            ),
          ),

6. Url Launcher

  • ์„ค์น˜ํ•˜๊ธฐ
flutter pub add url_launcher

ios์—์„œ ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ios/Runner/Info.plist ํŒŒ์ผ์—์„œ ํ•ด๋‹น ๋ถ€๋ถ„์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

	<array>
  		<string>https</string>
	</array>
  • ์‚ฌ์šฉํ•ด๋ณด๊ธฐ
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

void main() => runApp(
      const MaterialApp(
        home: Material(
          child: Center(
            child: ElevatedButton(
              onPressed: onButtonTap,
              child: Text('Show Flutter homepage'),
            ),
          ),
        ),
      ),
    );

  //์˜ˆ์ œ 1
  onButtonTap() async {
    final url=Uri.parse('์ด๋™ํ•  ์ฃผ์†Œ');
    await launchUrl(url);
  }
 //์˜ˆ์ œ 2
  onButtonTap() async {
 	launchUrlString('์ด๋™ํ•  ์ฃผ์†Œ');
  }

}

Webtoon App์—์„œ ํ•ด๋‹น ์›นํˆฐ์˜ ํšŒ์ฐจ๋กœ ์ด๋™์‹œํ‚ค๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.

  onButtonTap() async {
    await launchUrlString(
        "https://comic.naver.com/webtoon/detail?titleId=$webtoonId&no=${episode.id}");
        //webtoonId , episodeId์— ํ•ด๋‹นํ•˜๋Š” ํšŒ์ฐจ๋กœ ์ด๋™
  }

์ฐธ๊ณ ๊ฐ•์˜
https://nomadcoders.co/flutter-for-beginners/lobby

0๊ฐœ์˜ ๋Œ“๊ธ€