Aplikasi Live Streaming Ceramah Menggunakan Flutter

Aplikasi Live Streaming Ceramah Menggunakan Flutter

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.

Category:
Share:

Comments