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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | |
... | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object Constants { | |
// API Base URL | |
const val BASE_URL = "<BASE URL API>" | |
// API Endpoint | |
const val LOGIN_URL = "api/login" | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data class LoginRequest ( | |
@SerializedName("username") | |
var username: String, | |
@SerializedName("password") | |
var password: String | |
) |
Berikut ini merupakan contoh response JSON jika sukses login.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface ApiService { | |
@POST(Constants.LOGIN_URL) | |
fun login(@Body request: LoginRequest): Call<LoginResponse> | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... | |
<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
0 Komentar