In this blog, we will learn how to create Smart TV app using Flutter. TV apps use the same architecture as those for phones and tablets. This approach means you can build new TV apps based on what you already know about building apps for Android, or extend your existing apps to also run on TV devices.
However, the interaction model for TV is substantially different. To make your app successful on TV devices you need to make some additional changes apart from phones and tablets.
We have already built an Android TV app for the CS-Cart platform where you can check the complete workflow as well.
Read more about our Flutter app development services.
Below Are the things we need to set for the TV app.
Add Leanback Support In the Manifest file
1 2 3 4 5 6 7 8 9 10 |
<manifest> <uses-feature android:name="android.software.leanback" android:required="true" /> ... </manifest> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> |
Set Touch screen False
1 2 3 4 5 6 7 |
<manifest> <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> ... </manifest> |
TV Banner
Android TV app used a Banner instead of a launcher icon.
We need to create a banner with a pixel size of 320×180.
Add to your drawable folder and add to your manifest file as shown below.
1 2 3 4 5 6 7 8 |
<application ... android:banner="@drawable/banner" > ... </application> |
Sample App
In this sample App, we will make a video player app.
We will use some static sample video URL to show the list of videos and play a video.
Add the Below dependencies to your pubspec.yaml
1 2 3 |
video_player: ^2.7.0 cached_network_image: ^3.2.3 |
main. dart
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 |
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'homescreen.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), }, child: MaterialApp( title: 'Webkul Tv App', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: const HomeScreen(), ), ); } } |
HomScreen.dart
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:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:webkul_tv_app/rawdata.dart'; import 'package:webkul_tv_app/videoplayerscreen.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @override State<HomeScreen> createState() => _HomeScreen(); } class _HomeScreen extends State<HomeScreen> with SingleTickerProviderStateMixin { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Webkul Tv App'),), body :SingleChildScrollView( child:sampleVideoGrid()), ) ); } Widget sampleVideoGrid() { return SingleChildScrollView( child: Column( children: [ GridView( shrinkWrap: true, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, ), children: images .map((url) => InkWell( onTap: () { Navigator.push( context, CupertinoPageRoute( builder: (context) => VideoPlayerScreen( url: links[images.indexOf(url)], ), ), ); }, child: Padding( padding: const EdgeInsets.all(8.0), child: Image.network( url, height: 150, width: 150, ), ), )) .toList(), ), ], ), ); } } |
VideoPlayerScreen.dart
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 |
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; class VideoPlayerScreen extends StatefulWidget { final String? url; VideoPlayerScreen({required this.url}); @override State<StatefulWidget> createState() =>_VideoPlayerScreen(); } class _VideoPlayerScreen extends State<VideoPlayerScreen> { late VideoPlayerController _controller; @override void initState() { _controller = VideoPlayerController.networkUrl(Uri.parse(widget.url??"")); _controller.addListener(() { setState(() {}); }); _controller.setLooping(true); _controller.initialize().then((_) => setState(() {})); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: SingleChildScrollView( child: Stack( alignment: Alignment.bottomCenter, children: [ Container(child: VideoPlayer(_controller),height:400,width: double.infinity,), _ControlsOverlay(controller: _controller), VideoProgressIndicator(_controller, allowScrubbing: true), ], ), ), ), ); } } class _ControlsOverlay extends StatelessWidget { const _ControlsOverlay({Key? key, required this.controller}) : super(key: key); static const _examplePlaybackRates = [ 0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0, ]; final VideoPlayerController controller; @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Container( color: Colors.black26, child: Padding( padding: const EdgeInsets.all(8.0), child: AnimatedSwitcher( duration: Duration(milliseconds: 50), reverseDuration: Duration(milliseconds: 200), child: Row( children: [ MaterialButton( onPressed: () async{ var position= await controller.position; controller.seekTo(Duration(seconds: position!.inSeconds-5)); }, child: Icon( Icons.arrow_back_ios, color: Colors.white, size: 20.0, ), ), SizedBox(width: 20,), controller.value.isPlaying ? MaterialButton( child: Icon( Icons.pause, color: Colors.white, size: 30.0, ), onPressed: () { controller.value.isPlaying ? controller.pause() : controller.play(); }, ) : MaterialButton( child: Icon( Icons.play_arrow, color: Colors.white, size: 30.0, ), onPressed: () { controller.value.isPlaying ? controller.pause() : controller.play(); }, ), SizedBox(width: 20,), MaterialButton( onPressed: () async{ var position= await controller.position; controller.seekTo(Duration(seconds: position!.inSeconds+5)); }, child: Icon( Icons.arrow_forward_ios, color: Colors.white, size: 20.0, ), ), SizedBox(width: 20,), PopupMenuButton( initialValue: controller.value.playbackSpeed, tooltip: 'Playback speed', color: Colors.white, onSelected: (speed) { controller.setPlaybackSpeed(speed); }, itemBuilder: (context) { return [ for (final speed in _examplePlaybackRates) PopupMenuItem( value: speed, child: Text('${speed}x',), ) ]; }, child: Text('${controller.value.playbackSpeed}x',style: TextStyle(color: Colors.white),), ), ], ), ), ), ), ], ); } } |
Rawdata.dart
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 |
List<String> images = [ "https://storage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ElephantsDream.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerBlazes.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerEscapes.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerFun.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerJoyrides.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerMeltdowns.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/images/Sintel.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/images/SubaruOutbackOnStreetAndDirt.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/images/TearsOfSteel.jpg" ]; List<String> links = [ "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", "https://storage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4", "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4", "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4", "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4", "https://storage.googleapis.com/gtv-videos-bucket/sample/Sintel.jpg", "https://storage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4", "https://storage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4" ]; |
Now our sample app is ready to run.
Conclusion :
Finally, Let’s check the app output.
Thanks for reading this Article. Feel free to share your ideas or any feedback.