Memahami Threading

Sejauh ini, kita sudah tahu bagaimana service dibuat dan mengapa sangat penting untuk memastikan bahwa tugas-tgas yang berjalan lama/panjang harus ditangani dengan benar, terutama ketika meng-update thread UI. Sebelumnya dalam latihan tentang topik-topik "Networking", kita juga sudah tahu bagaimana menggunakan class 'AsyncTask untuk mengeksekusi kode yang berjalanan di background. Bagian ini akan merangkum secara singkat beberapa cara untuk menangani tugas-tugas yang berjalan lama/panjang dengan benar dengan menggunakan berbagai method yang ada.

Pertama, kita buat terlebih dahulu project baru dan kita beri nama "Threading". Kemudian kita modifikasi file "activity_main.xml" seperti berikut:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context="com.example.threading.MainActivity">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello" />

<Button
android:id="@+id/btnStartCounter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start"
android:onClick="startCounter"/>

<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"/>
</LinearLayout>

Misalkan kita akan menampilkan suatu counter pada 'activity', dari 0 hingga 1000. Di dalam class "MainActivity" kita tuliskan kode seperti berikut:
package com.example.threading;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

TextView txtView1;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

txtView1 = (TextView) findViewById(R.id.textView1);
}

public void startCounter(View view) {
for (int i=0; i<=1000; i++) {
txtView1.setText(String.valueOf(i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
}
}
}

Ketika kita jalankan app dan kita klik tombol 'Start', app akan diam/tidak merespon dan setelah beberapa saat kita mungkin akan melihat pesan seperti berikut:


UI diam/tidak merespon karena app akan berusaha secara kontinyu menampilkan nilai counter pada saat yang sama dengan jeda selama satu detik setelah ditampilkan. Hal ini menyebabkan UI tertambat karena menunggu untuk menampilkan counter sampai selesai. Hasilnya adalah app yang tidak responsif danmembuat user frustasi.

Untuk memecahkan masalah ini, salah satu kemungkinannya adalah dengan membungkus bagian kode yang berisi loop dengan class 'Thread' dan 'Runnable', seperti berikut:
public void startCounter(View view) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<=1000; i++) {
txtView1.setText(String.valueOf(i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
}
}
}).start();
}
Di dalam kode di atas tersebut, kita terlebih dahulu membuat suatu class yang mengimplementasikan interface 'Runnable'. Di dalam class ini, kita menempatkan kode-kode yang melakukan tugas/pekerjaan yang panjang/lama di dalam method 'run()'. Blok 'Runnable' kemudian di-start dengan menggunakan class 'Thread'.
Catatan:
Runnable adalah blok kode yang bisa dieksekusi oleh suatu thread.
Tetapi app di atas tersebut tidak akan berhasil dan akan membuat crash bila kita menjalankannya. Kode tersebut yang ditempatkan di dalam blok 'Runnable' ada pada thread yang terpisah, dan di dalam contoh di atas tadi kita mencoba meng-update UI dari thread yang lain, yang tidak aman untuk dijalankan karena UI Android tidaklah bersifat thread-safe. Untuk memecahkan hal ini, kita perlu menggunakan method 'post()' dari suatu 'View' untuk membuat blok 'Runnable' lainnya untuk ditambahkan ke antrian pesan. Singkatnya, blok 'Runnable' yang baru dibuat akan dieksekusi di thread UI, jadi akan aman untuk dieksekusi app kita:
public void startCounter(View view) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<=1000; i++) {
final int valueOfi = i;

/*meng-update UI*/
txtView1.post(new Runnable() {
@Override
public void run() {
/*Thread UI untuk meng-update*/
txtView1.setText(String.valueOf(valueOfi));
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
}
}
}).start();
}

App tersebut sekarang akan berjalan dengan benar, tetapi terlalu rumit dan membuat kode kita sulit di-kelola.

Alternatif kedua untuk meng-update UI dari thread yang lain adalah dengan menggunakan class 'Handler'. Suatu 'Handler' akan memungkinkan kita mengirim dan mem-proses pesan, mirip dengan menggunakan method 'post()' dari 'View'. Potongan kode berikut menunjukkan class 'Handler' yang dipanggil 'UIupdate' yang meng-update UI dengan menggunakan pesan yang diterimanya:
catatan:
supaya kode berikut bisa berjalan, kita perlu import android.os.Handler dan jua menambahkan modifier 'static' ke 'txtView1'
/*digunakan untuk mengupdate UI pada main activity*/
static Handler UIupdater = new Handler() {
@Override
public void handleMessage(Message msg) {
byte[] buffer = (byte[]) msg.obj;

/*konversi semua array byte menjadi string*/
String strReceived = new String(buffer);

/*menampilkan text yang diterima di TextView*/
txtView1.setText(strReceived);
Log.d("Threading", "running");
}
};

public void startCounter(View view) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<=1000; i++) {
/*meng-update UI main activity*/
MainActivity.UIupdater.obtainMessage(
0, String.valueOf(i).getBytes()).sendToTarget();
/*insert delay*/
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
}
}
}).start();
}

Untuk mempelajari lebih detil tentang class 'Handler' bisa dilihat di dokumentasi di: https://developer.android.com/reference/android/os/Handler.html

Sejauh ini, kedua method yang baru saja kita pelajari memungkinkan kita untuk meng-update UI dari thread yang terpisah. Di Android, kita bisa menggunakan class 'AsyncTask' yang lebih sederhana untuk melakukan hal tersebut. Dengan menggunakan 'AsyncTask', kita bisa menulis ulang kode di atas seperti berikut:
private class DoCountingTask extends AsyncTask {
protected Void doInBackground(Void... params) {
for (int i = 0; i < 1000; i++) {
/*memberi report tentang progress-nya*/
publishProgress(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
if (isCancelled()) break;
}
return null;
}

protected void onProgressUpdate(Integer... progress) {
txtView1.setText(progress[0].toString());
Log.d("Threading", "updateing...");
}
}

public void startCounter(View view) {
task = (DoCountingTask) new DoCountingTask().execute();
}
Kode tersebut di atas akan meng-update UI dengan aman dari thread yang lain. Bagaimana dengan menghentikan tugas/pekerjaan di thread? Bila kita menjalankan app tersebut dan men-klik tombol 'Start', counter akan mulai menampilkan dari nol. Tetapi, jika menekan tombol 'back' di emulator/device, tugas/pekerjaan akan terus berjalan meskipun 'activity' sudah di-destroy. Kita akan mem-verifikasi ini melalui jendela 'Log'. Bila kita ingin men-stop tugas, kita gunakan potongan kode berikut di bawah ini:
package com.example.threading; 
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

static TextView txtView1;

DoCountingTask task;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

txtView1 = (TextView) findViewById(R.id.textView1);
}

public void startCounter(View view) {
task = (DoCountingTask) new DoCountingTask().execute();
}

public void stopCounter(View view) {
task.cancel(true);
}

private class DoCountingTask extends AsyncTask {
protected Void doInBackground(Void... params) {
for (int i = 0; i < 1000; i++) {
/*memberi report tentang progress-nya*/
publishProgress(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
if (isCancelled()) break;
}
return null;
}

protected void onProgressUpdate(Integer... progress) {
txtView1.setText(progress[0].toString());
Log.d("Threading", "updateing...");
}
}

@Override
protected void onPause() {
super.onPause();
stopCounter(txtView1);
}
}
Untuk men-stop subclass 'AsyncTask, kita perlu mendapatkan instans-nya terlebih dahulu. Untuk men-stop tugas/pekerjaan yang berjalan, kita panggil method 'cancel()'. Di dalam tugas tersebut, kita panggil method 'isCancelled()' untuk men-cek apakah tugas harus diakhiri atau tidak.

No comments: