Flutter Phone Authentication and System Read User Data With Provider


Bismillah, kali ini saya akan berbagi bagaimana best practice cara membaca data user yang telah melakukan authentication pada aplikasi yang di bangun menggunakan Flutter dengan State Management Provider. Sebelum ke pembahasan lebih lanjut ada beberapa hal yang harus di pahami terlebih dahulu sebagai berikut:

State Management

Pada saat membangun aplikasi dengan Flutter kita tentu tidak asing lagi dengan yang namanya state. Lalu apa dan bagaimana sih penjelasan yang mendetail tentang state ini?

Definisi State Management

State Management atau dalam bahasa indonesia Manajemen Negara mengacu pada manajemen keadaan satu atau lebih kontrol antarmuka pengguna seperti bidang teks, tombol OK, tombol radio, dan lain-lain. Dalam antarmuka pengguna grafis. Dalam teknik pemrograman antarmuka pengguna ini, keadaan satu kontrol UI tergantung pada keadaan kontrol UI lainnya. 

Di Flutter sendiri sebenarnya sudah memiliki management state bawaannya yaitu dengan menggunakan setState() yang biasa digunakan pada class Statefull Widget.

Cara kerjanya yaitu state akan mengambil semua data yang diperlukan untuk membangun User Interface (UI) pada waktu tertentu dan dijalankan di memori perangkat baik itu assets, variable, font, dan yang lainnya.

Secara konsep state terbagi menjadi local state dan app state.

Local State

Sebagai contoh local state kita bisa lihat pada kode berikut,

 class _MyHomepageState extends State<MyHomepage> {  
  int _index = 0;  
  @override  
  Widget build(BuildContext context) {  
   return BottomNavigationBar(  
    currentIndex: _index,  
    onTap: (newIndex) {  
     setState(() {  
      _index = newIndex;  
     });  
    },  
    // ... items ...  
   );  
  }  
 }  

Variabel _index merupakan local variable atau juga bisa disebut dengan local state karena bersifat private sehingga tidak bisa diakses di kelas lain. Salah satu cara yang umum untuk memperbarui variabel tersebut kita dapat menggunakan cara setState() di dalam kelas yang berisi variabel tersebut.

App State

Pada beberapa kasus tertentu membutuhkan state yang dapat berjalan di seluruh layar aplikasi atau juga bisa disebut dengan app state. Berdasarkan dokumentasi Flutter untuk mengimplementasikannya terdapat beberapa cara yaitu InheritedWidget, InheritedModel, Redux, Fish-Redux, BLoC, GetIt, MobX, Binder, GetX, Riverpod, dan Provider.

Provider

Provider ini sendiri merupakan salah satu managemen state yang mana isinya adalah sebuah design coding yang mana kita memisahkan antara logic dan view kita bertujuan agar logic tersebut juga bersifat re-usable. 

Kalau kita sebelumnya sudah mengenal java atau kotlin maka ini sama saja seperti design pattern yang mana ada MVP dan MVVM jika di web maka kita mengenal MVC.

Bagaimana sampai sini semoga sudah bisa memahami apa itu state management ya, mari kita lanjut!

Firebase Authentication

Dalam membangun sebuah aplikasi biasanya terdapat authentication yang mana authentication ini berguna untuk memastikan user yang masuk aplikasi adalah user yang sebenarnya memiliki identitas. Proses ini memastikan supaya kalau ada yang mengaku sebagai orang lain bisa terbaca sebagai orang lain.

Setelah melakukan authentication pada sebuah aplikasi akan membutuhkan sistem untuk mengecek dan membacanya.

Pada kasus kali ini saya akan mencontohkan sistem authentication dengan OTP dan pembacaan data user secara global nantinya agar bisa di panggil di mana saja pada aplikasi.

Siapkan segelas kopi dan cemilan secukupnya, mari kita lanjut ke praktik!

Persiapan Flutter Project

Pertama-tama buka terminal dan posisikan ke dalam folder yang anda inginkan, contoh saya akan menyimpan project ini ke dalam folder Documents/Projects/FlutterProject/Research/.

Lalu buatlah project Flutter baru dengan menggunakan printah

 flutter create --org my.id.sendiagustian snippetauth

Jika selesai akan tampil output seperti berikut,

Buka folder project dengan Text Editor atau IDE favorite anda.

Persiapan Firebase Project

Kita perlu menyiapkan Firebase Project kita yang nantinya kita hubungkan ke aplikasi yang kita buat.

  • Buka dan buat project Firebase melalui halaman website officialnya disini firebase.google.com.
  • Selanjutnya klik Get started tombol seperti pada gambar berikut,



  • Lalu anda akan di larikan ke halaman console.firebase.google.com di sini anda tambahkan project firebasenya.



  • Berikutnya, masukkan nama Proyek & pilih lokasi Analytics. Lokasi analitik mewakili negara/wilayah organisasi Anda dan menetapkan mata uang untuk pelaporan pendapatan. Buatkan dan persiapkan firebase project lalu tambahkan aplikasi ikutilah langkah-langkah yang telah di intruksikan. 



  • Setelah proyek berhasil dibuat, Firebase akan menampilkan prompt yang mengatakan Your new project is ready. Klik Lanjutkan untuk menyelesaikan alur.



  • Sampai sini project Firebase kita telah siap

Konfigurasi aplikasi dengan Firebase

Firebase ini bisa di konfigurasikan dengan aplikasi Android, Ios, dan WEB. Untuk kali ini saya hanya akan konfigurasikan dengan aplikasi android.

Untuk konfigurasinya ikuti langkah-langkah berikut,

  • Di bagian Add an app to get started, klik ikon android untuk menambahkan aplikasi Android ke Firebase.



  • Pada layar berikutnya, masukkan nama paket Android & Nama panggilan aplikasi Anda dan klik Daftar Aplikasi. Nama paket Anda biasanya ada applicationId di build.gradle file tingkat aplikasi Anda. Jika ditentukan, nama panggilan aplikasi akan digunakan di seluruh konsol Firebase untuk mewakili aplikasi ini. Nama panggilan tidak terlihat oleh pengguna.

    Untuk persiapan kita juga siapkan sertifikat SHA-1 agar proses debug authentication bisa dijalankan, karena untuk authentication ini memerlukan sertifikat SHA-1, untuk menyiapkan ini kinjungi client auth lalu masukan.



  • Unduh google-services.json file & letakkan di app direktori root proyek Anda. Pastikan nama file konfigurasi tidak ditambahkan dengan karakter tambahan, seperti (2).



  • Buka folder android file build.gradle lalu tambahkan baris berikut,
     buildscript {  
      repositories {   
       google() // Google's Maven repository  
      }  
      dependencies {  
       ...  
       // Add this line  
       classpath 'com.google.gms:google-services:4.3.10'  
      }  
     }  
    
  • Buka folder android/app file build.gradle lalu tambahkan baris berikut di paling bawah
  •  apply plugin: 'com.google.gms.google-services'  
    
  • masih di folder android/app file build.gradle ubah minSdkVersion menjadi 23 seperti berikut

     defaultConfig {  
         applicationId "my.id.sendiagustian.snippetauth"  
         minSdkVersion 23  
         targetSdkVersion 30  
         versionCode flutterVersionCode.toInteger()  
         versionName flutterVersionName  
       }  
    
  • Berikutnya adalah menambahkan depedencies-depedencies di file pubspac.yaml berikut
  •  dependencies:  
      flutter:  
       sdk: flutter  
      cupertino_icons: ^1.0.2  
      firebase_auth: 3.1.4  
      firebase_core: 1.8.0  
      cloud_firestore: 2.5.4  
      provider: 6.0.1  
      country_list_pick: 1.0.1+5  
  • Sekarang buka folder lib file main.dart ubah fungsi main menjadi seperti berikut
     Future<void> main() async {  
      WidgetsFlutterBinding.ensureInitialized();  
      await Firebase.initializeApp();  
      runApp(const MyApp());  
     }  
    
  • Mantap sampai sini kita sudah mengkonfigurasi aplikasi dengan Firebase!

Mempersiapkan Struktur Folder Project

Sebelum melakukan code lebih jauh alangkah baiknya kita mempersiapkan dulu struktur project yang akan kita pakai seperti apa agai nantinya file-file code kita tidak berantakan. Yang paling unum dan sederhana kita akan memisahkan code-code untuk UI dan Logic nantinya.

Untuk struktur folder project yang akan saya praktekkan nanti kurang lebih akan ada databases, models, screens dan widgets.

Oke, disini kita siap melakukan code lebih lanjut lagi.

Menyiapkan Widget 

Pertama-tama kita akan meniapkan dulu Widget yang kemungkinan akan terus menerus dipanggil.

Buat file app_widget.dart di folder widgets lalu tuliskan kode berikut,

 import 'package:flutter/material.dart';  

 class AppWidget {  
  static void loadingPageIndicator({  
   required BuildContext context,  
  }) {  
   showGeneralDialog(  
    context: context,  
    barrierDismissible: true,  
    transitionDuration: const Duration(milliseconds: 100),  
    barrierLabel: '',  
    barrierColor: Colors.black.withOpacity(0.6),  
    pageBuilder: (context, animation1, animation2) {  
     return Container();  
    },  
    transitionBuilder: (BuildContext context, a1, a2, widget) {  
     return Transform.scale(  
      scale: a1.value,  
      child: Opacity(  
       opacity: a1.value,  
       child: WillPopScope(  
        onWillPop: () => Future.value(false),  
        child: const Center(  
         child: SizedBox(  
          width: 35,  
          height: 35,  
          child: CircularProgressIndicator(),  
         ),  
        ),  
       ),  
      ),  
     );  
    },  
   );  
  }  

  // Untuk manampilkan snackbar  
  static ScaffoldFeatureController showSnackBar({  
   required BuildContext context,  
   required Widget content,  
   required Duration duration,  
  }) {  
   return ScaffoldMessenger.of(context).showSnackBar(  
    SnackBar(  
     behavior: SnackBarBehavior.floating,  
     content: content,  
     duration: duration,  
    ),  
   );  
  }  
 }  

kode di atas adalah untuk membuat kelas AppWidget yang di dalamnya terdapat static void loadingPageIndicator() dan static ScaffoldFeatureController showSnackBar(),  kelas AppWidget ini nantinya akan sering kita panggil.

static void loadingPageIndicator() ini berfungsi untuk menunggu proses fungsi yang berjalan selesai ketika mulai dijalankan nantinya.

Sedangkan static ScaffoldFeatureController showSnackBar() ini berfungsi untuk menampilkan SnackBar yang biasa digunakan untuk menampilkan info kesalahan atau lainnya.

Menyiapkan UI Screen

Buka main.dart lalu tambahkan kode berikut,

 import 'package:firebase_core/firebase_core.dart';  
 import 'package:flutter/material.dart';  
 import 'package:provider/provider.dart';  
 import 'package:snippetphoneauth/screens/auth/auth_checker.dart';  
  
 ...

 class MyApp extends StatelessWidget {  
  const MyApp({Key? key}) : super(key: key);  

  @override  
  Widget build(BuildContext context) {  
   return MaterialApp(  
    debugShowCheckedModeBanner: false,  
    title: 'Snippet Phone Auth',  
    theme: ThemeData(  
     primarySwatch: Colors.blue,  
    ),  
    home: const AuthChecker(),  
   );  
  }  
 }  

di sini kita belum membuat kelas AuthChecker() nya mari kita buat juga, buat folder auth di dalam folder screens dan buat file auth_checker.dart lalu ketikan kode berikut,

 import 'package:flutter/material.dart';  
 import 'package:provider/provider.dart';  
 import 'package:snippetphoneauth/screens/auth/login.dart';  
 import 'package:snippetphoneauth/screens/auth/providers/auth_provider.dart';  
 import 'package:snippetphoneauth/screens/home.dart';  
 
 // Auth status pengecekan  
 class AuthChecker extends StatelessWidget {  
  const AuthChecker({Key? key}) : super(key: key);  

  @override  
  Widget build(BuildContext context) {  
   return Consumer(builder: (_, AuthProvider authProvider, __) {  
    // Pengecekan variable auth status untuk menentukan route ke halaman  
    if (authProvider.auth.currentUser != null) {  
     return  const HomeScreen();  
} else { // masuk ke halaman login jika user auth tidak terisi return const LoginScreen(); } }); } }

Kita juga perlu menyiapkan kelas AuthProvider(), di dalam folder auth buat folder providers dan buat file auth_provider.dart ketikkan kode di bawah ini,

 import 'package:firebase_auth/firebase_auth.dart';  
 
 class AuthProvider with ChangeNotifier {  
  // Pendekralasian/inisiasi object auth firebase  
  FirebaseAuth auth = FirebaseAuth.instance;  
 }  

Siapkan juga kelas HomeScreen() di folder screens buat file home.dart ketikan kode di bawah ini

 import 'package:flutter/material.dart';  

 class HomeScreen extends StatelessWidget {  
  const HomeScreen({Key? key}) : super(key: key);  

  @override  
  Widget build(BuildContext context) {  
   return Scaffold(  
    appBar: AppBar(  
     title: const Text('Snippet Phone Auth'),  
     actions: [  
      IconButton(  
       onPressed: () {  
        //TODO  
       },  
       icon: const Icon(  
        Icons.logout,  
       ),  
      ),  
     ],  
    ),  
    body: const Center(child: Text('Home Screen')),  
   );  
  }  
 }  

Sekarang kita buat kelas LoginScreen() di folder auth buat file login.dart ketikkan kode berikut,

 import 'package:country_list_pick/country_list_pick.dart';  
 import 'package:flutter/material.dart';  
 import 'dart:io';  
 import 'package:provider/provider.dart';  
 import 'package:snippetphoneauth/screens/auth/providers/auth_provider.dart';  

 class LoginScreen extends StatelessWidget {  
  const LoginScreen({Key? key}) : super(key: key); 
 
  @override  
  Widget build(BuildContext context) {  
   return WillPopScope(  
    onWillPop: () => exit(0),  
    child: Scaffold(  
     body: SingleChildScrollView(  
      padding: const EdgeInsets.only(top: 24.0),  
      child: Column(  
       mainAxisAlignment: MainAxisAlignment.start,  
       crossAxisAlignment: CrossAxisAlignment.start,  
       children: <Widget>[  
        Padding(  
         padding: const EdgeInsets.only(left: 16, top: 40),  
         child: Container(  
          padding: const EdgeInsets.only(bottom: 18),  
          child: const Text(  
           "Snippet Auth",  
           style: TextStyle(  
            color: Colors.blue,  
            fontWeight: FontWeight.bold,  
            fontStyle: FontStyle.italic,  
            fontSize: 25,  
           ),  
          ),  
         ),  
        ),  
        const Padding(  
         padding: EdgeInsets.only(left: 16, top: 16),  
         child: Text(  
          "Masuk",  
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),  
         ),  
        ),  
        const Padding(  
         padding: EdgeInsets.only(left: 16, top: 8),  
         child: Text(  
          "Silahkan masuk dengan nomor HP-mu",  
          style: TextStyle(  
           fontSize: 14,  
          ),  
         ),  
        ),  
        Padding(  
         padding: const EdgeInsets.only(left: 16, top: 30),  
         child: RichText(  
          text: const TextSpan(  
           text: 'Nomor HP',  
           style: TextStyle(  
            color: Colors.black87,  
            fontWeight: FontWeight.bold,  
            fontSize: 14,  
           ),  
           children: [  
            TextSpan(  
             text: ' *',  
             style: TextStyle(  
              color: Colors.red,  
              fontWeight: FontWeight.bold,  
              fontSize: 14,  
             ),  
            )  
           ],  
          ),  
         ),  
        ),  
        Padding(  
         padding: const EdgeInsets.only(left: 16, top: 10, right: 16),  
         child: Row(  
          children: <Widget>[  
           Container(  
            height: 35,  
            decoration: BoxDecoration(  
             color: Colors.blue,  
             borderRadius: BorderRadius.circular(15),  
            ),  
            child: Consumer<AuthProvider>(  
             builder: (_, AuthProvider authProvider, __) {  
              return CountryListPick(  
               appBar: AppBar(  
                title: const Text('Pilih Kode Negara'),  
               ),  
               theme: CountryTheme(  
                isShowFlag: true,  
                isShowTitle: true,  
                isShowCode: true,  
                isDownIcon: true,  
                showEnglishName: false,  
               ),  
               initialSelection: authProvider.countryCode.dialCode,  
               onChanged: (CountryCode? code) {  
                authProvider.countryCode = code!;  
               },  
               pickerBuilder: (context, CountryCode? countryCode) {  
                return Row(  
                 mainAxisAlignment: MainAxisAlignment.center,  
                 crossAxisAlignment: CrossAxisAlignment.center,  
                 children: [  
                  Text(  
                   countryCode!.dialCode!,  
                   style: const TextStyle(  
                    fontSize: 14,  
                    color: Colors.white,  
                    fontWeight: FontWeight.w500,  
                   ),  
                  ),  
                  const Icon(  
                   Icons.arrow_drop_down,  
                   color: Colors.white,  
                  )  
                 ],  
                );  
               },  
              );  
             },  
            ),  
           ),  
           Expanded(  
            child: Padding(  
             padding: const EdgeInsets.only(left: 10, right: 16),  
             child: Consumer<AuthProvider>(  
              builder: (_, AuthProvider authProvider, __) {  
               return Form(  
                child: TextFormField(  
                 onChanged: (String text) {  
                  authProvider.notifyState();  
                 },  
                 keyboardType: TextInputType.phone,  
                 controller: authProvider.phoneNumberController,  
                 decoration: InputDecoration(  
                  enabledBorder: const UnderlineInputBorder(  
                   borderSide: BorderSide(  
                    color: Colors.grey,  
                   ),  
                  ),  
                  focusedBorder: const UnderlineInputBorder(  
                   borderSide: BorderSide(  
                    color: Colors.blue,  
                   ),  
                  ),  
                  border: const UnderlineInputBorder(  
                   borderSide: BorderSide(  
                    color: Colors.grey,  
                   ),  
                  ),  
                  hintText: "8XX-XXXX-XXX",  
                  suffixIcon: IconButton(  
                   onPressed: () {  
                    authProvider.phoneNumberController  
                      .clear();  
                   },  
                   icon: const Icon(  
                    Icons.clear,  
                    color: Colors.black87,  
                   ),  
                  ),  
                 ),  
                ),  
               );  
              },  
             ),  
            ),  
           )  
          ],  
         ),  
        )  
       ],  
      ),  
     ),  
     floatingActionButton: Consumer<AuthProvider>(  
      builder: (_, AuthProvider authProvider, __) {  
       return FloatingActionButton(  
        backgroundColor:  
          authProvider.phoneNumberController.text.length > 8  
            ? Colors.blue  
            : Colors.grey[400],  
        onPressed: () {  
         // TODO  
        },  
        child: const Icon(  
         Icons.arrow_forward,  
        ),  
       );  
      },  
     ),  
    ),  
   );  
  }  
 }  

karena di halaman login ini kita sudah memerlukan beberapa state maka kita akan mulai membuat state dengan membuat getter dan setter yang diperlukan dihalaman login ini.

di file auth_provider.dart tadi kita tambahkan kode menjadi seperti berikut,

 import 'package:cloud_firestore/cloud_firestore.dart';  
 import 'package:country_list_pick/country_list_pick.dart';  
 import 'package:firebase_auth/firebase_auth.dart';    
 import 'package:flutter/material.dart';
  
 // Provider untuk halaman auth  
 class AuthProvider with ChangeNotifier {  
  
  ... 

  // Pendekralasian/inisiasi object firebase firestore  
  FirebaseFirestore firestore = FirebaseFirestore.instance;  

  // Deklarasi variabel Varification Id untuk auth  
  String? verificationId;  

  // Deklarasi variabel country code untuk menyimpan code  
  CountryCode _countryCode = CountryCode(dialCode: '+62');  

  // Deklarasi variabel _phoneNumberController, untuk Controller TextField  
  TextEditingController _phoneNumberController = TextEditingController();  

  // getter untuk menampilkan var _phoneNumberController  
  TextEditingController get phoneNumberController => _phoneNumberController;  

  // Setter untuk mengubah controller textfield _phoneNumberController  
  set phoneNumberController(TextEditingController phoneNumberController) {  
   _phoneNumberController = phoneNumberController;  
   notifyListeners();  
  }  

  // Deklarasi variabel _otpCodeController, untuk Controller TextField  
  TextEditingController _otpCodeController = TextEditingController();  

  // getter untuk menampilkan var _otpCodeController  
  TextEditingController get otpCodeController => _otpCodeController;  

  // Setter untuk mengubah controller textfield _otpCodeController  
  set otpCodeController(TextEditingController otpCodeController) {  
   _otpCodeController = otpCodeController;  
   notifyListeners();  
  }  

  // getter untuk menampilkan countryCode  
  CountryCode get countryCode => _countryCode;  

  // Setter untuk mengubah countrycode  
  set countryCode(CountryCode countryCode) {  
   _countryCode = countryCode;  
   notifyListeners();  
  }  
  
  // Untuk notis state kosong
  void notifyState() {
    notifyListeners();
  }
 }  

Karena kita sudah menggunakan Consumer maka jangan lupa kita tambahkan dulu ChangeNotifierProvider di posisi paling atasnya kita akan simpan ini di atas widget MaterialApp agar state yang tersimpan bisa kita panggil kapan saja di dalam aplikasi nantinya dan ini yang dinamakan App State yang sudah dijelaskan diatas tadi.

Buka file main.dart lalu tambahkan kode menjadi seperti berikut,

 import 'package:firebase_core/firebase_core.dart';  
 import 'package:flutter/material.dart';  
 import 'package:provider/provider.dart';  
 import 'package:snippetphoneauth/screens/auth/auth_checker.dart';  
 import 'package:snippetphoneauth/screens/auth/providers/auth_provider.dart';  

 ...

 class MyApp extends StatelessWidget {  
  const MyApp({Key? key}) : super(key: key);  

  @override  
  Widget build(BuildContext context) {  
   return ChangeNotifierProvider.value(  
    value: AuthProvider(),  
    child: MaterialApp(  
     debugShowCheckedModeBanner: false,  
     title: 'Snippet Phone Auth',  
     theme: ThemeData(  
      primarySwatch: Colors.blue,  
     ),  
     home: const AuthChecker(),  
    ),  
   );  
  }  
 }  

Sampai disini seharusnya jika tidak ada error atau kesalahan bisa kita coba jalankan projectnya dulu, hasil yang diharapkan ketika selesai dijalankan ini akan menampilkan halaman login terlebih dahulu.

Seperti tampilan berikut,


Jika sudah bisa dijalankan alhamdulillah keren, namun jika ada kesalahan atau hal yang tidak di pahami sampai sini coba kalian tuliskan pertanyaan di kolom komentar ya.

Kita lanjut dulu sebelum membuat fungsi loginnya sekarang kita perlu membuat UI input OTP terlebih dahulu. Mari kita buat, di folder auth buat file verification.dart lalu ketikkan kode berikut,

 import 'package:flutter/material.dart';  
 import 'package:provider/provider.dart'; 
 import 'package:snippetphoneauth/screens/auth/providers/auth_provider.dart';  

 class VerificationScreen extends StatelessWidget {  
  final String beUserPhone;  
  const VerificationScreen({  
   Key? key,  
   required this.beUserPhone,  
  }) : super(key: key);  

  @override  
  Widget build(BuildContext context) {  
   return Scaffold(  
    body: Column(  
     mainAxisAlignment: MainAxisAlignment.start,  
     crossAxisAlignment: CrossAxisAlignment.start,  
     children: <Widget>[  
      Container(  
       padding: const EdgeInsets.only(left: 5, top: 48),  
       child: IconButton(  
        onPressed: () {  
         Navigator.pop(context);  
        },  
        icon: const Icon(Icons.arrow_back),  
       ),  
      ),  
      Container(  
       padding: const EdgeInsets.only(left: 16, top: 15),  
       child: const Text(  
        "Kode OTP sudah di kirim!",  
        style: TextStyle(  
         fontSize: 17,  
         fontWeight: FontWeight.bold,  
        ),  
       ),  
      ),  
      Container(  
       padding: const EdgeInsets.only(left: 16, top: 8, right: 16),  
       child: Text(  
        "Masukkan kode OTP yang kami kirim ke nomor HP-mu, $beUserPhone.",  
        maxLines: 3,  
        style: const TextStyle(  
         fontSize: 14,  
        ),  
       ),  
      ),  
      Container(  
       padding: const EdgeInsets.only(left: 16, top: 30),  
       child: RichText(  
        text: const TextSpan(  
         text: 'Kode OTP',  
         style: TextStyle(  
          color: Colors.black87,  
          fontWeight: FontWeight.bold,  
          fontSize: 14,  
         ),  
         children: [  
          TextSpan(  
           text: ' *',  
           style: TextStyle(  
            color: Colors.red,  
            fontWeight: FontWeight.bold,  
            fontSize: 14,  
           ),  
          )  
         ],  
        ),  
       ),  
      ),  
      Container(  
       padding: const EdgeInsets.only(left: 16, top: 10),  
       child: Row(  
        children: <Widget>[  
         Expanded(  
          child: Form(  
           child: Container(  
            padding: const EdgeInsets.only(left: 0, right: 16),  
            child: Consumer<AuthProvider>(  
             builder: (_, AuthProvider authProvider, __) {  
              return TextFormField(  
               maxLength: 6,  
               onChanged: (String text) {  
                authProvider.notifyState();  
               },  
               keyboardType: TextInputType.number,  
               controller: authProvider.otpCodeController,  
               decoration: InputDecoration(  
                counterText: '',  
                enabledBorder: const UnderlineInputBorder(  
                 borderSide: BorderSide(color: Colors.grey),  
                ),  
                focusedBorder: const UnderlineInputBorder(  
                 borderSide: BorderSide(  
                  color: Colors.blue,  
                 ),  
                ),  
                border: const UnderlineInputBorder(  
                 borderSide: BorderSide(color: Colors.grey),  
                ),  
                hintText: " • • • • • •",  
                suffixIcon: IconButton(  
                 onPressed: () {  
                  authProvider.otpCodeController.clear();  
                 },  
                 icon: const Icon(  
                  Icons.clear,  
                  color: Colors.black87,  
                 ),  
                ),  
               ),  
              );  
             },  
            ),  
           ),  
          ),  
         )  
        ],  
       ),  
      ),  
     ],  
    ),  
    floatingActionButton: Consumer<AuthProvider>(  
     builder: (_, AuthProvider authProvider, __) {  
      return FloatingActionButton(  
       backgroundColor: authProvider.otpCodeController.text.length > 5  
         ? Colors.blue  
         : Colors.grey[400],  
       onPressed: () {  
        // TODO  
       },  
       child: const Icon(  
        Icons.arrow_forward,  
       ),  
      );  
     },  
    ),  
   );  
  }  
 }  

Nah sampai sini kita telah selesai menyiapkan UI screen aplikasi kita.

Menyiapkan Model dan Database

Sebelum membuat fungsi loginnya kita akan membutuhkan model data dan query ke database nantinya mari kita siapkan terlebih dahulu.

Di dalam folder lib buatkan folder models dan databases.

Kita akan buatkan dari model terlebih dahulu buatkan file user_model.dart di dalam folder models tadi yang sudah di buat, lalu ketikan kode berikut,

 import 'package:cloud_firestore/cloud_firestore.dart';  

 class UserModel {  
  late final String docUID;  
  late final String phone;  
  late final String status;  
  late final DateTime? logedInDate;  
  UserModel({  
   required this.docUID,  
   required this.phone,  
   required this.status,  
   this.logedInDate,  
  });  

  factory UserModel.fromData(DocumentSnapshot doc) {  
   return UserModel(  
    docUID: doc.id,  
    phone: doc.get('phone'),  
    status: doc.get('status'),  
    logedInDate: doc.get('logedInDate').toDate(),  
   );  
  }  

  Map<String, Object?> toData() {  
   return {  
    'docUID': docUID,  
    'phone': phone,  
    'status': status,  
    'logedInDate': logedInDate,  
   };  
  }  

  static UserModel get initialData {  
   return UserModel(  
    docUID: '',  
    phone: '',  
    status: '',  
   );  
  }  
 }  

Selanjutnya kita buat file snippet_auth_database.dart di dalam folder databases lalu ketikan kode berikut,

 import 'package:cloud_firestore/cloud_firestore.dart';  
 import 'package:flutter/material.dart';  
 import 'package:snippetphoneauth/models/user_model.dart';  

 class PhoneAuthDatabase {  
  FirebaseFirestore firestore = FirebaseFirestore.instance;  
  String collectionName = 'snippet-phone-auth';  

  // Get user data  
  Future<DocumentSnapshot> getUserData(String uid) async {  
   return await firestore.collection(collectionName).doc(uid).get();  
  }  

  // Create new user  
  Future createUser(UserModel userModel) async {  
   try {  
    await firestore  
      .collection(collectionName)  
      .doc(userModel.docUID)  
      .set(userModel.toData());  
   } catch (e) {  
    debugPrint(e.toString());  
   }  
  }  

  // Update data user  
  Future updateDataUser(String uid, {required var data}) async {  
   try {  
    await firestore.collection(collectionName).doc(uid).update(data);  
   } catch (e) {  
    debugPrint(e.toString());  
   }  
  }  
 }  

Oke, Model dan Databases sudah siap kita gunakan.

Membuat Fungsi Authentication

Karena segala kebutuhan kita sudah disiapkan sebelumnya sekarang kita langsung buatkan fungsi authentication OTPnya saja.

Di file auth_provider.dart kita tambahkan kode dengan fungsi-fungsi menjadi seperti berikut,

 import 'package:cloud_firestore/cloud_firestore.dart';  
 import 'package:country_list_pick/country_list_pick.dart';  
 import 'package:firebase_auth/firebase_auth.dart';  
 import 'package:flutter/material.dart';  
 import 'package:snippetphoneauth/databases/snippet_auth_database.dart';  
 import 'package:snippetphoneauth/models/user_model.dart';  
 import 'package:snippetphoneauth/screens/auth/auth_checker.dart';  
 import 'package:snippetphoneauth/screens/auth/verification.dart';  
 import 'package:snippetphoneauth/widgets/app_widget.dart';  

 // Provider untuk halaman auth  
 class AuthProvider with ChangeNotifier {  
  
  ...

  // Fungsi stream get data user ke collection yang dijalankan oleh StremProvider  
  Stream<UserModel>? get user {  
   // pengecekan user login auth  
   if (auth.currentUser != null) {  
    // Nilai dikembalikan untuk mengupdate data model user data  
    return firestore  
      .collection('snippet-phone-auth')  
      .doc(auth.currentUser!.uid)  
      .snapshots()  
      .map((userModel) {  
     return UserModel.fromData(userModel);  
    });  
   } else {  
    // Nilai dikembalikan null jika user belum login auth  
    return null;  
   }  
  }  

  // Fungsi untuk verifikasi nomber hp agar mendapatkan OTP  
  Future<void> verifyPhone(  
   BuildContext context,  
   String phoneToSend,  
  ) async {  
   try {  
    // ignore: prefer_function_declarations_over_variables  
    final PhoneCodeSent smsOTPSent = (String verId, int? forceCodeResend) {  
     verificationId = verId;  
    };  
    // Fungsi bawaan auth_firebase untuk verifikasi nomor telpon  
    await auth.verifyPhoneNumber(  
      // Nomber yang ingin di verifikasi  
      phoneNumber: phoneToSend,  
      // CodeSent  
      codeSent: smsOTPSent,  
      // Waktu timeout fungsi  
      timeout: const Duration(seconds: 10),  
      // Mengirim SMS dengan kode 6 digit ke nomor telepon yang ditentukan, atau memasukkan pengguna dan [verifikasiCompleted] dipanggil.  
      codeAutoRetrievalTimeout: (String verId) {  
       verificationId = verId;  
       if (verificationId != null) {  
        notifyListeners();  
        Navigator.of(context).pop();  
        Navigator.push(  
         context,  
         MaterialPageRoute(  
          builder: (_) {  
           return VerificationScreen(  
            beUserPhone: phoneToSend,  
           );  
          },  
         ),  
        );  
       } else {  
        Navigator.of(context).pop();  
        notifyListeners();  
       }  
      },  
      // Memulai proses verifikasi nomor telepon untuk nomor telepon yang diberikan.  
      verificationCompleted: (PhoneAuthCredential phoneAuthCredential) {  
       if (verificationId != null) {  
        notifyListeners();  
        Future.delayed(const Duration(seconds: 1), () {  
         Navigator.of(context).pop();  
         Navigator.push(  
          context,  
          MaterialPageRoute(  
           builder: (_) {  
            return VerificationScreen(  
             beUserPhone: phoneToSend,  
            );  
           },  
          ),  
         );  
        });  
       } else {  
        Navigator.of(context).pop();  
        notifyListeners();  
       }  
      },  
      // Ketika verifikasi gagal  
      verificationFailed: (FirebaseAuthException e) {  
       Navigator.of(context).pop();  
       if (e.code == 'invalid-phone-number') {  
        debugPrint('Nomor yang dimasukan salah.');  
        AppWidget.showSnackBar(  
         context: context,  
         content: const Text('Nomor yang anda masukan salah.'),  
         duration: const Duration(seconds: 3),  
        );  
       } else if (e.code == 'too-many-requests') {  
        debugPrint('Terlalu banyak request');  
        AppWidget.showSnackBar(  
         context: context,  
         content: const Text(  
          'Anda terlalu sering request OTP, tunggu beberapa saat lalu coba lagi.',  
         ),  
         duration: const Duration(seconds: 3),  
        );  
       } else {  
        debugPrint('Error Message : $e');  
       }  
      });  
   } catch (e) {  
    Navigator.of(context).pop();  
    debugPrint('Catch Error Message : $e');  
   }  
  }  

  // Fungsi untuk verifikasi kode OTP  
  Future<void> verifyOTP(BuildContext context, String phone, String otp) async {  
   AppWidget.loadingPageIndicator(context: context);  
   phoneNumberController.clear();  
   otpCodeController.clear();  
   AuthCredential credential;  
   UserCredential authLogin;  
   User? currentUser;  
   try {  
    // Dari verify id dan otp code di simpan dalam credential  
    credential = PhoneAuthProvider.credential(  
     verificationId: verificationId!,  
     smsCode: otp,  
    );  
    // Lakukan login dengan credential lalu simpan ke dalam User Credential  
    authLogin = await auth.signInWithCredential(  
     credential,  
    );  
    // Simpan User Credential ke dalam model user auth untuk di gunakan  
    currentUser = authLogin.user;  
   } catch (e) {  
    debugPrint('$e');  
    if (e.toString().contains("firebase_auth/invalid-verification-code")) {  
     AppWidget.showSnackBar(  
      context: context,  
      content: const Text(  
        'Kode OTP salah, silahkan coba kirim ulang kode dan coba lagi.'),  
      duration: const Duration(seconds: 3),  
     );  
    }  
   }  
   // Lakukan pengecekan untuk melanjutkan ke home jika user auth sudah terisi  
   if (currentUser != null) {  
    await PhoneAuthDatabase()  
      .getUserData(currentUser.uid)  
      .then((userDataDoc) async {  
     final UserModel userModel = UserModel(  
      docUID: currentUser!.uid,  
      phone: phone,  
      status: 'active',  
      logedInDate: DateTime.now(),  
     );  
     if (userDataDoc.exists) {  
      await PhoneAuthDatabase().updateDataUser(  
       currentUser.uid,  
       data: {  
        'logedInDate': DateTime.now(),  
       },  
      );  
     } else {  
      await PhoneAuthDatabase().createUser(userModel);  
     }  
    });  
    // Jika auth login telah terisi maka alihkan kembali ke halaman auth checker  
    Navigator.pushReplacement(  
     context,  
     MaterialPageRoute(  
      builder: (_) {  
       return const AuthChecker();  
      },  
     ),  
    );  
   }  
   // Jika user auth tidak terisi maka jangan lanjutkan ke home  
   else {  
    Navigator.of(context).pop();  
    notifyListeners();  
   }  
  }  

  // Fungsi untuk log out  
  Future<String> logOut(BuildContext context) async {  
   AppWidget.loadingPageIndicator(context: context);  
   // Destroy isi model user auth (untuk menandakan bahwa user telah logout)  
   await auth.signOut();  
   // Navigasi ke halaman pengecekan  
   Navigator.pushReplacement(  
    context,  
    MaterialPageRoute(  
     builder: (_) {  
      return const AuthChecker();  
     },  
    ),  
   );  
   notifyListeners();  
   return 'logout';  
  }
 }  

Lalu buka lagi file login.dart kita tambahkan code untuk submit menjadi seperti berikut,

 import 'package:country_list_pick/country_list_pick.dart';  
 import 'package:flutter/material.dart';  
 import 'dart:io';  
 import 'package:provider/provider.dart';  
 import 'package:snippetphoneauth/screens/auth/providers/auth_provider.dart';  
 import 'package:snippetphoneauth/widgets/app_widget.dart';  

 class LoginScreen extends StatelessWidget {  
  const LoginScreen({Key? key}) : super(key: key);  

  @override  
  Widget build(BuildContext context) {  
   return WillPopScope(  
    onWillPop: () => exit(0),  
    child: Scaffold(  

     ...

     floatingActionButton: Consumer<AuthProvider>(  
      builder: (_, AuthProvider authProvider, __) {  
       return FloatingActionButton(  
        backgroundColor:  
          authProvider.phoneNumberController.text.length > 8  
            ? Colors.blue  
            : Colors.grey[400],  
        onPressed: () {  
         if (authProvider.phoneNumberController.text.trim()[0] == '0') {  
          _onSubmit(  
           context,  
           '${authProvider.countryCode.dialCode!}${authProvider.phoneNumberController.text.trim().substring(1, authProvider.phoneNumberController.text.trim().length).trim()}',  
          );  
         } else {  
          _onSubmit(  
           context,  
           '${authProvider.countryCode.dialCode!}${authProvider.phoneNumberController.text.trim()}',  
          );  
         }  
        },  
        child: const Icon(  
         Icons.arrow_forward,  
        ),  
       );  
      },  
     ),  
    ),  
   );  
  }  

  // Fungsi onsubmit untuk menjalankan verifikasi phone dan melanjutkan ke verifi OTP  
  Future<void> _onSubmit(  
   BuildContext context,  
   String phone,  
  ) async {  
   debugPrint('phone : $phone');  
   if (phone.length > 8) {  
    AppWidget.loadingPageIndicator(context: context);  
    Provider.of<AuthProvider>(  
     context,  
     listen: false,  
    ).verifyPhone(context, phone);  
   }  
  }  
 }  

Selanjutnya kita juga perlu menambahkan fungsi ke verification.dart menjadi seperti berikut,

 import 'package:flutter/material.dart';  
 import 'package:provider/provider.dart';  
 import 'package:snippetphoneauth/screens/auth/providers/auth_provider.dart';  
 import 'package:snippetphoneauth/widgets/app_widget.dart';  

 class VerificationScreen extends StatelessWidget {  
  final String beUserPhone;  
  const VerificationScreen({  
   Key? key,  
   required this.beUserPhone,  
  }) : super(key: key);  

  @override  
  Widget build(BuildContext context) {  
   return Scaffold(  

    ...

    floatingActionButton: Consumer<AuthProvider>(  
     builder: (_, AuthProvider authProvider, __) {  
      return FloatingActionButton(  
       backgroundColor: authProvider.otpCodeController.text.length > 5  
         ? Colors.blue  
         : Colors.grey[400],  
       onPressed: () {  
        _onSubmit(  
         context,  
         beUserPhone,  
         authProvider.otpCodeController.text.trim(),  
        );  
       },  
       child: const Icon(  
        Icons.arrow_forward,  
       ),  
      );  
     },  
    ),  
   );  
  }  

  // Fungsi onsubmit untuk menjalankan verifikasi OTP  
  void _onSubmit(BuildContext context, String phone, String otpCode) {  
   if (otpCode.length > 5) {  
    // Memanggil fungsi verify otp number di auth provider  
    Provider.of<AuthProvider>(context, listen: false).verifyOTP(  
     context,  
     phone,  
     otpCode,  
    );  
   } else {  
    AppWidget.showSnackBar(  
     context: context,  
     content: const Text('Kode OTP yang dimasukkan kurang.'),  
     duration: const Duration(seconds: 3),  
    );  
   }  
  }  
 }  

Sampai di sini bisa kita coba melakukan login kurang lebih akan menjadi seperti ini,

Sistem Baca Data User Authentication

Terakhir adalah bagaimana caranya kita mengambil data user authenticationnya, disini saya membuat sistem sendiri dengan menggunakan StreamProvider.

Karena kita sudah mempunyai App State dari AuthProvider dan di dalamnya ada fungsi stream getter user ke collection 'snippet-phone-auth' dan langsung di return ke dalam model UserModel maka nantinya kita tinggal menyimpan variable yang diisi dengan Provider.of<UserModel>(context); dan memanggilnya di manapun. Berikut implementasinya, 

Pertama kita perlu membungkus halaman yang mau membaca user model dengan StreamProvider disini saya akan coba mengimplementasikannya di halaman HomeScreen().

Karena HomeScreen() dipanggil di AuthChecker() maka kita akan menambahkan kode menjadi seperti berikut,

 import 'package:flutter/material.dart';  
 import 'package:provider/provider.dart';  
 import 'package:snippetphoneauth/models/user_model.dart';  
 import 'package:snippetphoneauth/screens/auth/login.dart';  
 import 'package:snippetphoneauth/screens/auth/providers/auth_provider.dart';  
 import 'package:snippetphoneauth/screens/home.dart';  

 // Auth status pengecekan  
 class AuthChecker extends StatelessWidget {  
  const AuthChecker({Key? key}) : super(key: key);  

  @override  
  Widget build(BuildContext context) {  
   return Consumer(builder: (_, AuthProvider authProvider, __) {  
    // Pengecekan variable auth status untuk menentukan route ke halaman  
    if (authProvider.auth.currentUser != null) {  
     return StreamProvider<UserModel>.value(  
      value: AuthProvider().user,  
      initialData: UserModel.initialData,  
      child: const HomeScreen(),  
     );  
    } else {  
     // masuk ke halaman login jika user auth tidak terisi  
     return const LoginScreen();  
    }  
   });  
  }  
 }  

Untuk pemanggilan UserModel di HomeScreen sebagai berikut, oh iya kita tambahkan juga fungsi untuk logoutnya.

 import 'package:flutter/material.dart';  
 import 'package:provider/provider.dart';  
 import 'package:snippetphoneauth/models/user_model.dart';  
 import 'package:snippetphoneauth/screens/auth/providers/auth_provider.dart';  

 class HomeScreen extends StatelessWidget {  
  const HomeScreen({Key? key}) : super(key: key);  

  @override  
  Widget build(BuildContext context) {  
   final UserModel userModel = Provider.of<UserModel>(context);  
   return Scaffold(  
    appBar: AppBar(  
     title: const Text('Snippet Phone Auth'),  
     actions: [  
      // fungsi logout
      Consumer<AuthProvider>(  
       builder: (_, authProvider, __) {  
        return IconButton(  
         onPressed: () {  
          authProvider.logOut(context);  
         },  
         icon: const Icon(  
          Icons.logout,  
         ),  
        );  
       },  
      ),  
     ],  
    ),  
    body: Column(  
     crossAxisAlignment: CrossAxisAlignment.center,  
     mainAxisAlignment: MainAxisAlignment.center,  
     children: [ 
      // pemanggilan userModel secara realtime 
      Text(userModel.docUID),  
      Text(userModel.phone),  
      Text(userModel.status),  
      Text(userModel.logedInDate.toString()),  
      const Text('Home Screen'),  
     ],  
    ),  
   );  
  }  
 }  

Selesai! Silahkan coba jalankan kembali projectnya.

Link github https://github.com/sendiagustian/ follow dulu ya😬 dan cari repository snippet-phone-auth.

Demikian lah yang bisa saya sampaikan semoga tutorial dan cara yang saya gunakan ini bisa bermanfaat, apabila ada pertanyaan silahkan tulis di kolom komentar ya.

See you next post! 😉

Sendi Agustian
Sendi Agustian Berbagi kata-kata, cerita tentang pengalaman dan pemahaman. Juga sampingan dalam hal koding serta berbagi mengenai dunia Informasi Teknologi.

1 komentar untuk "Flutter Phone Authentication and System Read User Data With Provider"

Comment Author Avatar
Komentar ini telah dihapus oleh administrator blog.