[Flutter] Youtube API 데이터 받기

송상민·2022년 12월 20일
1

Flutter

목록 보기
13/13
post-thumbnail

회사에서 Youtube Api를 받아서 유튜브 재생목록의 정보를 불러와 화면에 띄워주는 페이지를 구현했다.
그냥 몇개의 데이터를 불러와서 띄워주는건 상관없었는데 10개씩 재생목록을 계속 불러와주는 것을 구현하는데 조금 걸렸다.

그래서 나중에도 써먹기 위해 정리를 해보려고한다.


준비💡

먼저 youtube에서 API로 데이터를 받으려면 API키를 생성해야한다.

Google API 콘솔 이 곳에 들어가서 로그인 후 프로젝트를 만든 다음


왼쪽 세번째 보이는 사용자 인증 정보를 클릭한다.

클릭하게 되면 위의 화면이 나오게 되는데 거기서 사용자 인증 정보 만들기를 클릭한 뒤 API 키를 클릭하면 API 키가 생성된다.

만들어진 API키는 잊지말고 복사하거나 외워두자!
이제 만들어진 API키를 누르면


이 화면이 나온다.
Android 앱으로 제한사항을 바꿔주고 저장!

리스트가 50개 이하 코딩💻

YoutubeVideos

먼저 YoutubeApi를 통해 데이터를 받을 객체를 선언한다

class YoutubeVideos {
  String kind;
  String etag;
  String nextPageToken;
  List<Item> items;
  PageInfo pageInfo;

  YoutubeVideos(
      {required this.kind,
        required this.etag,
        required this.nextPageToken,
        required this.items,
        required this.pageInfo});

  factory YoutubeVideos.fromJson(dynamic json) {
    var list = json['items'] as List;

    return YoutubeVideos(
      kind: json['kind'] == null ? '' : json['kind'] as String,
      etag: json['etag'] == null ? '' : json['etag'] as String,
      nextPageToken: json['nextPageToken'] == null ? '' : json['nextPageToken'] as String,
      items: list.isNotEmpty ? list.map((i) => Item.fromJson(i)).toList() : [],
      pageInfo: PageInfo.fromJson(json['pageInfo']),
    );
  }
}

class PageInfo {
  int totalResults;
  int resultsPerPage;

  PageInfo({required this.totalResults, required this.resultsPerPage});

  factory PageInfo.fromJson(dynamic json) {
    return PageInfo(
      totalResults: json['totalResults'] == null ? 0 : json['totalResults'] as int,
      resultsPerPage: json['resultsPerPage'] == null ? 0 : json['resultsPerPage'] as int,
    );
  }
}

class Item {
  String kind;
  String etag;
  String id;
  Snippet snippet;

  Item({required this.kind, required this.etag, required this.id, required this.snippet});

  factory Item.fromJson(dynamic json) {
    return Item(
      kind: json['kind'] == null ? '' : json['kind'] as String,
      etag: json['etag'] == null ? '' : json['etag'] as String,
      id: json['id'] == null ? '' : json['id'] as String,
      snippet: Snippet.fromJson(json['snippet']),
    );
  }
}

class Snippet {
  String publishedAt;
  String channelId;
  String title;
  String description;
  Thumbnail thumbnails;
  String channelTitle;
  String playlistId;
  int position;
  ResourceId resourceId;
  String videoOwnerChannelTitle;
  String videoOwnerChannelId;

  Snippet({
    required this.publishedAt,
    required this.channelId,
    required this.title,
    required this.description,
    required this.thumbnails,
    required this.channelTitle,
    required this.playlistId,
    required this.position,
    required this.resourceId,
    required this.videoOwnerChannelTitle,
    required this.videoOwnerChannelId,
  });

  factory Snippet.fromJson(dynamic json) {
    return Snippet(
      publishedAt: json['publishedAt'] == null ? '' : json['publishedAt'] as String,
      channelId: json['channelId'] == null ? '' : json['channelId'] as String,
      title: json['title'] == null ? '' : json['title'] as String,
      description: json['description'] == null ? '' : json['description'] as String,
      thumbnails: Thumbnail.fromJson(json['thumbnails']),
      channelTitle: json['channelTitle'] == null ? '' : json['channelTitle'] as String,
      playlistId: json['playlistId'] == null ? '' : json['playlistId'] as String,
      position: json['position'] == null ? 0 : json['position'] as int,
      resourceId: ResourceId.fromJson(json['resourceId']),
      videoOwnerChannelTitle:
      json['videoOwnerChannelTitle'] == null ? '' : json['videoOwnerChannelTitle'] as String,
      videoOwnerChannelId:
      json['videoOwnerChannelId'] == null ? '' : json['videoOwnerChannelId'] as String,
    );
  }
}

class Thumbnail {
  String default_;
  String medium;
  String high;
  String standard;
  String maxres;

  Thumbnail({
    required this.default_,
    required this.medium,
    required this.high,
    required this.standard,
    required this.maxres,
  });

  factory Thumbnail.fromJson(dynamic json) {
    return Thumbnail(
      default_: json['default'] == null ? '' : json['default']['url'] as String,
      medium: json['medium'] == null ? '' : json['medium']['url'] as String,
      high: json['high'] == null ? '' : json['high']['url'] as String,
      standard: json['standard'] == null ? '' : json['standard']['url'] as String,
      maxres: json['maxres'] == null ? '' : json['maxres']['url'] as String,
    );
  }
}

class Thumb {
  String url;
  int width = 0;
  int height = 0;

  Thumb({
    required this.url,
    required this.width,
    required this.height,
  });

  factory Thumb.fromJson(dynamic json) {
    return Thumb(
      url: json['url'] == null ? '' : json['url'] as String,
      width: json['width'] == null ? 0 : json['width'] as int,
      height: json['height'] == null ? 0 : json['height'] as int,
    );
  }
}

class ResourceId {
  String kind;
  String videoId;

  ResourceId({
    required this.kind,
    required this.videoId,
  });

  factory ResourceId.fromJson(dynamic json) {
    return ResourceId(
      kind: json['kind'] == null ? '' : json['kind'] as String,
      videoId: json['videoId'] == null ? '' : json['videoId'] as String,
    );
  }
}

fetchYoutubeList

불러온 데이터를 내가 만든 객체 변수에 넣어 리턴한다.

late Future<YoutubeClipVideos> youtubeList;
YoutubeClipVideos? youtube;


  void initState() {
    super.initState();
    youtubeList = fetchYoutubeList();
  }

Future<YoutubeClipVideos> fetchYoutubeList() async {
    var part = 'snippet';
    var maxResults = 10; //자신이 가지고 오고 싶은 개수
    var playlistId = 'Youtube url에 있는 재생목록 ID';
    var key = '자신의 API 키';

    var url = 'https://www.googleapis.com/youtube/v3/playlistItems?'
        'playlistId=$playlistId&part=$part&maxResults=$maxResults&key=$key';
    var response = await http.get(Uri.parse(url));

    if (response.statusCode == 200) {
      var decodedData = jsonDecode(response.body);
      youtube = YoutubeClipVideos.fromJson(decodedData);
      return youtube!;
    } else {
      throw Exception('Failed to load mail auth result');
    }
  }

View

불러온 데이터의 길이만큼 영상목록을 만든다.

FutureBuilder(
              future: youtubeList,
              builder: (BuildContext context, AsyncSnapshot snapshot) 
              // 이 곳 부분에 데이터를 받아오기 전까지 보여줄 Loading창을 구현하면 좋다.
                        Container(
                          height: 145,
                          child: ListView.builder(
                            scrollDirection: Axis.horizontal,
                            itemCount: snapshot.data.items.length,
                            itemBuilder: (context, index) {
                              var items = snapshot.data.items;

                              String thumb = '';
                              var thumbnails = items[index].snippet.thumbnails;

                              return Container(
                                width: 140.0,
                                child: Material(
                                  color: Colors.white,
                                  child: InkWell(
                                    onTap: () {
                                      var vodId = items[index].snippet.resourceId.videoId;
                                      var list = items[index].snippet.playlistId;
                                      var link =
                                          "https://www.youtube.com/watch?v=$vodId&list=$list";
                                      launch(link, forceSafariVC: false);
                                    },
                                    child: Column(
                                      children: <Widget>[
                                        //썸네일부분 구현 필요
                                    
                                        Text(
                                          items[index].snippet.title, // 제목 부분
                                          maxLines: 2,
                                          overflow: TextOverflow.ellipsis,
                                          style: TextStyle(
                                            fontSize: 14.0,
                                            height: 1.3, //줄간격
                                            color: Colors.black,
                                          ),
                                        ),
                                      ],
                                    ),
                                  ),
                                ),
                              );
                            },
                          ),
                        ),
                      ],
                    ),
                  );
                }
              }),
        ),

생략한 부분이 좀 있지만 이렇게 구현하면 내가 지정해놓은 maxResults 길이만큼 불러올 수 있게 된다. 근데 유튜브는 한번 데이터를 받을 때 최대 길이가 50이다. 그래서 50개가 넘는 동영상을 가진 플레이리스트는 다른 방법을 사용해야한다.


리스트가 50개 이상 코딩💻

fetchYoutubeList

Future<List<Item>> fetchYoutubeList(int lastIdx) async {
    if(lastIdx == 0) {
      var part = 'snippet';
      var playlistId = 'PlayListID';
      var maxResults = 10;
      var key = 'API 키';

      var url = 'https://www.googleapis.com/youtube/v3/playlistItems?'
          'playlistId=$playlistId&part=$part&key=$key&maxResults=$maxResults';
      var response = await http.get(Uri.parse(url));

      if (response.statusCode == 200) {
        var decodedData = jsonDecode(response.body);
        var programListJson = decodedData['items'] as List;

        youtubeVideos = programListJson.map((Json) => Item.fromJson(Json)).toList();
        print(youtubeVideos.toString());


      } else {
        throw Exception('Failed to load mail auth result');
      }
    }else{
      var part = 'snippet';
      var playlistId = 'PlayListID';
      var maxResults = 10;
      var key = 'API 키';

      var url = 'https://www.googleapis.com/youtube/v3/playlistItems?'
          'playlistId=$playlistId&part=$part&key=$key&maxResults=$maxResults&pageToken=$nextPageToken';
      var response = await http.get(Uri.parse(url));

      if (response.statusCode == 200) {
        var decodedData = jsonDecode(response.body);
        nextPageToken = decodedData['nextPageToken'];
        var programListJson = decodedData['items'] as List;

        var newNews = programListJson.map((tagJson) => Item.fromJson(tagJson)).toList();
        setState(() {
          youtubeVideos.addAll(List.generate(newNews.length, (index) => newNews[index]));
          videoCount += 10;
        });
        _refreshController.loadComplete();

      } else {
        throw Exception('Failed to load mail auth result');
      }
    }


    return youtubeVideos;

  }

View

아래로 내리면 더보기처럼 List가 늘어나게 하기 위해 RefreshConfiguration을 사용했다.


Future _onLoad() async {
    await fetchYoutubeList(1);
  }

return FutureBuilder(
        future: youtubeList,
        builder: (BuildContext context, AsyncSnapshot snapshot) {
            return RefreshConfiguration(
              footerTriggerDistance: 200, // 추가로딩 트리거 거
              hideFooterWhenNotFull: true,
              enableLoadingWhenNoData: false,
              child: SmartRefresher(
                enablePullUp: (snapshot.data.length >= totalCount) ? false : true,
                header: MaterialClassicHeader(
                  color: Colors.blue,
                ),
                footer: CustomFooter(
                  loadStyle: LoadStyle.ShowWhenLoading,
                  builder: (BuildContext context, LoadStatus? mode) {
                    Widget body = CupertinoActivityIndicator();

                    return Container(
                      height: mode == LoadStatus.idle ? 0 : 55.0,
                      child: Center(child: body),
                    );
                  },
                ),
                controller: _refreshController,
                onLoading: _onLoad,
                child: ListView.builder(
                  scrollDirection: Axis.vertical,
                  itemCount: snapshot.data.length,
                    itemBuilder: (context, index){
                      var items = snapshot.data;


                      String thumb = '';
                      var thumbnails = items[index].snippet.thumbnails;
                      if (thumbnails.maxres != "") {
                        thumb = thumbnails.maxres;
                      } else if (thumbnails.standard != "") {
                        thumb = thumbnails.standard;
                      } else if (thumbnails.high != "") {
                        thumb = thumbnails.high;
                      } else if (thumbnails.medium != "") {
                        thumb = thumbnails.medium;
                      } else if (thumbnails.default_ != "") {
                        thumb = thumbnails.default_;
                      }

                      return Container(
                        width: 140.0,
                        //마진 맨앞 뒤로 다르게 줄 것
                        margin: EdgeInsets.only(
                            top: 8,
                            left: 20,
                            right: 20),
                        child: Material(
                          color: Colors.white,
                          child: InkWell(
                            onTap: () {
                              var vodId = items[index].snippet.resourceId.videoId;
                              var list = items[index].snippet.playlistId;
                              var link =
                                  "https://www.youtube.com/watch?v=$vodId&list=$list";
                              launch(link, forceSafariVC: false);
                            },
                            child: Column(
                              children: <Widget>[

                                //썸네일
                                Container(
                                  width: deviceWidth,
                                  height: 200,
                                  child: Stack(
                                    children: [
                                      //썸네일 부분
                                      Positioned.fill(
                                          child: Image.network(thumb, fit: BoxFit
                                              .cover)),
                                    ],
                                  ),
                                ),

                                //제목
                                Container(
                                  alignment: Alignment.centerLeft,
                                  margin: EdgeInsets.only(left: 5, right: 10, top: 12),
                                  child: Text(
                                    items[index].snippet.title.toString().substring(11, 29),
                                    maxLines: 1,
                                    overflow: TextOverflow.ellipsis,
                                    style: TextStyle(
                                      fontSize: 17,
                                      height: 1.6, //줄간격
                                      color: Colors.black,
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                ),

                                //날짜
                                Container(
                                  alignment: Alignment.centerLeft,
                                  margin: EdgeInsets.only(left: 5, right: 10, bottom: 12),
                                  child: Text(items[index].snippet.publishedAt.toString().substring(0,10),
                                    maxLines: 1,
                                    textAlign: TextAlign.left,
                                    overflow: TextOverflow.ellipsis,
                                    style: TextStyle(
                                      fontSize: 15,
                                      height: 1.6, //줄간격
                                      color: Colors.black,
                                      fontFamily: "NanumBarunGothic",
                                    ),
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                      );
                    }),
              )
            );
          }
        });
  }

이렇게 유튜브 API 데이터를 받아와서 재생목록의 썸네일과 제목을 받아왔다! 생략된 부분이 많은데 잘 채워넣으면 될 것 같다. 구현하면서 나름 나쁘지 않았었던 것 같다!

profile
실력있는 Flutter 개발자가 되어보자

0개의 댓글