flutterdartvideotinderyoutube-player-flutter

From a list of YouTube videos rendered in a card stack, the 1st video starts playing in every card on swipe


I am trying to create a Tinder-like swipe functionality with the Youtube videos. I'll provide a detailed description of what I am trying to achieve.

Step by Step breakdown:

  1. Fetch the Youtube videos using the Youtube Data API v3.

youtube _model.dart

// To parse this JSON data, do
//
//     final youtubeSearchVideos = youtubeSearchVideosFromJson(jsonString);

import 'dart:convert';

YoutubeSearchVideos youtubeSearchVideosFromJson(String str) =>
    YoutubeSearchVideos.fromJson(json.decode(str));

String youtubeSearchVideosToJson(YoutubeSearchVideos data) =>
    json.encode(data.toJson());

class YoutubeSearchVideos {
  YoutubeSearchVideos({
    required this.kind,
    required this.etag,
    this.nextPageToken,
    this.prevPageToken,
    required this.regionCode,
    required this.pageInfo,
    required this.items,
  });

  String kind;
  String etag;
  String? nextPageToken;
  String? prevPageToken;
  String regionCode;
  PageInfo pageInfo;
  List<Item> items;

  factory YoutubeSearchVideos.fromJson(Map<String, dynamic> json) =>
      YoutubeSearchVideos(
        kind: json["kind"],
        etag: json["etag"],
        nextPageToken: json["nextPageToken"],
        prevPageToken: json["prevPageToken"],
        regionCode: json["regionCode"],
        pageInfo: PageInfo.fromJson(json["pageInfo"]),
        items: List<Item>.from(json["items"].map((x) => Item.fromJson(x))),
      );

  Map<String, dynamic> toJson() => {
        "kind": kind,
        "etag": etag,
        "nextPageToken": nextPageToken,
        "prevPageToken": prevPageToken,
        "regionCode": regionCode,
        "pageInfo": pageInfo.toJson(),
        "items": List<dynamic>.from(items.map((x) => x.toJson())),
      };
}

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

  String kind;
  String etag;
  Id id;
  Snippet snippet;

  factory Item.fromJson(Map<String, dynamic> json) => Item(
        kind: json["kind"],
        etag: json["etag"],
        id: Id.fromJson(json["id"]),
        snippet: Snippet.fromJson(json["snippet"]),
      );

  Map<String, dynamic> toJson() => {
        "kind": kind,
        "etag": etag,
        "id": id.toJson(),
        "snippet": snippet.toJson(),
      };
}

class Id {
  Id({
    required this.kind,
    required this.videoId,
  });

  String kind;
  String videoId;

  factory Id.fromJson(Map<String, dynamic> json) => Id(
        kind: json["kind"],
        videoId: json["videoId"],
      );

  Map<String, dynamic> toJson() => {
        "kind": kind,
        "videoId": videoId,
      };
}

class Snippet {
  Snippet({
    required this.publishedAt,
    required this.channelId,
    required this.title,
    required this.description,
    required this.thumbnails,
    required this.channelTitle,
    required this.liveBroadcastContent,
    required this.publishTime,
  });

  DateTime publishedAt;
  String channelId;
  String title;
  String description;
  Thumbnails thumbnails;
  String channelTitle;
  String liveBroadcastContent;
  DateTime publishTime;

  factory Snippet.fromJson(Map<String, dynamic> json) => Snippet(
        publishedAt: DateTime.parse(json["publishedAt"]),
        channelId: json["channelId"],
        title: json["title"],
        description: json["description"],
        thumbnails: Thumbnails.fromJson(json["thumbnails"]),
        channelTitle: json["channelTitle"],
        liveBroadcastContent: json["liveBroadcastContent"],
        publishTime: DateTime.parse(json["publishTime"]),
      );

  Map<String, dynamic> toJson() => {
        "publishedAt": publishedAt.toIso8601String(),
        "channelId": channelId,
        "title": title,
        "description": description,
        "thumbnails": thumbnails.toJson(),
        "channelTitle": channelTitle,
        "liveBroadcastContent": liveBroadcastContent,
        "publishTime": publishTime.toIso8601String(),
      };
}

class Thumbnails {
  Thumbnails({
    required this.thumbnailsDefault,
    required this.medium,
    required this.high,
  });

  Default thumbnailsDefault;
  Default medium;
  Default high;

  factory Thumbnails.fromJson(Map<String, dynamic> json) => Thumbnails(
        thumbnailsDefault: Default.fromJson(json["default"]),
        medium: Default.fromJson(json["medium"]),
        high: Default.fromJson(json["high"]),
      );

  Map<String, dynamic> toJson() => {
        "default": thumbnailsDefault.toJson(),
        "medium": medium.toJson(),
        "high": high.toJson(),
      };
}

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

  String url;
  int width;
  int height;

  factory Default.fromJson(Map<String, dynamic> json) => Default(
        url: json["url"],
        width: json["width"],
        height: json["height"],
      );

  Map<String, dynamic> toJson() => {
        "url": url,
        "width": width,
        "height": height,
      };
}

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

  int totalResults;
  int resultsPerPage;

  factory PageInfo.fromJson(Map<String, dynamic> json) => PageInfo(
        totalResults: json["totalResults"],
        resultsPerPage: json["resultsPerPage"],
      );

  Map<String, dynamic> toJson() => {
        "totalResults": totalResults,
        "resultsPerPage": resultsPerPage,
      };
}

youtube_api_service.dart

    import 'package:http/http.dart' as http;
    import 'package:starcast_intros/models/youtube_search.dart';
    import 'package:starcast_intros/private_keys.dart';
    
    class YoutubeApi {
      static const String youtubeAPI =
          'https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResults=5&q=surfing&type=video&videoDefinition=standard&videoDimension=2d&videoDuration=short&videoEmbeddable=true&key=$YOUTUBE_DATA_API_KEY';
    
      Future<YoutubeSearchVideos> fetchVideos() async {
        try {
          final response = await http.get(Uri.parse(youtubeAPI));
    
          if (response.statusCode == 200) {
            return youtubeSearchVideosFromJson(response.body);
          }
    
          throw Exception('Failed to fetch videos ${response.body}');
        } catch (e) {
          print(e);
          throw Exception('Failed to fetch videos $e');
        }
      }
    }

2. After retrieving the list of youtube video IDs from the API, render the Youtube videos like Tinder cards which can be swiped left or right.

import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:starcast_intros/models/youtube_search.dart';
import 'package:starcast_intros/services/youtube_api.dart';
import 'package:tcard/tcard.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);
  static const HOME = 'Home';

  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  late Future<YoutubeSearchVideos> futureVideos;

  @override
  void initState() {
    super.initState();

    final youtubeAPI = YoutubeApi();
    futureVideos = youtubeAPI.fetchVideos();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<YoutubeSearchVideos>(
      future: futureVideos,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          List<Widget> cards = List.generate(
            snapshot.data!.items.length,
            (int index) {
              YoutubePlayerController _controller = YoutubePlayerController(
                initialVideoId: snapshot.data!.items[index].id.videoId,
                flags: YoutubePlayerFlags(
                  autoPlay: false,
                  mute: true,
                  isLive: false,
                  disableDragSeek: true,
                  loop: false,
                  forceHD: false,
                ),
              );

              return Container(
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(16.0),
                  boxShadow: [
                    BoxShadow(
                      offset: Offset(0, 17),
                      blurRadius: 23.0,
                      spreadRadius: -13.0,
                      color: Colors.black54,
                    )
                  ],
                ),
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(16.0),
                  child: YoutubePlayer(
                    controller: _controller,
                  ),
                ),
              );
            },
          );

          return TCard(
            size: Size(
              MediaQuery.of(context).size.width,
              MediaQuery.of(context).size.height,
            ),
            cards: cards,
          );
        } else if (snapshot.hasError) {
          return Text('${snapshot.error}');
        }

        // By default, show a loading spinner.
        return SpinKitDoubleBounce(
          color: Theme.of(context).accentColor,
          size: 75.0,
        );
      },
    );
  }
}
  1. Please note that you would need a Youtube API key (create it using Google Console) to retrieve the list of videos. I am using the Youtube Search API. Probably, you can use the below-given JSON if you do not wanna make a request or create an API key:

{ "kind": "youtube#searchListResponse", "etag": "E2FpjhO0gVzn8gmf9Q1VSJ72Rwk", "nextPageToken": "CAUQAA",
"regionCode": "IN", "pageInfo": { "totalResults": 1000000, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#searchResult", "etag": "HmJbuO71viHMk8216TydPkfPIAg", "id": { "kind": "youtube#video", "videoId": "xIspFfN3vfs" }, "snippet": { "publishedAt": "2020-03-06T22:13:55Z", "channelId": "UCR03gYk1xLMV4ko8ljxTeIA", "title": "Nick O'Bea : How to Wing surf", "description": "Wing Surfing How to: Skills and Drills on the wing with Nick O'Bea 5'1"x26" 90 liters from supsurfmachines.com 6 meter F1 Swing in 10-12 mph 1020 AXIS ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/xIspFfN3vfs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/xIspFfN3vfs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/xIspFfN3vfs/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Rush Mark Rush", "liveBroadcastContent": "none", "publishTime": "2020-03-06T22:13:55Z" } }, { "kind": "youtube#searchResult", "etag": "PbsGbvz61v4Fpq4DwtLexeIB708", "id": { "kind": "youtube#video", "videoId": "0zO26j6vNGg" }, "snippet": { "publishedAt": "2016-08-10T08:07:07Z", "channelId": "UCjYiM-YLuOQN-4S0I7PfgVg", "title": "Alison Teal: Surfing Hawaii Volcano Eruption", "description": "Kilauea Volcano is erupting on the Big Island of Hawaii and flowing into the ocean for the first time since 2011. Alison, a surfer and film maker, travels the world ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/0zO26j6vNGg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/0zO26j6vNGg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/0zO26j6vNGg/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Alison's Adventures", "liveBroadcastContent": "none", "publishTime": "2016-08-10T08:07:07Z" } }, { "kind": "youtube#searchResult", "etag": "Ab0zXs-KAbFMVeH4jBajBEj60yU", "id": { "kind": "youtube#video", "videoId": "pPGaGZTMc_4" }, "snippet": { "publishedAt": "2011-12-05T17:08:27Z", "channelId": "UChug4c-a2tUGgZ-XeEKdxZQ", "title": "Strongbow Neon Night Surfing Bondi", "description": "To mark the start of summer, Strongbow joined forces with legendary surfing filmmaker Jack McCoy (Endless Summer II), Bali Strickland and Eugene Tan ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/pPGaGZTMc_4/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/pPGaGZTMc_4/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/pPGaGZTMc_4/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Surfers Village TV", "liveBroadcastContent": "none", "publishTime": "2011-12-05T17:08:27Z" } }, { "kind": "youtube#searchResult", "etag": "XjmemrMgMcym-3mlYs53ie_w3t4", "id": { "kind": "youtube#video", "videoId": "4uwtqRBE4Kk" }, "snippet": { "publishedAt": "2010-08-13T02:10:28Z", "channelId": "UCTYHNSWYy4jCSCj1Q1Fq0ew", "title": "Andy Irons - i surf because short film", "description": "Andy Irons is one of the world's greatest ever surfers. A 3 times world champion made famous by his epic battles with Kelly Slater. But outside all the victories ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/4uwtqRBE4Kk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/4uwtqRBE4Kk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/4uwtqRBE4Kk/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Billabong", "liveBroadcastContent": "none", "publishTime": "2010-08-13T02:10:28Z" } }, { "kind": "youtube#searchResult", "etag": "FZQyT7mq6dN1LX4M5RjXzVJxrtQ", "id": { "kind": "youtube#video", "videoId": "kGvs0Nv5zJo" }, "snippet": { "publishedAt": "2013-11-15T09:31:05Z", "channelId": "UCNSfJB-VQeHpv5ThtV1VtBA", "title": "Wave cinematographer captures surfer's last wave", "description": "On Wednesday morning, well known Wave Cinematographer Larry Haynes was filming those big sets from the shore, and was rolling on Kirk Passmore as the ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/kGvs0Nv5zJo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/kGvs0Nv5zJo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/kGvs0Nv5zJo/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "KITV", "liveBroadcastContent": "none", "publishTime": "2013-11-15T09:31:05Z" } } ] }

Once the card is stacked with different videos, I swipe the card at the top. As soon as I swipe, the video in the 1st card appears in the card beneath (2nd card). I expected the 2nd video to play in the 2nd card as all the video IDs are different.

If I just drag a little and hold it, I can see the thumbnail of the 2nd video in the 2nd card. But, as soon as I swipe right, the video in the 2nd card (2nd video) gets replaced with the video in the 1st card (1st video).

This repeats until the last card.

Any help to crack this would be much appreciated. Thanks in anticipation.

Cheers.


Solution

  • Add a unique key to each of the YouTube cards, You could use the YouTube ID as a key

    When to Use Keys