Updated 29 February 2024
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.
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> |
1 2 3 4 5 6 7 |
<manifest> <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> ... </manifest> |
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> |
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.
1 2 3 |
video_player: ^2.7.0 cached_network_image: ^3.2.3 |
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(), ), ); } } |
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(), ), ], ), ); } } |
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),), ), ], ), ), ), ), ], ); } } |
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.
Finally, Let’s check the app output.
Thanks for reading this Article. Feel free to share your ideas or any feedback.
If you have more details or questions, you can reply to the received confirmation email.
Back to Home
12 comments
Thanks for your comment.
We have added VideoPlayerScreen.dart file in the article.
Please check.
If you have any query or suggestion then please do let us know.
video_player: ^2.7.0
Do you give any advice?
Spanish:
llevo rato intentando hacer esto, he corrido todo tu ejercicio tal cual y todo muy bien!, pero cuando intento enviarla a la play store, me rechazan la app, y me dice… La arquitectura de la app o las opciones solicitadas no son compatibles con este dispositivo, cual podria ser la falla ? me ha pasado lo mismo con un app que hice , aprendi a usar el focus, etc, pero siempre me dice rechazada, podrias darme alguna orientacion por favor ?
There can be various reasons for the rejection of App on the PlayStore.
We recommend checking permissions in your manifest file.
Make sure that you are not using any permissions or feature that don’t support TV hardware.
please also check “android.software.leanback” it should be required=”true”.
If you want any customisable Flutter app, then feel free to reach our sales team at [email protected]
Thanks
may i know if you have build this only for the Android tv?
as if you only provide
then flutter app will not work.