Cara Membuat Aplikasi Laundry dengan Android Studio

Table of Contents
Konsep Aplikasi Loundry

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:

  1. Aplikasi Pelanggan: Digunakan pelanggan untuk melihat daftar jasa, jenis barang, daftar harga, membuat pesanan, melacak pesanan, dan melakukan pembayaran.
  2. 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

  1. Instalasi Android Studio: Pastikan memiliki versi terbaru Android Studio. Dapat mengunduhnya dari situs resmi developer.android.com.
  2. 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 direktori app/ di proyek Android.

Langkah 3: Desain Antarmuka Pengguna (UI/UX) Aplikasi Pelanggan

Contoh Desain UI Aplikasi Laundry

Desain yang intuitif dan menarik adalah kunci untuk pengalaman pengguna yang baik. Rencanakan setiap layar dengan cermat.

  1. 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.
  2. Komponen UI Penting:
    • Activity dan Fragment untuk setiap layar atau bagian layar.
    • ConstraintLayout, LinearLayout, RelativeLayout untuk menata elemen UI.
    • TextView, ImageView, Button, EditText.
    • RecyclerView dengan CardView untuk menampilkan daftar jasa, jenis barang, atau riwayat pesanan dengan rapi.
    • Spinner atau RadioGroup untuk pilihan jenis barang atau opsi tambahan.
    • BottomNavigationView atau NavigationView (Drawer) untuk navigasi utama.
    • FloatingActionButton untuk memulai pesanan baru.
  3. 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)

Layar Pendaftaran Aplikasi Laundry

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)

Layar Login Aplikasi Laundry

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: String
    • items: Array of Objects (setiap objek berisi itemId, itemName, quantity, subtotalPrice)
    • totalAmount: Double
    • pickupAddress: String
    • deliveryAddress: String
    • pickupDateTime: Timestamp
    • deliveryDateTime: Timestamp (opsional, setelah selesai)
    • status: String (misal: "Pending", "Diproses", "Siap Ambil", "Selesai", "Dibatalkan")
    • orderDate: Timestamp
    • notes: 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)

Daftar Jasa Laundry

Layar ini akan menampilkan daftar jasa laundry yang tersedia (misal: "Cuci Kering", "Cuci Setrika", "Setrika Saja").

  1. Layout (activity_service_list.xml): Gunakan RecyclerView untuk menampilkan daftar jasa. Setiap item daftar bisa berupa CardView untuk tampilan yang lebih menarik.
  2. Adapter (ServiceAdapter.kt): Buat adapter untuk RecyclerView yang mengikat data Service ke tampilan item individual.
  3. Fetching Data: Di ServiceListActivity/Fragment, inisialisasi FirebaseFirestore dan ambil data dari koleksi services.

// 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.

  1. Layout (activity_order_form.xml):
    • TextView untuk menampilkan nama jasa yang dipilih.
    • RecyclerView atau LinearLayout 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.
  2. Fetching Data: Ambil data items dari Firestore dan tampilkan. Hitung total harga secara dinamis berdasarkan input pengguna.
  3. Logika Penghitungan Harga:
    • Harga = basePricePerKg (dari jasa) * quantity * priceMultiplier (dari jenis barang).
    • Jumlahkan semua subtotal untuk mendapatkan total pesanan.
  4. Menyimpan Pesanan: Setelah konfirmasi, simpan objek Order baru ke koleksi orders 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)

Riwayat Pesanan Laundry

Layar ini akan menampilkan daftar pesanan yang pernah dibuat pengguna.

  1. Layout (activity_order_history.xml): Gunakan RecyclerView untuk menampilkan setiap item pesanan.
  2. Adapter (OrderAdapter.kt): Buat adapter untuk RecyclerView yang mengikat data Order ke tampilan item individual.
  3. Fetching Data: Ambil data dari koleksi orders di Firestore, dengan filter userId 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.

Aplikasi jadi

Post a Comment