How to build TikTok with Flutter?
In this tutorial series we'll clone TikTok using Flutter.

Along the way we'll learn how to use the core widgets of Flutter.
- Col
- Row
- Material
- Scaffold
- AppBar
- PageView
- Drawer
- BottomNavigationBar
- SafeArea
- ListView
- GestureDetector
We'll begin by implementing the navigation features of TikTok.
Specifically, we'll create a BottomNavigationBar. This navigation bar will allow the user to access different content on different screens.
Create New Project
Instantiate a new project and run it.
flutter create fluttok
cd fluttok
flutter run
We're using Flutter 3.0.2, Dart 2.17.3 • DevTools 2.12.2
Replace everything inside of main.dart
import 'package:flutter/material.dart';
import 'package:fluttok/navigation/DrawerNav.dart';
main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: DrawerNav(),
debugShowCheckedModeBanner: false,
);
}
}
We remove all the comments and do the following.
- Line 2 Import a custom widget,
DrawerNav. - Line 14 Pass the
DrawerNavwidget as a parameter toMaterialApp, specifically thehomeparameter. - Line 15 Hide the debug banner by passing
falseto thedebugShowCheckedModeBannerparameter ofMaterialApp.
Create Drawer Navigator
Structure/place project folders and files following best practice.
.
├── ...
├── pubspec.yaml
└── lib/
├── navigation/ # Create this folder
│ └── DrawerNav.dart # Create this file
└── main.dart
The path matches the import statement we used a moment ago, where lib is the name of our project, fluttok.
import 'package:fluttok/navigation/DrawerNav.dart';
Define DrawerNav as a stateful widget inside of DrawerNav.dart. We need it to be stateful so we can navigate between the multiple pages/screens using the BottomNavigationBar
// ./lib/navigation/DrawerNav.dart
import 'package:flutter/material.dart';
class TikTokPage extends StatefulWidget {
final MaterialColor color;
const TikTokPage({Key? key, required this.color}) : super(key: key);
@override
State<TikTokPage> createState() => _TikTokPageState();
}
class _TikTokPageState extends State<TikTokPage> {
@override
Widget build(BuildContext context) {
return Container(color: widget.color);
}
}
class DrawerNav extends StatefulWidget {
const DrawerNav({Key? key}) : super(key: key);
@override
State<DrawerNav> createState() => _DrawerNav();
}
class _DrawerNav extends State<DrawerNav> {
int _selectedIndex = 0;
static List<Widget> get _widgetOptions => <Widget>[
const TikTokPage(color: Colors.yellow),
const TikTokPage(color: Colors.blue),
const TikTokPage(color: Colors.green),
const TikTokPage(color: Colors.teal),
const TikTokPage(color: Colors.pink),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _widgetOptions.elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
elevation: 0,
onTap: _onItemTapped,
showUnselectedLabels: true,
currentIndex: _selectedIndex,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.black87,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'Home',
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: 'Discover',
icon: Icon(Icons.arrow_circle_up_sharp),
),
BottomNavigationBarItem(
label: '',
icon: Icon(Icons.add)
),
BottomNavigationBarItem(
label: 'Inbox',
icon: Icon(Icons.inbox),
),
BottomNavigationBarItem(
label: 'Profile',
icon: Icon(Icons.account_box_rounded),
),
],
),
);
}
}
Refresh and we'll our app works and has a bottom navigation bar, awesome.

There's a lot going on so let's review.
class TikTokPage extends StatefulWidget {
final MaterialColor color;
const TikTokPage({Key? key, required this.color}) : super(key: key);
@override
State<TikTokPage> createState() => _TikTokPageState();
}
class _TikTokPageState extends State<TikTokPage> {
@override
Widget build(BuildContext context) {
return Container(color: widget.color);
}
}
The TikTokPage widget is a placeholder. The interesting part of this widget is that it requires a parameter color when created.
- Line 2: Define
colorproperty for theTikTokPageclass/widget. - Line 4: Require the
colorparameter in the constructor of theTikTokPagewidget. - Line 2: Consume the
colorparameter in the build method ofTikTokPage, producing a dynamic background colors for each page/screen.
This pattern/technique is common with other JS frameworks such as React & Vue as well.
Create Bottom Navigator Bar
Key use of the Bottom Navigation Bar and it's required parameters/properties.
class _DrawerNav extends State<DrawerNav> {
int _selectedIndex = 0;
static List<Widget> get _widgetOptions => <Widget>[
const TikTokPage(color: Colors.yellow),
const TikTokPage(color: Colors.blue),
const TikTokPage(color: Colors.green),
const TikTokPage(color: Colors.teal),
const TikTokPage(color: Colors.pink),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _widgetOptions.elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
elevation: 0,
onTap: _onItemTapped,
showUnselectedLabels: true,
currentIndex: _selectedIndex,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.black87,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'Home',
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: 'Discover',
icon: Icon(Icons.arrow_circle_up_sharp),
),
BottomNavigationBarItem(
label: '',
icon: Icon(Icons.add)
),
BottomNavigationBarItem(
label: 'Inbox',
icon: Icon(Icons.inbox),
),
BottomNavigationBarItem(
label: 'Profile',
icon: Icon(Icons.account_box_rounded),
),
],
),
);
}
}
- Line 2: Define
_selectedIndexproperty which defaults to 0. This is which tab is selected. - Lines 4-10: Define array of page widgets which will be the pages/screens in the bottom navigation bar. Notice how each instance of TikTokPage is passed a different
colorparameter. - Lines 12-16: Define handler for when a tab bar item is tapped by the user.
- Lines 21: Select one page/screen inside of,
_widgetOptions, to pass to theScaffoldwidget'sbodyparameter. We use the_selectedIndexstate variable to identify which element/item/page/screen to pass. - Lines 24: Pass
_onItemTappedto the parameteronTapof `Scaffold. Once again this handles changing the page/screen viewed/focused. - Lines 29-50: Define the bottom navigation bar items including their label and icon.
Refactor TikTokPage
Let's extract the TikTokPage widget to it's own file so we can follow best practices.
.
├── ...
├── pubspec.yaml
└── lib/
├── navigation/
│ └── DrawerNav.dart
├── pages/ # Create this folder
│ └── TikTokPage.dart # Create this file
└── main.dart
Cut and paste the TikTokPage widget to the TikTokPage.dart file.
import 'package:flutter/material.dart';
class TikTokPage extends StatefulWidget {
final MaterialColor color;
const TikTokPage({Key? key, required this.color}) : super(key: key);
@override
State<TikTokPage> createState() => _TikTokPageState();
}
class _TikTokPageState extends State<TikTokPage> {
@override
Widget build(BuildContext context) {
return Container(color: widget.color);
}
}
Import the TikTokPage file into the DrawerNav.dart file
// ./lib/navigation/DrawerNav.dart
import 'package:flutter/material.dart';
import 'package:fluttok/pages/TikTokPage.dart';
Create Media Content Widget
Above the TikTokPage widget, create a new MediaContent widget which will contain the logic for each of our videos.
class MediaContent extends StatefulWidget {
const MediaContent({Key? key}) : super(key: key);
@override
State<MediaContent> createState() => _MediaContentState();
}
class _MediaContentState extends State<MediaContent> {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
margin: const EdgeInsets.all(5),
);
}
}
- Line 12: Give the screen a red background so we can more easily identify it.
- Line 13: Give the screen margin so we can more easily identify it.
Create Vertical Scroll through the list of videos
Refactor TikTokPage to have a PageView in it's build method. This widget implements vertical scroll with the use of a controller.
class TikTokPage extends StatefulWidget {
final MaterialColor color;
const TikTokPage({Key? key, required this.color}) : super(key: key);
@override
State<TikTokPage> createState() => _TikTokPageState();
}
class _TikTokPageState extends State<TikTokPage> {
PageController controller = PageController(initialPage: 0);
@override
Widget build(BuildContext context) {
return PageView(
controller: controller,
scrollDirection: Axis.vertical,
children: [
for (var i = 0; i < 5; i++) const MediaContent(),
],
);
}
}
-
Line 11: Instantiate a
PageControllerwith aninitialPageof 0 which we'll use to control thePageView. -
Line 16: Pass the
PageControllerto thecontrollerparameter ofPageView. -
Line 19: Use a loop to create several instances of
MediaContentfor testing.

We should now see that we can scroll vertically, excellent.