Android Kotlin #11: Autentikasi – Login dan Menyimpan Token JWT
Halo lagi, calon developer aplikasi wisata!
Sampai saat ini, aplikasi kita bisa melakukan operasi CRUD dengan bebas. Tapi di dunia nyata, tidak semua orang boleh sembarangan nambah, edit, atau hapus data. Di API kita, endpoint-endpoint tertentu (POST, PUT, DELETE) sudah dilindungi dengan JWT (JSON Web Token). Artinya, hanya pengguna yang sudah login yang bisa mengaksesnya. Nah, sekarang kita akan menambahkan fitur login di aplikasi Android, menyimpan token yang didapat, dan menyertakannya di setiap request yang membutuhkan autentikasi.
Yang Akan Kita Pelajari
- Membuat halaman login (sederhana) dengan username dan password.
- Memanggil endpoint login API dan mendapatkan token.
- Menyimpan token dengan DataStore (pengganti SharedPreferences yang modern).
- Membuat AuthInterceptor untuk menambahkan token ke setiap request Retrofit.
- Mengarahkan pengguna ke halaman utama jika sudah login, atau ke login jika belum.
Langkah 0: Siapkan API dan Endpoint Login
API Wisata kita di seri sebelumnya sudah memiliki endpoint login: POST /api/auth/login yang menerima JSON { "username": "...", "password": "..." } dan mengembalikan token dalam format:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Pastikan API berjalan dan bisa diakses dari emulator (http://10.0.2.2:7000).
Langkah 1: Tambahkan Dependencies
Buka build.gradle (Module: app). Tambahkan dependency untuk DataStore dan juga interceptor (jika belum).
// DataStore
implementation "androidx.datastore:datastore-preferences:1.1.0"
// Untuk lifecyle (viewModelScope)
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0"
// Retrofit & interceptor sudah ada sebelumnya
Klik Sync Now.
Langkah 2: Buat Model untuk Login Request dan Response
Buat data class untuk login request di file baru atau di dalam file yang sama:
data class LoginRequest(
val username: String,
val password: String
)
data class LoginResponse(
val token: String
)
Langkah 3: Tambahkan Endpoint Login di ApiService
Buka ApiService.kt dan tambahkan:
@POST("api/auth/login")
suspend fun login(@Body request: LoginRequest): Response<LoginResponse>
Langkah 4: Membuat TokenManager dengan DataStore
DataStore adalah cara modern menyimpan data kecil secara asynchronous. Kita akan membuat kelas TokenManager yang bertugas menyimpan dan mengambil token.
Buat file baru: TokenManager.kt
package com.example.wisataapp
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore by preferencesDataStore("auth_prefs")
class TokenManager(private val context: Context) {
companion object {
private val TOKEN_KEY = stringPreferencesKey("jwt_token")
}
// Simpan token
suspend fun saveToken(token: String) {
context.dataStore.edit { preferences ->
preferences[TOKEN_KEY] = token
}
}
// Ambil token sebagai Flow (akan dipantau perubahannya)
fun getToken(): Flow<String?> {
return context.dataStore.data.map { preferences ->
preferences[TOKEN_KEY]
}
}
// Hapus token (untuk logout)
suspend fun clearToken() {
context.dataStore.edit { preferences ->
preferences.remove(TOKEN_KEY)
}
}
}
Penjelasan:
preferencesDataStoremembuat instance DataStore dengan nama "auth_prefs".stringPreferencesKeymendefinisikan kunci untuk token.saveTokenmenyimpan token ke DataStore (suspend function).getTokenmengembalikanFlow<String?>yang bisa diamati.
Langkah 5: Membuat AuthInterceptor untuk Menyisipkan Token
Kita perlu menambahkan header Authorization: Bearer <token> ke setiap request yang membutuhkan autentikasi. Cara terbaik adalah dengan Interceptor di OkHttp.
Buat file baru: AuthInterceptor.kt
package com.example.wisataapp
import okhttp3.Interceptor
import okhttp3.Response
import kotlinx.coroutines.runBlocking
class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// Ambil token secara blocking (karena interceptor berjalan di background thread)
val token = runBlocking { tokenManager.getToken().firstOrNull() }
// Jika token ada, tambahkan header Authorization
val newRequest = if (!token.isNullOrBlank()) {
request.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
} else {
request
}
return chain.proceed(newRequest)
}
}
Perhatikan: kita gunakan runBlocking untuk mengambil token dari Flow karena interceptor berjalan di thread background dan tidak mendukung suspend. Ini aman karena DataStore operasinya cepat.
Langkah 6: Update RetrofitInstance dengan Interceptor
Sekarang kita perlu mengintegrasikan interceptor ke dalam OkHttpClient yang digunakan Retrofit. Modifikasi RetrofitInstance.kt:
package com.example.wisataapp
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitInstance {
private const val BASE_URL = "http://10.0.2.2:7000/"
// Buat instance TokenManager (perlu context, akan di-set dari Application)
lateinit var tokenManager: TokenManager
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
private val client: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(AuthInterceptor(tokenManager)) // tambahkan interceptor auth
.build()
}
val api: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
Perhatikan bahwa tokenManager harus diinisialisasi sebelum digunakan. Kita akan inisialisasi di Application class.
Langkah 7: Membuat Aplikasi Class untuk Inisialisasi Global
Buat class baru: WisataApplication.kt (atau MyApp) yang extends Application. Di sini kita inisialisasi TokenManager dan set ke RetrofitInstance.
package com.example.wisataapp
import android.app.Application
class WisataApplication : Application() {
override fun onCreate() {
super.onCreate()
// Inisialisasi TokenManager
val tokenManager = TokenManager(applicationContext)
RetrofitInstance.tokenManager = tokenManager
}
}
Jangan lupa daftarkan aplikasi ini di AndroidManifest.xml dengan menambahkan atribut android:name=".WisataApplication" di tag <application>.
Langkah 8: Membuat Halaman Login (Activity)
Buat activity baru: LoginActivity.kt dan layout activity_login.xml. Juga buat ViewModel untuk login.
activity_login.xml sederhana:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/editUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Username"
android:inputType="text" />
<EditText
android:id="@+id/editPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:inputType="textPassword"
android:layout_marginTop="8dp" />
<Button
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login"
android:layout_marginTop="16dp" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:visibility="gone" />
</LinearLayout>
LoginViewModel.kt:
package com.example.wisataapp
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class LoginViewModel : ViewModel() {
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> get() = _isLoading
private val _errorMessage = MutableLiveData<String?>()
val errorMessage: LiveData<String?> get() = _errorMessage
private val _loginSuccess = MutableLiveData<String?>() // token
val loginSuccess: LiveData<String?> get() = _loginSuccess
fun login(username: String, password: String) {
viewModelScope.launch {
_isLoading.value = true
try {
val response = RetrofitInstance.api.login(LoginRequest(username, password))
if (response.isSuccessful) {
val token = response.body()?.token
if (token != null) {
_loginSuccess.value = token
} else {
_errorMessage.value = "Token tidak ditemukan"
}
} else {
_errorMessage.value = "Login gagal: ${response.code()}"
}
} catch (e: Exception) {
_errorMessage.value = "Error: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
}
LoginActivity.kt:
package com.example.wisataapp
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
class LoginActivity : AppCompatActivity() {
private lateinit var editUsername: EditText
private lateinit var editPassword: EditText
private lateinit var btnLogin: Button
private lateinit var progressBar: ProgressBar
private val loginViewModel: LoginViewModel by viewModels()
private lateinit var tokenManager: TokenManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
editUsername = findViewById(R.id.editUsername)
editPassword = findViewById(R.id.editPassword)
btnLogin = findViewById(R.id.btnLogin)
progressBar = findViewById(R.id.progressBar)
tokenManager = TokenManager(this)
// Cek apakah sudah login? Jika sudah, langsung ke MainActivity
tokenManager.getToken().observe(this) { token ->
if (!token.isNullOrBlank()) {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}
btnLogin.setOnClickListener {
val username = editUsername.text.toString().trim()
val password = editPassword.text.toString().trim()
if (username.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "Username dan password harus diisi", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
loginViewModel.login(username, password)
}
loginViewModel.isLoading.observe(this, Observer { loading ->
progressBar.visibility = if (loading) android.view.View.VISIBLE else android.view.View.GONE
btnLogin.isEnabled = !loading
})
loginViewModel.errorMessage.observe(this, Observer { error ->
error?.let {
Toast.makeText(this, it, Toast.LENGTH_LONG).show()
}
})
loginViewModel.loginSuccess.observe(this, Observer { token ->
token?.let {
// Simpan token menggunakan TokenManager
lifecycleScope.launch {
tokenManager.saveToken(it)
// Setelah token disimpan, observer di atas akan otomatis mengarahkan ke MainActivity
}
}
})
}
}
Jangan lupa import androidx.lifecycle.lifecycleScope dan tambahkan dependency jika perlu.
Langkah 9: Mengatur Activity Awal (Launch)
Ubah AndroidManifest.xml agar LoginActivity menjadi activity pertama (launcher), dan MainActivity tidak lagi menjadi launcher.
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity" />
Langkah 10: Menambahkan Logout
Untuk melengkapi, kita bisa tambahkan tombol logout di MainActivity yang menghapus token dan kembali ke login. Misalnya di menu atau di suatu tempat.
Contoh di MainActivity tambahkan tombol logout dan fungsi:
// Di MainActivity, tambahkan tombol logout misal di toolbar atau FAB khusus
// Panggil tokenManager.clearToken() dan arahkan ke LoginActivity
lifecycleScope.launch {
tokenManager.clearToken()
startActivity(Intent(this@MainActivity, LoginActivity::class.java))
finish()
}
Jangan lupa untuk mendapatkan instance TokenManager (bisa dari RetrofitInstance.tokenManager).
Langkah 11: Uji Coba
- Jalankan API.
- Hapus data aplikasi atau uninstall dari emulator agar token lama hilang.
- Jalankan aplikasi. Harusnya muncul halaman login.
- Coba login dengan username dan password yang sudah didaftarkan di API (misal joko/rahasia123). Jika berhasil, akan langsung masuk ke MainActivity.
- Coba akses fitur tambah kategori. Karena sekarang request menyertakan token, seharusnya berhasil (tidak error 401).
- Coba logout dan akses lagi, harus kembali ke login.
Selamat! Aplikasi Kamu Sekarang Punya Autentikasi
Sekarang aplikasi wisata kamu sudah aman. Hanya pengguna terdaftar yang bisa menambah, mengedit, atau menghapus data. Kamu telah mempelajari cara menyimpan token dengan DataStore, membuat interceptor, dan mengelola alur login.
Rangkuman
- DataStore: menyimpan token dengan aman dan asinkron.
- AuthInterceptor: menambahkan token ke setiap request Retrofit.
- LoginActivity: menangani input user dan memanggil API login.
- Setelah login, token disimpan dan otomatis digunakan untuk request berikutnya.
- Logout membersihkan token dan mengembalikan ke login.
Selanjutnya?
Di artikel berikutnya (Android Kotlin #12: Upload Gambar dari Galeri ke API), kita akan menambahkan fitur upload gambar untuk tempat wisata. Kamu akan belajar memilih gambar dari galeri, kompresi, dan mengirimnya ke endpoint upload API. Sampai jumpa!
Ditulis oleh Kakak programmer yang dulu juga sering lupa menambahkan header Authorization. Kalau ada pertanyaan, tulis di komentar ya!