Memakai Database SQLite Di Android


Pada artikel Mencari Lokasi Dengan GPS Di Android, saya membuat aplikasi yang mencari koordinat lokasi perangkat saat ini melalui GPS. Akan lebih baik bila aplikasi latihan tersebut memiliki fasilitas untuk menyimpan koordinat yang telah saya kunjungi. Ini akan mempermudah saya mencari kembali rumah seseorang atau tempat tertentu yang sudah pernah saya kunjungi bila saya lupa (atau tersesat) di kemudian hari. Salah satu solusi yang bisa saya pakai adalah dengan menyimpan data lokasi ke dalam database.

Pada Android, setiap aplikasi memiliki akses ke database SQLite untuk menyimpanan data secara internal. Database yang dibuat oleh sebuah aplikasi hanya bisa dipakai (dilihat dan diubah) oleh aplikasi tersebut saja. Aplikasi lain akan memakai database berbeda yang khusus untuk aplikasi tersebut. Dengan demikian, database SQLite bawaan ini tidak ditujukan untuk pertukaran data seperti halnya pada database client server umumnya. Ia hanya ditujukan sebagai penampungan data permanen bagi masing-masing aplikasi; menulis data ke database dapat dianggap sebagai solusi penampungan data yang lebih terstruktur dibandingkan dengan menulis data ke file.

Untuk memakai database, langkah pertama yang saya lakukan adalah membuat sebuah turunan dari SQLiteOpenHelper seperti pada kode program berikut ini:

public class DbHelper extends SQLiteOpenHelper {

    public static final int DATABASE_VERSION = 1;

    public DbHelper(Context context) {
        super(context, "MyDb", null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE lokasi (" +
            " _id INTEGER PRIMARY KEY, " +
            "nama TEXT NOT NULL, " +
            "longitude REAL, " +
            "latitude REAL, " + 
            "altitude REAL, " +
            "akurasi REAL, " +
            "azimuth REAL)"
        );
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }

}

Method onCreate() pada SQLiteOpenHelper akan dikerjakan pada saat database pertama kali dibuat. Saya bisa membuat seluruh tabel yang dibutuhkan serta mengisi nilai default pada method ini. Selain itu, SQLiteOpenHelper juga memiliki informasi versi database. Bila suatu saat saya melakukan perubahan pada struktur tabel di database, saya bisa meningkatkan angka milik DATABASE_VERSION. Dengan demikian, bila database versi sebelumnya sudah ada di perangkat pengguna, maka method onUpgrade() akan dikerjakan (bukan lagi onCreate()!). Saya bisa mengerjakan SQL ALTER TABLE di method ini untuk mengubah struktur tabel lama menjadi sesuai dengan struktur yang baru.

Sekarang, saya akan membuat sebuah menu baru di menu_main.xml seperti berikut ini:

<menu ...>

  ...

  <item android:id="@+id/menu_simpan" app:showAsAction="ifRoom"
     android:title="Simpan" />

</menu>

Setelah itu, saya menambahkan aksi yang akan dikerjakan saat menu di-klik seperti berikut ini:

...
// Simpan ke database
menu.findItem(R.id.menu_simpan).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
    @Override
    public boolean onMenuItemClick(MenuItem item) {
        if (lokasiTerakhir != null) {
            final Lokasi lokasi = new Lokasi(lokasiTerakhir);
            // Input nama lokasi
            AlertDialog.Builder builder = new AlertDialog.Builder(LokasiActivity.this);
            final EditText txtNamaLokasi = new EditText(LokasiActivity.this);
            builder.setView(txtNamaLokasi).setTitle("Nama Lokasi").setPositiveButton("Simpan", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    lokasi.setNama(txtNamaLokasi.getText().toString());
                    simpan(lokasi);
                }
            }).setNegativeButton("Batal", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.cancel();
                }
            }).create().show();
        }
        return true;
    };
});
...

Kode program di atas membuat sebuah object Lokasi baru berdasarkan yang sudah ada dengan menggunakan copy constructor yang saya tambahkan. Setelah itu, ia akan menampilkan sebuah dialog untuk mengisi nama lokasi seperti pada gambar berikut ini:

Tampilan Dialog

Tampilan Dialog

Bila pengguna memilih Simpan, maka method simpan() akan dikerjakan. Isi method tersebut terlihat seperti pada contoh berikut ini:

public class LokasiActivity extends Activity implements LocationListener, SensorEventListener {

    ...

    public void simpan(final Lokasi lokasi) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                DbHelper dbHelper = new DbHelper(LokasiActivity.this);
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("nama", lokasi.getNama());
                values.put("longitude", lokasi.getLongitude());
                values.put("latitude", lokasi.getLatitude());
                values.put("altitude", lokasi.getAltitude());
                values.put("akurasi", lokasi.getAkurasi());
                values.put("azimuth", lokasi.getAzimuth());
                db.insert("lokasi", null, values);
                LokasiActivity.this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(LokasiActivity.this, "Lokasi berhasil disimpan!", Toast.LENGTH_SHORT).show();
                    }
                });
            }
        }).start();
    }

}

Pada kode program di atas, saya mengerjakan operasi yang mengakses database pada sebuah thread terpisah diluar main thread. Setelah memperoleh sebuah SQLiteDatabase melalui getWritableDatabase(), saya bisa mengerjakan method seperti execSQL() dan insert() untuk menambah record baru di tabel. Pada contoh di atas, saya menggunakan insert() yang lebih aman dari SQL injection karena ia akan membuat prepared statement yang menggunakan parameter.

Untuk menampilkan seluruh record yang ada di database, saya akan membuat sebuah activity baru. Sebagai contoh, saya menamakannya RiwayatActivity dan memakai kode program seperti berikut ini:

public class RiwayatActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        DbHelper dbHelper = new DbHelper(this);
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = db.rawQuery("SELECT * FROM lokasi", null);
        ListView listView = new ListView(this);
        listView.setAdapter(new RiwayatCursorAdapter(this, cursor));
        setContentView(listView);
    }

}

Kode program di atas memanggil rawQuery() untuk memberikan perintah SQL SELECT untuk mengambil data. Hasil kembaliannya berupa Cursor yang memungkinkan saya melakukan navigasi record per record. Selain menggunakan rawQuery(), saya juga bisa menggunakan query() yang tidak memakai SQL secara langsung. Untuk menampilkan Cursor ke dalam ListView, saya membuat sebuah turunan CursorAdapter seperti berikut ini:

public class RiwayatCursorAdapter extends CursorAdapter {

    public RiwayatCursorAdapter(Context context, Cursor c) {
        super(context, c, false);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        TextView textView = new TextView(context);
        textView.setTextSize(18);
        textView.setPadding(16, 16, 16, 16);
        return textView;
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        String nama = cursor.getString(cursor.getColumnIndexOrThrow("nama"));
        ((TextView) view).setText(nama);
    }

}

Saya kemudian menambahkan sebuah menu baru pada menu_main.xml seperti berikut ini:

<menu...>

  ...

  <item android:id="@+id/menu_riwayat" app:showAsAction="never"
    android:title="Riwayat" />

</menu>

Bila menu tersebut dipilih, kode program berikut ini akan dikerjakan:

...
// Tampilkan riwayat
menu.findItem(R.id.menu_riwayat).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
    @Override
    public boolean onMenuItemClick(MenuItem item) {
        startActivity(new Intent(LokasiActivity.this, RiwayatActivity.class));
        return true;
    }
});
...

Kode program di atas akan memanggil RiwayatActivity sehingga daftar lokasi yang tersimpan di database akan ditampilkan. Nilai yang ditampilkan pada ListView hanya nama lokasi. Bila pengguna memilih salah satu lokasi di ListView tersebut, saya ingin LokasiActivity kembali dipanggil untuk menampilkan informasi khusus untuk lokasi tersebut. Dengan demikian, bila LokasiActivity dipanggil tanpa parameter, maka ia akan menampilkan lokasi dari GPS; sebaliknya, bila ia dipanggil dengan parameter berupa sebuah Lokasi, maka ia akan menampilkan informasi untuk Lokasi tersebut. Oleh sebab itu, saya mengubah LokasiActivity menjadi seperti berikut ini:

public class LokasiActivity extends Activity implements LocationListener, SensorEventListener {

  ...

  @Override
  protected void onStart() {
    super.onStart();
    Intent intent = getIntent();
    if (intent.hasExtra("lokasi")) {
      lokasiTerakhir = intent.getParcelableExtra("lokasi");
      refreshTampilan();
    } else {
       ...
    }
  }

  public void refreshTampilan() {
    TextView output = (TextView) findViewById(R.id.output);
    String strOutput = "DMS: " + lokasiTerakhir.koordinatDMS() + "n" +
      "DD: " + lokasiTerakhir.koordinatDD() + "n" +
      "Altitude: " + lokasiTerakhir.getAltitude() + " mn" +
      "Akurasi: " + lokasiTerakhir.getAkurasi() + " mn" +
      "Azimuth: " + lokasiTerakhir.getAzimuth() + " derajatn";
    output.setText(strOutput);

    // Share lokasi DD
    if ((lokasiTerakhir != null) && (shareActionProvider != null)) {
      Intent lokasiDDIntent = new Intent();
      lokasiDDIntent.setAction(Intent.ACTION_SEND);
      lokasiDDIntent.putExtra(Intent.EXTRA_TEXT, lokasiTerakhir.koordinatDD());
      lokasiDDIntent.setType("text/plain");
      shareActionProvider.setShareIntent(lokasiDDIntent);
    }
  }

  @Override
  public void onLocationChanged(Location location) {
    // Hitung rotasi
    ...
    lokasiTerakhir.setAzimuth(azimuth);
    refreshTampilan();
  }

  @Override
  protected void onPause() {
    super.onPause();
    if (locationManager != null) {
      locationManager.removeUpdates(this);
    }
    if (sensorManager != null) {
      sensorManager.unregisterListener(this);
    }
  }

  ...

}

Pada kode program di atas, saya memeriksa apakah ada sebuah parameter "lokasi" yang berisi sebuah Parceable yang mewakili object Lokasi. Karena sekarang Lokasi perlu dilewatkan antar activity, saya perlu mengimplementasikan Parceable pada Lokasi seperti pada contoh berikut ini:

public class Lokasi implements Parcelable {

  ...

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(nama);
    dest.writeDouble(longitude);
    dest.writeDouble(latitude);
    dest.writeDouble(altitude);
    dest.writeFloat(akurasi);
    dest.writeFloat(azimuth);
  }

  public static final Creator<Lokasi> CREATOR = new Creator<Lokasi>() {
    @Override
    public Lokasi createFromParcel(Parcel source) {
      return new Lokasi(source);
    }

    @Override
    public Lokasi[] newArray(int size) {
      return new Lokasi[size];
    }
  };

  private Lokasi(Parcel source) {
    nama = source.readString();
    longitude = source.readDouble();
    latitude = source.readDouble();
    altitude = source.readDouble();
    akurasi = source.readFloat();
    azimuth = source.readFloat();
  }

}

Komunikasi antar activity tidak bisa sekedar melewatkan referensi object seperti di pemograman Java pada umumnya. Hal ini karena activity yang berisi object yang dilewatkan bisa saja sudah dimusnahkan Android pada saat activity baru membutuhkan memori lebih. Dengan demikian, bila variabel dilewatkan melalui referensi, maka referensi tersebut akan merujuk pada memori yang sudah dimusnahkan. Salah satu solusi yang bisa ditempuh adalah dengan melewatkan Parceable sehingga object bisa di-‘buat ulang’ dengan mudah. Pada sebuah Parcelable, saya wajib menyediakan sebuah CREATOR yang memberi tahu bagaimana cara menghasilkan object ini. Urutan disini harus sama dengan urutan pada writeToParcel().

Sekarang, saya menambahkan listener yang akan dikerjakan bila salah satu item di RiwayatActivity dipilih oleh pengguna:

public class RiwayatActivity extends Activity implements AdapterView.OnItemClickListener {

  private RiwayatCursorAdapter riwayatCursorAdapter;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    listView.setOnItemClickListener(this);
  }

  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    Cursor cursor = (Cursor) riwayatCursorAdapter.getItem(position);
    Lokasi lokasi = new Lokasi(
      cursor.getString(cursor.getColumnIndexOrThrow("nama")),
      cursor.getDouble(cursor.getColumnIndexOrThrow("longitude")),
      cursor.getDouble(cursor.getColumnIndexOrThrow("latitude")),
      cursor.getDouble(cursor.getColumnIndexOrThrow("altitude")),
      cursor.getFloat(cursor.getColumnIndexOrThrow("akurasi")),
      cursor.getFloat(cursor.getColumnIndexOrThrow("azimuth"))
    );
    Intent intent = new Intent(this, LokasiActivity.class);
    intent.putExtra("lokasi", lokasi);
    startActivity(intent);
  }
}

Pada kode program di atas, saya mendapatkan Cursor untuk baris yang dipilih dengan memanggil getItem() milik CursorAdapter. Setelah itu, saya membaca nilai setiap kolom pada record tersebut guna membuat sebuah Lokasi baru. Untuk melewatkan object sebagai sebagai parameter bagi activity yang dipanggil, saya menggunakan putExtra() dari Intent.

Sampai disini, saya sudah bisa melihat informasi posisi untuk setiap record yang tersimpan di database SQLite. Sebagai latihan terakhir, saya akan menambahkan fasilitas untuk menghapus record. Langkah pertama yang saya lakukan adalah membuat sebuah context menu baru yang akan ditampilkan bila pengguna menahan jari agak lama pada salah satu baris di ListView yang berisi lokasi. Sebagai contoh, saya memberinya nama menu_context_riwayat.xml dan mendeklarasikan sebuah item menu seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@+id/hapus" android:title="Hapus" />

</menu>

Setelah itu, saya mengubah kode program untuk RiwayatActivity menjadi seperti berikut ini:

public class RiwayatActivity extends Activity implements AdapterView.OnItemClickListener {

  private RiwayatCursorAdapter riwayatCursorAdapter;
  SQLiteDatabase db;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    db = dbHelper.getReadableDatabase();
    Cursor cursor = db.rawQuery("SELECT * FROM lokasi", null);
    ...
    listView.setOnItemClickListener(this);
    registerForContextMenu(listView);
  }

  ...

  @Override
  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
    getMenuInflater().inflate(R.menu.menu_context_riwayat, menu);
  }

  @Override
  public boolean onContextItemSelected(MenuItem item) {
    if (item.getItemId() == R.id.menu_riwayat_hapus) {
      long id = ((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).id;
      int jumlahDihapus = db.delete("lokasi", "_id=?", new String[] {String.valueOf(id)});
      Toast.makeText(this, jumlahDihapus + " berhasil dihapus.", Toast.LENGTH_SHORT).show();
      Cursor cursor = db.rawQuery("SELECT * FROM lokasi", null);
      riwayatCursorAdapter.changeCursor(cursor);
      riwayatCursorAdapter.notifyDataSetChanged();
      return true;
    }
    return false;
  }

}

Pada kode program di atas, saya mendaftarkan context menu ListView dengan menggunakan registerForContextMenu(). Setelah itu, saya membuat menu ini pada onCreateContextMenu(). Bila salah satu item di context menu dipilih, saya bisa mengetahui id atau posisi (di ListView) yang sedang aktif dengan memanggil getMenuInfo() dari MenuItem. Pada contoh di atas, saya menghapus record dengan nilai field _id yang sesuai dengan item menu terpilih. Saya bisa menghapus record dengan memanggil method delete() dari SQLiteDatabase (selain itu, saya juga bisa membuat query SQL secara manual dengan memanggil rawQuery()). Setelah record dihapus, saya perlu memperbaharui ListView. Oleh sebab itu, saya men-query ulang isi tabel untuk memperoleh Cursor terbaru dan memakainya sebagai Cursor terbaru dengan memanggil method changeCursor() dari CursorAdapter. Saya juga perlu memanggil notifyDataSetChanged() dari CursorAdapter untuk memberi tahu ListView bahwa ia perlu digambar ulang. Pada aplikasi yang serius, seluruh kode program yang berhubungan dengan database sebaiknya dikerjakan pada thread terpisah agar tidak membuat UI menjadi tidak responsif. Pada contoh disini, saya tidak melakukannya agar kode program tetap sederhana.

Perihal Solid Snake
I'm nothing...

One Response to Memakai Database SQLite Di Android

  1. Ping-balik: Menampilkan Peta Dengan Google Maps API | The Solid Snake

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: