Menangani Configuration Changes (Screen Rotation) di Android

Bagus Aji Santoso 3 November 2017

Menangani Configuration Changes (Screen Rotation) di Android

Pendahuluan

Ada beberapa situasi dimana Activity dapat di destroy dan dihapus dari memori lalu dibuat lagi dari awal, misalnya saat screen rotation (perubahan orientasi layar). Saat situasi ini terjadi, kita harus mempersiapkan kapan Activity dibuat ulang dengan menyimpan dan mengembalikan state Activity sebelumnya.

Menyimpan dan mengembalikan State Activity

Saat activity mulai berhenti, method onSaveinstaceState() akan dipanggil oleh Activity sehingga ia bisa menyimpan data dalam bentuk key-value. Implementasi bawaan method ini akan menyimpan semua informasi seputar view milik activity (misalnya EditText) dan scroll position milik ListView.

Untuk menyimpan state tambahan untuk activity kita, pertama kita harus meng-implement onSaveInstanceState() dan menambahkan data baru ke objek Bundle. Berikut contohnya:

public class MainActivity extends Activity {
    static final String SOME_VALUE = "int_value";
    static final String SOME_OTHER_VALUE = "string_value";

    @Override
    protected void onSaveInstanceState(Bundle savedInstanceState) {
        // Menyimpan data tertentu (String) ke Bundle
        savedInstanceState.putInt(SOME_VALUE, someIntValue);
        savedInstanceState.putString(SOME_OTHER_VALUE, someStringValue);
        // Selalu simpan pemanggil superclass di bawah agar data di view tetap tersimpan
        super.onSaveInstanceState(savedInstanceState);
    }
}

Sistem akan memanggil method tadi sebelum sebuah Activity di destroy. Nanti saat activity dibuat lagi, sistem akan memanggil onRestoreInstanceState sehingga kita bisa mengambil data yang sebelumnya di Bundle:

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    // Selalu panggil superclass agar data di view tetap ada
    super.onRestoreInstanceState(savedInstanceState);
    // Ambil data yang sebelumnya sudah disimpan dari Bundle
    someIntValue = savedInstanceState.getInt(SOME_VALUE);
    someStringValue = savedInstanceState.getString(SOME_OTHER_VALUE);
}

Pengambilan data kembali di atas juga dapat dilakukan di dalam onCreate tapi lebih mudah dilakukan di onRestoreInstanceState untuk memastikan semua inisialisasi telah selesai dilakukan sehingga subclass Activity kita bisa menggunakan onRestoreInstanceState dari parent classnya atau membuat implementasi sendiri (namun secara umum pembacaan Bundle dari onSaveInstanceState dapat dilakukan di onCreate). Baca jawaban Stacoverflow ini untuk lebih detailnya.

Catat bahwa onSaveInstanceState dan onRestoreInstanceState tidak ada ada garansi untuk dipanggil bersama. Method onSaveInstanceState() dipanggil tiap activity akan di destroy. Namun, ada kasus dimana onSaveInstanceState dipanggil tapi activity tidak di destroy sehingga onRestoreInstanceState tidak dipanggil.

Baca juga panduan Recreating an Activity dari dokumentasi Android.

Menyimpan dan Mengembalikan State Fragment

Fragment juga memiliki method onSaveInstanceState() yang juga dapat kita pakai untuk menyimpan state:

public class MySimpleFragment extends Fragment {
    private int someStateValue;
    private final String SOME_VALUE_KEY = "someValueToSave";
   
    // Dipanggil saat configuration changes terjadi dan Fragment siap menyimpan state
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putInt(SOME_VALUE_KEY, someStateValue);
        super.onSaveInstanceState(outState);
    }
}

Kemudian kita dapat mengambil data dari state yang disimpan ini dapat dilakukan di onCreateView:

public class MySimpleFragment extends Fragment {
   // ...

   // Membaca layout XML fragment
   @Override
   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.my_simple_fragment, container, false);
        if (savedInstanceState != null) {
            someStateValue = savedInstanceState.getInt(SOME_VALUE_KEY);
            // Lakukan ssesuatu dengan someStateValue jika diperlukan
        }
        return view;
   }
}

Untuk menggunakan state fragment dengan baik, kita perlu memastikan bahwa kita tidak membuat ulang fragment tersebut saat configuration change terjadi. Maksudnya kita tidak menginisialisasi fragment apabila mereka sudah ada. Fragment yang dibuat dari dalam suatu Activity perlu dicari menggunakan tag saat configuration change terjadi:

public class ParentActivity extends AppCompatActivity {
    private MySimpleFragment fragmentSimple;
    private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (savedInstanceState != null) { // savedInstanceState ada, fragment mungkin masih ada
           // cari fragment yang sudah ada itu dengan tag
           fragmentSimple = (MySimpleFragment)  
              getSupportFragmentManager().findFragmentByTag(SIMPLE_FRAGMENT_TAG);
        } else if (fragmentSimple == null) { 
           // buat hanya jika fragment tersebut belum ada
           fragmentSimple = new MySimpleFragment();
        }
    }
}

Untuk melakukan hal di atas, kita perlu memberikan tag saat menempelkan fragment di dalam activity:

public class ParentActivity extends AppCompatActivity {
    private MySimpleFragment fragmentSimple;
    private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... kode-kode di atas ...
        // Selalu berikan sebuah tag ke fragment
        if (!fragmentSimple.isInLayout()) {
            getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.container, fragmentSimple, SIMPLE_FRAGMENT_TAG)
                .commit();
        }
    }
}

Dengan pola ini, kita dapat melakukan pemakaian ulang fragment serta mengembalikan state-nya saat configuration change terjadi.

Retain Fragment

Dalam banyak kasus kita bisa menghindari masalah saat sebuah Activity dibuat ulang dengan menggunakan fragment. Jika tampilan aplikasi dan state disimpan di sebuah fragment, kita dapat membuat agar fragment tersebut tetap ada di memori saat activity dibuat ulang:

public class RetainedFragment extends Fragment {
    // objek data yang kita ingin tetap ada tanpa dibuat ulang
    private MyDataObject data;

    // method ini hanya dipanggil sekali untuk fragment ini
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // jangan hapus fragment ini saat activity dibuat ulang. 
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}

Pendekatan ini menjaga agar fragment tidak di destroy saat lifecycle activity berlangsung melainkan di simapn di dalam Fragment Manager. Lihat dokumentasi resmi Android untuk pembahasan Handling Runtime Changes.

Menangani State List

ListView

Sering saat kita memutar layar, aplikasi akan memuat ulang list dari posisi nol. Untuk menyimpan posisi terkahir dan informasi lain seputar ListView, kita dapat menyimpannya di onPause dan mengembalikannya di onViewCreated seperti pada contoh:

// YourActivity.java
private static final String LIST_STATE = "listState";
private Parcelable mListState = null;

// Simpan state List di sini
@Override
protected void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    mListState = getListView().onSaveInstanceState();
    state.putParcelable(LIST_STATE, mListState);
}

// Kembalikan state List di sini
@Override
protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    mListState = state.getParcelable(LIST_STATE);
}


@Override
protected void onResume() {
    super.onResume();
    loadData(); // memastikan data sudah dimasukkan ke dalam adapter terlebih dahulu
    // Hanya panggil bagian ini setelah item data sudah masukkan lagi ke adapter
    // misalnya setelah berhasil memanggil data dari internet
    if (mListState != null) {
        myListView.onRestoreInstanceState(mListState);
        mListState = null;
    }
}

Periksa artikel ini dan jawaban di stackoverflow untuk lebih lengkapnya.

Perlu diingat kita perlu membaca data terlebih dahulu ke dalam adapter sebelum memanggil onRestoreInstanceState. Dengan kata lain, jangan memanggil onRestoreInstanceState ListView sebelum data sudah dibaca dari internet ata dari database.

RecyclerView

Berikut ini kode untuk RecyclerView bila ingin menyimpan state-nya:

// YourActivity.java
public final static String LIST_STATE_KEY = "recycler_list_state";
Parcelable listState;

protected void onSaveInstanceState(Bundle state) {
     super.onSaveInstanceState(state);
     // Simpan state List
     listState = mLayoutManager.onSaveInstanceState();
     state.putParcelable(LIST_STATE_KEY, listState);
}

protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    // Kembalikan state List dan posisi item
    if(state != null)
        listState = state.getParcelable(LIST_STATE_KEY);
}

@Override
protected void onResume() {
    super.onResume();
    if (listState != null) {
        mLayoutManager.onRestoreInstanceState(listState);
    }
}

Check out this blog post and stackoverflow post for more details.

Mengunci Orientasi Layar

Jika ingin menguncu orientasi layar kita perlu menambah properti android:screenOrientation di dalam sebuah <activity> di AndroidManifest.xml:

<activity
    android:name="com.techblogon.screenorientationexample.MainActivity"
    android:screenOrientation="portrait"
    android:label="@string/app_name" >
    <!-- ... -->
</activity>

Dengan begitu setiap activity yang ditambahkan properti ini akan selalu ditampilkan dapat mode "portrait".

Mengelola Configuration Change Secara Manual

Cara berikut ini merupakan cara terakhir jika tidak ingin sebuah activity untuk di restart saat configuration change terjadi. Langkah ini merupakan langkah yang tidak disarankan, kecuali jika memang terpaksa tidak ingin agar activity di restart ulang saat configuration change terjadi.

Untuk melakukannya, tambahkan android:configChanges ke activity di dalam AndroidManifest.xml`:

<activity android:name=".MyActivity"
          android:configChanges="orientation|screenSize|keyboardHidden"
          android:label="@string/app_name">

Sekarang, saat salah satu konfigurasi di atas terjadi, activity tidak di restart namun memanggil method onConfigurationChanged():

// Di dalam activity yang melakukan configuration change
// Periksa orientasi layar saat ini, lalu tampilkan Toast sesuai layar
@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Periksa orientasi layar
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
        Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
    }
}

Untuk opsi lain bagi properti android:configChanges baca dokumentasi ini android:configChanges](http://developer.android.com/guide/topics/manifest/activity-element.html#config) dan keals Configuration.

*Diterjemahkan dari Handling Configuration Change *