Sponsor

Android Login Retrofit OAuth2

Pada tutorial kali ini, saya akan membahas tutorial pemrograman android dengan menggunakan kotlin. Library yang akan digunakan adalah Retrofit untuk http request ke API Server yang menggunakan otentifikasi OAuth atau JWT, OkHttpClient untuk request interceptor (bearer token / api key akan disisipkan saat request ke protected resources) dan Kotlin Android Extensions untuk menggantikan fungsi findViewById.

OkHttpClient Interceptor juga dapat digunakan untuk intersepsi request ketika bearer token expired otomatis melakukan request token baru dengan metode refresh token pada OAuth2. Berikut merupakan resume langkah-langkah pembuatannya dengan menggunakan android studio.

Konfigurasi Library pada Gradle (Module)

Buat project baru pada android studio dengan Empty Activiy, isi nama aplikasi dan nama package, tempat penyimpanan project, minimum OS (SDK) dan pilih Kotlin sebagai bahasa pemrogramannya. Pada jendela project browser,  buka file Gradle Scripts > build.gradle (Module: <nama_app>.app). Tambahkan baris berikut lalu lakukan sync gradle.

plugins {
...
id 'kotlin-android-extensions'
...
}
dependencies {
...
// Network
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
implementation 'com.squareup.okhttp3:okhttp:4.2.1'
...
}
view raw build.gradle hosted with ❤ by GitHub

Buatlah package baru dengan susunan sebagai berikut:
  • com.<company_name>.<app_name>
    • data
      • requests
      • responses
    • ui
    • utils

Package Utils

Buatlah Kotlin File/Class dengan tipe object dibawah package utils dengan nama Constants. File ini digunakan untuk membuat konstanta / variabel yg bersifat tetap dan dapat memudahkan jika suatu saat terjadi perubahan variabel pada aplikasi dan variabel sudah digunakan di banyak komponen aplikasi. File Constants ini berisi pengaturan base url  API maupun daftar endpoint API.


object Constants {
// API Base URL
const val BASE_URL = "<BASE URL API>"
// API Endpoint
const val LOGIN_URL = "api/login"
}
view raw Constants.kt hosted with ❤ by GitHub

Buatlah Kotlin File/Class dengan tipe Class dibawah package utils dengan nama SessionManager. File ini berisi SharedPreferences android yang dapat digunakan untuk menyimpan / mengakses data seperti session jika di pemrograman berbasis web. Class SessionManager digunakan untuk menyimpan Bearer Token dari hasil response login yang sukses. Bearer token ini berfungsi seperti apiKey untuk mengakses service atau endpoint yang diproteksi. SharedPreferences ini dapat diakses di activity / fragment android.

/**
* Session manager untuk menyimpan dan mengambil session dari SharedPreferences
*/
class SessionManager(context: Context) {
private var prefs: SharedPreferences = context.getSharedPreferences(context.getString(R.string.app_name), Context.MODE_PRIVATE)
companion object {
const val ACCESS_TOKEN = "access_token"
}
/**
* Fungsi simpan access_token
*/
fun saveAccessToken(token: String) {
val editor = prefs.edit()
editor.putString(ACCESS_TOKEN, token)
.apply()
}
/**
* Fungsi get access_token
*/
fun fetchAccessToken(): String? {
return prefs.getString(ACCESS_TOKEN, null)
}
/**
* Fungsi delete access_token / clear shared preferences
*/
fun deleteAccessToken() {
val editor = prefs.edit()
editor.clear()
.apply()
}
}

Konfigurasi Retrofit

Sebelum membuat class dan interface Retrofit, kita persiapkan dahulu request dan response untuk API login. Request untuk login hanya terdiri dari 2 field, username dan password. Buat file Kotlin File/Class dengan tipe Class dibawah package requests dengan nama LoginRequest. Tambahkan data pada class LoginRequest seperti berikut.

data class LoginRequest (
@SerializedName("username")
var username: String,
@SerializedName("password")
var password: String
)
view raw LoginRequest.kt hosted with ❤ by GitHub

Berikut ini merupakan contoh response JSON jika sukses login.

{
"status": {
"code": 200,
"message": "OK",
"error": false,
"error_message": []
},
"data": {
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiI5MWU2ZWU2Ny1iMDY5LTQ5YTYtOTBkMC01MDM2MGIxNjQ0ZTAiLCJqdGkiOiIxMWEwMjEyYzJkYTU3NTc5M2UwMmY2NjQ2NWViYzlmNTMwNWFiZWIyMzIzNmExYWMzMDg1NTIwZTBmNDZlMDc1YmM5ZDUzNTVkOWNmYzBkNSIsImlhdCI6MTYwNDIxMTMxOCwibmJmIjoxNjA0MjExMzE4LCJleHAiOjE2MDQyMTQ5MTgsInN1YiI6IjEiLCJzY29wZXMiOlsic3VwZXJtYW4iXX0.AiWFqCQHIWeg5iaOERkGdiruaqU2mCShAmaYqNr6RdsBEuuIDIaFdi6uyrA-hI1mi0kYBe4D9CV8gUq0azCexmohkQtrtUiikLZpqb3TVzl1MJ9X0OhWeru2_GoS7m6rBB87p6ygXYk6cCYg_iTOWW887dKJFPc1RZaH9vXpvOOxy4_EUyAeN64jXkANMW3q8ModULrT7xAJjjnqyXWtK-aWtof8_9vwDnNwiJT42Yl2nkP6szzFnnMyQoSGSQr6e5AwdzDq30c5cv-r5S6LXRS4aqHesF8Pj9YIVm53PZGdXi6-5GKzygUE5C-39uVx3mczcFbhc78lpVDrN6_xyks525BsfuMJVoykr_38rm9TtF8GLWVTg0hEMlqqjXVV_ECsbHeWutG3rOCO-UopvtwqOLq7PEHSjAubecZQAPhyx5dOREGmU80VayT3zERzSLFZ-OJzVjntyY6Qk-KialyqG2tyv7vKzJ6W5HCYqlGg42Vizi1A-n1joL0OUAfr6d8HnmMAC9gsNwXhwF4MoGDnbUVIztG901gcL1D3K_ZprAIwgeHEecmVCm-arHmw1S9PFyP-WlsmBMdp-Ayz1Gcfqqn7VUSFp76yNmD1DpSzYOypPg8-DzEtF8dtYs8TEEvnBuJmmO5C9hgZwCQ-oKb_BMsnqN2Afa9q0VfJ5b0",
"refresh_token": "def5020065ef36c63c1f3793cc86adf37ddd12481ffbc72a73c383376b990e9de337c9346affc46062e73d37d7bc0a7998d1e9ac3edb21d02f379456c539cc6743813d640acc0fbd3e160df3e950ad8d44192e2b9344dde3465d9115f0c7bf5c6e0a1022c2e71390caa210f7bdd4be290bdbf8357c0bcf2921f895d76a3e86ce759af9f2ad616ef96eb30ce643a3d21a154618dbf128e9d654ef31a9ffdb04fb93f750952ad79ad3ce7f7b818232aa164bb042c08b329d2e5207679037fc7691d2c5438734a4c6d4dd364a10fdddc1aafd793f6016b80c3fe9eb11b2b95737bf3962aca6f6b71fd696e773247d379b83c2ba0bc340f0850636398ea1d20ea5f022d7b1eee2a3f82d8dfb8486c7a2d0e8718595041aff15c7bc5362ad1ba8e8492649d7a53d98b54b5286d4ed7fae75cd12be9f3b567a88afadfdde3a32214897de4a35e98229fc4a4d65f0a694f688d2986a0c57112bbceff564f3a563b275e7e526531910044010241a6b3cf48709d1ec03bb128635bee4a45470c74223abdfb27b0b86cb82396286cb8b001b8a"
}
}

Untuk dapat membuat data class dari json dengan mudah, install plugin JSON to kotlin pada android studio dan letakkan dibawah package responses dengan nama LoginResponse.


// LoginResponse.kt
data class LoginResponse(
val status: Status,
val `data`: Data
)
// Status.kt
data class Status(
val code: Int,
val error: Boolean,
@SerializedName("error_message")
val errorMessage: List<Any>,
val message: String
)
// Data.kt
data class Data(
@SerializedName("access_token")
val accessToken: String,
@SerializedName("expires_in")
val expiresIn: Int,
@SerializedName("refresh_token")
val refreshToken: String,
@SerializedName("token_type")
val tokenType: String
)

Buatlah Kotlin File/Class dengan tipe Interface dibawah package data dengan nama ApiService. File ini berisi daftar endpoint, method dan parameter yang ada pada API.

interface ApiService {
@POST(Constants.LOGIN_URL)
fun login(@Body request: LoginRequest): Call<LoginResponse>
}
view raw ApiService.kt hosted with ❤ by GitHub

Buatlah Kotlin File/Class dengan tipe Class dibawah package data dengan nama RequestInterceptor. File ini digunakan untuk intersepsi pada saat request ke API dan melakukan pengecekan shared preferences dengan nama key access_token (SessionManager), jika access_token tidak kosong akan ditambahkan Header pada request yang berisi Authorization: Bearer <access_token> sehingga pada ApiService tidak perlu lagi menambahkan  parameter header access_token / apikey untuk request ke protected resources.

class RequestInterceptor (context: Context) : Interceptor {
private val sessionManager = SessionManager(context)
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
// Jika token ada di session manager, token sisipkan di request header
sessionManager.fetchAccessToken()?.let {
requestBuilder.addHeader("Authorization", "Bearer $it")
}
return chain.proceed(requestBuilder.build())
}
}

Buatlah Kotlin File/Class dengan tipe Class dibawah package data dengan nama ApiClient. File berisi instance retrofit dan okHttpClient untuk melakukan http request ke API.

class ApiClient {
private lateinit var apiService: ApiService
fun getApiService(context: Context) : ApiService {
// Inisialisasi Retrofit jika belum di inisialisasi
if (!::apiService.isInitialized) {
val retrofit = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okhttpClient(context))
.build()
apiService = retrofit.create(ApiService::class.java)
}
return apiService
}
/**
* Inisilisasi OkhttpClient dengan interceptor
*/
private fun okhttpClient(context: Context): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(RequestInterceptor(context))
.build()
}
}
view raw ApiClient.kt hosted with ❤ by GitHub

Activity dan UI (View)

Pindahkan / refactor MainActivity yang berada dibawah package utama ke package UI, tambahkan juga activity baru (new empty activity) dengan nama LoginActivity. Alur / proses yang akan dibuat nantinya seperti ini.
App Start -> Cek Akses Token (MainActivity) -> Go To Login (LoginActivity) jika tidak ditemukan Akses Token atau Menampilkan Akses token dan Tombol Logout (MainActivity) jika Akses Token ditemukan (SessionManager)

Berikut ini merupakan design UI LoginActivity dan MainActivity

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.LoginActivity">
<RelativeLayout
android:id="@+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_weight="4"
android:src="@drawable/ic_login"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="0dp" />
<View
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp"/>
</LinearLayout>
<LinearLayout
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_margin="?actionBarSize"
android:textStyle="bold"
android:textSize="40sp"
android:textColor="#fff"
android:text="@string/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/edtUsername"
android:inputType="text"
android:hint="@string/login_username"
android:autofillHints="@string/login_username"
android:textColor="#fff"
android:textColorHint="#eee"
android:paddingStart="10dp"
android:paddingLeft="10dp"
android:layout_margin="20dp"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="@drawable/rounded_corner"
android:textSize="17sp"
tools:ignore="RtlHardcoded,RtlSymmetry" />
<EditText
android:id="@+id/edtPassword"
android:hint="@string/login_password"
android:textColor="#fff"
android:textColorHint="#eee"
android:paddingStart="10dp"
android:paddingLeft="10dp"
android:inputType="textPassword"
android:autofillHints="@string/login_password"
android:layout_margin="20dp"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="@drawable/rounded_corner"
android:textSize="17sp"
tools:ignore="RtlHardcoded,RtlSymmetry" />
<Button
android:id="@+id/btnLogin"
android:paddingRight="20dp"
android:paddingLeft="20dp"
android:background="@drawable/rounded_corner_button"
android:text="@string/login_button"
android:textColor="#fff"
android:layout_width="wrap_content"
android:layout_height="35dp" />
</LinearLayout>
<LinearLayout
android:layout_marginBottom="30dp"
android:gravity="center"
android:orientation="vertical"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_marginBottom="10dp"
android:text="@string/company_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<include
android:id="@+id/llProgressBar"
android:visibility="gone"
layout="@layout/progress_bar_text"/>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/tvToken"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/bearer_token" />
<Button
android:id="@+id/btnLogout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/blue_500"
android:textColor="@color/white"
android:layout_marginTop="16dp"
android:text="@string/logout_button" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:gravity="center"
android:orientation="vertical"
android:background="#CCFFFFFF"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/pbText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:gravity="center"
android:textColor="#2196f3"
android:layout_marginTop="8dp"
android:text="@string/loading_text"/>
</LinearLayout>

Berikut ini merupakan source code LoginActivity dan MainActivity

class LoginActivity : AppCompatActivity() {
private lateinit var sessionManager: SessionManager
private lateinit var apiClient: ApiClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
apiClient = ApiClient()
sessionManager = SessionManager(this)
btnLogin.setOnClickListener {
llProgressBar.visibility = View.VISIBLE
val username: String = edtUsername.text.trim().toString()
val password: String = edtPassword.text.trim().toString()
// Validasi
if(username.isEmpty() || password.isEmpty()) {
llProgressBar.visibility = View.GONE
Toast.makeText(this, "Username dan password wajib diisi!", Toast.LENGTH_LONG).show()
} else {
apiClient.getApiService(this).login(LoginRequest(username, password))
.enqueue(object : Callback<LoginResponse> {
override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
llProgressBar.visibility = View.GONE
val loginResponse = response.body()
if (loginResponse?.status?.code == 200 && loginResponse.data.accessToken.isNotEmpty()) {
sessionManager.saveAccessToken(loginResponse.data.accessToken)
toMain()
} else {
Toast.makeText(applicationContext, "Username dan password tidak sesuai", Toast.LENGTH_LONG).show()
}
}
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
llProgressBar.visibility = View.GONE
Toast.makeText(applicationContext, "Gagal kontak server", Toast.LENGTH_LONG).show()
}
})
}
}
}
fun toMain() {
Intent(applicationContext, MainActivity::class.java).also {
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(it)
}
}
}

class MainActivity : AppCompatActivity() {
private lateinit var sessionManager: SessionManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sessionManager = SessionManager(this)
val token = sessionManager.fetchAccessToken()
// Cek token di session manager, jika tidak ada tampilkan login activity
if(token != null) {
tvToken.text = token
} else {
toLogin()
}
btnLogout.setOnClickListener {
toLogin()
}
}
fun toLogin() {
sessionManager.deleteAccessToken()
Intent(this, LoginActivity::class.java).also {
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(it)
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Android Manifest dan Permissions

Finally untuk dapat melakukan http request API, maka diperlukan android permission Internet. Konfigurasi MainActivity sebagai LAUNCHER (Activity yang pertama diload ketika aplikasi start) dengan menggunakan intent filter.
...
<uses-permission android:name="android.permission.INTERNET" />
<application
...
<activity
android:name=".ui.LoginActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
</activity>
<activity
android:name=".ui.MainActivity"
android:theme="@style/Theme.LoginOauth">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
...
</application>
...


TL;DR

Source code dan drawable assets lengkap dapat diunduh dari repo github

Posting Komentar

0 Komentar