Bună, în acest articol voi aborda conceptele MVVM, Retrofit, Dagger și LiveData pentru Kotlin și le voi explica utilizând un proiect exemplu. Proiectul nostru va fi un proces de autentificare Firebase, desigur, vom face acest lucru fără a utiliza SDK-ul Firebase, ci prin intermediul unui RestAPI. Mai întâi, să explic conceptele menționate în titlu.

MVVM (Model-View-ViewModel): În Kotlin, MVVM este un design arhitectural de software care împarte componentele aplicației în trei straturi principale: Model, View și ViewModel. Acest lucru face ca codul să fie organizat, lizibil și ușor de gestionat.

Retrofit: Este o bibliotecă client REST care oferă un client HTTP sigur în ceea ce privește tipurile pentru Android și Java. Permite comunicarea rapidă și ușoară cu API-urile. Convertește automat JSON și alte formate de date și le sincronizează cu modelele de date.

Dagger: Este un cadru de injecție a dependențelor rapid și eficient pentru Java și Android. Simplifică gestionarea și furnizarea dependențelor, asigurând astfel o dependență redusă și flexibilitate între componentele aplicației.

LiveData: Face parte din componentele Android Jetpack și oferă clase observabile pentru deținerea datelor. LiveData urmărește automat modificările datelor și sincronizează aceste modificări cu componentele UI. De asemenea, este sensibil la ciclul de viață, ajutând astfel la prevenirea scurgerilor de memorie și a erorilor pe parcursul ciclului de viață al aplicației.

Coroutines: Este o structură ușoară și scalabilă în limbajul Kotlin care facilitează programarea concurentă și asincronă. Datorită mecanismului său de suspendare neblocant, poate efectua o mare cantitate de operații concurente utilizând mai puține resurse. Această structură permite scrierea unui cod mai ușor de înțeles și mai curat, oferind în același timp îmbunătățiri în ceea ce privește performanța și utilizarea resurselor.

Utilizarea combinată a acestor tehnologii permite consolidarea arhitecturii MVVM în Kotlin și dezvoltarea de aplicații mai eficiente, organizate și testabile.

Kotlin-MVVM

Acum, înainte de a începe proiectul, puteți examina documentația Firebase Auth REST API făcând clic aici. După ce am creat proiectul, să stabilim structura MVVM. Să creăm două pachete, data și ui. Apoi, să creăm folderele activity și viewmodel în directorul UI. În directorul Data, să creăm folderele di, helper, model, remote și repository. Structura MVVM este atât de simplă 🙂 După ce am finalizat acești pași, să adăugăm dependențele corespunzătoare în proiect. Mai întâi, să deschidem fișierul build.gradle la nivel de proiect și să adăugăm codul de mai jos deasupra plugin-urilor.

buildscript {
    dependencies {
        // Dagger Hilt
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42'
    }
}


Apoi, să deschidem fișierul build.gradle la nivel de aplicație și să adăugăm dependențele de mai jos.

    // Dagger Hilt
    implementation 'com.google.dagger:hilt-android:2.42'
    kapt 'com.google.dagger:hilt-compiler:2.42'

    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'


    // ViewModel and LiveData
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
    implementation 'androidx.activity:activity-ktx:1.3.1'


Vă rugăm să setați designul activity_main.xml conform indicațiilor de mai jos.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    tools:context=".ui.activity.MainActivity">


    <LinearLayout
        android:id="@+id/linearLayout2"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:layout_marginBottom="100dp"
        android:gravity="center"
        android:orientation="vertical"
        app:layout_constraintBottom_toTopOf="@+id/btnLogin"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent">

        <EditText
            android:id="@+id/edEmail"
            android:layout_width="300dp"
            android:layout_height="60dp"
            android:layout_marginBottom="20dp"
            android:background="@drawable/edtext_bg_selector"
            android:drawableStart="@drawable/baseline_email_24"
            android:drawablePadding="10dp"
            android:drawableTint="@android:color/darker_gray"
            android:hint="E-Mail"
            android:inputType="textWebEmailAddress"
            android:padding="15dp" />

        <EditText
            android:id="@+id/edPass"
            android:layout_width="300dp"
            android:layout_height="60dp"
            android:background="@drawable/edtext_bg_selector"
            android:drawableStart="@android:drawable/ic_lock_lock"
            android:drawablePadding="10dp"
            android:drawableTint="@android:color/darker_gray"
            android:hint="Password"
            android:inputType="textPassword"
            android:padding="10dp" />


    </LinearLayout>

    <Button
        android:id="@+id/btnLogin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="68dp"
        android:text="Login"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>


Acum putem începe să scriem clasele și serviciile noastre. Mai întâi, trebuie să creăm modelul pentru autentificarea pe care o vom efectua. Când examinăm documentația Google Auth REST API, vedem că există parametrii „E-mail”, „Parola” și „returnSecureToken” pentru autentificare. Putem seta clasa UserModel pe care am creat-o astfel:

import com.google.gson.annotations.SerializedName

data class UserModel(
    val email : String,
    val password : String,
    @SerializedName("returnSecureToken") val returnSecureToken: Boolean = true
)


Aici, returnSecureToken trebuie să întoarcă întotdeauna true.

Acum putem scrie modulele și serviciile noastre. Mai întâi, să creăm clasa NetworkModule în folderul remote pe care l-am creat.

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton


@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
    @Provides
    @Singleton

    fun provideRetrofit() : Retrofit =
        Retrofit.Builder()
            .baseUrl("https://identitytoolkit.googleapis.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    @Provides
    @Singleton
    fun provideApiService(retrofit:Retrofit) : Services = retrofit.create(Services::class.java)
}


Aici, am folosit o bibliotecă de injecție a dependențelor numită Dagger Hilt. Acum să explicăm fiecare dintre aceste adnotări.

@Module: Această etichetă indică faptul că clasa este un modul care furnizează dependențe. Acest modul definește cum vor fi create și furnizate anumite dependențe.

@InstallIn: Această etichetă indică în ce componentă Dagger Hilt trebuie instalat modulul. În acest exemplu, este specificat că modulul trebuie să fie încărcat într-o componentă SingletonComponent::class, care există pe parcursul întregii durate de viață a aplicației și furnizează dependențe singleton.

@Provides: Această etichetă indică faptul că funcția va fi folosită pentru a furniza o dependență. Dagger Hilt folosește funcțiile marcate cu această etichetă pentru a crea dependențele și a le furniza locațiilor în care este necesară injecția.

@Singleton: Această etichetă indică faptul că dependența trebuie furnizată ca singleton. Acest lucru înseamnă că dependența va fi creată doar o singură dată și același exemplu va fi utilizat pe parcursul întregii durate de viață a aplicației.

În acest exemplu, se creează un modul numit NetworkModule, care furnizează un Retrofit și un ApiService singleton ce există pe parcursul întregii durate de viață a aplicației. Funcțiile provideRetrofit și provideApiService definește cum vor fi create aceste dependențe și cum vor fi folosite de Dagger Hilt. Aceste dependențe pot fi apoi injectate în locurile necesare din aplicație. Așadar, în loc să apelăm serviciul direct, îl vom apela prin intermediul Dagger și îl vom gestiona dintr-un singur loc. Orice modificare în serviciu nu va afecta locurile în care acesta a fost apelat. Puteți examina subiectul injecției de dependență în detaliu făcând clic aici.

Acum să creăm serviciul nostru. Parametrul pe care îl vom folosi pentru Google Auth REST API este „v1/accounts:signInWithPassword?key=/YOUR KEY HERE/”. Puteți găsi cheia dvs. în secțiunea Setări proiect din Firebase. În imaginea de mai jos, partea încadrată în roșu este cheia Web API specifică proiectului dvs.

 

firebasewebapikey


Apoi, să creăm interfața de serviciu în modul următor.

 

import com.example.retrofitdeapp.data.model.UserModel
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST

interface Services {
 @POST("v1/accounts:signInWithPassword?key=/*YOUR KEY HERE*/")
 suspend fun login(@Body userModel: UserModel) : Response<UserModel>
}


Acum că am definit metodele pentru comunicarea cu API-ul:

@POST: Această etichetă indică faptul că funcția va efectua o cerere POST. Sufixul URL furnizat în interiorul etichetei este utilizat pentru a crea URL-ul complet la care va fi trimisă cererea.

“v1/accounts:signInWithPassword?key=/YOUR KEY HERE/” Această parte conține sufixul URL la care va fi trimisă cererea API. Partea /YOUR KEY HERE/ trebuie înlocuită cu cheia reală a API-ului.

suspend fun login(@Body userModel: UserModel) : Response<UserModel> Această parte definește o funcție numită login. Funcția primește un UserModel și efectuează o cerere POST la API, returnând un Response<UserModel>. Folosind cuvântul suspend înaintea funcției, se indică faptul că funcția va fi executată cu modelul de concurență al Kotlin, numit “coroutines”. Astfel, funcția va funcționa asincron, permițându-vă să continuați alte operații fără a aștepta finalizarea acesteia.

@Body userModel: UserModel Eticheta @Body aici specifică datele care vor fi adăugate în corpul (body) cererii. În acest caz, obiectul userModel va fi adăugat în corpul cererii API.

În rezumat, această interfață de servicii definește o cerere API care va fi utilizată cu Retrofit. Această cerere primește un UserModel și efectuează o cerere POST la API, returnând un răspuns care conține un UserModel atunci când se finalizează cu succes.

După ce am creat și aceasta, să trecem la folderul di și să creăm clasa de bază necesară pentru Dagger Hilt. Numele acestei clase este, de obicei, “HiltApplication”.

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class HiltApplication : Application() {
}


Acest cod definește clasa de bază a aplicației Android pentru biblioteca de injecție de dependențe Dagger Hilt. Dagger Hilt este o bibliotecă care gestionează și furnizează graful de dependențe al aplicației.

@HiltAndroidApp: Această etichetă indică faptul că această clasă este clasa de bază a aplicației Android care va fi folosită de Dagger Hilt. Prin intermediul acestei adnotări, Dagger Hilt construiește și gestionează graful de dependențe al aplicației.

class HiltApplication : Application() Această parte definește o nouă clasă numită HiltApplication și este derivată din clasa Application a Android. Această clasă este utilizată pentru inițializarea și gestionarea injecției de dependențe Dagger Hilt pe parcursul ciclului de viață al aplicației.

Această clasă oferă o structură de bază pentru inițierea și gestionarea injecției de dependențe în aplicația dvs. utilizând Dagger Hilt. Astfel, puteți gestiona dependențele din aplicația dvs. într-un mod mai organizat, flexibil și testabil. În plus, nu uitați să efectuați următoarea modificare de nume în fișierul AndroidManifest.xml.

android:name=".data.di.HiltApplication"


Acum, să trecem la folderul repository și să creăm clasa LoginRepository.

import com.example.retrofitdeapp.data.remote.Services
import com.example.retrofitdeapp.data.model.UserModel
import retrofit2.Response
import javax.inject.Inject

class LoginRepository @Inject constructor(private val apiService : Services) {
    suspend fun loginUser(userModel: UserModel) : Response<UserModel>{
        return apiService.login(userModel)
    }
}


Am definit o clasă numită LoginRepository. Această clasă este o clasă Repository care va fi utilizată pentru operațiunile de autentificare a utilizatorilor. Clasele Repository comunică cu sursa de date pentru a obține și a procesa datele. În acest exemplu, clasa LoginRepository permite autentificarea utilizatorilor utilizând serviciul API.

@Inject constructor(private val apiService : Services) Această parte este folosită pentru a injecta dependențele în clasa LoginRepository folosind biblioteca Dagger Hilt de dependency injection. În acest exemplu, se injectează un serviciu API numit Services. Eticheta @Inject indică faptul că Dagger Hilt trebuie să injecteze această clasă ca o dependență.

suspend fun loginUser(userModel: UserModel) : Response<UserModel> Această parte definește o funcție numită loginUser. Funcția primește un UserModel și, utilizând serviciul API, efectuează autentificarea utilizatorului, returnând un răspuns (Response<UserModel>) care conține un UserModel dacă operațiunea s-a finalizat cu succes. Folosind cuvântul cheie suspend înaintea funcției, se specifică faptul că această funcție va fi executată folosind Kotlin Coroutines. Astfel, funcția va rula asincron, permițând continuarea altor operațiuni fără a aștepta finalizarea acesteia.

return apiService.login(userModel) Această linie apelează funcția login de pe apiService (obiectul Services injectat) și transmite obiectul userModel ca parametru. Astfel, se realizează cererea API și rezultatul returnat este utilizat ca valoare de returnare pentru funcția loginUser.

În rezumat, această clasă LoginRepository este o clasă Repository care efectuează operațiunile de autentificare a utilizatorilor folosind serviciul API. Această clasă injectează serviciul API folosind Dagger Hilt pentru dependency injection și utilizează Kotlin Coroutines pentru a asigura execuția asincronă în timpul autentificării utilizatorilor.

Pentru a gestiona operațiunea de autentificare reușită care va fi returnată atunci când ne autentificăm, să creăm o clasă numită LoginResult în ViewModel.

sealed class Result<out T> {
    object Loading : Result<Nothing>()
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Throwable) : Result<Nothing>()
}


Aici, am definit o clasă generală Result în Kotlin. Această clasă poate fi utilizată pentru a reprezenta rezultatele operațiunilor și poate modela diferite stări, cum ar fi rezultatele reușite, erorile și starea de încărcare. În acest exemplu, clasa Result este definită ca o “sealed class” (clasă sigilată). Clasele sigilate permit definirea unui număr limitat de subclase și oferă un control mai puternic al tipurilor.

sealed class Result<out T> Această parte definește o clasă sigilată numită Result. Această clasă vine cu un parametru de tip covariant T. T reprezintă tipul datelor care vor fi returnate în cazul operațiunilor reușite.

object Loading : Result<Nothing>() Această parte definește un obiect numit Loading, care este o subclasă a clasei Result. Acest obiect este utilizat pentru a reprezenta starea de încărcare a operațiunii. Prin utilizarea parametrului de tip Nothing, se specifică faptul că starea de încărcare nu va conține un rezultat al unei operațiuni reușite.

data class Success<out T>(val data: T) : Result<T>() Această parte definește o subclasă a clasei Result, numită Success, care reprezintă operațiunile reușite. Această clasă de date vine cu un parametru de tip covariant T și definește o proprietate numită data, care conține rezultatul operațiunii reușite.

data class Error(val exception: Throwable) : Result<Nothing>() Această parte definește o subclasă a clasei Result, numită Error, care reprezintă operațiunile eșuate. Această clasă de date conține o proprietate de tip Throwable, numită exception, care este utilizată pentru a reprezenta eroarea. Prin utilizarea parametrului de tip Nothing, se specifică faptul că starea de eroare nu va conține un rezultat al unei operațiuni reușite.

Această clasă sigilată Result poate fi utilizată pentru a reprezenta rezultatele operațiunilor cu un control mai puternic al tipurilor și poate fi aplicată în diferite domenii, cum ar fi cererile API, operațiunile de bază de date. Astfel, diferite stări, cum ar fi operațiunile reușite, erorile și starea de încărcare, pot fi gestionate într-un mod mai organizat și sigur. Aici o folosim pentru a verifica rezultatul returnat în procesul de autentificare.

Acum, putem să scriem LoginViewModel. Să creăm clasa LoginViewModel în directorul viewmodel.

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.retrofitdeapp.data.model.UserModel
import com.example.retrofitdeapp.data.repository.LoginRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.example.retrofitdeapp.data.helper.Result



@HiltViewModel
class LoginViewModel @Inject constructor(private val loginRepository: LoginRepository) : ViewModel() {

    private val _loginResult = MutableLiveData<Result<Unit>>(Result.Loading)
    val loginResult : LiveData<Result<Unit>> = _loginResult
        fun loginUser(userModel: UserModel){

        viewModelScope.launch {
            val response = loginRepository.loginUser(userModel)
            if(response.isSuccessful){
                Log.e("LOGIN", "Login Successfull")
                _loginResult.value = Result.Success(Unit)
            }

            else
            {
                Log.e("LOGIN", "Login Failed")
                Log.e("Gelen yanıt:",response.toString())
                _loginResult.value = Result.Error(Exception("Login Failed"))

            }
        }
    }
}


@HiltViewModel:
Această etichetă permite bibliotecii de injecție a dependențelor Dagger Hilt să accepte această clasă ca un ViewModel și să asigure injectarea dependențelor necesare.

class LoginViewModel @Inject constructor(private val loginRepository: LoginRepository) : ViewModel() Această parte definește o clasă numită LoginViewModel, care moștenește clasa ViewModel. Anotația @Inject este utilizată pentru a injecta dependența LoginRepository.

_loginResult și loginResult: Aceste două proprietăți conțin un obiect LiveData de tip Result<Unit> care reprezintă rezultatul autentificării utilizatorului. Proprietatea MutableLiveData denumită _loginResult este utilizată în ViewModel, în timp ce proprietatea LiveData denumită loginResult este expusă în exterior.

fun loginUser(userModel: UserModel) Această parte definește o funcție care va fi utilizată pentru autentificarea utilizatorilor. Această funcție primește un obiect UserModel și inițiază procesul de autentificare.

viewModelScope.launch: Această linie lansează o rutină Kotlin Coroutine în cadrul ViewModel. Acest lucru permite inițierea unei operațiuni asincrone, fără a aștepta finalizarea acesteia înainte de a trece la alte operațiuni.

val response = loginRepository.loginUser(userModel) Această linie apelează funcția loginUser a LoginRepository injectat pentru a obține răspunsul de la API.

if(response.isSuccessful) și blocurile else: În această parte, se verifică dacă răspunsul API este reușit sau nu. Dacă se primește un răspuns reușit, se actualizează valoarea _loginResult la Result.Success(Unit) pentru a indica succesul autentificării. În cazul unui răspuns nereușit, mesajele de eroare sunt înregistrate în jurnal, iar valoarea _loginResult este actualizată cu un Result.Error.

Această clasă folosește Kotlin Coroutines și viewModelScope pentru a asigura funcționarea asincronă în timpul autentificării utilizatorilor. Când procesul de autentificare este reușit, valoarea _loginResult este actualizată la Result.Success(Unit), iar în caz contrar este actualizată cu un Result.Error. Astfel, stratul UI poate face actualizări necesare în funcție de valoarea loginResult și poate oferi feedback utilizatorului. S-ar putea întreba de ce nu facem aceste operațiuni în locul unde creăm obiectul. Cu toate acestea, efectuăm aceste operațiuni în ViewModel în loc să le efectuăm în UI, deoarece scriem cod curat, conform structurii MVVM. Acum să finalizăm ultimul pas în MainActivity.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import com.example.retrofitdeapp.ui.viewmodel.LoginViewModel
import androidx.activity.viewModels
import androidx.lifecycle.LifecycleOwner
import com.example.retrofitdeapp.data.model.UserModel
import com.example.retrofitdeapp.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
import com.example.retrofitdeapp.data.helper.Result



@AndroidEntryPoint
class MainActivity : AppCompatActivity(), LifecycleOwner {

    private lateinit var binding: ActivityMainBinding
    private val loginViewModel : LoginViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)

        setContentView(binding.root)

        binding.apply{

            btnLogin.setOnClickListener()
            {
                if(edEmail.text.toString().isNullOrBlank() || edPass.text.toString().isNullOrBlank())
                {
                    Toast.makeText(applicationContext,"Please enter valid e-mail and password!", Toast.LENGTH_SHORT).show()
                }

                else
                {
                    val newLogin = UserModel(edEmail.text.toString(), edPass.text.toString())
                    loginViewModel.loginUser(newLogin)

                }

            }
        }

        loginViewModel.loginResult.observe(this) { result ->
            when (result) {
                is Result.Loading -> {
                    // Loading....
                }
                is Result.Success -> {
                    Toast.makeText(applicationContext, "Login Succesful!", Toast.LENGTH_SHORT).show()
                }
                is Result.Error -> {
                    Toast.makeText(applicationContext, "Incorrect E-Mail or Password!", Toast.LENGTH_SHORT).show()
                }
            }
        }


    }
}


Aici nu trebuie să explic lucrurile făcute pentru UI, nu-i așa? 🙂 Ascultăm LiveData loginResult creat în loginViewModel. Nu trebuie să faceți această ascultare atunci când faceți clic pe buton, deoarece oricum rulează asincron în fundal. Nu m-am atins de partea de încărcare, aici, în timp ce utilizatorul se autentifică, puteți să-l lăsați pe utilizator să aștepte 1-2 secunde și să extrageți imediat datele relevante în acest timp.

Am discutat despre toate structurile MVVM, Retrofit, Dagger, LiveData, Coroutines într-un singur proiect, lovind 5 păsări cu o singură piatră 🙂 O altă întrebare pe care ați putea să o puneți ar fi „De ce să ne ocupăm de toate acestea când Firebase SDK le oferă?” Nu toate API-urile au un SDK specific. De exemplu, este posibil să nu doriți să vă ocupați de scrierea unui SDK pentru un serviciu API pe care l-ați scris. În acest caz, veți avea nevoie de aceste cinci concepte în același mod.

Faceți clic aici pentru a accesa depozitul GitHub complet al proiectului. Aștept comentariile voastre pentru părțile pe care nu le înțelegeți. Mult succes în munca voastră.