Absensi GPS
Arsitektur Sistem
Backend
Frontend
API
  • Struktur
  • Alur Kerja
  • Implementasi GPS
  • Pengembangan dan Pengujian
Deployment
Pengujian
Arsitektur Sistem
Backend
Frontend
API
  • Struktur
  • Alur Kerja
  • Implementasi GPS
  • Pengembangan dan Pengujian
Deployment
Pengujian
  • Aplikasi Android

Aplikasi Android

Pendahuluan

Aplikasi Android untuk Absensi GPS adalah komponen penting dalam sistem yang memungkinkan siswa melakukan absensi menggunakan GPS pada perangkat mereka. Aplikasi ini dibangun menggunakan Kotlin dan mengimplementasikan fitur-fitur seperti scanning server, login, dan absensi dengan lokasi GPS.

Fitur Utama

  1. Splash Screen dengan Scanning Server

    • Mencari IP server dalam subnet yang sama
    • Menyimpan IP server untuk digunakan dalam request API
  2. Login Siswa

    • Autentikasi menggunakan ID Siswa dan Password
    • Menyimpan informasi siswa untuk sesi berikutnya
  3. Absensi dengan GPS

    • Absen Masuk dengan lokasi GPS
    • Absen Keluar dengan lokasi GPS
    • Menampilkan status absensi hari ini

Struktur Proyek

app/
├── src/
│   ├── main/
│   │   ├── java/com/example/absensigps/
│   │   │   ├── api/
│   │   │   │   ├── ApiClient.kt
│   │   │   │   ├── ApiService.kt
│   │   │   │   └── model/
│   │   │   │       ├── LoginRequest.kt
│   │   │   │       ├── LoginResponse.kt
│   │   │   │       ├── AbsensiRequest.kt
│   │   │   │       ├── AbsensiResponse.kt
│   │   │   │       └── ServerStatusResponse.kt
│   │   │   ├── ui/
│   │   │   │   ├── splash/
│   │   │   │   │   └── SplashActivity.kt
│   │   │   │   ├── login/
│   │   │   │   │   └── LoginActivity.kt
│   │   │   │   └── main/
│   │   │   │       └── MainActivity.kt
│   │   │   ├── util/
│   │   │   │   ├── Constants.kt
│   │   │   │   ├── LocationHelper.kt
│   │   │   │   ├── NetworkScanner.kt
│   │   │   │   └── SharedPreferencesManager.kt
│   │   │   └── AbsensiApp.kt
│   │   └── res/
│   │       ├── layout/
│   │       │   ├── activity_splash.xml
│   │       │   ├── activity_login.xml
│   │       │   └── activity_main.xml
│   │       ├── values/
│   │       └── drawable/
│   └── AndroidManifest.xml
└── build.gradle

Implementasi Fitur

1. Scanning Server

Pada saat aplikasi pertama kali dibuka, aplikasi akan melakukan scanning untuk menemukan server dalam subnet yang sama.

// NetworkScanner.kt
class NetworkScanner {
    interface ScanListener {
        fun onServerFound(serverIp: String)
        fun onScanComplete()
        fun onError(message: String)
    }
    
    fun scanNetwork(subnet: String, listener: ScanListener) {
        // Implementasi scanning server dalam subnet
        // Menggunakan endpoint /api/mobile/status untuk verifikasi
    }
}

// SplashActivity.kt
class SplashActivity : AppCompatActivity() {
    private lateinit var networkScanner: NetworkScanner
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_splash)
        
        // Cek apakah sudah ada server tersimpan
        val serverIp = SharedPreferencesManager.getServerIp()
        if (serverIp.isNotEmpty()) {
            // Verifikasi server
            verifyServer(serverIp)
        } else {
            // Scan server
            scanServer()
        }
    }
    
    private fun scanServer() {
        networkScanner = NetworkScanner()
        networkScanner.scanNetwork(getLocalSubnet(), object : NetworkScanner.ScanListener {
            override fun onServerFound(serverIp: String) {
                SharedPreferencesManager.saveServerIp(serverIp)
                navigateToLogin()
            }
            
            override fun onScanComplete() {
                if (SharedPreferencesManager.getServerIp().isEmpty()) {
                    showServerNotFoundDialog()
                }
            }
            
            override fun onError(message: String) {
                showErrorMessage(message)
            }
        })
    }
}

2. Login Siswa

Setelah server ditemukan, aplikasi akan menampilkan halaman login.

// LoginActivity.kt
class LoginActivity : AppCompatActivity() {
    private lateinit var binding: ActivityLoginBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLoginBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        binding.btnLogin.setOnClickListener {
            val idSiswa = binding.etIdSiswa.text.toString()
            val password = binding.etPassword.text.toString()
            
            if (validateInput(idSiswa, password)) {
                login(idSiswa, password)
            }
        }
    }
    
    private fun login(idSiswa: String, password: String) {
        val loginRequest = LoginRequest(idSiswa, password)
        
        ApiClient.apiService.login(loginRequest).enqueue(object : Callback<LoginResponse> {
            override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
                if (response.isSuccessful) {
                    val loginResponse = response.body()
                    if (loginResponse?.success == true) {
                        // Simpan data siswa
                        SharedPreferencesManager.saveSiswaId(loginResponse.data.siswa.id)
                        SharedPreferencesManager.saveSiswaName(loginResponse.data.siswa.nama)
                        
                        // Navigasi ke MainActivity
                        startActivity(Intent(this@LoginActivity, MainActivity::class.java))
                        finish()
                    } else {
                        showErrorMessage(loginResponse?.message ?: "Login gagal")
                    }
                } else {
                    showErrorMessage("Login gagal: ${response.message()}")
                }
            }
            
            override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
                showErrorMessage("Terjadi kesalahan: ${t.message}")
            }
        })
    }
}

3. Absensi dengan GPS

Halaman utama aplikasi menampilkan profil siswa dan tombol untuk absensi masuk atau keluar.

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var locationHelper: LocationHelper
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        // Inisialisasi LocationHelper
        locationHelper = LocationHelper(this)
        
        // Tampilkan data siswa
        displaySiswaInfo()
        
        // Cek status absensi
        checkAbsensiStatus()
        
        // Setup tombol absensi
        binding.btnAbsen.setOnClickListener {
            if (binding.btnAbsen.text == "Absen Masuk") {
                confirmAbsenMasuk()
            } else {
                confirmAbsenKeluar()
            }
        }
    }
    
    private fun checkAbsensiStatus() {
        val siswaId = SharedPreferencesManager.getSiswaId()
        
        ApiClient.apiService.getAbsensiStatus(siswaId).enqueue(object : Callback<AbsensiStatusResponse> {
            override fun onResponse(call: Call<AbsensiStatusResponse>, response: Response<AbsensiStatusResponse>) {
                if (response.isSuccessful) {
                    val statusResponse = response.body()
                    if (statusResponse?.success == true) {
                        updateAbsensiUI(statusResponse.data.absensi)
                    }
                }
            }
            
            override fun onFailure(call: Call<AbsensiStatusResponse>, t: Throwable) {
                showErrorMessage("Gagal mendapatkan status absensi: ${t.message}")
            }
        })
    }
    
    private fun updateAbsensiUI(absensi: AbsensiStatus) {
        if (absensi.sudah_absen_masuk && !absensi.sudah_absen_keluar) {
            // Sudah absen masuk, belum absen keluar
            binding.btnAbsen.text = "Absen Keluar"
            binding.tvStatusAbsen.text = "Sudah Absen Masuk: ${absensi.waktu_masuk}"
        } else if (absensi.sudah_absen_masuk && absensi.sudah_absen_keluar) {
            // Sudah absen masuk dan keluar
            binding.btnAbsen.isEnabled = false
            binding.tvStatusAbsen.text = "Absensi Hari Ini Selesai"
        } else {
            // Belum absen
            binding.btnAbsen.text = "Absen Masuk"
            binding.tvStatusAbsen.text = "Belum Absen"
        }
    }
    
    private fun confirmAbsenMasuk() {
        AlertDialog.Builder(this)
            .setTitle("Konfirmasi Absen Masuk")
            .setMessage("Apakah Anda yakin ingin melakukan absen masuk?")
            .setPositiveButton("Ya") { _, _ ->
                absenMasuk()
            }
            .setNegativeButton("Tidak", null)
            .show()
    }
    
    private fun absenMasuk() {
        locationHelper.getCurrentLocation { location ->
            val lokasi = "${location.latitude},${location.longitude}"
            val siswaId = SharedPreferencesManager.getSiswaId()
            
            val request = AbsensiRequest(siswaId, lokasi)
            
            ApiClient.apiService.absenMasuk(request).enqueue(object : Callback<AbsensiResponse> {
                override fun onResponse(call: Call<AbsensiResponse>, response: Response<AbsensiResponse>) {
                    if (response.isSuccessful) {
                        val absensiResponse = response.body()
                        if (absensiResponse?.success == true) {
                            showSuccessMessage("Absen masuk berhasil")
                            checkAbsensiStatus()
                        } else {
                            showErrorMessage(absensiResponse?.message ?: "Absen masuk gagal")
                        }
                    } else {
                        showErrorMessage("Absen masuk gagal: ${response.message()}")
                    }
                }
                
                override fun onFailure(call: Call<AbsensiResponse>, t: Throwable) {
                    showErrorMessage("Terjadi kesalahan: ${t.message}")
                }
            })
        }
    }
    
    private fun confirmAbsenKeluar() {
        AlertDialog.Builder(this)
            .setTitle("Konfirmasi Absen Keluar")
            .setMessage("Apakah Anda yakin ingin melakukan absen keluar?")
            .setPositiveButton("Ya") { _, _ ->
                absenKeluar()
            }
            .setNegativeButton("Tidak", null)
            .show()
    }
    
    private fun absenKeluar() {
        // Implementasi absen keluar (mirip dengan absen masuk)
    }
}

API Client

Untuk komunikasi dengan backend, aplikasi menggunakan Retrofit.

// ApiClient.kt
object ApiClient {
    private const val BASE_URL = "http://${SharedPreferencesManager.getServerIp()}/api/"
    
    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(getOkHttpClient())
            .build()
    }
    
    val apiService: ApiService by lazy {
        retrofit.create(ApiService::class.java)
    }
    
    private fun getOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor { chain ->
                val request = chain.request().newBuilder()
                    .addHeader("Authorization", "Bearer ${Constants.API_KEY}")
                    .build()
                chain.proceed(request)
            }
            .build()
    }
}

// ApiService.kt
interface ApiService {
    @GET("mobile/status")
    fun checkServerStatus(): Call<ServerStatusResponse>
    
    @POST("mobile/auth/login")
    fun login(@Body request: LoginRequest): Call<LoginResponse>
    
    @POST("mobile/absensi/masuk")
    fun absenMasuk(@Body request: AbsensiRequest): Call<AbsensiResponse>
    
    @POST("mobile/absensi/keluar")
    fun absenKeluar(@Body request: AbsensiRequest): Call<AbsensiResponse>
    
    @GET("mobile/absensi/status")
    fun getAbsensiStatus(@Query("siswa_id") siswaId: Int): Call<AbsensiStatusResponse>
}

Model Data

// LoginRequest.kt
data class LoginRequest(
    val id_siswa: String,
    val password: String
)

// LoginResponse.kt
data class LoginResponse(
    val success: Boolean,
    val message: String,
    val data: LoginData
)

data class LoginData(
    val siswa: Siswa
)

data class Siswa(
    val id: Int,
    val id_siswa: String,
    val nama: String,
    val kelas: Kelas
)

data class Kelas(
    val id: Int,
    val nama_kelas: String
)

// AbsensiRequest.kt
data class AbsensiRequest(
    val siswa_id: Int,
    val lokasi: String
)

// AbsensiResponse.kt
data class AbsensiResponse(
    val success: Boolean,
    val message: String,
    val data: AbsensiData?
)

data class AbsensiData(
    val tanggal: String,
    val waktu_masuk: String?,
    val waktu_keluar: String?,
    val status: String
)

// AbsensiStatusResponse.kt
data class AbsensiStatusResponse(
    val success: Boolean,
    val data: AbsensiStatusData
)

data class AbsensiStatusData(
    val siswa: Siswa,
    val absensi: AbsensiStatus
)

data class AbsensiStatus(
    val tanggal: String,
    val sudah_absen_masuk: Boolean,
    val sudah_absen_keluar: Boolean,
    val waktu_masuk: String?,
    val waktu_keluar: String?,
    val status: String
)

Helper Class

LocationHelper

// LocationHelper.kt
class LocationHelper(private val context: Context) {
    private lateinit var fusedLocationClient: FusedLocationProviderClient
    
    init {
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
    }
    
    fun getCurrentLocation(callback: (Location) -> Unit) {
        if (checkLocationPermission()) {
            fusedLocationClient.lastLocation
                .addOnSuccessListener { location ->
                    if (location != null) {
                        callback(location)
                    } else {
                        requestNewLocationData(callback)
                    }
                }
        } else {
            requestLocationPermission()
        }
    }
    
    private fun requestNewLocationData(callback: (Location) -> Unit) {
        val locationRequest = LocationRequest.create().apply {
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
            interval = 5000
            fastestInterval = 2000
        }
        
        if (checkLocationPermission()) {
            fusedLocationClient.requestLocationUpdates(
                locationRequest,
                object : LocationCallback() {
                    override fun onLocationResult(locationResult: LocationResult) {
                        locationResult.lastLocation?.let { callback(it) }
                        fusedLocationClient.removeLocationUpdates(this)
                    }
                },
                Looper.getMainLooper()
            )
        }
    }
    
    private fun checkLocationPermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.ACCESS_FINE_LOCATION
        ) == PackageManager.PERMISSION_GRANTED
    }
    
    private fun requestLocationPermission() {
        ActivityCompat.requestPermissions(
            context as Activity,
            arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
            LOCATION_PERMISSION_REQUEST_CODE
        )
    }
    
    companion object {
        private const val LOCATION_PERMISSION_REQUEST_CODE = 1001
    }
}

SharedPreferencesManager

// SharedPreferencesManager.kt
object SharedPreferencesManager {
    private const val PREF_NAME = "AbsensiGpsPrefs"
    private const val KEY_SERVER_IP = "server_ip"
    private const val KEY_SISWA_ID = "siswa_id"
    private const val KEY_SISWA_NAME = "siswa_name"
    
    private fun getSharedPreferences(context: Context): SharedPreferences {
        return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
    }
    
    fun saveServerIp(serverIp: String) {
        getSharedPreferences(AbsensiApp.instance).edit().putString(KEY_SERVER_IP, serverIp).apply()
    }
    
    fun getServerIp(): String {
        return getSharedPreferences(AbsensiApp.instance).getString(KEY_SERVER_IP, "") ?: ""
    }
    
    fun saveSiswaId(siswaId: Int) {
        getSharedPreferences(AbsensiApp.instance).edit().putInt(KEY_SISWA_ID, siswaId).apply()
    }
    
    fun getSiswaId(): Int {
        return getSharedPreferences(AbsensiApp.instance).getInt(KEY_SISWA_ID, 0)
    }
    
    fun saveSiswaName(siswaName: String) {
        getSharedPreferences(AbsensiApp.instance).edit().putString(KEY_SISWA_NAME, siswaName).apply()
    }
    
    fun getSiswaName(): String {
        return getSharedPreferences(AbsensiApp.instance).getString(KEY_SISWA_NAME, "") ?: ""
    }
    
    fun clearSession() {
        getSharedPreferences(AbsensiApp.instance).edit().remove(KEY_SISWA_ID).remove(KEY_SISWA_NAME).apply()
    }
}

Manifest

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.absensigps">
    
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    
    <application
        android:name=".AbsensiApp"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AbsensiGps"
        android:usesCleartextTraffic="true">
        
        <activity
            android:name=".ui.splash.SplashActivity"
            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=".ui.login.LoginActivity" />
        <activity android:name=".ui.main.MainActivity" />
    </application>
</manifest>

Kesimpulan

Aplikasi Android untuk Absensi GPS menyediakan antarmuka yang mudah digunakan bagi siswa untuk melakukan absensi dengan GPS. Aplikasi ini terintegrasi dengan backend Laravel melalui API dan mengimplementasikan fitur-fitur seperti scanning server, login, dan absensi dengan lokasi GPS.

Dengan aplikasi ini, siswa dapat dengan mudah melakukan absensi tanpa perlu hadir secara fisik di tempat absensi, dan guru dapat memantau kehadiran siswa secara real-time melalui platform admin.

Last Updated:: 5/15/25, 9:30 PM
Contributors: Nur Wahyudin