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
Splash Screen dengan Scanning Server
- Mencari IP server dalam subnet yang sama
- Menyimpan IP server untuk digunakan dalam request API
Login Siswa
- Autentikasi menggunakan ID Siswa dan Password
- Menyimpan informasi siswa untuk sesi berikutnya
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.