Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
📦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.
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.
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
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});
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<VideoProvider>(context, listen: false).fetchVideos();
});
}
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> {
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
},
),
];
}
Widget buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
close(context, '');
},
);
}
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']),
),
);
},
);
},
);
}
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);
},
);
},
);
}
}
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//...
import 'package:youtube_clone/screens/home_screen.dart';
//...
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
//...
return MaterialApp(
//...
home: const HomeScreen(),
);
}
}
Create a new video_player_screen.dart
file in the lib/screens
folder and add the code below for the Video player screen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
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});
_VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}
class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
late VideoPlayerController _controller;
final TextEditingController _commentController = TextEditingController();
void initState() {
super.initState();
_initializeVideo();
}
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(() {});
}
});
}
}
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(() {});
}
}
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});
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: VideoPlayer(controller),
),
),
);
}
}
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//...
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']),
),
);
},
),
],
);
}
//...
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.
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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});
_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;
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 ?? "")
],
),
),
),
);
}
}
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//...
import 'package:youtube_clone/screens/auth_screen.dart';
//...
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);
},
);
},
),
),
],
),
);
}
//...
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.
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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});
_ProfileScreenState createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final ImagePicker _picker = ImagePicker();
XFile? _thumbnailFile;
XFile? _videoFile;
VideoPlayerController? _videoPlayerController;
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
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'),
),
],
),
),
);
},
);
},
);
}
void dispose() {
_videoPlayerController?.dispose();
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}
}
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//...
import 'package:youtube_clone/screens/profile_screen.dart';
//...
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);
},
);
},
),
),
],
),
);
}
//...
Now once you sign up or sign in, you will be able to access the ProfileScreen
from the HomeScreen
widget.
In this "Building a Youtube Clone with Strapi CMS and Flutter" blog series, here's a summary of what we learned:
The complete code for this tutorial is available here on my Github repository. I hope you enjoyed this series. Happy coding!