Pendahuluan
Perkembangan teknologi informasi yang begitu pesat, membuat berbagai macam kebiasaan mengalami pergeseran fungsi, salah satunya adalah kebiasaan menonton televisi berpindah ke media lainnya yang menawarkan lebih dari televisi. Kegiatan dakwah pun mengalami modernisasi dalam penyampaiannya, jika dahulu para ustadz hanya menghiasi mimbar-mimbar masjid sampai wara-wiri ditelevisi bagi da'i kondang, maka dengan bantuan teknologi saat ini, para ustadz sudah memiliki medianya masing-masing untuk menyapaikan dakwah kepada para pendengarnya.
Channel Youtube menjadi salah satu pilihan yang banyak digandrungi, mulai dari para anak alay, politisi, akademisi hingga pendakwah. Selain terdapat media untuk menyiarkan video rekeman juga terdapat media yang dapat menyiarkan video yang diambil secara langsung layaknya live yang ada ditelevisi.
Artikel ini akan membahas bagaimana membuat aplikasi yang dapat menghimpun semua video live streaming ceramah menggunakan Flutter. Beberapa fitur diantaranya: Playlist video yang sedang live, embed video Youtube, pencarian video berdasarkan nama kata kunci, membuat widget positioning, dan lain sebagainya.
Baca Juga: Aplikasi Qur'an Digital & Play Audio Menggunakan Flutter
Install Flutter & Library
Seperti biasa, kita akan memulai dengan Flutter fresh install, maka pada command line jalankan command untuk mengunduh Flutter
flutter create daengweb_ceramah
Buka project yang baru saja diunduh, kemudian saatnya kita untuk meng-install library yang dibutuhkan. Dari file pubspec.yaml
, tambahkan code berikut tepat di dalam dependencies
http: ^0.12.0+2
provider: ^3.1.0
flutter_webview_plugin: ^0.3.8
Note: http
adalah library untuk http request, provider
adalah library untuk state management, dan flutter_webview_plugin
adalah library untuk meng-embed web kedalam aplikasi mobile dimana dalam hal ini kita akan meng-embed video Youtube.
Setelah di-save, biasanya proses instalasi akan berjalan secara otomatis. Akan tetapi kita juga bisa melakukannya secara manual dengan command flutter pub get
.
Folder & File Structure
Agar memudahkan dalam penulisan, maka kita akan membuat struktur file dan folder dari project ini. Maksudnya adalah semua file yang dibutuhkan akan kita buat dan ditempatkan didalam foldernya masing-masing, adapun codingan-nya akan dimasukkan saat dibutuhkan.
Pertama, buat folder models
, screens
dan widgets
di dalam folder lib
.
Kemudian buat file video_model.dart
didalam folder models
, file video_detail.dart
dan video_list.dart
didalam folder screens
dan yang terakhir adalah file video_item.dart
didalam folder widgets
.
How To Get Youtube API Token
Youtube telah menyediakan API untuk mengambil content yang ada didalamnya, maka tentu saja sebelum menggunakan API dari Youtube, kita harus membuat account dan mengaktifkan library Youtube v3 serta men-generate token yang berfungsi sebagai sarana otentikasi.
Buka halaman console.developers.google.com, kemudian pada menu navigasi pilih select project untuk menampilkan modal list project yang kamu miliki. Dari modal tersebut, klik tombol New Project.
Masukkan nama project yang kamu inginkan dan tekan tombol Create.
Kemudian dari sidebar menu, pilih API & Services > Library
Dari kotak pencarian, masukkan keyword Youtube dan pilih Youtube Data API V3. Selanjutnya klik Enable untuk mengaktifkan.
Setelah library tersebut diaktifkan, dari sidebar menu pilih Credentials dan buat token dengan menekan tombol Create Credentials > API Key.
Google akan men-generate API key dari Youtube dan menampilkannya setelah proses tersebut selesai (red: beberapa detik). Simpan API key yang telah di-generate karena nantinya akan digunakan saat proses request data video.
Membuat Playlist Video Live Streaming
Hal pertama yang akan kita lakukan adalah menampilkan list video dari ustadz yang sedang melakukan live streaming. Buka file main.dart
dan modifikasi menjadi:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './models/video_model.dart'; //IMPORT MODEL PROVIDER YANG BERFUNGSI SEBAGAI STATE MANAGEMENT
import './screens/video_list.dart'; //SCREEN INI UNTUK MENAMPILKAN LIST VIDEO
import './screens/video_detail.dart'; //SCREEN INI UNTUK MENJALANKAN VIDEO YANG DIPILIH (TAPI AKAN DIKERJAKAN PADA BAGIAN AKHIR ARTIKEL)
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => VideoProvider(),) //DEFINISIKAN STATE MANAGEMENT AGAR DAPAT DIGUNAKAN DISEMUA SCREEN / PAGE
],
child: MaterialApp(
title: 'DaengWeb.id',
theme: ThemeData(
primarySwatch: Colors.pink,
),
home: VideoList(), //SCREEN PERTAMA YANG DILOAD KETIKA APLIKASI DIJALANKAN ADALAH LIST VIDEO
routes: {
'/detail': (ctx) => VideoDetail() //DEFINISIKAN ROUTING UNTUK MELIHAT DETAIL VIDEO
},
),
);
}
}
Tahap selanjutnya adalah dengan membuat code untuk meng-handle data state serta method yang akan meng-handle request data ke API Youtube. Buka file models/video_model.dart
dan masukkan code berikut.
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; //IMPORT HTTP LIBRARY
import 'dart:convert';
//CLASS INI UNTUK MENDEFINISIKAN SEPERTI APA FORMAT DATA YANG DIINGINKAN
class VideoModel {
//DEFINISIKAN VARIABLE APA SAJA YANG DIBUTUHKAN
final String videoId;
final String title;
final String channelId;
final String channelTitle;
final String image;
//BUAT CONSTRUCTOR, AGAR KETIKA CLASS INI DIGUNAKAN MAKA WAJIB MENGIRIMKAN DATA YANG DIMINTA DI CONSTRUCTOR
VideoModel({
@required this.videoId,
@required this.title,
@required this.channelId,
@required this.channelTitle,
@required this.image,
});
//FORMATTING DATA MENJADI FORMAT YANG DIINGINKAN
//MENGGUNAKAN METHOD fromJson, DIMANA EXPECT DATANYA ADALAH MAP DENGAN KEY STRING DAN VALUE DYNAMIC
factory VideoModel.fromJson(Map<String, dynamic> json) {
//UBAH FORMAT DATANYA SESUAI FORMAT YANG DIMINTA PADA CONSTRUCTOR
return VideoModel(
videoId: json['id']['videoId'],
title: json['snippet']['title'],
channelId: json['snippet']['channelId'],
channelTitle: json['snippet']['channelTitle'],
image: json['snippet']['thumbnails']['high']['url'],
);
}
}
//CLASS INI SEBAGAI STATE MANAGEMENT
class VideoProvider with ChangeNotifier {
List<VideoModel> _items = []; //DEFINISIKAN VARIABLE UNTUK MENAMPUNG DATA VIDEO
//KARENA VARIABLE DIATAS ADA PRIVATE, MAKA KITA BUAT GETTER AGAR DAPAT DIAKSES DARI LUAR CLASS INI
List<VideoModel> get items {
return [..._items];
}
//BUAT FUNGSI UNTUK MENGAMBIL DATA DARI API YOUTUBE
Future<void> getVideo(String requestKeyword) async {
final keyword = 'ustadz ' + requestKeyword; //DIMANA KEYWORDNYA MENGGUNAKAN PREFIX USTADZ SEHINGGA HANYA AKAN MENGAMBIL DATA YANG TERKAIT DENGAN USTADZ
final apiToken = 'API TOKEN ANDA'; //MASUKKAN API TOKEN YANG KAMU DAPATKAN DARI PROSES SEBELUMNYA
//ENDPOINT API YOUTUBE UNTUK MENGAMBIL VIDEO-NYA BERDASARKAN PENCARIAN DAN EVENTYPE = LIVE
final url =
'https://www.googleapis.com/youtube/v3/search?part=snippet&eventType=live&relevanceLanguage=id&maxResults=25&q=$keyword&type=video&key=$apiToken';
final response = await http.get(url); //KIRIM REQUEST
final extractData = json.decode(response.body)['items']; //DECODE JSON YANG DITERIMA
//JIKA DIA NULL, MAKA HENTIKAN PROSESNYA
if (extractData == null) {
return;
}
//JIKA TIDAK MAKA ASSIGN DATA NYA KE DALAM VARIABLE _items
//DENGAN FORMAT DATA MENGGUNAKAN fromJson()
_items =
extractData.map<VideoModel>((i) => VideoModel.fromJson(i)).toList();
notifyListeners(); //INFORMASIKAN JIKA TERJADI PERUBAHAN DATA
}
//METHOD UNTUK MENGAMBIL DATA VIDEO BERDASARKAN VIDEOID, METHOD INI DIGUNAKAN PADA SCREEN DETAIL NANTINYA
VideoModel findVideo(String videoId) {
return _items.firstWhere((q) => q.videoId == videoId);
}
}
Tugas kita selanjutnya adalah menampilkan data yang diperoleh dari API Youtube, maka buka file /screens/video_list.dart
dan ketikkan code berikut
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../widgets/video_item.dart'; //IMPORT WIDGET VideoItem (WIGET INI UNTUK MENAMPILKAN LIST VIDEO)
import '../models/video_model.dart'; //IMPORT MODEL YANG DIBUAT SEBELUMNYA
class VideoList extends StatefulWidget {
@override
_VideoListState createState() => _VideoListState();
}
class _VideoListState extends State<VideoList> {
bool loading = false; //VARIABLE YANG AKAN DIGUNAKAN UNTUK MENAMPILKAN LOADING ATAU TIDAK
bool isSearch = false; //VARIABLE YG AKAN DIGUNAKAN KETIKA ICON CARI/CANCEL DITEKAN PADA APPBAR
TextEditingController _searchController = TextEditingController(); //CONTROLLER YANG AKAN MENG-HANDLE TEXTFIELD PENCARIAN
//KETIKA SCREEN INI DI-LOAD
@override
void initState() {
//MAKA KITA TUNDA 0 SECOND AGAR PROVIDER BISA DIGUNAKAN
Future.delayed(Duration.zero).then((_) {
//SET LOADING JADI TRUE UNTUK MENAMPILKAN LOADING INDICATOR
setState(() {
loading = true;
});
//JALANKAN METHOD getVideo() DARI MODEL VIDEOPROVIDER DAN KIRIMKAN PARAMETER STRING DARI VALUE CONTROLLER TEXTFIELD PENCARIAN
Provider.of<VideoProvider>(context, listen: false).getVideo(_searchController.text).then((_) {
//APABILA BERHASIL, SET LOADING JADI FALSE
setState(() {
loading = false;
});
});
});
super.initState();
}
//BUAT PRIVATE METHOD YANG AKAN DIGUNAKAN UNTUK PENCARIAN DAN PULL TO REFRESH (KETIKA HALAMAN DITARIK KEBAWAH
//ADAPUN PENJELASAN YANG ADA DI DALAMNYA SAMA SAJA
Future<void> _refreshData() async {
setState(() {
loading = true;
});
await Provider.of<VideoProvider>(context, listen: false)
.getVideo(_searchController.text)
.then((_) {
setState(() {
loading = false;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
//JIKA ISSEARCH TRUE MAKA TEXTFIELD DITAMPILKAN
title: isSearch ? TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: 'Cari...',
),
onSubmitted: (_) {
//KETIKA DISUBMIT MAKA AKAN MENJALANKAN FUNGSI _refreshData()
_refreshData();
//DAN FOCUS TEXTFIELDNYA DIHILANGKAN
FocusScope.of(context).unfocus();
},
//SAMA SAJA FUNGSINYA HANYA BEDA EVENT
onEditingComplete: () {
_refreshData();
FocusScope.of(context).unfocus();
},
) : Text('DW Live Streaming'), //JIKA ISSEARCH FALSE MAKA TEXT INI AKAN DITAMPILKAN
actions: <Widget>[
IconButton(
//JIKA ISSEARCH TRUE MAKA ICON CANCEL DITAMPILKAN, SELAIN ITU ICON SEARCH
icon: Icon(isSearch ? Icons.cancel:Icons.search),
onPressed: () {
setState(() {
isSearch = !isSearch; //KETIKA ICONNYA DITEKAN, MAKA VALUE ISSEARCH DIUBAH
_searchController.clear(); //VALUE DARI TEXTFIELD DIHAPUS
});
},
)
],
),
//JIKA LOADING TRUE
body: loading
? Center(
//MAKA PROGRESS INDICATOR AKAN DIRENDER
child: CircularProgressIndicator(),
)
//SELAIN ITU AKAN MENAMPILKAN DATA VIDEO
: Container(
padding: EdgeInsets.symmetric(horizontal: 1, vertical: 2),
//REFRESH INDICATOR AKAN BERJALAN KETIKA PADA POSISI PALING ATAS SCREEN KEMUDIAN DITARIK UNTUK ME-REFRESH DATA
child: RefreshIndicator(
//KETIKA PROSES TERSEBUT BERLANGSUNG, MAKA METHOD _refreshData DIJALANKAN
onRefresh: _refreshData,
//CONSUMER BERFUNGSI UNTUK MENGAMBIL DATA DARI STATE, SEHINGGA HANYA WIDGET YANG DIAPITNYA SAJA YANG DI-RENDER
child: Consumer<VideoProvider>(
//LOOPING DATANYA MENGGUNAKAN LISTVIEW BUILDER
builder: (ctx, data, _) => ListView.builder(
//LOOPING NYA AKAN DILAKUKAN SEBANYAK JUMLAH DATANYA
itemCount: data.items.length,
//TAMPILAN DATANYA AKAN DI-HANDLE OLEH FILE video_item.dart
//MAKA DATA YANG PERLU DITAMPILKAN AKAN DKIRIMKAN KE FILE TERSEBUT
itemBuilder: (ctx, i) => VideoItem(
data.items[i].videoId,
data.items[i].title,
data.items[i].channelTitle,
data.items[i].image,
),
),
),
),
),
);
}
}
Bagian penutup dari sub-heading ini adalah melengkapi code untuk menampilkan masing-masing informasi video. Buka file video_item.dart
dan tambahkan code:
import 'package:flutter/material.dart';
class VideoItem extends StatelessWidget {
//DEFINISIKAN VARIABLE APA SAJA YANG DIBUTUHKAN
final String videoId;
final String title;
final String image;
final String channelTitle;
//BUAT CONSTRUCTOR UNTUK MENERIMA / MEMINTA DATA DARI YANG MENGGUNAKAN CLASS INI
VideoItem(this.videoId, this.title, this.channelTitle, this.image);
//METHOD YANG AKAN MENG-HANDLE KETIKA CARD DITAP
void _detailScreen(BuildContext context) {
//FUNGSI UNTUK BERPINDAH KE SCREEN /DETAIL DAN MENGIRIMKAN VIDEOID SEBAGAI PARAMETER
Navigator.of(context).pushNamed('/detail', arguments: videoId);
}
@override
Widget build(BuildContext context) {
//MENGGUNAKAN WIDGET INKWEEL UNTUK MENDETEKSI ONTAP
return InkWell(
onTap: () => _detailScreen(context), //ONTAPNYA MENJALANKAN METHOD DIATAS
child: Card(
//SET SHAPE CARDNYA MENGGUNAKAN BORDER RADIUS AGAR SEDIKIT MELENGKUNG
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 4, //BUAT SHADOWNYA 4
margin: EdgeInsets.all(10),
child: Column(
children: <Widget>[
//CHILD CARD YANG PERTAMA KITA GUNAKAN STACK AGAR DAPAT MENGATUR POSISI YANG DIINGINKAN DARI CHILDREN YANG DIMILIKINYA
Stack(
children: <Widget>[
//UNTUK MEMBUAT RECTANGLE DENGAN SETIAP SISINYA DI CUSTOM MENGGUNAKAN BORDER RADIUS
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
//CHILDNYA KITA TAMPILKAN IMAGE DARI VIDEO TERKAIT (THUMBNAIL)
child: Image.network(
image, //IMAGE URLNYA KITA DAPATKAN DARI VARIABLE IMAGE
height: 200,
width: double.infinity, //WIDTHNYA DISET SELUAS MUNGKIN YG BISA DIJANGKAU
fit: BoxFit.cover,
),
),
//WIDGET INI DIGUNAKAN UNTUK MENGATUR POSISI DARI WIDGET YANG DIAPIT OLEHNYA
Positioned(
bottom: 20, //MISALNYA KITA SET 20 DARI BAWAH STACK
right: 15, //DAN DARI KANAN SEBESAR 15
//ADAPUN WIDGET YANG DI-SET POSISINYA ADALAH SEBUAH CONTAINER
child: Container(
//YANG MEMILIKI LEBAR SEBESAR 300
width: 300,
color: Colors.black54, //DAN WARNA HITAM AGAK TRANSPARAN
padding: EdgeInsets.symmetric(
vertical: 5,
horizontal: 20,
),
//DAN ISI DARI CONTAINER ITU ADALAH SEBUAH TEXT UNTUK MENAMPILKAN JUDUL VIDEO
child: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white),
softWrap: true,
overflow: TextOverflow.fade,
),
),
)
],
),
//CHILD CARD YANG KEDUA ADALAH UNTUK MENAMPILKAN NAMA CHANNEL DARI VIDEO TERKAIT
Padding(
padding: EdgeInsets.all(10),
//KITA BUAT JADI ROW
child: Row(
children: <Widget>[
//POSISI KIRI MENAMPILKAN ICON
IconTheme(data: IconThemeData(color: Colors.blueGrey), child: Icon(Icons.shop_two),),
SizedBox(
width: 4,
),
//DAN SETLAHNYA ADALAH NAMA CHANNEL
Expanded(child: Text('Channel: $channelTitle', style: TextStyle(color: Colors.blueGrey),))
],
),
)
],
),
),
);
}
}
Embed Video From Youtube
Code terakhir dari widget diatas akan ketika di-tap akan mengarahkan kita ke route /detail
, dimana route tersebut merujuk pada file video_detail.dart
. Buka file tersebut dan masukkan code berikut untuk meng-embed video berdasarkan VideoId
yang diterima melalui argument yang dikirimkan route sebelumnya.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; //LOAD LIBRARY WEBVIEW UNTUK MENG-EMBED VIDEO
import '../models/video_model.dart'; //IMPORT MODEL PROVIDER
class VideoDetail extends StatelessWidget {
@override
Widget build(BuildContext context) {
final id = ModalRoute.of(context).settings.arguments as String; //TERIMA ARGUMEN YANG DIKIRIMKAN
final data =
Provider.of<VideoProvider>(context, listen: false).findVideo(id); //CARI VIDEO TERKAIT PADA VARIABLE _ITEMS MENGGUNAKAN FUNGSI findVideo() DARI MODEL YANG KITA PUNYA
return Center(
//GUNAKAN WIDGET WEBVIEW
child: WebviewScaffold(
//DAN EMBED VIDEONYA, DIMANA ID VIDEO BERDASARKAN DATA DARI HASIL PENCARIAN
//SEBENARNYA KITA BISA LANGSUNG MENGGUNAKAN ID DARI ARGUMEN, TAPI BIAR LEBIH PANJANG CODENYA JADI KITA GUNAKAN PENCARIAN HAHAHA
url: "https://www.youtube.com/embed/${data.videoId}",
),
);
}
}
Note: Ketika screen dimiringkan maka akan full screen.
Baca Juga: Aplikasi Qur'an Digital & Play Audio Menggunakan Flutter
Baca Juga: Aplikasi Tracking Resi Ekspedisi Flutter
Kesimpulan
Teknik yang digunakan hampir serupa dengan artikel sebelumnya, meskipun ada beberapa bagian yang baru diangkat seperti webview, refresh indicator, positioning widget, dan lain sebagainya. Tujuan dari artikel ini adalah membiasakan diri untuk menggunakan widget dari Flutter dengan menyelesaikan sebuah case sederhana.
Adapun dokumentasi code dari artikel ini bisa dilihat di Github.
Comments