DEV Community

Cover image for Build a Youtube Clone with Strapi and Flutter: Part 3
Strapi for Strapi

Posted on

Build a Youtube Clone with Strapi and Flutter: Part 3

Introduction

Welcomet to the Part 3 of our blog series. I would advise you to read Part 1 and Part 2 to understand how we got to this point.

For reference purposes, here's the outline of this blog series:

In Part 3, we'll learn how to build the frontend with Flutter and consume the APIs to implement a functional YouTube clone application. Before we move futher, let's look at the folder structure for the Flutter app we'll be building:

📦youtube_clone
┣ 📂tests: Test files to check app functionality.
┣ 📂assets: General assets like images or fonts.
┣ 📂ios: iOS-specific files.
┣ 📂android: Android-specific files.
┣ 📂assets
 ┃ ┗ 📂images: Stores image assets.
 📂lib
 ┃ ┣ 📂providers: Handles state for WebSocket, user, and video data.
 ┃ ┃ ┣ 📜socket_provider.dart: Manages WebSocket data.
 ┃ ┃ ┣ 📜user_provider.dart: Manages user state.
 ┃ ┃ ┗ 📜video_provider.dart: Manages video state.
 ┃ ┣ 📂services: Deals with API calls for users and videos.
 ┃ ┃ ┣ 📜user_service.dart: User API logic.
 ┃ ┃ ┗ 📜video_service.dart: Video API logic.
 ┃ ┣ 📂utils: Useful functions.
 ┃ ┃ ┗ 📜getter.dart: Fetching data helpers.
 ┃ ┗ 📜main.dart: App's starting point.
 ┣ 📂web: Web-specific files.
 ┣ 📂windows: Windows-specific files.
 ┣ 📜analysis_options.yaml: Code analysis settings.
 ┣ 📜package-lock.json: Locks npm dependencies.
 ┣ 📜pubspec.lock: Locks Dart package versions.
 ┗ 📜pubspec.yaml: Project settings and dependencies.
Enter fullscreen mode Exit fullscreen mode

Creating User Interface with Flutter

Now that you set up your Flutter project, configured the permissions, and assets, and installed the required project dependencies, let's proceed to building out the user interface.

Creating the Home Screen

Create a new directory named screens in your lib directory. In the screens directory, create a home_screen.dart file and add the code snippet below:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:youtube_clone/providers/user_provider.dart';
import 'package:youtube_clone/providers/video_provider.dart';
import 'package:youtube_clone/utils/getter.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

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

class _HomeScreenState extends State<HomeScreen> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      Provider.of<VideoProvider>(context, listen: false).fetchVideos();
    });
  }

  @override
  Widget build(BuildContext context) {
    final userProvider = Provider.of<UserProvider>(context);
    return Scaffold(
      appBar: AppBar(
        title: Row(
          children: [
            Image.asset(
              'assets/images/YouTube_logo.png',
              width: 90,
            ),
            const Spacer(),
            IconButton(
              icon: const Icon(Icons.search, color: Colors.white),
              onPressed: () {
                showSearch(
                  context: context,
                  delegate: VideoSearchDelegate(),
                );
              },
            ),
            GestureDetector(
              onTap: () {},
              child: userProvider.token != null
                  ? CircleAvatar(
                      backgroundImage: NetworkImage(
                        '${getBaseUrl()}${userProvider.user?['profile_picture']['url']}',
                      ),
                      radius: 20,
                    )
                  : ElevatedButton(
                      onPressed: () {},
                      child: const Text('Sign in'),
                    ),
            ),
          ],
        ),
      ),
      body: Column(
        children: [
          Expanded(
            child: Consumer<VideoProvider>(
              builder: (context, videoProvider, child) {
                if (videoProvider.videos.isEmpty) {
                  return const Center(child: CircularProgressIndicator());
                }

                return ListView.builder(
                  itemCount: videoProvider.videos.length,
                  itemBuilder: (context, index) {
                    final video = videoProvider.videos[index];
                    return _buildVideoTitle(video);
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildVideoTitle(Map<String, dynamic> video) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Stack(
          children: [
            Image.network(
              '${getBaseUrl()}${video['thumbnail']['url']}',
              width: double.infinity,
              height: 200,
              fit: BoxFit.cover,
            ),
          ],
        ),
        ListTile(
          contentPadding:
              const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
          leading: CircleAvatar(
            backgroundImage: NetworkImage(
              '${getBaseUrl()}${video['uploader']['profile_picture']['url']}',
            ),
            radius: 20,
          ),
          title: Text(
            video['title'],
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
          ),
          subtitle: Text(
            '${video['uploader']['username']}${video['views']?.length.toString()} views • ${_formatDaysAgo(video['publishedAt'])}',
          ),
          trailing: IconButton(
            icon: const Icon(Icons.more_vert),
            onPressed: () {
            },
          ),
        ),
      ],
    );
  }

  String _formatDaysAgo(String publishedAt) {
    final publishedDate = DateTime.parse(publishedAt);
    final now = DateTime.now();
    final difference = now.difference(publishedDate).inDays;

    if (difference == 0) {
      return 'Today';
    } else if (difference == 1) {
      return 'Yesterday';
    } else {
      return '$difference days ago';
    }
  }
}

class VideoSearchDelegate extends SearchDelegate<String> {
  @override
  List<Widget> buildActions(BuildContext context) {
    return [
      IconButton(
        icon: const Icon(Icons.clear),
        onPressed: () {
          query = '';
        },
      ),
    ];
  }

  @override
  Widget buildLeading(BuildContext context) {
    return IconButton(
      icon: const Icon(Icons.arrow_back),
      onPressed: () {
        close(context, '');
      },
    );
  }

  @override
  Widget buildResults(BuildContext context) {
      final videoProvider = Provider.of<VideoProvider>(context, listen: false);
      final results = videoProvider.videos
          .where((video) =>
              video['title'].toLowerCase().contains(query.toLowerCase()))
          .toList();

      return ListView.builder(
        itemCount: results.length,
        itemBuilder: (context, index) {
          final video = results[index];
          return ListTile(
            title: Text(video['title']),
            onTap: () async {
              await Provider.of<VideoProvider>(context, listen: false)
                  .increaseViews(video['documentId']);

              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) =>
                      VideoPlayerScreen(videoId: video['documentId']),
                ),
              );
            },
          );
        },
      );
    }

  @override
  Widget buildSuggestions(BuildContext context) {
    final videoProvider = Provider.of<VideoProvider>(context, listen: false);
    final suggestions = videoProvider.videos
        .where((video) =>
            video['title'].toLowerCase().contains(query.toLowerCase()))
        .toList();

    return ListView.builder(
      itemCount: suggestions.length,
      itemBuilder: (context, index) {
        final suggestion = suggestions[index];
        return ListTitle(
          title: Text(suggestion['title']),
          onTap: () {
            query = suggestion['title'];
            showResults(context);
          },
        );
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In the HomeScreen widget, the initState method triggers a fetch for video data using the VideoProvider as soon as the screen is loaded. The app bar features a logo, a search button to show video search results, and a profile avatar or sign-in button depending on the user's authentication state. The body of the screen displays a list of videos fetched from the backend, with each video showing a thumbnail, uploader information, and other details. The VideoSearchDelegate enables searching and filtering of videos by title, showing matching results and suggestions as the user types.

Now update your main.dart file to render the HomeScreen widget:

//...
import 'package:youtube_clone/screens/home_screen.dart';

//...

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    //...
    return MaterialApp(
      //...
      home: const HomeScreen(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating the Home Screen.png

Creating the Video Player Screen

Create a new video_player_screen.dart file in the lib/screens folder and add the code below for the Video player screen.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
import 'package:youtube_clone/providers/user_provider.dart';
import 'package:youtube_clone/providers/video_provider.dart';
import 'package:youtube_clone/utils/getter.dart';

class VideoPlayerScreen extends StatefulWidget {
  final String videoId;

  const VideoPlayerScreen({super.key, required this.videoId});

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

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  late VideoPlayerController _controller;
  final TextEditingController _commentController = TextEditingController();

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

  @override
  void didUpdateWidget(covariant VideoPlayerScreen oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.videoId != oldWidget.videoId) {
      _initializeVideo();
    }
  }

  void _initializeVideo() {
    final videoProvider = Provider.of<VideoProvider>(context, listen: false);
    final video = videoProvider.videos
        .firstWhere((v) => v['documentId'] == widget.videoId, orElse: () => {});
    print('${getBaseUrl()}${video['video_file']['url']}');
    if (video.isNotEmpty) {
      _controller = VideoPlayerController.network(
          '${getBaseUrl()}${video['video_file']['url']}')
        ..initialize().then((_) {
          if (mounted) {
            setState(() {});
          }
        });
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    _commentController.dispose();
    super.dispose();
  }

  void _submitComment(BuildContext context, String videoId) {
    if (_commentController.text.isNotEmpty) {
      final videoProvider = Provider.of<VideoProvider>(context, listen: false);
      final userProvider = Provider.of<UserProvider>(context, listen: false);
      videoProvider.commentOnVideo(
          videoId, _commentController.text, userProvider.user?['documentId']);
      _commentController.clear();
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    final userProvider = Provider.of<UserProvider>(context, listen: false);
    return Consumer<VideoProvider>(
      builder: (context, videoProvider, child) {
        final video = videoProvider.videos.firstWhere(
            (v) => v['documentId'] == widget.videoId,
            orElse: () => {});
        if (video.isEmpty) {
          return Scaffold(
            appBar: AppBar(
              leading: IconButton(
                icon: const Icon(Icons.arrow_back),
                onPressed: () {
                  Navigator.pop(context);
                },
              ),
            ),
            body: Center(child: Text('Video not found')),
          );
        }

        return Scaffold(
          appBar: AppBar(
            leading: IconButton(
              icon: const Icon(Icons.arrow_back),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
          ),
          body: Stack(
            children: [
              SingleChildScrollView(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    if (_controller.value.isInitialized)
                      AspectRatio(
                        aspectRatio: _controller.value.aspectRatio,
                        child: VideoPlayer(_controller),
                      ),
                    Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: [
                              IconButton(
                                icon: Icon(
                                  _controller.value.isPlaying
                                      ? Icons.pause
                                      : Icons.play_arrow,
                                ),
                                onPressed: () {
                                  setState(() {
                                    _controller.value.isPlaying
                                        ? _controller.pause()
                                        : _controller.play();
                                  });
                                },
                              ),
                              IconButton(
                                icon: const Icon(Icons.fullscreen),
                                onPressed: () {
                                  Navigator.push(
                                    context,
                                    MaterialPageRoute(
                                      builder: (context) =>
                                          FullscreenVideoPlayer(
                                              controller: _controller),
                                    ),
                                  );
                                },
                              ),
                            ],
                          ),
                          const SizedBox(height: 8),
                          Text(
                            video['title'] ?? 'No Title',
                            style: const TextStyle(
                                fontSize: 16, fontWeight: FontWeight.bold),
                          ),
                          const SizedBox(height: 4),
                          Text(
                            video['description'] ?? 'No Description',
                            style: const TextStyle(fontSize: 14),
                          ),
                          const SizedBox(height: 16),
                          Row(children: [
                            CircleAvatar(
                              backgroundImage: NetworkImage(
                                  '${getBaseUrl()}${video['uploader']['profile_picture']['url']}'),
                            ),
                            const SizedBox(width: 8),
                            Text(
                              (video['uploader']['username']).toString(),
                            ),
                            const SizedBox(width: 10),
                            Text(
                              (video['uploader']['subscribers']?.length ?? 0)
                                  .toString(),
                            ),
                            const SizedBox(width: 10),
                            Row(
                              children: [
                                TextButton(
                                  onPressed: () async {
                                    await videoProvider
                                        .likeVideo(video['documentId']);
                                  },
                                  child: const Icon(Icons.thumb_up),
                                ),
                                Text(video['likes'].length.toString()),
                              ],
                            ),
                            const SizedBox(width: 15),
                            // Check if the user is logged in
                            if (userProvider.user != null)
                              ElevatedButton(
                                onPressed: () async {
                                        await videoProvider.subscribeToChannel(
                                            video['uploader']['id']);
                                      },
                                child: Text(
                                  video['uploader']['subscribers'] != null &&
                                          video['uploader']['subscribers']!.any(
                                              (subscriber) =>
                                                  subscriber['id'] ==
                                                  userProvider.user!['id'])
                                      ? "Unsubscribe"
                                      : "Subscribe",
                                ),
                              ),
                          ]),
                        ],
                      ),
                    ),
                    Row(
                      children: [
                        const Text("Comments"),
                        const SizedBox(width: 8),
                        Text(video['comments'].length.toString()),
                      ],
                    ),

                    Container(
                      padding: const EdgeInsets.all(16.0),
                      color: Colors.black12,
                      child: Column(
                        children: video['comments'].map<Widget>((comment) {
                          return ListTile(
                            leading: CircleAvatar(
                              backgroundImage: NetworkImage(
                                  '${getBaseUrl()}${comment['user']['profile_picture']['url']}'),
                            ),
                            title: Text(comment['user']['username']),
                            subtitle: Text(comment['text']),
                          );
                        }).toList(),
                      ),
                    ),
                    const SizedBox(height: 70),
                  ],
                ),
              ),
              if (userProvider.token != null)
                Positioned(
                  left: 0,
                  right: 0,
                  bottom: 0,
                  child: Container(
                    color: Theme.of(context).scaffoldBackgroundColor,
                    padding: const EdgeInsets.all(15),
                    child: Row(
                      children: [
                        Expanded(
                          child: TextField(
                            controller: _commentController,
                            decoration: const InputDecoration(
                              hintText: 'Add a comment...',
                              border: OutlineInputBorder(),
                            ),
                          ),
                        ),
                        const SizedBox(width: 8),
                        ElevatedButton(
                          onPressed: () =>
                              _submitComment(context, video['documentId']),
                          child: const Text('Post'),
                        ),
                      ],
                    ),
                  ),
                ),
            ],
          ),
        );
      },
    );
  }
}

class FullscreenVideoPlayer extends StatelessWidget {
  final VideoPlayerController controller;

  const FullscreenVideoPlayer({super.key, required this.controller});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: AspectRatio(
          aspectRatio: controller.value.aspectRatio,
          child: VideoPlayer(controller),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In the VideoPlayerScreen widget, the initState method initializes the VideoPlayerController to load and play the video when the screen is first built. The didUpdateWidget method ensures that the video is reloaded if the videoId changes. The build method displays the video player, controls for playback and fullscreen mode, video details, uploader information, and user interaction options like liking and subscribing. It also provides a comment section where authenticated users can post comments. The FullscreenVideoPlayer widget allows the video to be viewed in fullscreen mode.

To allow you to navigate to this screen when you click on any video from the HomeScreen widget, update the _buildVideoTile widget in the lib/screens/home_screen.dart file to add navigation to the VideoPlayerScreen widget.

  //...
  import 'package:youtube_clone/screens/video_player_screen.dart';


  //...
  Widget _buildVideoTitle(Map<String, dynamic> video) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        GestureDetector(
          onTap: () async {
            await Provider.of<VideoProvider>(context, listen: false)
                .increaseViews(video['documentId']);

            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) =>
                    VideoPlayerScreen(videoId: video['documentId']),
              ),
            );
          },
          child: Stack(
            children: [
              Image.network(
                '${getBaseUrl()}${video['thumbnail']['url']}',
                width: double.infinity,
                height: 200,
                fit: BoxFit.cover,
              ),
            ],
          ),
        ),
        ListTile(
          contentPadding:
              const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
          leading: CircleAvatar(
            backgroundImage: NetworkImage(
              '${getBaseUrl()}${video['uploader']['profile_picture']['url']}',
            ),
            radius: 20,
          ),
          title: GestureDetector(
            onTap: () async {
              await Provider.of<VideoProvider>(context, listen: false)
                  .increaseViews(video['documentId']);

              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) =>
                      VideoPlayerScreen(videoId: video['documentId']),
                ),
              );
            },
            child: Text(
              video['title'],
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
          ),
          subtitle: Text(
            '${video['uploader']['username']}${video['views']?.length.toString()} views • ${_formatDaysAgo(video['publishedAt'])}',
          ),
          trailing: IconButton(
            icon: const Icon(Icons.more_vert),
            onPressed: () {},
          ),
          onTap: () async {
            await Provider.of<VideoProvider>(context, listen: false)
                .increaseViews(video['documentId']);

            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) =>
                    VideoPlayerScreen(videoId: video['documentId']),
              ),
            );
          },
        ),
      ],
    );
  }
 //...
Enter fullscreen mode Exit fullscreen mode

In the above code, we added an onTab, which is triggered when the user clicks on a video. It calls the VideoProvider class increaseViews method which will increase the number of views for this video. Now, click on the video to view the VideoPlayerScreen widget.

Creating the Video Player Screen.png

Creating the Auth Screen

To allow users to authenticate into the application, including signing in and signing up, we'll create a new file named auth_screen.dart in the lib/screens directory for user sign-in and sign-up functionalities:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:youtube_clone/providers/user_provider.dart';
import 'package:youtube_clone/screens/home_screen.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';

class AuthScreen extends StatefulWidget {
  const AuthScreen({super.key});

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

class _AuthScreenState extends State<AuthScreen> {
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  final TextEditingController _usernameController = TextEditingController();
  XFile? _profilePicture;
  bool _isLogin = true;

  @override
  Widget build(BuildContext context) {
    final userProvider = Provider.of<UserProvider>(context);
    final ImagePicker _picker = ImagePicker();

    return Scaffold(
      appBar: AppBar(
        title: Text(_isLogin ? 'Login' : 'Sign Up'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: SingleChildScrollView(
          child: Column(
            children: [
              if (!_isLogin)
                TextField(
                  controller: _usernameController,
                  decoration: const InputDecoration(labelText: 'Username'),
                ),
              TextField(
                controller: _emailController,
                decoration: const InputDecoration(labelText: 'Email'),
              ),
              TextField(
                controller: _passwordController,
                decoration: const InputDecoration(labelText: 'Password'),
                obscureText: true,
              ),
              const SizedBox(height: 20),
              if (!_isLogin)
                _profilePicture == null
                    ? TextButton(
                        onPressed: () async {
                          final pickedFile = await _picker.pickImage(
                              source: ImageSource.gallery);
                          setState(() {
                            _profilePicture = pickedFile;
                          });
                        },
                        child: const Text('Select Profile Picture'),
                      )
                    : Column(
                        children: [
                          Image.file(
                            File(_profilePicture!.path),
                            height: 100,
                            width: 100,
                          ),
                          TextButton(
                            onPressed: () async {
                              final pickedFile = await _picker.pickImage(
                                  source: ImageSource.gallery);
                              setState(() {
                                _profilePicture = pickedFile;
                              });
                            },
                            child: const Text('Change Profile Picture'),
                          ),
                        ],
                      ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () async {
                  if (_isLogin) {
                    await userProvider.login(
                      _emailController.text,
                      _passwordController.text,
                    );
                  } else {
                    await userProvider.signup(
                      File(_profilePicture!.path),
                      _emailController.text,
                      _usernameController.text,
                      _passwordController.text,
                    );
                  }

                  if (userProvider.token != null) {
                    Navigator.pushAndRemoveUntil(
                      context,
                      MaterialPageRoute(
                          builder: (context) => const HomeScreen()),
                      (Route<dynamic> route) => false,
                    );
                  }
                },
                child: Text(_isLogin ? 'Login' : 'Sign Up'),
              ),
              const SizedBox(height: 20),
              TextButton(
                onPressed: () {
                  setState(() {
                    _isLogin = !_isLogin;
                  });
                },
                child: Text(_isLogin
                    ? 'Don\'t have an account? Sign Up'
                    : 'Already have an account? Login'),
              ),
              const SizedBox(height: 20),
              Text(userProvider.message ?? "")
            ],
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In the AuthScreen class, we added functionality to toggle between login and sign-up forms using a boolean flag. For the sign-up process, we integrated an ImagePicker to allow users to select or change their profile picture, which is displayed in the UI. The authentication button now handles both login and sign-up actions, and upon successful authentication, navigates to the HomeScreen while clearing the navigation stack to prevent returning to the AuthScreen. Lastly, we included user feedback messages and utilized setState to update the UI based on user interactions and form inputs.

To access this screen, you need to update the _build widget in the lib/screens/home_screen.dart file to add navigation to the AuthScreen widget:

//...
import 'package:youtube_clone/screens/auth_screen.dart';

 //...
  @override
  Widget build(BuildContext context) {
    final userProvider = Provider.of<UserProvider>(context);
    return Scaffold(
      appBar: AppBar(
        title: Row(
          children: [
            Image.asset(
              'assets/images/YouTube_logo.png',
              width: 90,
            ),
            const Spacer(),
            IconButton(
              icon: const Icon(Icons.search, color: Colors.white),
              onPressed: () {
                showSearch(
                  context: context,
                  delegate: VideoSearchDelegate(),
                );
              },
            ),
            GestureDetector(
              child: userProvider.token != null
                  ? CircleAvatar(
                      backgroundImage: NetworkImage(
                        '${getBaseUrl()}${userProvider.user?['profile_picture']['url']}',
                      ),
                      radius: 20, 
                    )
                  : ElevatedButton(
                      onPressed: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => const AuthScreen()),
                        );
                      },
                      child: const Text('Sign in'),
                    ),
            ),
          ],
        ),
      ),
      body: Column(
        children: [
          // _buildFilterBar(),
          Expanded(
            child: Consumer<VideoProvider>(
              builder: (context, videoProvider, child) {
                if (videoProvider.videos.isEmpty) {
                  return const Center(child: CircularProgressIndicator());
                }

                return ListView.builder(
                  itemCount: videoProvider.videos.length,
                  itemBuilder: (context, index) {
                    final video = videoProvider.videos[index];
                    return _buildVideoTile(video);
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
 //...
Enter fullscreen mode Exit fullscreen mode

Now click on the Sign in button from the HomeScreen to navigate to AuthScreen. Create a new account or log in with your Strapi admin credentials.

Creating the Auth Screen .png

Creating the Profile Page

After successfully signin or signup to the application, a user should be able to access their profile, so they can add new videos to their channel and see their channel information. To handle that, create a new profile_screen.dart file in the lib/screens directory and add the code below:

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
import 'package:youtube_clone/providers/user_provider.dart';
import 'dart:io';
import 'package:youtube_clone/providers/video_provider.dart';
import 'package:youtube_clone/screens/home_screen.dart';
import 'package:youtube_clone/utils/getter.dart';

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key});

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

class _ProfileScreenState extends State<ProfileScreen> {
  final ImagePicker _picker = ImagePicker();
  XFile? _thumbnailFile;
  XFile? _videoFile;
  VideoPlayerController? _videoPlayerController;
  final _titleController = TextEditingController();
  final _descriptionController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    final userProvider = Provider.of<UserProvider>(context);

    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        title: Row(children: [
          const Spacer(),
          ElevatedButton(
            onPressed: () async {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => const HomeScreen()),
              );
              await userProvider.logout();
            },
            child: const Text('Logout'),
          )
        ]),
        backgroundColor: Colors.black,
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  CircleAvatar(
                    radius: 40,
                    backgroundImage: NetworkImage(
                      '${getBaseUrl()}${userProvider.user?['profile_picture']['url']}',
                    ),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(userProvider.user?['username'],
                            style: const TextStyle(
                                fontSize: 24,
                                color: Colors.white,
                                fontWeight: FontWeight.bold)),
                        Text('@${userProvider.user?['username']}',
                            style: const TextStyle(color: Colors.grey)),
                        Text(
                            '${userProvider.user?['subscribers']?.length ?? 0} subscribers • ${userProvider.user?['videos']?.length ?? 0} videos',
                            style: const TextStyle(color: Colors.grey)),
                      ],
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 16),
              Text(
                userProvider.user?['bio'] ?? '',
                style: TextStyle(color: Colors.white),
              ),
              SizedBox(height: 16),
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.grey[800],
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(18.0),
                        ),
                      ),
                      child: Text('Manage videos'),
                      onPressed: _showAddVideoModal,
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _showAddVideoModal() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (context) {
        final videoProvider =
            Provider.of<VideoProvider>(context, listen: false);
        final userProvider = Provider.of<UserProvider>(context);

        return StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return SingleChildScrollView(
              child: Container(
                padding: EdgeInsets.only(
                  bottom: MediaQuery.of(context).viewInsets.bottom,
                  left: 16,
                  right: 16,
                  top: 16,
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    TextField(
                      controller: _titleController,
                      decoration: InputDecoration(labelText: 'Title'),
                    ),
                    TextField(
                      controller: _descriptionController,
                      decoration: InputDecoration(labelText: 'Description'),
                      maxLines: 3,
                    ),
                    SizedBox(height: 16),
                    ElevatedButton(
                      onPressed: () async {
                        final XFile? image = await _picker.pickImage(
                            source: ImageSource.gallery);
                        setState(() {
                          _thumbnailFile = image;
                        });
                      },
                      child: Text('Pick Thumbnail Image'),
                    ),
                    SizedBox(height: 8),
                    _thumbnailFile != null
                        ? Image.file(File(_thumbnailFile!.path), height: 100)
                        : Container(),
                    SizedBox(height: 16),
                    ElevatedButton(
                      onPressed: () async {
                        final XFile? video = await _picker.pickVideo(
                            source: ImageSource.gallery);
                        setState(() {
                          _videoFile = video;
                          if (video != null) {
                            _videoPlayerController =
                                VideoPlayerController.file(File(video.path))
                                  ..initialize().then((_) {
                                    setState(() {});
                                  });
                          }
                        });
                      },
                      child: Text('Pick Video File'),
                    ),
                    SizedBox(height: 8),
                    _videoFile != null
                        ? AspectRatio(
                            aspectRatio:
                                _videoPlayerController?.value.aspectRatio ??
                                    16 / 9,
                            child: VideoPlayer(_videoPlayerController!),
                          )
                        : Container(),
                    SizedBox(height: 16),
                    ElevatedButton(
                      onPressed: () async {
                        if (_thumbnailFile != null && _videoFile != null) {
                          await videoProvider.uploadFile(
                              File(_thumbnailFile!.path),
                              File(_videoFile!.path),
                              _titleController.text,
                              _descriptionController.text,
                              userProvider.user?['documentId']);
                          Navigator.pop(context); // Close the modal
                        } else {
                          showDialog(
                            context: context,
                            builder: (BuildContext context) {
                              return AlertDialog(
                                title: const Text('Missing Files'),
                                content: const Text(
                                    'Please select both an image and a video.'),
                                actions: [
                                  TextButton(
                                    onPressed: () {
                                      Navigator.of(context).pop();
                                    },
                                    child: const Text('OK'),
                                  ),
                                ],
                              );
                            },
                          );
                        }
                      },
                      child: Text('Upload Video'),
                    ),
                  ],
                ),
              ),
            );
          },
        );
      },
    );
  }

  @override
  void dispose() {
    _videoPlayerController?.dispose();
    _titleController.dispose();
    _descriptionController.dispose();
    super.dispose();
  }
}
Enter fullscreen mode Exit fullscreen mode

In the provided ProfilePage code, we've created a user profile page with functionality to manage and upload videos. The profile page displays user information such as profile picture, username, subscriber count, and bio. It includes an option to manage videos, which opens a modal bottom sheet for uploading new videos. Within this modal, users can pick a thumbnail image and video file, preview them, and upload them through the VideoProvider. The code also handles the video playback preview using VideoPlayerController.

Update the _build widget in the lib/screens/home_screen.dart file to add navigation to the ProfileScreen widget:

//...
import 'package:youtube_clone/screens/profile_screen.dart';

  //...
  @override
  Widget build(BuildContext context) {
    final userProvider = Provider.of<UserProvider>(context);
    return Scaffold(
      appBar: AppBar(
        title: Row(
          children: [
            Image.asset(
              'assets/images/YouTube_logo.png',
              width: 90,
            ),
            const Spacer(),
            IconButton(
              icon: const Icon(Icons.search, color: Colors.white),
              onPressed: () {
                showSearch(
                  context: context,
                  delegate: VideoSearchDelegate(),
                );
              },
            ),
            GestureDetector(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => const ProfileScreen()),
                );
              },
              child: userProvider.token != null
                  // ignore: dead_code
                  ? CircleAvatar(
                      backgroundImage: NetworkImage(
                        '${getBaseUrl()}${userProvider.user?['profile_picture']['url']}',
                      ),
                      radius: 20, 
                    )
                  // ignore: dead_code
                  : ElevatedButton(
                      onPressed: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => const AuthScreen()),
                        );
                      },
                      child: const Text('Sign in'),
                    ),
            ),
          ],
        ),
      ),
      body: Column(
        children: [
          Expanded(
            child: Consumer<VideoProvider>(
              builder: (context, videoProvider, child) {
                if (videoProvider.videos.isEmpty) {
                  return const Center(child: CircularProgressIndicator());
                }

                return ListView.builder(
                  itemCount: videoProvider.videos.length,
                  itemBuilder: (context, index) {
                    final video = videoProvider.videos[index];
                    return _buildVideoTitle(video);
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
 //...
Enter fullscreen mode Exit fullscreen mode

Now once you sign up or sign in, you will be able to access the ProfileScreen from the HomeScreen widget.

Creating the Profile Page.png

Series Wrap Up

In this "Building a Youtube Clone with Strapi CMS and Flutter" blog series, here's a summary of what we learned:

  • How to set up the Strapi CMS backend with collections, create data relationships, create custom endpoints for liking, commenting, and viewing videos, set up Socket.io, and create lifecycle methods to listen to real-time updates on the collections
  • How to set up a new Flutter project, configure permissions, create the app services, and state management to handle real-time functionalities and UI updates.
  • How to build the front end with Flutter and consume the APIs to implement a functional YouTube clone application.

The complete code for this tutorial is available here on my Github repository. I hope you enjoyed this series. Happy coding!

Top comments (0)