Tutorial Android #4: Dashboard Nasabah – Menampilkan Saldo dan Mutasi dengan RecyclerView
Halo para nasabah kece! Setelah berhasil login, sekarang saatnya kita membangun dashboard utama untuk nasabah. Di sini mereka bisa melihat saldo terkini dan daftar mutasi transaksi (setor/tarik) dalam bentuk daftar yang rapi. Kita akan pakai RecyclerView untuk daftar transaksi, dan CardView untuk menampilkan saldo. Siap jadi teller digital?
Apa yang akan kita lakukan?
- ✅ Membuat activity baru:
DashboardActivity. - ✅ Desain layout dengan CardView untuk saldo dan RecyclerView untuk daftar transaksi.
- ✅ Menambahkan SwipeRefreshLayout agar pengguna bisa menarik layar untuk memuat ulang data.
- ✅ Membuat model class untuk data saldo dan transaksi.
- ✅ Menambahkan endpoint di ApiService untuk mengambil saldo dan mutasi.
- ✅ Membuat adapter RecyclerView untuk menampilkan transaksi.
- ✅ Mengambil data dari API menggunakan Retrofit (dengan token).
- ✅ Menampilkan loading indicator (ProgressBar) saat memuat data.
- ✅ Menangani error (koneksi, token expired) dengan Toast.
- ✅ Memformat angka ke dalam format Rupiah.
Langkah 1: Menambahkan Dependencies (jika belum)
Pastikan di build.gradle (Module: app) sudah ada library untuk RecyclerView, CardView, dan SwipeRefreshLayout. Tambahkan jika belum:
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
Jangan lupa Sync Now setelah menambahkan.
Langkah 2: Membuat Model Class untuk Saldo dan Mutasi
Buat class SaldoResponse.java untuk response dari endpoint /api/nasabah/saldo (berdasarkan tutorial Laravel #6):
package com.example.banktabunganapp;
import com.google.gson.annotations.SerializedName;
public class SaldoResponse {
@SerializedName("no_rekening")
private String noRekening;
@SerializedName("saldo")
private double saldo;
@SerializedName("nama")
private String nama;
@SerializedName("email")
private String email;
// getters
public String getNoRekening() { return noRekening; }
public double getSaldo() { return saldo; }
public String getNama() { return nama; }
public String getEmail() { return email; }
}
Buat class MutasiResponse.java untuk menampung daftar transaksi. Karena endpoint /api/nasabah/mutasi mengembalikan objek dengan field mutasi yang berisi array, kita perlu model untuk item transaksi.
package com.example.banktabunganapp;
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class MutasiResponse {
@SerializedName("no_rekening")
private String noRekening;
@SerializedName("saldo_saat_ini")
private double saldoSaatIni;
@SerializedName("mutasi")
private List<TransaksiItem> mutasi;
public static class TransaksiItem {
@SerializedName("id")
private int id;
@SerializedName("tanggal")
private String tanggal;
@SerializedName("jenis")
private String jenis; // "Setor" atau "Tarik"
@SerializedName("jumlah")
private double jumlah;
@SerializedName("keterangan")
private String keterangan;
@SerializedName("petugas")
private String petugas;
// getters
public int getId() { return id; }
public String getTanggal() { return tanggal; }
public String getJenis() { return jenis; }
public double getJumlah() { return jumlah; }
public String getKeterangan() { return keterangan; }
public String getPetugas() { return petugas; }
}
// getters
public String getNoRekening() { return noRekening; }
public double getSaldoSaatIni() { return saldoSaatIni; }
public List<TransaksiItem> getMutasi() { return mutasi; }
}
Langkah 3: Menambahkan Endpoint di ApiService
Buka ApiService.java dan tambahkan method baru:
@GET("nasabah/saldo")
Call<SaldoResponse> getSaldo(@Header("Authorization") String token);
@GET("nasabah/mutasi")
Call<MutasiResponse> getMutasi(@Header("Authorization") String token);
Perhatikan kita mengirim token di header dengan format "Bearer {token}". Nanti kita akan menyusunnya di ApiClient atau langsung di panggilan.
Langkah 4: Membuat Adapter untuk RecyclerView
Buat class TransaksiAdapter.java yang extends RecyclerView.Adapter:
package com.example.banktabunganapp;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;
public class TransaksiAdapter extends RecyclerView.Adapter<TransaksiAdapter.ViewHolder> {
private List<MutasiResponse.TransaksiItem> transaksiList;
public TransaksiAdapter(List<MutasiResponse.TransaksiItem> transaksiList) {
this.transaksiList = transaksiList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_transaksi, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
MutasiResponse.TransaksiItem item = transaksiList.get(position);
holder.tvTanggal.setText(item.getTanggal());
holder.tvJenis.setText(item.getJenis());
holder.tvKeterangan.setText(item.getKeterangan());
holder.tvPetugas.setText(item.getPetugas());
// Format jumlah ke Rupiah
NumberFormat formatRupiah = NumberFormat.getCurrencyInstance(new Locale("id", "ID"));
String jumlah = formatRupiah.format(item.getJumlah());
holder.tvJumlah.setText(jumlah);
// Ubah warna teks berdasarkan jenis
if (item.getJenis().equalsIgnoreCase("Setor")) {
holder.tvJumlah.setTextColor(holder.itemView.getContext().getColor(android.R.color.holo_green_dark));
} else {
holder.tvJumlah.setTextColor(holder.itemView.getContext().getColor(android.R.color.holo_red_dark));
}
}
@Override
public int getItemCount() {
return transaksiList.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvTanggal, tvJenis, tvJumlah, tvKeterangan, tvPetugas;
public ViewHolder(@NonNull View itemView) {
super(itemView);
tvTanggal = itemView.findViewById(R.id.tvTanggal);
tvJenis = itemView.findViewById(R.id.tvJenis);
tvJumlah = itemView.findViewById(R.id.tvJumlah);
tvKeterangan = itemView.findViewById(R.id.tvKeterangan);
tvPetugas = itemView.findViewById(R.id.tvPetugas);
}
}
}
Langkah 5: Membuat Layout Item untuk RecyclerView
Buat file layout baru item_transaksi.xml di folder res/layout/:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/tvJenis"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16sp"/>
<TextView
android:id="@+id/tvJumlah"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/tvTanggal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#666"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/tvKeterangan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#666"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/tvPetugas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="#999"
android:layout_marginTop="4dp"/>
</LinearLayout>
Langkah 6: Desain Layout DashboardActivity
Buat layout activity_dashboard.xml dengan CardView untuk saldo, SwipeRefreshLayout, RecyclerView, dan ProgressBar:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="match_parent"
android:orientation="vertical"
android:background="#f5f5f5">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#3b7dbd"
app:title="Dashboard Nasabah"
app:titleTextColor="#fff"
app:navigationIcon="?attr/homeAsUpIndicator"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total Saldo"
android:textSize="16sp"
android:textColor="#666"/>
<TextView
android:id="@+id/tvSaldo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp 0"
android:textSize="32sp"
android:textStyle="bold"
android:textColor="#2e7d32"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/tvNoRekening"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No. Rekening: -"
android:textSize="14sp"
android:textColor="#999"
android:layout_marginTop="8dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Riwayat Transaksi"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewMutasi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"/>
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
Langkah 7: Implementasi DashboardActivity
Buat DashboardActivity.java:
package com.example.banktabunganapp;
import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Locale;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class DashboardActivity extends AppCompatActivity {
private TextView tvSaldo, tvNoRekening;
private RecyclerView recyclerView;
private TransaksiAdapter adapter;
private SwipeRefreshLayout swipeRefreshLayout;
private ProgressBar progressBar;
private SessionManager sessionManager;
private ApiService apiService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dashboard);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); // untuk logout nanti
tvSaldo = findViewById(R.id.tvSaldo);
tvNoRekening = findViewById(R.id.tvNoRekening);
recyclerView = findViewById(R.id.recyclerViewMutasi);
swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout);
progressBar = findViewById(R.id.progressBar);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new TransaksiAdapter(new ArrayList<>());
recyclerView.setAdapter(adapter);
sessionManager = new SessionManager(this);
apiService = ApiClient.getService();
// Ambil data pertama kali
loadData();
// SwipeRefresh layout
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
loadData();
}
});
}
private void loadData() {
String token = sessionManager.getToken();
if (token == null) {
Toast.makeText(this, "Token tidak ditemukan. Silakan login ulang.", Toast.LENGTH_SHORT).show();
finish();
return;
}
String authHeader = "Bearer " + token;
// Tampilkan progress
swipeRefreshLayout.setRefreshing(true);
progressBar.setVisibility(View.VISIBLE);
// Panggil API saldo
Call saldoCall = apiService.getSaldo(authHeader);
saldoCall.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
if (response.isSuccessful()) {
SaldoResponse saldo = response.body();
NumberFormat formatRupiah = NumberFormat.getCurrencyInstance(new Locale("id", "ID"));
tvSaldo.setText(formatRupiah.format(saldo.getSaldo()));
tvNoRekening.setText("No. Rekening: " + saldo.getNoRekening());
} else {
Toast.makeText(DashboardActivity.this, "Gagal mengambil saldo", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call call, Throwable t) {
Toast.makeText(DashboardActivity.this, "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
// Panggil API mutasi
Call mutasiCall = apiService.getMutasi(authHeader);
mutasiCall.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
swipeRefreshLayout.setRefreshing(false);
progressBar.setVisibility(View.GONE);
if (response.isSuccessful()) {
MutasiResponse mutasiResponse = response.body();
adapter = new TransaksiAdapter(mutasiResponse.getMutasi());
recyclerView.setAdapter(adapter);
} else {
Toast.makeText(DashboardActivity.this, "Gagal mengambil mutasi", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call call, Throwable t) {
swipeRefreshLayout.setRefreshing(false);
progressBar.setVisibility(View.GONE);
Toast.makeText(DashboardActivity.this, "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
@Override
public boolean onSupportNavigateUp() {
// Handle tombol back di toolbar untuk logout
sessionManager.logout();
finish();
return true;
}
}
Langkah 8: Menambahkan Intent dari LoginActivity
Pastikan di LoginActivity.java, setelah login sukses, kita mengarahkan ke DashboardActivity untuk nasabah (seperti yang sudah dilakukan di tutorial #3). Kodenya sudah ada di method navigateToDashboard().
Langkah 9: Uji Coba
Jalankan aplikasi. Login sebagai nasabah yang sudah memiliki transaksi. Seharusnya muncul saldo dan daftar mutasi. Tarik layar ke bawah untuk refresh. Coba juga matikan koneksi internet untuk melihat error handling.
Jika ada error, periksa Logcat dan pastikan token terkirim dengan benar (format "Bearer token").
/api/nasabah/saldo dan /api/nasabah/mutasi belum ada di backend, kamu harus membuatnya terlebih dahulu (lihat tutorial Laravel #6). Pastikan juga di backend, endpoint tersebut menggunakan middleware auth:sanctum agar token diperlukan.
Kesimpulan
- ✅ Dashboard nasabah berhasil dibuat dengan RecyclerView dan CardView.
- ✅ Data saldo dan mutasi diambil dari API menggunakan Retrofit dengan token.
- ✅ SwipeRefreshLayout memungkinkan refresh manual.
- ✅ Format Rupiah menggunakan NumberFormat.
- ✅ Error handling menggunakan Toast.
Di tutorial selanjutnya (#5: Ubah Password dan Edit Profil) kita akan menambahkan fitur pengaturan profil. Sampai jumpa!
Daftar Tutorial Android (Lanjutan)
- #5: Ubah Password dan Edit Profil
- #6: Login Multi-Level (Admin, Petugas, Nasabah)
- #7: Dashboard Petugas – Transaksi Setor/Tarik
- dan seterusnya...