Cara Membuat Aplikasi Laundry dengan Android Studio

Di tengah kesibukan modern, layanan laundry menjadi kebutuhan esensial. Memiliki aplikasi mobile dapat merevolusi bisnis laundry, memungkinkan pelanggan memesan layanan dengan mudah, melacak status cucian, dan melihat detail harga secara transparan. Artikel ini akan memandu langkah demi langkah dalam membangun aplikasi laundry yang fungsional dan menarik menggunakan Android Studio, platform pengembangan aplikasi Android terkemuka.
Target Pembaca:
- Pengembang Android pemula hingga menengah.
- Pemilik bisnis laundry yang ingin mengotomatisasi dan memperluas layanan.
Prasyarat:
- Pemahaman dasar tentang pemrograman Java atau Kotlin.
- Pemahaman dasar tentang XML untuk desain layout.
- Instalasi Android Studio di komputer.
- Pemahaman dasar tentang konsep basis data.
Arsitektur Aplikasi Laundry (Gambaran Umum)
Aplikasi laundry umumnya memiliki dua bagian utama:
- Aplikasi Pelanggan: Digunakan pelanggan untuk melihat daftar jasa, jenis barang, daftar harga, membuat pesanan, melacak pesanan, dan melakukan pembayaran.
- Aplikasi Admin/Manajemen (Opsional, bisa juga berbasis web): Digunakan pemilik laundry untuk mengelola jasa, daftar harga, pesanan, pelanggan, dan laporan keuangan.
Artikel ini akan lebih fokus pada pengembangan aplikasi sisi pelanggan.
Langkah 1: Persiapan dan Konfigurasi Android Studio
- Instalasi Android Studio: Pastikan memiliki versi terbaru Android Studio. Dapat mengunduhnya dari situs resmi developer.android.com.
- Membuat Proyek Baru:
- Buka Android Studio.
- Pilih "Start a new Android Studio project".
- Pilih template "Empty Activity" atau "Basic Activity".
- Berikan nama aplikasi yang relevan (misalnya, "AplikasiLaundryKu").
- Pilih bahasa pemrograman (Java atau Kotlin, disarankan Kotlin).
- Pilih SDK minimum yang didukung.
Langkah 2: Konfigurasi Build.gradle (Module: app)
File build.gradle (Module: app)
adalah tempat mendeklarasikan dependensi dan pengaturan build proyek. Kita akan menambahkan library yang diperlukan untuk fungsionalitas seperti Firebase Authentication, desain UI, basis data, dan lainnya.
Buka file build.gradle (Module: app)
dan pastikan bagian dependencies
terlihat mirip dengan ini (versi library bisa berbeda tergantung waktu):
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android' // Jika menggunakan Kotlin
id 'com.google.gms.google-services' // Tambahkan ini untuk Firebase
}
android {
namespace 'com.aplikasilaundryku' // Ganti dengan namespace aplikasi
compileSdk 34 // Contoh versi SDK
defaultConfig {
applicationId "com.aplikasilaundryku" // Ganti dengan ID aplikasi
minSdk 24 // Contoh versi SDK minimum
targetSdk 34 // Contoh versi target SDK
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true // Aktifkan View Binding untuk akses UI yang lebih mudah
}
}
dependencies {
// AndroidX Libraries
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.12.0' // Desain Material
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// Firebase (Pastikan telah mengonfigurasi Firebase Project terlebih dahulu)
implementation 'com.google.firebase:firebase-auth:23.0.0' // Firebase Authentication
implementation 'com.google.firebase:firebase-firestore:25.0.0' // Firestore (untuk data jasa, harga, pesanan)
implementation 'com.google.firebase:firebase-storage:21.0.0' // Firebase Storage (untuk gambar jika ada)
// RecyclerView dan CardView (Untuk menampilkan daftar jasa/barang)
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.cardview:cardview:1.0.0'
// Glide atau Picasso (Untuk memuat gambar dari URL, jika ada)
implementation 'com.github.bumptech.glide:glide:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
// Opsional: Untuk navigasi bottom bar atau drawer
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
Penting:
- Setelah menambahkan dependensi, klik "Sync Now" pada notifikasi yang muncul di Android Studio agar perubahannya diterapkan.
- Pastikan sudah mengonfigurasi proyek Firebase di konsol Firebase dan mengunduh file
google-services.json
ke direktoriapp/
di proyek Android.
Langkah 3: Desain Antarmuka Pengguna (UI/UX) Aplikasi Pelanggan

Desain yang intuitif dan menarik adalah kunci untuk pengalaman pengguna yang baik. Rencanakan setiap layar dengan cermat.
- Sketsa Wireframe dan Mockup: Buat sketsa kasar atau mockup digital untuk setiap layar penting:
- Layar Selamat Datang/Splash Screen.
- Layar Autentikasi (Login/Register).
- Layar Beranda (Menampilkan Jasa Populer/Promo).
- Layar Daftar Jasa Laundry (Cuci Kering, Cuci Setrika, Setrika Saja, dll.).
- Layar Detail Jasa (Menampilkan jenis barang yang dilayani dan harga per jenis).
- Layar Keranjang Pesanan/Form Pemesanan.
- Layar Riwayat Pesanan.
- Layar Profil Pengguna.
- Komponen UI Penting:
Activity
danFragment
untuk setiap layar atau bagian layar.ConstraintLayout
,LinearLayout
,RelativeLayout
untuk menata elemen UI.TextView
,ImageView
,Button
,EditText
.RecyclerView
denganCardView
untuk menampilkan daftar jasa, jenis barang, atau riwayat pesanan dengan rapi.Spinner
atauRadioGroup
untuk pilihan jenis barang atau opsi tambahan.BottomNavigationView
atauNavigationView
(Drawer) untuk navigasi utama.FloatingActionButton
untuk memulai pesanan baru.
- Material Design: Ikuti pedoman Material Design dari Google untuk tampilan yang modern dan konsisten.
Langkah 4: Fungsionalitas Autentikasi Pengguna (Login & Register)
Autentikasi adalah gerbang utama bagi pengguna. Kita akan membuat RegisterActivity
dan LoginActivity
menggunakan Firebase Authentication.
a. Class RegisterActivity
(Pendaftaran Pengguna Baru)

Buat layout XML (misal: activity_register.xml
) dengan EditText
untuk email, kata sandi, dan konfirmasi kata sandi, serta Button
untuk mendaftar.
<!-- activity_register.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".RegisterActivity">
<TextView
android:id="@+id/tv_register_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Daftar Akun Laundry"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="64dp"/>
<EditText
android:id="@+id/et_register_email"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress"
android:padding="12dp"
android:background="@drawable/rounded_edittext_background"
app:layout_constraintTop_toBottomOf="@id/tv_register_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp"/>
<EditText
android:id="@+id/et_register_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Kata Sandi"
android:inputType="textPassword"
android:padding="12dp"
android:background="@drawable/rounded_edittext_background"
app:layout_constraintTop_toBottomOf="@id/et_register_email"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
<Button
android:id="@+id/btn_register"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Daftar Sekarang"
android:backgroundTint="@color/purple_500"
android:textColor="@android:color/white"
android:padding="12dp"
app:layout_constraintTop_toBottomOf="@id/et_register_password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"/>
<TextView
android:id="@+id/tv_goto_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sudah punya akun? Masuk di sini"
android:textColor="@color/purple_700"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="@id/btn_register"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Untuk kode Kotlin/Java (RegisterActivity.kt
atau RegisterActivity.java
), gunakan FirebaseAuth.getInstance().createUserWithEmailAndPassword()
untuk mendaftarkan pengguna.
// RegisterActivity.kt (Kotlin)
package com.aplikasilaundryku
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.aplikasilaundryku.databinding.ActivityRegisterBinding
import com.google.firebase.auth.FirebaseAuth
class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding
private lateinit var auth: FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRegisterBinding.inflate(layoutInflater)
setContentView(binding.root)
auth = FirebaseAuth.getInstance()
binding.btnRegister.setOnClickListener {
val email = binding.etRegisterEmail.text.toString().trim()
val password = binding.etRegisterPassword.text.toString().trim()
if (email.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "Email dan kata sandi tidak boleh kosong", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
Toast.makeText(this, "Pendaftaran berhasil!", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, LoginActivity::class.java)) // Arahkan ke Login
finish()
} else {
Toast.makeText(this, "Pendaftaran gagal: ${task.exception?.message}", Toast.LENGTH_LONG).show()
}
}
}
binding.tvGotoLogin.setOnClickListener {
startActivity(Intent(this, LoginActivity::class.java))
}
}
}
b. Class LoginActivity
(Masuk Pengguna)

Buat layout XML (misal: activity_login.xml
) yang serupa dengan Register, tetapi tanpa konfirmasi kata sandi.
<!-- activity_login.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".LoginActivity">
<TextView
android:id="@+id/tv_login_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Masuk ke Akun Laundry"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="64dp"/>
<EditText
android:id="@+id/et_login_email"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress"
android:padding="12dp"
android:background="@drawable/rounded_edittext_background"
app:layout_constraintTop_toBottomOf="@id/tv_login_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp"/>
<EditText
android:id="@+id/et_login_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Kata Sandi"
android:inputType="textPassword"
android:padding="12dp"
android:background="@drawable/rounded_edittext_background"
app:layout_constraintTop_toBottomOf="@id/et_login_email"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
<Button
android:id="@+id/btn_login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Masuk"
android:backgroundTint="@color/purple_500"
android:textColor="@android:color/white"
android:padding="12dp"
app:layout_constraintTop_toBottomOf="@id/et_login_password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"/>
<TextView
android:id="@+id/tv_goto_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Belum punya akun? Daftar di sini"
android:textColor="@color/purple_700"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="@id/btn_login"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Untuk kode Kotlin/Java (LoginActivity.kt
atau LoginActivity.java
), gunakan FirebaseAuth.getInstance().signInWithEmailAndPassword()
.
// LoginActivity.kt (Kotlin)
package com.aplikasilaundryku
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.aplikasilaundryku.databinding.ActivityLoginBinding
import com.google.firebase.auth.FirebaseAuth
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private lateinit var auth: FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
auth = FirebaseAuth.getInstance()
// Jika pengguna sudah login, langsung ke HomeActivity
if (auth.currentUser != null) {
startActivity(Intent(this, HomeActivity::class.java)) // Ganti dengan Activity utama
finish()
}
binding.btnLogin.setOnClickListener {
val email = binding.etLoginEmail.text.toString().trim()
val password = binding.etLoginPassword.text.toString().trim()
if (email.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "Email dan kata sandi tidak boleh kosong", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
Toast.makeText(this, "Login berhasil!", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, HomeActivity::class.java)) // Ganti dengan Activity utama
finish()
} else {
Toast.makeText(this, "Login gagal: ${task.exception?.message}", Toast.LENGTH_LONG).show()
}
}
}
binding.tvGotoRegister.setOnClickListener {
startActivity(Intent(this, RegisterActivity::class.java))
}
}
}
c. Konfigurasi AndroidManifest.xml
Pastikan mendeklarasikan kedua Activity ini di file AndroidManifest.xml
.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AplikasiLaundryKu"
tools:targetApi="31">
<!-- LoginActivity sebagai Activity utama yang pertama kali dijalankan -->
<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>
<!-- RegisterActivity -->
<activity
android:name=".RegisterActivity"
android:exported="false" />
<!-- HomeActivity atau Activity utama setelah login -->
<activity
android:name=".HomeActivity"
android:exported="false" />
</application>
</manifest>
Catatan: Ubah HomeActivity
menjadi nama Activity utama aplikasi setelah pengguna berhasil login. Pastikan intent-filter
MAIN
dan LAUNCHER
mengarah ke LoginActivity
agar itu menjadi layar pembuka.
Langkah 5: Manajemen Data dan Basis Data (Firebase Firestore)
Kita akan menggunakan Firebase Cloud Firestore sebagai basis data NoSQL real-time untuk menyimpan data aplikasi laundry. Ini memudahkan sinkronisasi data antar perangkat dan pengelolaan backend.
a. Struktur Data di Firestore:
Definisikan struktur data untuk koleksi-koleksi utama:
services
(Jasa Laundry):id
: String (auto-generated ID)name
: String (misal: "Cuci Kering", "Cuci Setrika", "Setrika Saja")description
: String (deskripsi singkat jasa)basePricePerKg
: Double (harga dasar per kilogram)unit
: String (misal: "KG", "PCS", "Satuan")imageUrl
: String (opsional, URL gambar jasa)
items
(Jenis Barang/Pakaian):id
: String (auto-generated ID)name
: String (misal: "Baju", "Celana", "Sprei", "Selimut")priceMultiplier
: Double (faktor pengali harga jika ada, misal: 1.0 untuk normal, 1.2 untuk pakaian tebal)unit
: String (misal: "KG", "PCS")
orders
(Pesanan Pelanggan):orderId
: String (auto-generated ID)userId
: String (ID pengguna yang memesan)serviceId
: String (ID jasa yang dipilih)serviceName
: Stringitems
: Array of Objects (setiap objek berisiitemId
,itemName
,quantity
,subtotalPrice
)totalAmount
: DoublepickupAddress
: StringdeliveryAddress
: StringpickupDateTime
: TimestampdeliveryDateTime
: Timestamp (opsional, setelah selesai)status
: String (misal: "Pending", "Diproses", "Siap Ambil", "Selesai", "Dibatalkan")orderDate
: Timestampnotes
: String (catatan tambahan dari pelanggan)
b. Model Data (Kotlin/Java Data Classes)
Buat kelas data (data class di Kotlin atau POJO di Java) yang merepresentasikan struktur data di Firestore.
// Service.kt (Kotlin)
package com.aplikasilaundryku.models
data class Service(
val id: String = "",
val name: String = "",
val description: String = "",
val basePricePerKg: Double = 0.0,
val unit: String = "",
val imageUrl: String = ""
)
// Item.kt (Kotlin) - untuk jenis pakaian yang dilayani
package com.aplikasilaundryku.models
data class Item(
val id: String = "",
val name: String = "",
val priceMultiplier: Double = 1.0, // Pengali harga jika ada perbedaan
val unit: String = "" // KG atau PCS
)
// Order.kt (Kotlin)
package com.aplikasilaundryku.models
import com.google.firebase.firestore.ServerTimestamp
import java.util.Date
data class Order(
val orderId: String = "",
val userId: String = "",
val serviceId: String = "",
val serviceName: String = "",
val items: List<OrderItem> = emptyList(), // Daftar item dalam pesanan
val totalAmount: Double = 0.0,
val pickupAddress: String = "",
val deliveryAddress: String = "",
val pickupDateTime: Date? = null,
val deliveryDateTime: Date? = null,
val status: String = "Pending",
@ServerTimestamp
val orderDate: Date? = null,
val notes: String = ""
)
data class OrderItem(
val itemId: String = "",
val itemName: String = "",
val quantity: Double = 0.0, // Bisa berat (KG) atau jumlah (PCS)
val subtotalPrice: Double = 0.0
)
Langkah 6: Fungsionalitas Inti Aplikasi
a. Menampilkan Daftar Jasa Laundry (ServiceListActivity
atau ServiceListFragment
)

Layar ini akan menampilkan daftar jasa laundry yang tersedia (misal: "Cuci Kering", "Cuci Setrika", "Setrika Saja").
- Layout (
activity_service_list.xml
): GunakanRecyclerView
untuk menampilkan daftar jasa. Setiap item daftar bisa berupaCardView
untuk tampilan yang lebih menarik. - Adapter (
ServiceAdapter.kt
): Buat adapter untukRecyclerView
yang mengikat dataService
ke tampilan item individual. - Fetching Data: Di
ServiceListActivity
/Fragment
, inisialisasiFirebaseFirestore
dan ambil data dari koleksiservices
.
// ServiceListActivity.kt (Kotlin)
package com.aplikasilaundryku
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.aplikasilaundryku.databinding.ActivityServiceListBinding
import com.aplikasilaundryku.models.Service
import com.google.firebase.firestore.FirebaseFirestore
class ServiceListActivity : AppCompatActivity() {
private lateinit var binding: ActivityServiceListBinding
private lateinit var firestore: FirebaseFirestore
private lateinit var serviceAdapter: ServiceAdapter // Perlu membuat adapter ini
private val serviceList = mutableListOf<Service>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityServiceListBinding.inflate(layoutInflater)
setContentView(binding.root)
firestore = FirebaseFirestore.getInstance()
setupRecyclerView()
fetchServices()
}
private fun setupRecyclerView() {
serviceAdapter = ServiceAdapter(serviceList) { service ->
// Ketika item jasa diklik, pindah ke layar detail jasa/pemesanan
val intent = Intent(this, OrderFormActivity::class.java) // Ganti dengan Activity Form Pemesanan
intent.putExtra("serviceId", service.id)
intent.putExtra("serviceName", service.name)
intent.putExtra("basePricePerKg", service.basePricePerKg)
intent.putExtra("unit", service.unit)
startActivity(intent)
}
binding.rvServices.apply {
layoutManager = LinearLayoutManager(this@ServiceListActivity)
adapter = serviceAdapter
}
}
private fun fetchServices() {
firestore.collection("services")
.get()
.addOnSuccessListener { result ->
serviceList.clear()
for (document in result) {
val service = document.toObject(Service::class.java)
serviceList.add(service.copy(id = document.id)) // Salin ID dokumen
}
serviceAdapter.notifyDataSetChanged()
}
.addOnFailureListener { exception ->
Toast.makeText(this, "Gagal memuat jasa: ${exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
Perlu membuat ServiceAdapter.kt
dan item_service.xml
untuk layout setiap item di RecyclerView.
// ServiceAdapter.kt (Kotlin)
package com.aplikasilaundryku
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.aplikasilaundryku.models.Service // Sesuaikan package
class ServiceAdapter(
private val serviceList: List<Service>,
private val onItemClick: (Service) -> Unit
) : RecyclerView.Adapter<ServiceAdapter.ServiceViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServiceViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_service, parent, false)
return ServiceViewHolder(view)
}
override fun onBindViewHolder(holder: ServiceViewHolder, position: Int) {
val service = serviceList[position]
holder.bind(service)
}
override fun getItemCount(): Int = serviceList.size
inner class ServiceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvServiceName: TextView = itemView.findViewById(R.id.tv_service_name)
private val tvServiceDescription: TextView = itemView.findViewById(R.id.tv_service_description)
private val tvServicePrice: TextView = itemView.findViewById(R.id.tv_service_price)
// Tambahkan ImageView jika ada gambar
fun bind(service: Service) {
tvServiceName.text = service.name
tvServiceDescription.text = service.description
tvServicePrice.text = "Mulai dari Rp ${service.basePricePerKg} / ${service.unit}"
// Load gambar jika ada: Glide.with(itemView.context).load(service.imageUrl).into(imageView)
itemView.setOnClickListener {
onItemClick(service)
}
}
}
}
<!-- item_service.xml -->
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_service_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="18sp"
android:text="Nama Jasa Laundry"/>
<TextView
android:id="@+id/tv_service_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:text="Deskripsi singkat tentang jasa ini."/>
<TextView
android:id="@+id/tv_service_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textStyle="italic"
android:textSize="16sp"
android:text="Mulai dari Rp 0.000 / KG"/>
<!-- Tambahkan ImageView jika ingin menampilkan gambar jasa -->
<!-- <ImageView
android:id="@+id/iv_service_image"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginTop="8dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image" /> -->
</LinearLayout>
</androidx.cardview.widget.CardView>
b. Menampilkan Jenis Barang dan Harga per Pesanan (OrderFormActivity
)
Layar ini akan memungkinkan pelanggan memilih jenis barang, memasukkan jumlah/berat, dan melihat estimasi harga.
- Layout (
activity_order_form.xml
):TextView
untuk menampilkan nama jasa yang dipilih.RecyclerView
atauLinearLayout
dinamis untuk daftar jenis barang yang bisa ditambahkan/diubah jumlahnya.EditText
untuk input berat/jumlah.TextView
untuk menampilkan total estimasi harga.Button
untuk melanjutkan ke konfirmasi/pembayaran.- Input
EditText
untuk alamat penjemputan/pengiriman, tanggal/waktu.
- Fetching Data: Ambil data
items
dari Firestore dan tampilkan. Hitung total harga secara dinamis berdasarkan input pengguna. - Logika Penghitungan Harga:
- Harga =
basePricePerKg
(dari jasa) *quantity
*priceMultiplier
(dari jenis barang). - Jumlahkan semua subtotal untuk mendapatkan total pesanan.
- Harga =
- Menyimpan Pesanan: Setelah konfirmasi, simpan objek
Order
baru ke koleksiorders
di Firestore.
// OrderFormActivity.kt (Contoh Sederhana)
package com.aplikasilaundryku
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.aplikasilaundryku.databinding.ActivityOrderFormBinding
import com.aplikasilaundryku.models.Item
import com.aplikasilaundryku.models.Order
import com.aplikasilaundryku.models.OrderItem
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
import java.util.Date
class OrderFormActivity : AppCompatActivity() {
private lateinit var binding: ActivityOrderFormBinding
private lateinit var firestore: FirebaseFirestore
private lateinit var auth: FirebaseAuth
private lateinit var itemAdapter: ItemSelectionAdapter // Perlu membuat adapter ini
private val selectedItems = mutableMapOf<String, Pair<Item, Double>>() // itemId to (Item, quantity)
private var currentServiceId: String = ""
private var currentServiceName: String = ""
private var currentBasePricePerKg: Double = 0.0
private var currentUnit: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityOrderFormBinding.inflate(layoutInflater)
setContentView(binding.root)
firestore = FirebaseFirestore.getInstance()
auth = FirebaseAuth.getInstance()
getIntentData()
displayServiceInfo()
setupItemSelectionRecyclerView()
fetchAvailableItems()
binding.btnPlaceOrder.setOnClickListener {
placeOrder()
}
}
private fun getIntentData() {
currentServiceId = intent.getStringExtra("serviceId") ?: ""
currentServiceName = intent.getStringExtra("serviceName") ?: "Jasa Tidak Dikenal"
currentBasePricePerKg = intent.getDoubleExtra("basePricePerKg", 0.0)
currentUnit = intent.getStringExtra("unit") ?: "KG"
}
private fun displayServiceInfo() {
binding.tvSelectedService.text = "Jasa yang dipilih: $currentServiceName"
// Tampilkan informasi harga dasar jika perlu
}
private fun setupItemSelectionRecyclerView() {
itemAdapter = ItemSelectionAdapter(mutableListOf()) { item, quantity ->
if (quantity > 0.0) {
selectedItems[item.id] = Pair(item, quantity)
} else {
selectedItems.remove(item.id)
}
updateTotalPrice()
}
binding.rvItems.apply {
layoutManager = LinearLayoutManager(this@OrderFormActivity)
adapter = itemAdapter
}
}
private fun fetchAvailableItems() {
firestore.collection("items")
.get()
.addOnSuccessListener { result ->
val items = result.toObjects(Item::class.java).map { it.copy(id = it.id) }
itemAdapter.updateItems(items)
}
.addOnFailureListener { exception ->
Toast.makeText(this, "Gagal memuat jenis barang: ${exception.message}", Toast.LENGTH_SHORT).show()
}
}
private fun updateTotalPrice() {
var total = 0.0
val orderItems = mutableListOf<OrderItem>()
selectedItems.values.forEach { (item, quantity) ->
val subtotal = currentBasePricePerKg * quantity * item.priceMultiplier
total += subtotal
orderItems.add(OrderItem(item.id, item.name, quantity, subtotal))
}
binding.tvTotalPrice.text = "Total Estimasi: Rp ${String.format("%.2f", total)}"
}
private fun placeOrder() {
val userId = auth.currentUser?.uid
if (userId == null) {
Toast.makeText(this, "Harus login untuk membuat pesanan", Toast.LENGTH_SHORT).show()
return
}
val pickupAddress = binding.etPickupAddress.text.toString().trim()
val deliveryAddress = binding.etDeliveryAddress.text.toString().trim()
val notes = binding.etNotes.text.toString().trim()
if (pickupAddress.isEmpty() || deliveryAddress.isEmpty() || selectedItems.isEmpty()) {
Toast.makeText(this, "Lengkapi semua detail pesanan dan pilih setidaknya satu item", Toast.LENGTH_SHORT).show()
return
}
val orderItems = selectedItems.values.map { (item, quantity) ->
OrderItem(item.id, item.name, quantity, currentBasePricePerKg * quantity * item.priceMultiplier)
}
val totalAmount = orderItems.sumOf { it.subtotalPrice }
val order = Order(
userId = userId,
serviceId = currentServiceId,
serviceName = currentServiceName,
items = orderItems,
totalAmount = totalAmount,
pickupAddress = pickupAddress,
deliveryAddress = deliveryAddress,
pickupDateTime = Date(), // Atau dari DateTimePicker
notes = notes,
status = "Pending"
)
firestore.collection("orders")
.add(order)
.addOnSuccessListener { documentReference ->
// Perbarui ID pesanan dengan ID dokumen Firestore yang dihasilkan
firestore.collection("orders").document(documentReference.id)
.update("orderId", documentReference.id)
.addOnSuccessListener {
Toast.makeText(this, "Pesanan berhasil dibuat! ID: ${documentReference.id}", Toast.LENGTH_LONG).show()
// Arahkan ke Riwayat Pesanan atau layar konfirmasi
finish()
}
.addOnFailureListener { e ->
Toast.makeText(this, "Gagal memperbarui ID pesanan: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
.addOnFailureListener { e ->
Toast.makeText(this, "Gagal membuat pesanan: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
Perlu membuat ItemSelectionAdapter.kt
dan item_item_selection.xml
untuk layout setiap item yang dapat dipilih di RecyclerView.
// ItemSelectionAdapter.kt (Kotlin)
package com.aplikasilaundryku
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.RecyclerView
import com.aplikasilaundryku.models.Item // Sesuaikan package
class ItemSelectionAdapter(
private val items: MutableList<Item>,
private val onQuantityChanged: (Item, Double) -> Unit
) : RecyclerView.Adapter<ItemSelectionAdapter.ItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_item_selection, parent, false)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
}
override fun getItemCount(): Int = items.size
fun updateItems(newItems: List<Item>) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged()
}
inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvItemName: TextView = itemView.findViewById(R.id.tv_item_name)
private val tvItemUnit: TextView = itemView.findViewById(R.id.tv_item_unit)
private val etQuantity: EditText = itemView.findViewById(R.id.et_quantity)
fun bind(item: Item) {
tvItemName.text = item.name
tvItemUnit.text = "(${item.unit})"
etQuantity.setText("") // Reset quantity input
etQuantity.doAfterTextChanged { editable ->
val quantity = editable.toString().toDoubleOrNull() ?: 0.0
onQuantityChanged(item, quantity)
}
}
}
}
<!-- item_item_selection.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_item_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Nama Barang"
android:textSize="16sp"/>
<TextView
android:id="@+id/tv_item_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(KG)"
android:textSize="14sp"
android:layout_marginStart="8dp"/>
<EditText
android:id="@+id/et_quantity"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:hint="Qty"
android:gravity="center"
android:layout_marginStart="16dp"/>
</LinearLayout>
c. Menampilkan Riwayat Pesanan (OrderHistoryActivity
atau OrderHistoryFragment
)

Layar ini akan menampilkan daftar pesanan yang pernah dibuat pengguna.
- Layout (
activity_order_history.xml
): GunakanRecyclerView
untuk menampilkan setiap item pesanan. - Adapter (
OrderAdapter.kt
): Buat adapter untukRecyclerView
yang mengikat dataOrder
ke tampilan item individual. - Fetching Data: Ambil data dari koleksi
orders
di Firestore, dengan filteruserId
saat ini. Urutkan berdasarkan tanggal pesanan terbaru.
// OrderHistoryActivity.kt (Kotlin)
package com.aplikasilaundryku
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.aplikasilaundryku.databinding.ActivityOrderHistoryBinding
import com.aplikasilaundryku.models.Order
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
class OrderHistoryActivity : AppCompatActivity() {
private lateinit var binding: ActivityOrderHistoryBinding
private lateinit var firestore: FirebaseFirestore
private lateinit var auth: FirebaseAuth
private lateinit var orderAdapter: OrderAdapter // Perlu membuat adapter ini
private val orderList = mutableListOf<Order>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityOrderHistoryBinding.inflate(layoutInflater)
setContentView(binding.root)
firestore = FirebaseFirestore.getInstance()
auth = FirebaseAuth.getInstance()
setupRecyclerView()
fetchOrderHistory()
}
private fun setupRecyclerView() {
orderAdapter = OrderAdapter(orderList) { order ->
// Opsional: Ketika item pesanan diklik, tampilkan detail pesanan
Toast.makeText(this, "Detail pesanan: ${order.orderId}", Toast.LENGTH_SHORT).show()
// Dapat memulai Activity baru untuk menampilkan detail lebih lengkap
}
binding.rvOrderHistory.apply {
layoutManager = LinearLayoutManager(this@OrderHistoryActivity)
adapter = orderAdapter
}
}
private fun fetchOrderHistory() {
val userId = auth.currentUser?.uid
if (userId == null) {
Toast.makeText(this, "Harus login untuk melihat riwayat pesanan", Toast.LENGTH_SHORT).show()
return
}
firestore.collection("orders")
.whereEqualTo("userId", userId)
.orderBy("orderDate", Query.Direction.DESCENDING) // Urutkan dari terbaru
.get()
.addOnSuccessListener { result ->
orderList.clear()
for (document in result) {
val order = document.toObject(Order::class.java)
orderList.add(order.copy(orderId = document.id))
}
orderAdapter.notifyDataSetChanged()
if (orderList.isEmpty()) {
// Cek jika tvNoOrders adalah TextView
val tvNoOrders = binding.root.findViewById<TextView>(R.id.tv_no_orders)
if (tvNoOrders != null) {
tvNoOrders.visibility = View.VISIBLE
}
} else {
val tvNoOrders = binding.root.findViewById<TextView>(R.id.tv_no_orders)
if (tvNoOrders != null) {
tvNoOrders.visibility = View.GONE
}
}
}
.addOnFailureListener { exception ->
Toast.makeText(this, "Gagal memuat riwayat pesanan: ${exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
Perlu membuat OrderAdapter.kt
dan item_order.xml
untuk layout setiap item pesanan di RecyclerView.
// OrderAdapter.kt (Kotlin)
package com.aplikasilaundryku
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.aplikasilaundryku.models.Order // Sesuaikan package
import java.text.SimpleDateFormat
import java.util.Locale
class OrderAdapter(
private val orderList: List<Order>,
private val onItemClick: (Order) -> Unit
) : RecyclerView.Adapter<OrderAdapter.OrderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_order, parent, false)
return OrderViewHolder(view)
}
override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
val order = orderList[position]
holder.bind(order)
}
override fun getItemCount(): Int = orderList.size
inner class OrderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvOrderId: TextView = itemView.findViewById(R.id.tv_order_id)
private val tvOrderDate: TextView = itemView.findViewById(R.id.tv_order_date)
private val tvServiceAndItems: TextView = itemView.findViewById(R.id.tv_service_and_items)
private val tvTotalPrice: TextView = itemView.findViewById(R.id.tv_total_price)
private val tvStatus: TextView = itemView.findViewById(R.id.tv_order_status)
fun bind(order: Order) {
tvOrderId.text = "Order ID: ${order.orderId}"
order.orderDate?.let {
val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale.getDefault())
tvOrderDate.text = "Tanggal: ${dateFormat.format(it)}"
}
// Tampilkan jasa dan ringkasan item
val itemsSummary = order.items.joinToString(", ") { "${it.itemName} (${it.quantity})" }
tvServiceAndItems.text = "${order.serviceName} - $itemsSummary"
tvTotalPrice.text = "Total: Rp ${String.format("%.2f", order.totalAmount)}"
tvStatus.text = "Status: ${order.status}"
itemView.setOnClickListener {
onItemClick(order)
}
}
}
}
<!-- item_order.xml -->
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_order_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16sp"
android:text="Order ID: #XYZ123"/>
<TextView
android:id="@+id/tv_order_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:text="Tanggal: DD MMM"/>
<TextView
android:id="@+id/tv_service_and_items"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:text="Jasa: Cuci Setrika - Baju (2kg), Celana (1pcs)"/>
<TextView
android:id="@+id/tv_total_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textStyle="bold"
android:textSize="16sp"
android:text="Total: Rp 0.000"/>
<TextView
android:id="@+id/tv_order_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/design_default_color_primary"
android:textStyle="bold"
android:textSize="14sp"
android:text="Status: Pending"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
Langkah 7: Fitur Lanjutan dan Pertimbangan
- Notifikasi Real-time: Gunakan Firebase Cloud Messaging (FCM) untuk mengirim notifikasi status pesanan (misal: "Cucian selesai", "Siap dijemput").
- Sistem Pembayaran: Integrasikan gateway pembayaran pihak ketiga (misal: Midtrans, Xendit, Duitku) untuk pembayaran online.
- Peta & Lokasi: Gunakan Google Maps API untuk fitur penjemputan/pengiriman, atau menampilkan lokasi toko laundry.
- Profil Pengguna: Izinkan pengguna mengelola alamat, nomor telepon, dan preferensi.
- Ulasan & Rating: Memungkinkan pelanggan memberikan ulasan setelah pesanan selesai.
- Promo & Diskon: Tampilkan banner promosi atau terapkan kode diskon.
- Aplikasi Admin: Pertimbangkan untuk membuat aplikasi web atau mobile terpisah untuk admin guna mengelola pesanan, menu, dan data pelanggan.
Kesimpulan
Membangun aplikasi laundry memerlukan kombinasi desain yang baik, implementasi fitur inti, dan integrasi backend yang solid. Dengan mengikuti panduan ini dan memanfaatkan Firebase, memiliki fondasi yang kuat untuk mengembangkan aplikasi laundry profesional yang dapat meningkatkan efisiensi bisnis dan kepuasan pelanggan. Ingatlah untuk selalu menguji aplikasi secara menyeluruh dan melakukan iterasi berdasarkan umpan balik pengguna.

Post a Comment