{"id":19760408,"url":"https://github.com/refactory-id/training-sekolahdesain","last_synced_at":"2026-03-03T16:05:48.317Z","repository":{"id":68432182,"uuid":"339017735","full_name":"refactory-id/training-sekolahdesain","owner":"refactory-id","description":null,"archived":false,"fork":false,"pushed_at":"2021-02-15T12:03:16.000Z","size":5637,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-10T23:19:57.246Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/refactory-id.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-15T08:56:45.000Z","updated_at":"2021-02-15T12:03:19.000Z","dependencies_parsed_at":"2023-07-02T18:45:37.866Z","dependency_job_id":null,"html_url":"https://github.com/refactory-id/training-sekolahdesain","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/refactory-id%2Ftraining-sekolahdesain","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/refactory-id%2Ftraining-sekolahdesain/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/refactory-id%2Ftraining-sekolahdesain/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/refactory-id%2Ftraining-sekolahdesain/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/refactory-id","download_url":"https://codeload.github.com/refactory-id/training-sekolahdesain/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241090584,"owners_count":19907980,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-12T03:37:10.356Z","updated_at":"2026-03-03T16:05:48.263Z","avatar_url":"https://github.com/refactory-id.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# **`Android`**\n\n## **App Icon**\n\nAndroid Studio menyertakan tool yang disebut Image Asset Studio yang membantu untuk membuat ikon Aplikasi sendiri dari ikon material, gambar khusus, dan string teks. Proses ini menghasilkan sekumpulan ikon dengan resolusi yang sesuai untuk setiap pixel density yang didukung Aplikasi. Image Asset Studio menempatkan ikon yang baru dibuat dalam folder density-sepesifi di dalam folder `res/` dalam proyek Android. Saat runtime, Android menggunakan resource yang sesuai berdasarkan pixel density layar perangkat tempat aplikasi berjalan.\n\nImage Asset Studio membantu untuk membuat jenis ikon dibawah ini:\n\n1. Launcher icons\n2. Action bar and tab icons\n3. Notification icons\n\n### `Adaptive and legacy launcher icons`\n\nIcon launcher adalah grafik yang mewakili aplikasi kepada user. Icon launcher dapat:\n\n1. Muncul di daftar aplikasi yang diinstal di perangkat dan di layar Beranda.\n2. Mewakili pintasan ke aplikasi (misalnya, ikon pintasan kontak yang membuka informasi detail untuk kontak).\n3. Digunakan oleh launcher app.\n4. Membantu user menemukan aplikasi di Google Play.\n\n### `Action bar and tab icons`\n\nIkon bar action adalah elemen grafis yang ditempatkan di app bar action yang mewakili item aksi individu.\n\nIkon tab adalah elemen grafis yang digunakan untuk mewakili tab individu dalam antarmuka multi-tab. Setiap ikon tab memiliki dua status: tidak dipilih atau dipilih.\n\n### `Notification icons`\n\nNotifikasi adalah pesan yang dapat ditampilkan kepada user di luar UI normal pada aplikasi. Image Asset Studio menempatkan ikon notifikasi di lokasi yang baik di direktori `res/drawable-density/`\n\n### `Clip Art`\n\nImage Asset Studio memudahkan dalam mengimpor ikon material Google dalam format VectorDrawable dan PNG: cukup pilih ikon dari dialog Image Asset Studio saja.\n\n### `Images`\n\nAndroid dapat mengimpor gambar sendiri dan menyesuaikannya dengan jenis ikon. Image Asset Studio mendukung jenis file berikut: PNG (lebih disukai) JPG (dapat diterima) dan GIF (tidak disarankan).\n\n### `Text Strings`\n\nImage Asset Studio memungkinkan untuk mengetik string teks dalam berbagai font dan menempatkannya pada ikon. Text Strings mengubah ikon berbasis teks menjadi file PNG untuk pixel density yang berbeda.\n\n### `Run Image Asset Studio`\n\nUntuk menjalan Image Asset Studio pada Android Studio, ikut langkah dibawah ini:\n\n1. Pada window project, pilih tampilan Android pada dropdown pada gambar dibawah ini: ![Image](assets/14.png)\n2. Klik kanan pada **Windows** dan **Linux** atau CTRL + CLICK untuk **macOS** pada folder `res` (tanpa ada hint generated), kemudian pilih **New \u003e Image Asset** maka akan muncul dialog seperti dibawah ini: ![Images](assets/15.png)\n\n### `Refer to an image resource in code`\n\nAplikasi biasanya dapat merujuk ke resource gambar dengan cara umum dalam kode dan saat aplikasi berjalan, gambar yang sesuai akan ditampilkan secara otomatis bergantung pada perangkatnya:\n\n- Dalam kebanyakan kasus, Aplikasi bisa merujuk ke resource gambar sebagai @drawable dalam kode XML atau Drawable dalam kode Java / Kotlin.\n\n```Xml\n\u003cImageView\n    android:layout_height=\"wrap_content\"\n    android:layout_width=\"wrap_content\"\n    android:src=\"@drawable/myimage\" /\u003e\n```\n\n```Kotlin\nval drawable = resources.getDrawable(R.drawable.myimage, theme)\n```\n\n- Setelah memiliki resource gambar di direktori `res/` pada proyek Android, kemudian bisa mereferensikannya dari kode Java / Kotlin atau layout XML dengan menggunakan ID resourcenya.\n\n```Kotlin\nfindViewById\u003cImageView\u003e(R.id.myimageview).apply {\n    setImageResource(R.drawable.myimage)\n}\n```\n\nUntuk ikon launcher, pada file AndroidManifest.xml harus merujuk ke lokasi folder `mipmap/` Image Asset Studio menambahkan folder ini secara otomatis.\n\n```Kotlin\n\u003capplication android:name=\"ApplicationTitle\"\n         android:label=\"@string/app_label\"\n         android:icon=\"@mipmap/ic_launcher\" \u003e\n```\n\n## **Dialog**\n\nDialog adalah jendela kecil yang meminta user untuk mengambil keputusan atau memasukkan informasi tambahan. Dialog tidak memenuhi layar pada Android dan biasanya digunakan untuk peristiwa modal yang mengharuskan pengguna mengambil tindakan sebelum mereka dapat melanjutkan suatu proses.\n\n### `Type of Dialogs`\n\nClass Dialog adalah class dasar untuk membuat dialog, tetapi harus menghindari membuat instance Dialog secara langsung. Sebagai gantinya, gunakan salah satu subclass berikut ini:\n\n1. **AlertDialog**: Dialog yang dapat menampilkan judul, hingga tiga tombol, daftar item yang dapat dipilih atau kustom layout.\n\n2. **DatePickerDialog** atau **TimePickerDialog**: Dialog dengan UI yang telah ditentukan sebelumnya yang memungkinkan pengguna untuk memilih tanggal atau waktu.\n\n### `Creating a Dialog Fragment`\n\nBerbagai macam desain dialog, termasuk layout khusus dengan meng-extend DialogFragment dan membuat AlertDialog dalam metode callback `onCreateDialog()`.\n\nBerikut ini adalah AlertDialog dasar yang dikelola di dalam DialogFragment:\n\n```Kotlin\nclass FireMissilesDialogFragment : DialogFragment() {\n    override fun onCreateDialog(savedInstanceState: Bundle): Dialog {\n        return requireActivity().let {\n            val builder = AlertDialog.Builder(it)\n            builder.setMessage(R.string.dialog_fire_missiles)\n                    .setPositiveButton(R.string.fire,\n                            DialogInterface.OnClickListener { dialog, id -\u003e\n                                // FIRE ZE MISSILES!\n                            })\n                    .setNegativeButton(R.string.cancel,\n                            DialogInterface.OnClickListener { dialog, id -\u003e\n                                // User cancelled the dialog\n                            })\n            builder.create()\n        }\n    }\n}\n```\n\nSetelah membuat class DialogFragment diatas panggil method `show()` pada object tersebut dan akan menampilkan dialog seperti dibawah ini:\n\n![Images](assets/11.png)\n\n### `Build an Alert Dialog`\n\nClass AlertDialog memungkinkan untuk membuat berbagai desain dialog dan seringkali merupakan satu-satunya class dialog yang diperlukan. Seperti yang ditunjukkan pada gambar dibawah ini:\n\n![Images](assets/12.png)\n\nTitle\nIni opsional dan harus digunakan hanya jika area konten ditempati oleh pesan yang terperinci, daftar, atau kustom layoyt. Jika perlu menyatakan pesan atau pertanyaan sederhana (seperti dialog di gambar diatasnya) yang tidak memerlukan judul.\n\nContent Area\nIni dapat menampilkan pesan, daftar atau kustom layout lainnya.\n\nAction Buttons\nSeharusnya tidak ada lebih dari tiga tombol tindakan dalam dialog.\n\nClass AlertDialog.Builder menyediakan API yang memungkinkan untuk membuat AlertDialog dengan jenis konten ini, termasuk dengan kustom layout.\n\nUntuk membuat AlertDialog, bisa dengan meniru kode dibawah ini:\n\n**Activity.kt**\n\n```Kotlin\nval builder: AlertDialog.Builder? = activity?.let {\n    AlertDialog.Builder(it)\n}\n\nbuilder?.apply {\n    setPositiveButton(R.string.ok,\n            DialogInterface.OnClickListener { dialog, id -\u003e\n                // User clicked OK button\n            })\n    setNegativeButton(R.string.cancel,\n            DialogInterface.OnClickListener { dialog, id -\u003e\n                // User cancelled the dialog\n            })\n}\n\nbuilder?.setMessage(R.string.dialog_message)\n        .setTitle(R.string.dialog_title)\n\nval dialog: AlertDialog? = builder?.create()\ndialog?.show()\n```\n\nAda tiga tombol aksi berbeda yang dapat ditambahkan:\n\n1. **Positive Button**: menggunakan button ini untuk menerima dan melanjutkan tindakan (tindakan \"OK\").\n\n2. **Negative Button**: menggunakan button ini untuk membatalkan tindakan.\n\n3. **Neutral Button**\n   Menggunakan button ini jika user mungkin tidak ingin melanjutkan tindakan, tetapi tidak ingin membatalkannya. Neutral Button muncul di antara tombol positif dan negatif. Misalnya, tindakannya mungkin \"Remind me later\".\n\n### `Creating a Custom Layout`\n\nJika menginginkan layout khusus dalam dialog, buat layout dan tambahkan ke AlertDialog dengan memanggil method `setView()` pada objek AlertDialog.Builder.\n\nSecara default, layout kustom mengisi jendela dialog, tetapi masih dapat menggunakan metode AlertDialog.Builder untuk menambahkan tombol dan judul.\n\nContoh kustom layout pada dialog:\n\n![Images](assets/13.png)\n\n**dialog_signin.xml**\n\n```Xml\n\u003cLinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\u003e\n\n    \u003cImageView\n        android:src=\"@drawable/header_logo\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"64dp\"\n        android:scaleType=\"center\"\n        android:background=\"#FFFFBB33\"\n        android:contentDescription=\"@string/app_name\" /\u003e\n\n    \u003cEditText\n        android:id=\"@+id/username\"\n        android:inputType=\"textEmailAddress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"16dp\"\n        android:layout_marginLeft=\"4dp\"\n        android:layout_marginRight=\"4dp\"\n        android:layout_marginBottom=\"4dp\"\n        android:hint=\"@string/username\" /\u003e\n\n    \u003cEditText\n        android:id=\"@+id/password\"\n        android:inputType=\"textPassword\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"4dp\"\n        android:layout_marginLeft=\"4dp\"\n        android:layout_marginRight=\"4dp\"\n        android:layout_marginBottom=\"16dp\"\n        android:fontFamily=\"sans-serif\"\n        android:hint=\"@string/password\"/\u003e\n\n\u003c/LinearLayout\u003e\n```\n\nUntuk inflate layout dalam DialogFragment, pertama dapatkan LayoutInflater dengan memangil method `getLayoutInflater()` dan panggil `inflate()`, di mana parameter pertama adalah ID resource layout dan parameter kedua adalah root view untuk layout. Kemudian memanggil `setView()` untuk menempatkan layout dalam dialog.\n\n```Kotlin\noverride fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n    return requireActivity().let {\n        val builder = AlertDialog.Builder(it)\n        val inflater = it.layoutInflater\n\n        builder.setView(inflater.inflate(R.layout.dialog_signin, null))\n                .setPositiveButton(R.string.signin,\n                        DialogInterface.OnClickListener { dialog, id -\u003e\n                            // sign in the user ...\n                        })\n                .setNegativeButton(R.string.cancel,\n                        DialogInterface.OnClickListener { dialog, id -\u003e\n                            getDialog().cancel()\n                        })\n        builder.create()\n    }\n}\n```\n\n## **Menu**\n\nMenu adalah komponen user interface yang umum pada aplikasi apapun. Untuk memberikan pengalaman pengguna yang akrab dan konsisten harus menggunakan API Menu untuk menyajikan tindakan user dan opsi lain dalam Activity atau Fragment.\n\n### `Types of Menu`\n\nBerikut adalah beberapa jenis menu yang biasa digunakan di Android:\n\n1. **Options menu and app bar**: Menu opsi adalah kumpulan item menu utama yang dipakai untuk suatu Activity. Di sinilah harus menempatkan tindakan yang berdampak global pada aplikasi, seperti \"Search\", \"Compose email\" dan \"Setting\". ![Images](assets/08.png)\n\n2. **Context menu and contextual action mode**: Menu konteks adalah menu terangkat yang muncul saat user melakukan klik suatu elemen. Ini memberikan tindakan yang mempengaruhi konten yang dipilih atau bingkai konteks. Contextual action mode menampilkan item tindakan yang memengaruhi konten yang dipilih di bar pada bagian atas layar dan memungkinkan user untuk memilih beberapa item. ![Images](assets/09.png)\n\n3. **Popup menu**: Menu popup menampilkan daftar item dalam daftar vertikal yang ditambatkan ke tampilan yang memanggil menu tersebut. Hal ini bagus untuk memberikan luapan tindakan yang berhubungan dengan konten tertentu atau untuk memberikan opsi untuk bagian kedua dari sebuah perintah. Tindakan dalam popup menu tidak boleh secara langsung memengaruhi konten terkait — untuk itulah contextual action. Sebaliknya, menu popup ditujukan untuk tindakan tambahan yang berhubungan dengan wilayah konten dalam aktivitas. ![Images](assets/10.png)\n\n### `Defining a Menu in XML`\n\nUntuk semua jenis menu, Android menyediakan format XML standar untuk menetukan item menu, daripada membuat menu secara programn di Activity. Menu dan semua item menu pada Android didefinisikan dalam menu resource XML. Kemudian menu dapat diinflate pada MenuInflater di Activity atau Fragment.\n\nMenu resource Android menjadi praktik yang baik untuk beberapa alasan dibawah ini:\n\n1. Lebih mudah dalam memvisualisasikan struktur menu di dalam XML.\n2. Memisahkan antara konten menu pada aplikasi dengan kode dari Activity.\n3. Memungkinkan untuk membuat konfigurasi menu alternatif untuk berbagai versi platform, ukuran layar, dan konfigurasi lain dengan memanfaatkan framework app resource.\n\n`\u003cmenu\u003e`\nMendefinisikan Menu, yang merupakan wadah untuk item menu. Elemen `\u003cmenu\u003e` harus menjadi simpul akar untuk file dan dapat menampung satu atau lebih elemen `\u003citem\u003e` dan `\u003cgroup\u003e`.\n\n`\u003citem\u003e`\nMembuat MenuItem, yang mewakili satu item dalam menu. Elemen ini mungkin berisi elemen `\u003cmenu\u003e` bertingkat untuk membuat submenu.\n\n`\u003cgroup\u003e`\nPenampung opsional yang tidak terlihat untuk elemen `\u003citem\u003e`. Ini memungkinkan untuk mengkategorikan item menu sehingga mereka berbagi properti seperti status aktif dan visibilitas.\n\n**game_menu.xml**\n\n```Xml\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n\u003cmenu xmlns:android=\"http://schemas.android.com/apk/res/android\"\u003e\n    \u003citem android:id=\"@+id/new_game\"\n          android:icon=\"@drawable/ic_new_game\"\n          android:title=\"@string/new_game\"\n          android:showAsAction=\"ifRoom\"/\u003e\n    \u003citem android:id=\"@+id/help\"\n          android:icon=\"@drawable/ic_help\"\n          android:title=\"@string/help\" /\u003e\n\u003c/menu\u003e\n```\n\n`android:id`\nID sumber daya yang unik untuk item tersebut, yang memungkinkan aplikasi mengenali item ketika user memilihnya.\n\n`android:icon`\nReferensi ke drawable untuk digunakan sebagai ikon item.\n\n`android:title`\nReferensi ke string untuk digunakan sebagai judul item.\n\n`android:showAsAction`\nMenentukan kapan dan bagaimana item ini harus muncul sebagai item tindakan pada bar aplikasi.\n\n**group_menu.xml**\n\n```Xml\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n\u003cmenu xmlns:android=\"http://schemas.android.com/apk/res/android\"\u003e\n    \u003citem android:id=\"@+id/file\"\n          android:title=\"@string/file\" \u003e\n        \u003c!-- \"file\" submenu --\u003e\n        \u003cmenu\u003e\n            \u003citem android:id=\"@+id/create_new\"\n                  android:title=\"@string/create_new\" /\u003e\n            \u003citem android:id=\"@+id/open\"\n                  android:title=\"@string/open\" /\u003e\n        \u003c/menu\u003e\n    \u003c/item\u003e\n\u003c/menu\u003e\n```\n\n### `Create Option Menu`\n\nUntuk menentukan menu opsi untuk suatu Activity, ubah method onCreateOptionsMenu() (Fragment menyediakan callback onCreateOptionsMenu() tersendiri). Dalam method ini dapat inflate sumber daya menu (didefinisikan dalam XML) ke dalam Menu yang disediakan dalam callback. Sebagai contoh:\n\n**Activity.kt**\n\n```Kotlin\noverride fun onCreateOptionsMenu(menu: Menu): Boolean {\n    val inflater: MenuInflater = menuInflater\n    inflater.inflate(R.menu.game_menu, menu)\n    return true\n}\n```\n\n### `Handling click events`\n\nSaat user memilih item dari menu opsi (termasuk item tindakan di bar aplikasi), sistem akan memanggil method onOptionsItemSelected() di Activity. Metode ini melewati MenuItem yang dipilih. Kemudian akan mengidentifikasi item dengan memanggil getItemId() yang mengembalikan ID unik untuk item menu (ditentukan oleh atribut android:id dalam resource menu atau dengan integer yang diberikan ke metode add()). Setelah ini baru dapat mencocokkan ID ini dengan item menu yang diketahui untuk melakukan tindakan yang di sesuaikan. Sebagai contoh:\n\n**Activity.kt**\n\n```Kotlin\noverride fun onOptionsItemSelected(item: MenuItem): Boolean {\n    // Handle item selection\n    return when (item.itemId) {\n        R.id.new_game -\u003e {\n            newGame()\n            true\n        }\n        R.id.help -\u003e {\n            showHelp()\n            true\n        }\n        else -\u003e super.onOptionsItemSelected(item)\n    }\n}\n```\n\n## **RecyclerView**\n\nRecyclerView memudahkan untuk menampilkan kumpulan data yang besar secara efisien. RecyclerView menyediakan kumpulan data dan menentukan tampilan untuk setiap item, dan library RecyclerView secara dinamis membuat elemen saat dibutuhkan saja.\n\nSeperti namanya, RecyclerView berarti mendaur ulang elemen individual tersebut. Saat item di-scroll dari layar, RecyclerView tidak merusak tampilannya. Sebaliknya, RecyclerView menggunakan kembali tampilan untuk item baru yang telah di-scroll di layar. Penggunaan ulang ini sangat meningkatkan kinerja, meningkatkan responsivitas aplikasi dan mengurangi konsumsi daya.\n\n### `Installation`\n\nUntuk menggunakan RecyclerView, perlu ditambahkan sbeuah package pada `build.gradle` app:\n\n```Groovy\nimplementation 'com.google.android.material:material:1.2.1'\n```\n\n### `Steps to Use RecyclerView`\n\nJika akan menggunakan RecyclerView, ada beberapa hal yang perlu dilakukan. Disini akan dibahas secara rinci di bagian berikut ini.\n\nPertama-tama, tentukan seperti apa tampilan item dari list. Biasanya akan menggunakan salah satu LayoutManager pada library RecyclerView.\n\nDesain bagaimana setiap elemen dalam daftar akan terlihat dan diperlakukan. Berdasarkan desain ini, perlu sebuah class yang meng-extend dari class ViewHolder. ViewHolder menyediakan semua fungsionalitas untuk item pada list. View holder adalah pembungkus di sekitar view dan tampilan tersebut dikelola oleh RecyclerView.\n\nTentukan Adapter yang mengaitkan data list dengan tampilan ViewHolder.\n\n### `LayoutManager`\n\nItem pada RecyclerView diatur oleh kelas LayoutManager. Library RecyclerView menyediakan tiga LayoutManager yang umum digunakan dalam menangani situasi layout:\n\n1. LinearLayoutManager mengatur item dalam daftar satu dimensi.\n2. GridLayoutManager mengatur semua item dalam kisi dua dimensi:\n   - Jika grid disusun secara vertikal, GridLayoutManager mencoba membuat semua elemen di setiap baris memiliki lebar dan tinggi yang sama, tetapi baris yang berbeda dapat memiliki ketinggian yang berbeda pula.\n   - Jika grid disusun secara horizontal, GridLayoutManager mencoba membuat semua elemen di setiap kolom memiliki lebar dan tinggi yang sama, tetapi kolom yang berbeda dapat memiliki lebar yang berbeda pula.\n3. StaggeredGridLayoutManager mirip dengan GridLayoutManager, tetapi tidak mengharuskan item dalam satu baris memiliki tinggi yang sama (untuk kisi vertikal) atau item dalam kolom yang sama memiliki lebar yang sama (untuk kisi horizontal). Hasilnya adalah item dalam satu baris atau kolom dapat saling mengimbangi.\n\n### `Implement RecyclerView`\n\nSetelah menentukan layoutnya selanjutnya perlu menerapkan Adapter dan ViewHolder pada RecyclerView. Kedua kelas ini bekerja sama untuk menentukan bagaimana data akan ditampilkan. ViewHolder adalah pembungkus di sekitar Tampilan yang berisi layout untuk item individu dalam sebuah list. Adapter membuat objek ViewHolder sesuai kebutuhan dan juga menyetel data untuk tampilan tersebut. Proses mengaitkan tampilan ke datanya disebut binding data.\n\nSaat membuat adapter perlu mengganti tiga metode utama dari RecyclerView.Adapater yaitu:\n\n1. onCreateViewHolder(): RecyclerView memanggil metode ini setiap kali perlu membuat ViewHolder baru. Metode ini membuat dan menginisialisasi ViewHolder dan View terkaitnya, tetapi tidak mengisi konten tampilan — ViewHolder belum terikat ke data tertentu.\n\n2. onBindViewHolder(): RecyclerView memanggil metode ini untuk mengaitkan ViewHolder dengan data. Metode mengambil data yang sesuai dan menggunakan data tersebut untuk mengisi layout di view holder. Misalnya, jika RecyclerView menampilkan daftar nama, metode tersebut mungkin menemukan nama yang sesuai dalam daftar dan mengisi tampilan pada widget TextView.\n\n3. getItemCount(): RecyclerView memanggil metode ini untuk mendapatkan ukuran kumpulan data. Misalnya, dalam aplikasi buku alamat, ini mungkin jumlah total alamat. RecyclerView menggunakan ini untuk menentukan kapan tidak ada lagi item yang bisa ditampilkan.\n\nBerikut adalah contoh umum adapter sederhana dengan ViewHolder yang menampilkan daftar data. Dalam kasus ini, RecyclerView menampilkan daftar elemen teks sederhana. Adapter meneruskan larik string, berisi teks untuk elemen ViewHolder.\n\n**UserAdapter.kt**\n\n```Kotlin\nclass UserAdapter(private val context: Context) : RecyclerView.Adapter\u003cUserAdapter.ViewHolder\u003e() {\n\n    private var users = listOf\u003cUserModel\u003e()\n\n    fun setData(list: List\u003cUserModel\u003e) {\n        users = list\n        notifyDataSetChanged()\n    }\n\n    inner class ViewHolder(private val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root) {\n        fun bindData(userModel: UserModel) {\n            binding.run {\n                tvId.text = userModel.id.toString()\n                tvName.text = userModel.name\n            }\n        }\n    }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        return ViewHolder(ItemUserBinding.inflate(LayoutInflater.from(context), parent, false))\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n        holder.bindData(users[position])\n    }\n\n    override fun getItemCount(): Int = users.size\n}\n```\n\n**Activity.kt**\n\n```Kotlin\nclass EightActivity : AppCompatActivity(), MainView {\n\n    private val binding by lazy { ActivityEightBinding.inflate(layoutInflater) }\n    private val presenter by lazy { MainPresenterImpl(this) }\n    private val adapter by lazy { UserAdapter(this) }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(binding.root)\n\n        binding.rvUser.adapter = adapter\n        binding.etFilter.addTextChangedListener { text -\u003e presenter.filterUserByName(text.toString()) }\n\n        presenter.getAllUser()\n    }\n\n    override fun onSuccessGetAllUser(users: List\u003cUserModel\u003e) {\n        adapter.setData(users)\n    }\n\n    override fun onSuccessFilterUserByName(users: List\u003cUserModel\u003e) {\n        adapter.setData(users)\n    }\n}\n```\n\n## **Shared Preferences**\n\nSharedPreferences adalah penyimpanan lokal untuk small data collection yang mempunyai key dan value di Android. Objek SharedPreferences merujuk ke file berisi pasangan dari key dan value serta menyediakan metode sederhana untuk membaca dan menulis data. Setiap file SharedPreferences dikelola oleh framework Android dan dapat bersifat pribadi atau dibagikan.\n\n### `Save Data`\n\nSharedPreferences menyimpan data yang bersifat simple seperti Int, Boolean, String, Float, Set\u0026lt;String\u0026gt; dsb. Dalam menyimpan dan mengubah data di SharedPreferences perlu memanggil API SharedPreferences.Editor kemudian apply penambahan dan perubahannya.\n\n### `Read Data`\n\nMengambil data pada SharedPreferences harus eksplisit tipe datanya, semisal mengambil data String maka harus menggunakan method getString(key). SharedPreferences juga menyediakan input untuk default valuenya, ketika data yang disimpan adalah null atau tidak ada.\n\n**LocalStorage.kt**\n\n```Kotlin\nclass LocalStorage(private val context: Context) {\n\n    companion object {\n        private const val NAME = \"id.refactory.androidmaterial\"\n        private const val KEY_NAME = \"KEY_NAME\"\n        private const val KEY_AGE = \"KEY_AGE\"\n        private const val KEY_WEIGHT = \"KEY_WEIGHT\"\n        private const val KEY_IS_SINGLE = \"KEY_IS_SINGLE\"\n    }\n\n    private val sharedPreferences by lazy {\n        context.getSharedPreferences(NAME, Context.MODE_PRIVATE)\n    }\n\n    private inline fun \u003creified T\u003e SharedPreferences.save(key: String, value: T) {\n        edit {\n            when (value) {\n                is Int -\u003e putInt(key, value)\n                is String -\u003e putString(key, value)\n                is Float -\u003e putFloat(key, value)\n                is Boolean -\u003e putBoolean(key, value)\n                is Long -\u003e putLong(key, value)\n                is Set\u003c*\u003e -\u003e putStringSet(key, value as? Set\u003cString\u003e ?: setOf())\n            }\n        }\n    }\n\n    fun setName(name: String) {\n        sharedPreferences.save(KEY_NAME, name)\n    }\n\n    fun getName(): String {\n        return sharedPreferences.getString(KEY_NAME, \"\") ?: \"\"\n    }\n\n    fun setAge(age: Int) {\n        sharedPreferences.save(KEY_AGE, age)\n    }\n\n    fun getAge(): Int {\n        return sharedPreferences.getInt(KEY_AGE, -1)\n    }\n\n    fun setWeight(weight: Float) {\n        sharedPreferences.save(KEY_WEIGHT, weight)\n    }\n\n    fun getWeight(): Float {\n        return sharedPreferences.getFloat(KEY_WEIGHT, -1.0f)\n    }\n\n    fun setIsSingle(single: Boolean) {\n        sharedPreferences.save(KEY_IS_SINGLE, single)\n    }\n\n    fun getIsSingle(): Boolean {\n        return sharedPreferences.getBoolean(KEY_IS_SINGLE, true)\n    }\n}\n```\n\n**Activity.kt**\n\n```Kotlin\n\nval localStorage by lazy { LocalStorage(this) }\n\nlocalStorage.setName(tieName.text.toString())\nlocalStorage.setAge(tieAge.text.toString().toIntOrNull() ?: -1)\nlocalStorage.setWeight(tieWeight.text.toString().toFloatOrNull() ?: -1.0f)\nlocalStorage.setIsSingle(cbIsSingle.isChecked)\n```\n\n## **Intent**\n\nIntent adalah object perpesanan yang digunakan untuk meminta tindakan dari komponen, intent juga memfasilitasi komunikasi antar komponen dalam berbagai cara:\n\n- Starting an activity: Activity merepresentasikan sebagai sebuah screen di dalam Android, untuk memanggil sebuah Activity baru perlu menggunakan namany intent yang akan dilemparkan ke method startActivity().\n- Starting a service: Intent dapat memanggil sebuah service secara background task tanpa memerlukan interface yang ditampilkan kepada user.\n- Delivering a broadcast: Intent mampu mengirimkan sebuah Broadcast yang mampu diterima oleh semua komponen. Sistem dapat mengirimkan berbagai macam broadcast untuk event sistem Android.\n\n### `Intent Types`\n\nIntent memiliki dua macam tipe, diantaranya:\n\n- Explicit Intent menentukan komponen mana yang akan memenuhi kriteria dari intent dengan menyediakan target nama paket aplikasi atau nama kelas komponen yang sepenuhnya memenuhi syarat.\n\n```Kotlin\n// Executed in an Activity, so 'this' is the Context\n// The fileUrl is a string URL, such as \"http://www.example.com/image.png\"\nval downloadIntent = Intent(this, DownloadService::class.java).apply {\n    data = Uri.parse(fileUrl)\n}\nstartService(downloadIntent)\n```\n\n- Implisit Intent tidak menjelaskan nama secara spesifik dari taget komponen, melainkan mendeklarasikan tindakan umum dari aplikasi lain untuk menanganinya.\n\n```Kotlin\n// Create the text message with a string\nval sendIntent = Intent().apply {\n    action = Intent.ACTION_SEND\n    putExtra(Intent.EXTRA_TEXT, textMessage)\n    type = \"text/plain\"\n}\n\n// Verify that the intent will resolve to an activity\nif (sendIntent.resolveActivity(packageManager) != null) {\n    startActivity(sendIntent)\n}// Create the text message with a string\nval sendIntent = Intent().apply {\n    action = Intent.ACTION_SEND\n    putExtra(Intent.EXTRA_TEXT, textMessage)\n    type = \"text/plain\"\n}\n\n// Verify that the intent will resolve to an activity\nif (sendIntent.resolveActivity(packageManager) != null) {\n    startActivity(sendIntent)\n}\n```\n\n### `Action`\n\nAction di dalam Intent merupakan sebuah string yang menentukan tindakan secara umum yang akan dilakukan. Dalam kasus intent broadcast, inilah tindakan yang dijalankan dan sedang akan dilaporkan. Tindakan tersebut sangat menentukan bagaimana intent disusun terutama informasi yang terkandung dalam data dan extra intent. Berikut ini merupakan beberapa action yang sering digunakan di dalam Android:\n\n- `ACTION_MAIN` untuk action Activity sebagai entry point saat aplikasi dibuka.\n- `ACTION_VIEW` untuk action Activity dalam menampilkan informasi kepada user, seperti foto pada galeri, koordinat pada peta dsb.\n- `ACTION_SEND` untuk action Activity dalam berbagi data kepada aplikasi lainnya, seperti email, medsos dsb.\n\n### `Data`\n\nURI objek yang mereferensikan data yang akan ditindaklanjuti berdasarkan tipe MIME dari data tersebut. Jenis data yang disediakan biasanya ditentukan oleh action Intent. Misal actionnya adalah ACTION_EDIT maka datanya harus berupa URI dari dokument yang akan diedit.\n\n### `Category`\n\nString yang berisi informasi tambahan tentang jenis komponen yang harus menangani Intent tersebut. Sejumlah deskripsi kategori bisa ditempatkan dalam sebuah Intent, namun kebanyakannya Intent tidak memerlukan sebuah kategori. Berikut beberapa kategori yang umum digunakan:\n\n- `CATEGORY_BROWSABLE` Activity target memungkinkan dirinya untuk dimulai dari browser web untuk menampilkan data yang direferensikan oleh URL, seperti gambar atau pesan email.\n- `CATEGORY_LAUNCHER` Activity tersebut adalah activity awal dari sebuah tugas dan dicantumkan di launcher aplikasi sistem.\n\n### `Flags`\n\nFlags didefinisikan di kelas Intent yang berfungsi sebagai metadata untuk Intent tersebut. Flag dapat menginstruksikan sistem Android mengenai cara untuk meluncurkan activity (misalnya, tugas mana yang harus dimiliki activity) dan cara memperlakukannya setelah diluncurkan (misalnya, apakah activity tersebut termasuk dalam daftar activity terbaru).\n\n## **Fragment**\n\nFragment mewakili porsi dari user interface pada sebuah FragmentActivity, Fragment mensegmentasi aplikasi menjadi beberapa layar independen yang dikumpulkan dalam suatu Activity. Fragment dapat digabung dengan beberapa fragment lainnya dalam satu Activity untuk membuat tampilan dengan multi panel dan menggunakan fragment dalam Activity yang berbeda.\n\n### `Fragment LifeCycle`\n\nLifecycle dari Fragment hampir sama dengan Activity, namun fragment memiliki tambahan lifecycle dalam berhubungan dengan Activity. Lifecycle Fragment berjalan bersamaan dengan lifecycle Activity.\n\n![Images](assets/07.png)\n\n### `Create Fragment`\n\nFragment biasanya bagian dari sebuah user interface Activity dan berkontribusi dengan layout tersendiri pada Activity. Untuk membuat layout pada sebuah Fragment, harus mengimplementasikan sebuah method `onCreateView()` yang dipanggil pada saat Android sistem akan menggambar layout pada Fragment. method `onCreateView()` harus mengembalikan sebuah tipe `View` root pada fragment akan ditempatkan.\n\n```Kotlin\nclass BlankFragment : Fragment() {\n   override fun onCreateView(\n       inflater: LayoutInflater, container: ViewGroup?,\n       savedInstanceState: Bundle?\n   ): View? {\n       val view = inflater.inflate(R.layout.fragment_blank, container, false)\n       view.setOnClickListener { Toast.makeText(activity, \"Clicked\", Toast.LENGTH_SHORT).show() }\n       return view\n   }\n}\n```\n\n### `Create FrameLayout `\n\nFrameLayout biasanya digunakan untuk tempat menaruh sebuah layout Fragment, FrameLayout sebenarnya dirancang untuk memblokir area pada screen untuk menampilkan satu layout saja.\n\n```Xml\n\u003cFrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n   xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n   xmlns:tools=\"http://schemas.android.com/tools\"\n   android:id=\"@+id/flMain\"\n   android:layout_width=\"match_parent\"\n   android:layout_height=\"match_parent\"\n   tools:context=\".MainActivity\"/\u003e\n```\n\n### `Fragment Transaction`\n\nFragment Transaction digunakan untuk menampilkan layout fragment pada FrameLayout dengan banyak cara diantaranya dengan menggunakan method `add()` untuk menambahkan layout fragment pada Frame Layout dan `replace()` untuk mengganti layout fragment pada FrameLayout dengan layout fragment yang baru. Untuk memanggil sebuah Fragment Transaction pastikan untuk mendapatkan Fragment Manager terlebih dahulu.\n\n```Kotlin\nsupportFragmentManager.beginTransaction().add(R.id.flMain, BlankFragment()).commit()\nsupportFragmentManager.beginTransaction().replace(R.id.flMain, BlankFragment()).commit()\nsupportFragmentManager.beginTransaction().remove(BlankFragment())\n```\n\n## **Android Jetpack**\n\nAndroid Jetpack adalah sekumpulan libray untuk membantu developer dalam mengembangkan aplikasi mengikuti best practise, mengurangi kode yang boilerplate dan menulis kode yang berfungsi secara konsisten pada semua versi dan perangkat Android, sehingga developer hanya fokus kepada kode yang mereka kerjakan.\n\nBeberapa jenis library yang termasuk ke dalam Android Jetpack:\n\n1. Activity\n2. AppCompat\n3. Camera\n4. Compose\n5. DataBinding\n6. Fragment\n7. Hilt\n8. Lifecycle\n9. Material Design Component\n10. Navigation\n11. Paging\n12. Room\n13. Test\n14. Ads\n15. Annotation\n16. Arch Core\n\n## **Single Activity Architecture**\n\nSingle Activity Architecture merupakan sebuah konsep dalam mendesign aplikasi pada Android dengan mengandalkan hanya satu Activity untuk melampirkan Fragment. Fragment pada konsep ini digunakan untuk menampilkan data, mengerjakan tugas seperti layaknya Activity dalam menerima input dari user, dsb.\n\n### `Fragment`\n\nDalam menggunakan Single Activity Architecture, perlu sebuah Fragment untuk menggantikan Activity. Fragment yang perlu ditambahkan adalah Fragment dari Android Jetpack dengan cara sebagai berikut ini pada `build.gradle` app:\n\n```Groovy\ndepedencies {\n    // Fragment Android JetPack\n    def fragment_version = \"1.2.5\"\n    implementation \"androidx.fragment:fragment-ktx:$fragment_version\"\n}\n```\n\n### `Navigation Graph`\n\nNavigation Graph adalah sebuah file resource seperti layout, drawable, values, dsb yang berisikan semua tujuan dan aksi dari Fragment. Navigation Graph memberikan kemudahan kepada Developer dalam mengetahui jalur dari setiap Fragment, sehingga Developer akan mudah dalam mengetahui alur dari aplikasi yang dikembangkan.\n\nCara membuat Navigation Graph\n\n1. Pada `app/src/main/res` klik kanan (Linux dan Windows) atau CTRL + CLICK (macOS), kemudian pilih `New \u003e Android Resource File` seketika akan muncul dialog berikut ini: ![Image](assets/01.png) Isikan nama resourenya sebagai contoh `nav_graph` pada kolom file name, kemudian pilih resource typenya berupa `Navigation`, kemudian tekan `OK`.\n\n2. Setelah membuat file resouce Nav Graph, selanjutnya adalah membuat Nav Graph menggunakan Navigatior Editor dengan tampilan sebagai berikut ini: ![Image](assets/02.png)\n\n3. Setelah melakukan edit pada Nav Editor, selanjutnya adalah menambahkan NavHost pada layout Activity dengan cara berikut ini:\n\n```Xml\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n\u003candroidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".day11.ElevenActivity\"\u003e\n\n    \u003candroidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/nav_host_fragment_container\"\n        android:name=\"androidx.navigation.fragment.NavHostFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:defaultNavHost=\"true\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:navGraph=\"@navigation/nav_graph\" /\u003e\n\n\u003c/androidx.constraintlayout.widget.ConstraintLayout\u003e\n```\n\n#### `Safe Args`\n\nSafe Args merupakan cara untuk mengirimkan data kepada Fragment lain dalam Navigation Component dengan cara yang aman, cara aman yang dimaksudkan adalah daya yang terkirim bersifat explisit artinya sudah ditentukan diawal bahwa Origin Fragment akan mengirimkan data dengan tipe data yang sudah ditentukan diawala kepada Destination Fragment. Untuk menambahkan Safe Args perlu menambahkan konfigurasi pada `build.gradle` project berikut ini:\n\n```Groovy\nbuildscript {\n    repositories {\n        google()\n    }\n    dependencies {\n        def nav_version = \"2.3.1\"\n        classpath \"androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version\"\n    }\n}\n```\n\nkemudian menambahkan baris ini `apply plugin: \"androidx.navigation.safeargs.kotlin\"` pada `build.gradle` app.\n\nCara menggunakan Safe Args\n\n1. Mendefinisikan arguments yang akan dikirim dari Origin Fragment ke Destination Fragment pada Nav Editor, sebagai contoh kurang lebih seperti ini dalam mendefinisikan argument pada Nav Editor: ![Image](assets/03.png)\n\n2. Melakukan aksi ke Destination Fragment dengan membawa argument yang sudah ditentukan, sebagai contoh:\n\n   ```Kotlin\n   findNavController().navigate(\n               TodoFragmentDirections.actionTodoFragmentToTodoDetailFragment(todo)\n   )\n   ```\n\n3. Menerima argument yang diberikan oleh Origin Fragment, sehingga pada Destination Fragment dapat mempunyai data yang dikirimkan oleh Origin Fragment, sebagai contoh:\n\n   ```Kotlin\n   class DetailFragment : Fragment() {\n\n       private lateinit var binding: FragmentDetailBinding\n       private val args by navArgs\u003cDetailFragmentArgs\u003e()\n\n       override fun onCreateView(\n           inflater: LayoutInflater, container: ViewGroup?,\n           savedInstanceState: Bundle?\n       ): View? {\n           binding = FragmentDetailBinding.inflate(inflater, container, false).apply {\n               tvResult.text = args.data\n           }\n\n           return binding.root\n       }\n   }\n   ```\n\n## **View Model**\n\nViewModel adalah class yang dirancang untuk menyimpan dan mengelola data terkati UI dengan cara sadar pada lifecycle Activity maupun Fragment). ViewModel memungkinkan untuk menahan data dari perubahan konfigurasi seperti rotasi layar pada Android.\n\n### `Lifecycle View Model`\n\n![Image](assets/04.png)\n\nGambar diatas menjelaskan bahwa lingkup dari View Model cukup banyak mulai saat onCreate sampai onDestroy, sehingga ViewModel dapat menjaga data sampai dengan onDestroy.\n\n### `Menambahkan View Model`\n\nPertama tambahkan View Model dengan cara menambahkan baris seperti dibawah ini pada `build.gradle` app:\n\n```Groovy\ndepedencies {\n    def lifecycle_version = \"2.2.0\"\n\n    implementation \"androidx.lifecycle:lifecycle-extensions:$lifecycle_version\"\n    implementation \"androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version\"\n    implementation \"androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version\"\n    implementation \"androidx.lifecycle:lifecycle-common-java8:$lifecycle_version\"\n}\n```\n\n### `Menggunakan View Model`\n\nDengan menggunakan ViewModel, data pada Android dapat disimpan secara Synchronous maupun Asynchronous menggunakan Live Data, tidak hanya itu Live Data dapat melakukan observe pada data sehingga ketika data mengalami perubahan maka perubahan tersebut dapat ditampilkan secara langsung tanpa harus melakukan binding ulang.\n\n**TodoViewModel**.kt\n\n```Kotlin\nsealed class State {\n    data class Loading(val message: String = \"Loading...\") : State()\n    data class Success(val data: Set\u003cString\u003e) : State()\n    data class Error(val message: String = \"Oops something went wrong!\") : State()\n}\n\nclass TodoViewModel(private val app: Application) : AndroidViewModel(app) {\n    private val localStorage by lazy { LocalStorage(app.applicationContext) }\n\n    private val mutableTodoState by lazy { MutableLiveData\u003cState\u003e() }\n    val todoSate get() = mutableTodoState\n\n    fun getTodo() {\n        mutableTodoState.value = State.Loading()\n\n        Handler(Looper.getMainLooper()).postDelayed({\n            val data = localStorage.getTodo()\n            mutableTodoState.value = State.Success(data)\n        }, 1000)\n    }\n}\n```\n\n**TodoFragment.kt**\n\n```Kotlin\nclass TodoFragment : Fragment(), TodoListener {\n\n    private lateinit var binding: FragmentTodoBinding\n    private val adapter by lazy { TodoAdapter(requireActivity(), this) }\n    private val viewModel by viewModels\u003cTodoViewModel\u003e()\n\n    override fun onCreateView(\n        inflater: LayoutInflater, container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        binding = FragmentTodoBinding.inflate(inflater, container, false).apply {\n            rvTodo.adapter = adapter\n            btnAdd.setOnClickListener {\n                if (tieTodo.text.toString().isEmpty()) {\n                    Toast.makeText(\n                        requireActivity(),\n                        \"Todo tidak boleh kosong!\",\n                        Toast.LENGTH_SHORT\n                    ).show()\n                } else {\n                    viewModel.getTodo()\n\n                    tieTodo.setText(\"\")\n                }\n            }\n        }\n\n        viewModel.getTodo()\n        viewModel.todoSate.observe(viewLifecycleOwner, {\n            when (it) {\n                is State.Loading -\u003e {\n                    showLoading(true)\n                }\n                is State.Success -\u003e {\n                    showLoading(false)\n\n                    adapter.setData(it.data)\n                }\n                is State.Error -\u003e {\n                    showLoading(false)\n\n                    Toast.makeText(\n                        requireActivity(),\n                        it.message,\n                        Toast.LENGTH_SHORT\n                    ).show()\n                }\n            }\n        })\n\n        return binding.root\n    }\n\n    private fun showLoading(isLoading: Boolean) {\n        binding.pbLoading.visibility = if (isLoading) View.VISIBLE else View.INVISIBLE\n        binding.rvTodo.visibility = if (!isLoading) View.VISIBLE else View.INVISIBLE\n    }\n\n    override fun onClick(todo: String) {\n        findNavController().navigate(\n            TodoFragmentDirections.actionTodoFragmentToTodoDetailFragment(todo)\n        )\n    }\n\n    override fun onDelete(todo: String) {\n        adapter.deleteData(todo)\n    }\n}\n```\n\n## **Layout**\n\n![Image](assets/05.png)\n\nSebuah layout pada Android digunakan untuk menentukan tampilan letak dari setiap View dan ViewGroup Android. View merupakan tampilan yang dapat dilihat dan berinteraksi dengan user, sedangkan ViewGroup merupakan wadah yang mendefinisikan struktur letak untuk View dan juga ViewGroup lainnya. View biasanya dikenal dengan nama Widget, seperti TextView atau Button. ViewGroup biasanya dikenal dengan Layout seperti LinearLayout atau ConstraintLayout.\n\n### `Deklarasi Layout`\n\nMendklarasikan layout dapat dilakukan dengan dua cara:\n\n1. Mendeklarasikan View atau ViewGroup di dalam XML, membuat layout dengan cara seperti Web. Mendeklarasikan Layout dapat dilakukan dengan cara Drag and Drop menggunakan Android Studio Layout Editor.\n\n   ```Xml\n   \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n   \u003cLinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n               android:layout_width=\"match_parent\"\n               android:layout_height=\"match_parent\"\n               android:orientation=\"vertical\" \u003e\n       \u003cTextView android:id=\"@+id/text\"\n               android:layout_width=\"wrap_content\"\n               android:layout_height=\"wrap_content\"\n               android:text=\"Hello, I am a TextView\" /\u003e\n       \u003cButton android:id=\"@+id/button\"\n               android:layout_width=\"wrap_content\"\n               android:layout_height=\"wrap_content\"\n               android:text=\"Hello, I am a Button\" /\u003e\n   \u003c/LinearLayout\u003e\n   ```\n\n2. Mendeklarasikan layout dengan cara program.\n\n   ```Kotlin\n   class ElevenActivity : AppCompatActivity() {\n\n       override fun onCreate(savedInstanceState: Bundle?) {\n           super.onCreate(savedInstanceState)\n           val layout = LinearLayout(this).apply {\n               orientation = LinearLayout.VERTICAL\n               addView(TextView(applicationContext).apply { text = \"Hello, I am a TextView\" })\n               addView(Button(applicationContext).apply { text = \"Hello, I am a Button\" })\n           }\n           setContentView(layout)\n       }\n   }\n   ```\n\n### `Attributes`\n\nSetiap View dan juga ViewGroup mendukung banyak variasi attribute XML mereka sendiri. Beberapa attribute ada yang hanya dimiliki oleh View atau ViewGroup saja, contohnya View TextView memiliki textSize dan View Group RelativeLayout memiliki centerInParent.\n\n```Xml\n\u003cButton\n        android:id=\"@+id/btn_add\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:layout_marginTop=\"12dp\"\n        android:padding=\"12dp\"\n        android:text=\"Tambah\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/rv_todo\" /\u003e\n```\n\n### `ID`\n\nSetiap View maupun ViewGroup memiliki attribut ID yang digunakan mengidentifikasi View atau ViewGroup secara unik. Saat aplikasi dikompilasi, ID ini direferensikan sebagai integer, tetapi ID biasanya ditetapkan dalam file XML layout sebagai string.\n\n```Xml\n\u003cButton\n        android:id=\"@+id/btn_add\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content /\u003e\n```\n\n### `Layout Parameters`\n\nAttribut yang mengandung kata `layout_` digunakan untuk menentukan parameter tata letak View atau ViewGroup sesuai dengan ViewGroup tempatnya berada.\n\n```Xml\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n\u003candroidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".day11.fragments.HomeFragment\"\u003e\n\n    \u003ccom.google.android.material.textfield.TextInputLayout\n        style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:hint=\"Data yang akan dikirimkan\"\n        app:layout_constraintEnd_toStartOf=\"@id/btn_send\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\u003e\n\n        \u003ccom.google.android.material.textfield.TextInputEditText\n            android:id=\"@+id/tie_data\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"16sp\" /\u003e\n\n    \u003c/com.google.android.material.textfield.TextInputLayout\u003e\n\n    \u003cButton\n        android:id=\"@+id/btn_send\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"16dp\"\n        android:padding=\"12dp\"\n        android:text=\"Send\"\n        android:textSize=\"16sp\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" /\u003e\n\n\u003c/androidx.constraintlayout.widget.ConstraintLayout\u003e\n```\n\nSemua View dan ViewGroup memiliki attribute yang wajib diisi yaitu `layout_width` dan `layout_height`. Untuk menyetel `layout_width` dan `layout_height` dapat menggunakan salah satu konstanta berikut ini:\n\n1. `wrap_content` tampilan menyesuaikan ukurannya dengan dimensi yang dibutuhkan oleh kontennya.\n2. `match_parent` tampilan sebesar yang diizinkan oleh grup tampilan induknya.\n\n### `LinearLayout`\n\nLinearLayout merupakan class Layout yang mengatur View secara linear berdasarkan orientasinya, orientasi vertical berarti View akan tersusun linear dari atas ke bawah dan orientasi horizontal berarti View akan tersusun linear dari kiri ke kanan.\n\n```Xml\n\u003cLinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n   android:layout_width=\"match_parent\"\n   android:layout_height=\"match_parent\"\n   android:gravity=\"center\"\u003e\n\n   \u003cTextView\n       android:layout_width=\"0dp\"\n       android:layout_height=\"wrap_content\"\n       android:layout_weight=\"1\"\n       android:gravity=\"center\"\n       android:text=\"One\" /\u003e\n\n   \u003cTextView\n       android:layout_width=\"0dp\"\n       android:layout_height=\"wrap_content\"\n       android:layout_weight=\"1\"\n       android:gravity=\"center\"\n       android:text=\"Two\" /\u003e\n\n\u003c/LinearLayout\u003e\n```\n\n### `RelativeLayout`\n\nRelativeLayout merupakan class Layout yang mengatur View berdasarkan dengan View lainnya, sehingga tidak tersusun linear bahkan bisa menumpuk kalau tidak jelaskan hubungan dengan View lainnya.\n\n```Xml\n\u003cRelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n   android:layout_width=\"match_parent\"\n   android:layout_height=\"match_parent\"\n   android:gravity=\"center\"\u003e\n\n   \u003cTextView\n       android:layout_width=\"match_parent\"\n       android:layout_height=\"wrap_content\"\n       android:layout_toStartOf=\"@id/tvTwo\"\n       android:text=\"One\" /\u003e\n\n   \u003cTextView\n       android:id=\"@+id/tvTwo\"\n       android:layout_width=\"wrap_content\"\n       android:layout_height=\"wrap_content\"\n       android:layout_alignParentEnd=\"true\"\n       android:text=\"Two\" /\u003e\n\n\u003c/RelativeLayout\u003e\n```\n\n### `ConstraintLayout`\n\n**ConstraintLayout** adalah class Layout yang menggabungkan konsep tata letak dari **RelativeLayout** dan `layout_weight` pada **LinearLayout** sehingga dapa membuat sebuah layout tanpa nested layout (didalam Layout ada Layout lagi).\n\n```Xml\n\u003candroidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\u003e\n\n   \u003cTextView\n       android:id=\"@+id/tvOne\"\n       android:layout_width=\"0dp\"\n       android:layout_height=\"wrap_content\"\n       android:layout_toStartOf=\"@id/tvTwo\"\n       android:text=\"One\"\n       app:layout_constraintBottom_toBottomOf=\"parent\"\n       app:layout_constraintEnd_toStartOf=\"@id/tvTwo\"\n       app:layout_constraintHorizontal_weight=\"1\"\n       app:layout_constraintStart_toStartOf=\"parent\"\n       app:layout_constraintTop_toTopOf=\"parent\" /\u003e\n\n   \u003cTextView\n       android:id=\"@+id/tvTwo\"\n       android:layout_width=\"0dp\"\n       android:layout_height=\"wrap_content\"\n       android:text=\"Two\"\n       app:layout_constraintBottom_toBottomOf=\"parent\"\n       app:layout_constraintEnd_toEndOf=\"parent\"\n       app:layout_constraintHorizontal_weight=\"1\"\n       app:layout_constraintStart_toEndOf=\"@+id/tvOne\"\n       app:layout_constraintTop_toTopOf=\"parent\" /\u003e\n\n\u003c/androidx.constraintlayout.widget.ConstraintLayout\u003e\n```\n\n## **Retrofit**\n\nRetrofit adalah sebuah HTTP Client dengan menggunan tipe data yang aman dalam pemanggilan REST API pada Native Android. Retrofit menjadi HTTP Client yang sering digunakan di Native Android, karena mudah digunakan, dapat dikombinasikan dengan package lainnya seperti RxJava, Coroutines, OkHttp, dsb.\n\n### `GSON`\n\nGSON merupakan library Java yang digunakan untuk mengkonversi Java / Kotlin Object ke dalam bentuk JSON, GSON juga dapat mengkoversi String JSON menjadi Java / Kotlin Object.\n\n### `Installation`\n\nBerikut adalah perintah untuk menambahkan Retrofit dan juga GSON pada `build.gradle` app:\n\n```Groovy\ndependencies {\n    // Retrofit\n    def retrofit_version = \"2.9.0\"\n    implementation \"com.squareup.retrofit2:retrofit:$retrofit_version\"\n    implementation \"com.squareup.retrofit2:converter-gson:$retrofit_version\"\n}\n```\n\n### `Create Service`\n\nUntuk memangil response dari REST API pada Retrofit, pertama perlu sebuah service terlebih dahulu dengan contoh sebagai berikut ini:\n\n```Kotlin\ninterface UserService {\n   @GET(\"api\")\n   @Headers(\"Accept: application/json\")\n   fun getUser(@Query(\"results\") result: Int): Call\u003cResult\u003e\n}\n```\n\n### `Create Client`\n\nSetelah membuat Service langkah selanjutnya adalah membuat sebuah Client dari `BASE_URL` yang akan dipanggil.\n\n```Kotlin\nclass UserClient {\n   companion object {\n       fun service(): UserService {\n           val retrofit = Retrofit.Builder()\n               .baseUrl(\"https://randomuser.me/\")\n               .addConverterFactory(\n                   GsonConverterFactory.create(GsonBuilder().setLenient().create())\n               )\n               .build()\n\n           return retrofit.create(UserService::class.java)\n       }\n   }\n}\n```\n\n### `Call Service`\n\nPenggunaan Retrofit pada saat memang REST API dari server:\n\n```Kotlin\nUserClient.service().getUser(100).enqueue(object : Callback\u003cResult\u003e {\n   override fun onResponse(call: Call\u003cResult\u003e, response: Response\u003cResult\u003e) {\n       println(response.body()?.results)\n   }\n\n   override fun onFailure(call: Call\u003cResult\u003e, t: Throwable) {\n       t.printStackTrace()\n\n       println(t.message)\n   }\n})\n```\n\n## **Glide**\n\nGlide adalah pengelola sumber daya open source yang cepat dan efisien serta framework dalam memuat gambar yang menggabungkan media decoding, memory dan disk caching serta penyatuan resource ke dalam bentuk penggunaan interface yang simpel dan mudah.\n\nGlide mendukung untuk mengambil, decoding dan menampilkan video, gambar, dan animasi GIF.\n\nFokus utama dari Glide adalah membuat scrolling list yang mempunyai gambar apapun sehalus dan secepat mungkin, selain itu Glide juga efektif untuk hampir semua kasus mengenai mengambil, mengubah ukuran dan menampilkan gambar dari internet.\n\n### `Installation`\n\nMenambahkan Glide pada project dengan cara menambahkan baris dibawah ini pada `build.gradle` app:\n\n```Groovy\nplugins {\n    id \"androidx.navigation.safeargs.kotlin\"\n}\n\ndepedencies {\n    // Glide\n    def glide_version = \"4.11.0\"\n    implementation \"com.github.bumptech.glide:glide:$glide_version\"\n    kapt \"com.github.bumptech.glide:compiler:$glide_version\"\n}\n```\n\n### `Usage`\n\nPenggunaan Glide pada project dengan beberapa opsi tambahan seperti placeholder, center crop, circle crop dan error.\n\n```Kotlin\nGlide.with(view)\n   .load(picture.large)\n   .placeholder(R.drawable.ic_placeholder)\n   .centerCrop()\n   .apply { circleCrop() }\n   .error(R.drawable.ic_warning).into(view.ivUser)\n```\n\n## **Advance RecyclerView**\n\nRecyclerView mempunyai konfigurasi yang banyak, sehingga dapat memiliki banyak jenis tampilan yang akan muncul pada RecyclerView seperti LayoutManager, Item Decoration, Item Animator, Section Header dan View Types, Drag and Drop, Swipe dan Multi Selection.\n\n### `LayoutManager`\n\nLayoutManager bertanggung jawab untuk mengukur dan memposisikan tampilan item dalam RecyclerView serta menentukan kebijakan kapan harus mendaur ulang tampilan item yang tidak lagi terlihat oleh pengguna.\n\nDengan mengubah LayoutManager, RecyclerView dapat digunakan untuk mengimplementasikan scrolling vertikal yang standar, grid yang seragam, staggered grids, scrolling horizontal yang standar, dan banyak lagi.\n\n```Kotlin\nval adapter = UserAdapter()\nval linearLayoutManager = LinearLayoutManager(this)\nval verticalLinear = linearLayoutManager.apply { orientation = LinearLayoutManager.VERTICAL }\nval horizontalLinear = linearLayoutManager.apply { orientation = LinearLayoutManager.HORIZONTAL }\nval verticalStaggered = StaggeredGridLayoutManager(2, LinearLayoutManager.VERTICAL)\nval horizontalStaggered = StaggeredGridLayoutManager(2, LinearLayoutManager.HORIZONTAL)\nval verticalGrid = GridLayoutManager(this, 2).apply { orientation = LinearLayoutManager.VERTICAL }\nval horizontalGrid = GridLayoutManager(this, 2).apply { orientation = LinearLayoutManager.HORIZONTAL }\n\nrvUsers.layoutManager = verticalLinear\nrvUsers.layoutManager = horizontalLinear\nrvUsers.layoutManager = verticalStaggered\nrvUsers.layoutManager = horizontalStaggered\nrvUsers.layoutManager = verticalGrid\nrvUsers.layoutManager = horizontalGrid\nrvUsers.adapter = adapter\n```\n\n### `Item Decoration`\n\nItem Decoration memungkinkan aplikasi menambahkan gambar khusus dan offset tata letak ke tampilan item tertentu dari list data pada adapter. Ini dapat berguna untuk menggambar pemisah antara item, highlights, batas pengelompokan visual, dan lainnya.\n\n```Kotlin\nrvUsers.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))\n```\n\n### `Item Animator`\n\nItem Animator mendefinisikan animasi yang berlangsung pada saat item mengalami perubahan di adaptEr. Subclass ItemAnimator dapat digunakan untuk mengimplementasikan animasi kustom untuk tindakan pada item ViewHolder.\n\n```Kotlin\nrvUsers.itemAnimator = DefaultItemAnimator()\nadapter.list.toMutableList().add(0, etTask.text.toString())\nadapter.notifyItemInserted(0)\nadapter.list.toMutableList().removeAt(0)\nadapter.notifyItemRemoved(0)\nadapter.list.toMutableList()[0] = \"Hahahahahhaa\"\nadapter.notifyItemChanged(0)\n```\n\n### `Section Header and View Types`\n\nRecyclerView android dapat menampilkan lebih dari satu tipe view dengan tujuan untuk menampilkan data yang berbeda. Section Header juga dapat mengelompokkan item berdasarkan groupnya, seperti sebuah list contact dimana ada Section Header berupa Abjad dari nama contact dan dibawahnya ada contact yang diawali abjad tersebut.\n\n**UserAdapter**.kt\n\n```Kotlin\noverride fun getItemViewType(position: Int) = when (list[position]) {\n   is Product.ProductHeader -\u003e HEADER\n   is Product.ProductMore -\u003e MORE\n   is Product.ProductRow -\u003e ROW\n}\n\noverride fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {\n   return when (viewType) {\n       HEADER -\u003e HeaderViewHolderHolder(\n           LayoutInflater.from(parent.context).inflate(R.layout.item_header, parent, false)\n       )\n       MORE -\u003e MoreViewHolderHolder(\n           LayoutInflater.from(parent.context).inflate(R.layout.item_more, parent, false)\n       )\n       ROW -\u003e RowViewHolderHolder(\n           LayoutInflater.from(parent.context).inflate(R.layout.item_product, parent, false)\n       )\n       else -\u003e throw Exception(\"Layout isn't defined\")\n   }\n}\n```\n\n### `Drag and Drop`\n\nRecyclerView dapat menerima gesture seperti Drag and Drop, untuk melakukan hal ini perlu membuat sebuah class yang mewariskan dari `ItemTouchHelper.Callback()`\n\n```Kotlin\nclass ItemMoveCallback(private val contract: ItemTouchHelperContract) : ItemTouchHelper.Callback() {\n\n   override fun getMovementFlags(\n       recyclerView: RecyclerView,\n       viewHolder: RecyclerView.ViewHolder\n   ): Int { … }\n   override fun isLongPressDragEnabled() = false\n   override fun isItemViewSwipeEnabled() = false\n   override fun onMove(\n       recyclerView: RecyclerView,\n       viewHolder: RecyclerView.ViewHolder,\n       target: RecyclerView.ViewHolder\n   ): Boolean { … }\n   override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { … }\n   override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { … }\n   override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { … }\n\n   interface ItemTouchHelperContract {\n       fun onRowMoved(from: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder)\n       fun onRowSelected(myViewHolder: ProductAdapter.RowViewHolderHolder)\n       fun onRowClear(myViewHolder: ProductAdapter.RowViewHolderHolder)\n   }\n}\n```\n\n### `Multi Selection`\n\nRecyclerView dapat dicustom untuk kasus multi selection hanya dengan bermain kondisi saja.\n\n```Kotlin\nitemView.setOnClickListener {\n   selected = !selected\n\n   val index = adapter.list.indexOfFirst { it is Product.ProductRow \u0026\u0026 it.id == data.id }\n   adapter.list.toMutableList()[index] = this\n   adapter.notifyItemChanged(index)\n}\n\nitemView.ivCheck.visibility = if (selected) View.VISIBLE else View.GONE\n```\n\n## **View Pager**\n\nView Pager merupakan sebuah view yang dapat berisikan beberapa tampilan yang dapat melakukan transisi dari satu tampilan ke tampilan lainnya, contohnya seperti sebuah slideshow.\n\n### `Swipeable Screen`\n\nSwipeable Screen merupakan tampilan yang memungkinkan user untuk melakukan navigasi antar screens dengan menggunakan gestur horizontal seperti swipe, user dapat melakukan transisi pada screen sebelumnya atau screen sesudahnya. Swipeable Screen dapat dilakukan dengan menggunakan View Pager dan menggunakan FragmentStateAdapter untuk menyimpan daftar tampilan berupa fragment.\n\n### `Installation`\n\nUntuk membuat View Pager, pertama harus intall View Pager terlebih dahulu pada `build.gradle` app:`\n\n```Groovy\ndepedencies {\n    implementation \"androidx.viewpager2:viewpager2:1.0.0\"\n}\n```\n\n### `Usage`\n\nDalam memakai View Pager cukup mudah, pertama tambahkan View Pager di dalam layout dengan cara berikut ini:\n\n```Xml\n\u003candroidx.viewpager2.widget.ViewPager2\n        xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:id=\"@+id/vp_pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" /\u003e\n```\n\n### `FragmentStateAdapter`\n\nFragmentStateAdapter digunakan untuk mengatur setiap page berupa fragment pada ViewPager dan juga menyimpan dan mengembalikan state pada Fragment.\n\n```Kotlin\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentActivity\n\nprivate const val NUM_PAGES = 5\n\nclass ScreenSlidePagerActivity : FragmentActivity() {\n\n    private lateinit var viewPager: ViewPager2\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_screen_slide)\n\n        viewPager = findViewById(R.id.vp_pager)\n\n        val pagerAdapter = ScreenSlidePagerAdapter(this)\n        viewPager.adapter = pagerAdapter\n    }\n\n    override fun onBackPressed() {\n        if (viewPager.currentItem == 0) {\n            super.onBackPressed()\n        } else {\n            viewPager.currentItem = viewPager.currentItem - 1\n        }\n    }\n\n    private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {\n        override fun getItemCount(): Int = NUM_PAGES\n\n        override fun createFragment(position: Int): Fragment = ScreenSlidePageFragment()\n    }\n}\n```\n\n### `TabLayout`\n\nTabLayout menyediakan horizontal layout untuk menampilkan tab. Populasi tab yang akan ditampilkan dilakukan dengan cara membuat instance dari TabLayout.Tab. Dalam membuat tab dapat melalui method `newTab()`. Dari sana dapat mengubah label atau ikon tab melalui setText dan setIcon.\n\n```Xml\n\u003ccom.google.android.material.tabs.TabLayout\n        android:id=\"@+id/tl_tabs\"\n        android:layout_height=\"wrap_content\"\n        android:layout_width=\"match_parent\"/\u003e\n```\n\n```Kotlin\nval tabLayout = findViewById\u003cTabLayout\u003e(R.id.tl_tabs)\ntabLayout.addTab(tabLayout.newTab().setText(\"Tab 1\"))\ntabLayout.addTab(tabLayout.newTab().setText(\"Tab 2\"))\ntabLayout.addTab(tabLayout.newTab().setText(\"Tab 3\"))\n```\n\n### `TabLayoutMediator`\n\nTabLayoutMediator merupakan sebuah mediator yang digunakan untuk menghubungkan TabLayout dengan View Pager. TabLayoutMediator akan menyinkronkan posisi View Pager dengan tab yang dipilih saat tab dipilih, dan posisi scroll TabLayout saat user menyeret View Pager. TabLayoutMediator akan memanggil OnPageChangeCallback View Pager untuk menyesuaikan tab saat View Pager bergerak. TabLayoutMediator memanggil OnTabSelectedListener TabLayout untuk menyesuaikan View Pager saat tab bergerak. TabLayoutMediator memanggil AdapterDataObserver RecyclerView untuk membuat ulang konten tab saat set data berubah.\n\n**layout.xml**\n\n```Xml\n\u003candroidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n   xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n   xmlns:tools=\"http://schemas.android.com/tools\"\n   android:layout_width=\"match_parent\"\n   android:layout_height=\"match_parent\"\n   tools:context=\".fragments.MainFragment\"\u003e\n\n   \u003ccom.google.android.material.tabs.TabLayout\n       android:id=\"@+id/tl_main\"\n       android:layout_width=\"match_parent\"\n       android:layout_height=\"wrap_content\"\n       app:layout_constraintTop_toTopOf=\"parent\"\n       app:tabBackground=\"@color/colorPrimary\"\n       app:tabSelectedTextColor=\"@color/colorBackground\"\n       app:tabTextColor=\"@color/colorUnselected\" /\u003e\n\n   \u003candroidx.viewpager2.widget.ViewPager2\n       android:id=\"@+id/vp_main\"\n       android:layout_width=\"match_parent\"\n       android:layout_height=\"0dp\"\n       app:layout_constraintBottom_toBottomOf=\"parent\"\n       app:layout_constraintTop_toBottomOf=\"@id/tl_main\" /\u003e\n\n\u003c/androidx.constraintlayout.widget.ConstraintLayout\u003e\n```\n\n**Activity.kt**\n\n```Kotlin\nclass MainFragment : Fragment() {\n   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n       super.onViewCreated(view, savedInstanceState)\n\n       with(view) {\n           val pages = listOf(Page(\"Product\", ProductFragment()), Page(\"Bookmark\", BookmarkFragment()))\n           val adapter = MainViewPagerAdapter(pages, childFragmentManager, lifecycle)\n\n           vpMain.adapter = adapter\n           TabLayoutMediator(tlMain, vpMain) { tab, i -\u003e tab.text = pages[i].title }.attach()\n       }\n   }\n}\n\ndata class Page(val title: String, val fragment: Fragment)\n\nclass MainViewPagerAdapter(\n   private val list: List\u003cPage\u003e,\n   fragmentManager: FragmentManager,\n   lifecycle: Lifecycle\n) : FragmentStateAdapter(fragmentManager, lifecycle) {\n   override fun getItemCount(): Int = list.size\n   override fun createFragment(position: Int): Fragment = list[position].fragment\n}\n```\n\n## **SQLite**\n\nMenyimpan data ke database sangat ideal untuk data yang berulang atau terstruktur, seperti informasi kontak. API yang diperlukan untuk menggunakan database SQLite di Android tersedia dalam paket android.database.sqlite.\n\n### `Creating a Schema`\n\nSalah satu prinsip utama database SQL adalah skema: deklarasi formal tentang bagaimana database diatur. Skema ini tercermin dalam pernyataan SQL yang digunakan untuk membuat database. Dalam suatu hal mungkin merasa terbantu untuk membuat kelas pendamping yang disebut kelas kontrak, yang secara eksplisit menetapkan tata letak skema dengan cara yang sistematis dan mendokumentasikan dirinya sendiri.\n\nKelas kontrak adalah wadah untuk konstanta yang mendefinisikan nama untuk URI, tabel, dan kolom. Kelas kontrak memungkinkan untuk menggunakan konstanta yang sama di semua kelas lain dalam paket yang sama. Ini memungkinkan untuk mengubah nama kolom di satu tempat dan menyebarkannya ke seluruh kode di project.\n\n```Kotlin\nobject FeedReaderContract {\n    object FeedEntry : BaseColumns {\n        const val TABLE_NAME = \"entry\"\n        const val COLUMN_NAME_TITLE = \"title\"\n        const val COLUMN_NAME_SUBTITLE = \"subtitle\"\n        const val COLUMN_NAME_STATUS = \"status\"\n    }\n}\n```\n\n### `Create a Database`\n\nSetelah menentukan bagaimana database akan terlihat, setelah itu sebaiknya mengimplementasikan metode pembuatan dan pemeliharaan database dan tabel. Berikut adalah beberapa pernyataan umum untuk membuat dan menghapus tabel:\n\n```Kotlin\nprivate const val SQL_CREATE_ENTRIES =\n        \"CREATE TABLE ${FeedEntry.TABLE_NAME} (\" +\n                \"${BaseColumns._ID} INTEGER PRIMARY KEY,\" +\n                \"${FeedEntry.COLUMN_NAME_TITLE} TEXT,\" +\n                \"${FeedEntry.COLUMN_NAME_SUBTITLE} TEXT)\"\n\nprivate const val SQL_DELETE_ENTRIES = \"DROP TABLE IF EXISTS ${FeedEntry.TABLE_NAME}\"\nprivate const val SQL_MIGRATE_ENTRIES = \"ALTER TABLE ${FeedEntry.TABLE_NAME} ADD COLUMN ${FeedEntry.COLUMN_NAME_STATUS} INTEGER\"\n```\n\nKelas SQLiteOpenHelper berisi sekumpulan API yang berguna untuk mengelola database. Saat menggunakan kelas ini untuk mendapatkan referensi ke database, sistem akan menjalankan operasi yang berpotensi berjalan cukup lama untuk membuat dan mengupdate database hanya saat diperlukan dan bukan selama startup aplikasi. Yang perlu dilakukan hanyalah memanggil methode getWritableDatabase() atau getReadableDatabase().\n\nCatatan: Karena dapat berjalan lama, pastikan memanggil getWritableDatabase() atau getReadableDatabase() di backgroudn thread.\n\nUntuk menggunakan SQLiteOpenHelper, buatlah sebuah subclass yang menggantikan method callback onCreate() dan onUpgrade(). Mungkin juga ingin mengimplementasikan method onDowngrade() atau onOpen(), tetapi method ini tidak diperlukan.\n\nMisalnya berikut ini adalah implementasi SQLiteOpenHelper yang menggunakan beberapa perintah di atas:\n\n```Kotlin\nclass FeedReaderDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {\n    override fun onCreate(db: SQLiteDatabase) {\n        db.execSQL(SQL_CREATE_ENTRIES)\n    }\n    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {\n        db.execSQL(SQL_DELETE_ENTRIES)\n        onCreate(db)\n    }\n    override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {\n        onUpgrade(db, oldVersion, newVersion)\n    }\n    companion object {\n        const val DATABASE_VERSION = 1\n        const val DATABASE_NAME = \"FeedReader.db\"\n    }\n}\n```\n\nDan cara memanggilnya hanya cukup dengan perintah berikut ini:\n\n```Kotlin\nval dbHelper = FeedReaderDbHelper(context)\n```\n\n### `Insert`\n\nInsert data ke dalam database dengan memberikan object ContentValues ke metode insert(), kurang lebih sebagai berikut:\n\n```Kotlin\nval db = dbHelper.writableDatabase\n\nval values = ContentValues().apply {\n    put(FeedEntry.COLUMN_NAME_TITLE, title)\n    put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle)\n}\n\nval newRowId = db?.insert(FeedEntry.TABLE_NAME, null, values)\n```\n\n### `Read`\n\nSaat melakukan read data pada SQLite gunakan method query() dengan memasukkan tabel yang akan di tampilkan atau dengan pass nilai null untuk mendapatkan semua kolom.\n\n```Kotlin\nval db = dbHelper.readableDatabase\nval projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE)\n\nval selection = \"${FeedEntry.COLUMN_NAME_TITLE} = ?\"\nval selectionArgs = arrayOf(\"My Title\")\n\nval sortOrder = \"${FeedEntry.COLUMN_NAME_SUBTITLE} DESC\"\n\nval cursor = db.query(\n        FeedEntry.TABLE_NAME,   // The table to query\n        projection,             // The array of columns to return (pass null to get all)\n        selection,              // The columns for the WHERE clause\n        selectionArgs,          // The values for the WHERE clause\n        null,                   // don't group the rows\n        null,                   // don't filter by row groups\n        sortOrder               // The sort order\n)\n\nval itemIds = mutableListOf\u003cLong\u003e()\nwith(cursor) {\n    while (moveToNext()) {\n        val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID))\n        itemIds.add(itemId)\n    }\n}\n```\n\n### `Update`\n\nSaat akan melakukan update data pada SQLite gunakan method update() dengan memakai object dari ContentValues dan where clause pada SQLite.\n\n```Kotlin\nval db = dbHelper.writableDatabase\n\nval title = \"MyNewTitle\"\nval values = ContentValues().apply {\n    put(FeedEntry.COLUMN_NAME_TITLE, title)\n}\n\nval selection = \"${FeedEntry.COLUMN_NAME_TITLE} LIKE ?\"\nval selectionArgs = arrayOf(\"MyOldTitle\")\nval count = db.update(\n        FeedEntry.TABLE_NAME,\n        values,\n        selection,\n        selectionArgs)\n```\n\n### `DELETE`\n\nSaat melakukan delete data pada database SQLite dari tabel perlu memasukkan where clause pada method delete().\n\n```Kotlin\nval selection = \"${FeedEntry.COLUMN_NAME_TITLE} LIKE ?\"\nval selectionArgs = arrayOf(\"MyTitle\")\nval deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs)\n```\n\n### `Close Database`\n\nKarena method getWritableDatabase() dan getReadableDatabase() adalah method yang mahal untuk dipanggil saat database ditutup, maka dari itu harus membiarkan koneksi database terbuka selama mungkin pada saat mengaksesnya. Biasanya, untuk menutup database dilakukan pada onDestroy() dari Activity dan merupakan cara yang optimal.\n\n```Kotlin\noverride fun onDestroy() {\n    dbHelper.close()\n    super.onDestroy()\n}\n```\n\n### `Database Migration`\n\nDatabase migration digunakan untuk mengelola perubahan pada database tanpa harus merusak atau menghancurkan data pada database versi sebelumnya.\n\n```Kotlin\nclass FeedReaderDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {\n    override fun onCreate(db: SQLiteDatabase) {\n        db.execSQL(SQL_CREATE_ENTRIES)\n    }\n    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {\n        1 -\u003e {\n           db?.execSQL(SQL_DELETE_ENTRIES)\n           onCreate(db)\n       }\n       2 -\u003e db?.execSQL(SQL_MIGRATE_ENTRIES)\n    }\n    override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {\n        onUpgrade(db, oldVersion, newVersion)\n    }\n    companion object {\n        // If you change the database schema, you must increment the database version.\n        const val DATABASE_VERSION = 1\n        const val DATABASE_NAME = \"FeedReader.db\"\n    }\n}\n```\n\n## **Room**\n\nLibrary Room menyediakan lapisan abstraksi di atas SQLite untuk memungkinkan akses database yang lebih hebat sambil memanfaatkan fitur secara penuh dari SQLite.\n\nRoom membantu untuk membuat cache dari data aplikasi di perangkat yang menjalankan aplikasi yang dibuat. Cache ini, yang berfungsi sebagai satu-satunya sumber kebenaran data dari aplikasi, sehingga memungkinkan pengguna untuk melihat salinan informasi yang konsisten dalam aplikasi, terlepas dari apakah user memiliki koneksi internet atau tidak.\n\nTiga komponent penting di dalam Room\n\n1. Database: Pemegang data lokal dan berfungsi sebagai jalan akses utama untuk koneksi ke data rasional aplikasi yang bersifat tetap. Class yang ditandai dengan anotasi @Database harus memenuhi kriteria sebagai berikut ini:\n\n   - Harus menjadi abstract class yang meng-extend dari class RoomDatabase.\n   - Mempunyai daftar entitas yang berhubungan dengan database yang sudah dianotasikan dengan @Entity.\n   - Mempunyai sebuah abtrak method tanpa memiliki argument dan mengembalikan class yang dianotasikan dengan @Dao.\n\n2. Entity: Mempresentasikan tabel pada database SQLite.\n3. DAO: Berisikan method yang digunakan untuk mengakses data pada database.\n\nHubungan antar komponen Room tertera pada gambar dibawah ini:\n\n![Image](assets/06.png)\n\n### `Installation`\n\nUntuk menggunakan Room pada aplikasi, pertama lakukan install pada `build.gradle` app:\n\n```Groovy\ndependencies {\n  def room_version = \"2.2.5\"\n\n  implementation \"androidx.room:room-runtime:$room_version\"\n  kapt \"androidx.room:room-compiler:$room_version\"\n}\n```\n\n### `Creat an Entity`\n\nCara membuat table pada Room adalah dengan membuat class yang diberikan sebuah anotasi berupa @Entity.\n\n```Kotlin\n@Entity\ndata class User(\n    @PrimaryKey val uid: Int,\n    @ColumnInfo(name = \"first_name\") val firstName: String?,\n    @ColumnInfo(name = \"last_name\") val lastName: String?\n)\n```\n\n### `Create Dao`\n\nMembuat Dao untuk mengakses data pada database Room\n\n```Kotlin\n@Dao\ninterface UserDao {\n    @Query(\"SELECT * FROM user\")\n    fun getAll(): List\u003cUser\u003e\n\n    @Query(\"SELECT * FROM user WHERE uid IN (:userIds)\")\n    fun loadAllByIds(userIds: IntArray): List\u003cUser\u003e\n\n    @Query(\"SELECT * FROM user WHERE first_name LIKE :first AND \" +\n           \"last_name LIKE :last LIMIT 1\")\n    fun findByName(first: String, last: String): User\n\n    @Insert\n    fun insertAll(vararg users: User)\n\n    @Delete\n    fun delete(user: User)\n}\n```\n\n### `Create Database`\n\nDatabase pada Room harus meng-extend dari RoomDatabase dan memilki method abstrak dengan return Dao.\n\n```Kotlin\n@Database(entities = arrayOf(User::class), version = 1)\nabstract class AppDatabase : RoomDatabase() {\n    abstract fun userDao(): UserDao\n}\n```\n\n### `Call Database`\n\nMemanggil database Room.\n\n```Kotlin\nval db = Room.databaseBuilder(\n    applicationContext,\n    AppDatabase::class.java, \"database-name\"\n).build()\n```\n\n### `Relationship`\n\nKarena SQLite adalah database relasional, sehingga dapat menentukan hubungan antar entitas. Dalam mengekspresikan entitas atau objek data sebagai keseluruhan yang kohesif dalam logika database, objek tersebut harus berisi beberapa field. Dalam situasi ini dapat menggunakan anotasi @Embedded untuk merepresentasikan objek yang ingin di dekomposisi menjadi subfield dalam tabel. Lemudian dapat membuat kueri field yang disematkan seperti yang di lakukan untuk kolom individual lainnya.\n\nMisalnya class User dapat menyertakan field jenis alamat, yang mewakili komposisi field bernama jalan, kota, negara field, dan kode pos. Untuk menyimpan kolom yang disusun secara terpisah dalam tabel, sertakan field alamat di class User yang dianotasi dengan @Embedded, seperti yang ditunjukkan dalam kode berikut ini:\n\n```Kotlin\ndata class Address(\n    val street: String?,\n    val state: String?,\n    val city: String?,\n    @ColumnInfo(name = \"post_code\") val postCode: Int\n)\n\n@Entity\ndata class User(\n    @PrimaryKey val id: Int,\n    val firstName: String?,\n    @Embedded val address: Address?\n)\n```\n\n### `One to One Relationships`\n\nHubungan one to one antara dua entitas adalah hubungan di mana setiap instance dari entitas induk sesuai dengan satu instance entitas anak dan sebaliknya.\n\n**Entity.kt**\n\n```Kotlin\n@Entity\ndata class User(\n    @PrimaryKey val userId: Long,\n    val name: String,\n    val age: Int\n)\n\n@Entity\ndata class Library(\n    @PrimaryKey val libraryId: Long,\n    val userOwnerId: Long\n)\n\ndata class UserAndLibrary(\n    @Embedded val user: User,\n    @Relation(\n         parentColumn = \"userId\",\n         entityColumn = \"userOwnerId\"\n    )\n    val library: Library\n)\n```\n\n**Dao.kt**\n\n```Kotlin\n@Transaction\n@Query(\"SELECT * FROM User\")\nfun getUsersAndLibraries(): List\u003cUserAndLibrary\u003e\n```\n\n### `One to Many Relationships`\n\nHubungan one to many antara dua entitas adalah hubungan di mana setiap instance dari entitas induk sesuai dengan nol atau lebih instance dari entitas anak, tetapi setiap instance dari entitas anak hanya dapat sesuai dengan satu entitas induk.\n\n**Entity.kt**\n\n```Kotlin\n@Entity\ndata class User(\n    @PrimaryKey val userId: Long,\n    val name: String,\n    val age: Int\n)\n\n@Entity\ndata class Playlist(\n    @PrimaryKey val playlistId: Long,\n    val userCreatorId: Long,\n    val playlistName: String\n)\n\ndata class UserWithPlaylists(\n    @Embedded val user: User,\n    @Relation(\n          parentColumn = \"userId\",\n          entityColumn = \"userCreatorId\"\n    )\n    val playlists: List\u003cPlaylist\u003e\n)\n```\n\n**Dao.kt**\n\n```Kotlin\n@Transaction\n@Query(\"SELECT * FROM User\")\nfun getUsersWithPlaylists(): List\u003cUserWithPlaylists\u003e\n```\n\n### `Many to Many Relationships`\n\nHubungan many to many antara dua entitas adalah hubungan di mana setiap instance dari entitas induk sesuai dengan nol atau lebih instance dari entitas anak dan sebaliknya.\n\n**Entity.kt**\n\n```Kotlin\n@Entity\ndata class Playlist(\n    @PrimaryKey val playlistId: Long,\n    val playlistName: String\n)\n\n@Entity\ndata class Song(\n    @PrimaryKey val songId: Long,\n    val songName: String,\n    val artist: String\n)\n\n@Entity(primaryKeys = [\"playlistId\", \"songId\"])\ndata class PlaylistSongCrossRef(\n    val playlistId: Long,\n    val songId: Long\n)\n\ndata class PlaylistWithSongs(\n    @Embedded val playlist: Playlist,\n    @Relation(\n         parentColumn = \"playlistId\",\n         entityColumn = \"songId\",\n         associateBy = Junction(PlaylistSongCrossRef::class)\n    )\n    val songs: List\u003cSong\u003e\n)\n\ndata class SongWithPlaylists(\n    @Embedded val song: Song,\n    @Relation(\n         parentColumn = \"songId\",\n         entityColumn = \"playlistId\",\n         associateBy = Junction(PlaylistSongCrossRef::class)\n    )\n    val playlists: List\u003cPlaylist\u003e\n)\n```\n\n**Dao.kt**\n\n```Kotlin\n@Transaction\n@Query(\"SELECT * FROM Playlist\")\nfun getPlaylistsWithSongs(): List\u003cPlaylistWithSongs\u003e\n\n@Transaction\n@Query(\"SELECT * FROM Song\")\nfun getSongsWithPlaylists(): List\u003cSongWithPlaylists\u003e\n```\n\n## Android Coroutines\n\nCoroutine adalah pola desain konkurensi yang dapat di gunakan pada Android untuk menyederhanakan kode yang dijalankan secara asinkron. Coroutine ditambahkan di Kotlin pada versi 1.3 dan didasarkan pada konsep yang sudah matang dari bahasa pemrogramman lainnya.\n\nDi Android, coroutine membantu mengelola tugas yang berjalan lama yang mungkin memblokir thread utama dan menyebabkan aplikasi tidak responsif. Lebih dari 50% pengembang profesional yang menggunakan coroutine melaporkan produktivitasnya meningkat. Coroutine memungkinkan untuk menulis kode aplikasi yang lebih rapi dan ringkas.\n\n### `Features`\n\nCoroutines adalah solusi yang direkomendasikan untuk masalah asynchronous pada Android, beberapa fitur yang perlu diperhatikan didalam Coroutines adalah sebagai berikut ini:\n\n- Lightweight: Android dapat menjalankan banyak coroutines pada satu Thread karena dukungan dari suspension function yang dimana tidak memblokir Tread ketika Coroutines dijalankan. Coroutines juga memberikan solusi terhadap beban dari Thread dengan memberikan resource yang lebih sedikit daripada multithreading pada umumnya.\n\n- Fewer memory leaks: Coroutines merupakan sebuah mini thread dimana Coroutines hanya memakan beberapa resource RAM, namun untuk membuat sebuah proses yang berjalan bersamaan menggunakan coroutines harus diperhatikan dalam memakai Coroutines scope, sebab Coroutines hanya dapat berjalan pada Coroutines scope saja. Sebisa mungkin untuk menghindari GlobalScope yang akan membuat Coroutines memakai lebih banyak RAM.\n\n- Built-in cancellation support: Sebenarnya Coroutines adalah sebuah objek Job yang dimana Job adalah objet dapat dibatalkan, sehingga proses Coroutines dapat dibatalkan jika memang proses pada Coroutines belum selesai ketika aplikasi (Activity atau Fragment) sudah didestroy.\n\n- Jetpack integration: Coroutines merupakan fitur yang sudah mendukung untuk beberapa fitur dari Android Jetpack, seperti ViewModel, Room dsb. Coroutines juga sudah mendukung proses HTTP Request menggunakan Retrofit, sehingga proses pada Thread IO menjadi lebih ringan.\n\n### `Installation`\n\nUntuk menambahkan Coroutines pada Android, pertama tambahkan package Coroutines pada `build.gradle` app:\n\n```Groovy\ndependencies {\n    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'\n}\n```\n\n### `Usage`\n\nMemakai proses yang berat pada Main Thread bukanlah cara yang baik, karena proses tersebut perlu ditunggu sampai menghasilkan response yang kemudian akan ditampilkan pada Main Thread. Salah satu cara untuk menangani ini adalah dengan menggunakan background task menggunakan Coroutines pada ViewModel Android Jetpack.\n\n**AuthClient.kt**\n\n```Kotlin\nclass AuthClient {\n    companion object {\n        val service: AuthService by lazy {\n            val httpLoggingInterceptor = HttpLoggingInterceptor { message -\u003e\n                if (BuildConfig.DEBUG) Log.e(\"LOG_API\", message)\n            }\n            httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)\n\n            val okHttpClient = OkHttpClient()\n                .newBuilder()\n                .addInterceptor(httpLoggingInterceptor)\n                .build()\n\n            val retrofit = Retrofit\n                .Builder()\n                .baseUrl(ConstantUtil.BASE_URL_API)\n                .client(okHttpClient)\n                .addConverterFactory(\n                    GsonConverterFactory.create(GsonBuilder().setLenient().create())\n                )\n                .build()\n            retrofit.create(AuthService::class.java)\n        }\n    }\n}\n```\n\n**AuthService.kt**\n\n```Kotlin\ninterface AuthService {\n    @POST(\"v1/signin\")\n    suspend fun login(@Body body: HashMap\u003cString, Any\u003e): BaseModel\u003cAuthModel\u003e\n}\n```\n\n**AuthViewModel.kt**\n\n```Kotlin\nsealed class AuthState {\n    data class Loading(val message: String = \"Loading...\") : AuthState()\n    data class Error(val exception: Exception) : AuthState()\n    data class Login(val data: AuthModel) : AuthState()\n}\n\nclass AuthViewModel : ViewModel() {\n    private val authService by lazy { AuthClient.service }\n    private val mutableState by lazy { MutableLiveData\u003cAuthState\u003e() }\n    val state: LiveData\u003cAuthState\u003e get() = mutableState\n\n    fun login(email: String, password: String) {\n        val body = hashMapOf\u003cString, Any\u003e(\"email\" to email, \"password\" to password)\n\n        mutableState.value = AuthState.Loading()\n\n        viewModelScope.launch(Dispatchers.IO) {\n            try {\n                val authModel = authService.login(body).data\n                mutableState.postValue(AuthState.Login(authModel))\n            } catch (exc: Exception) {\n                exc.printStackTrace()\n                mutableState.postValue(AuthState.Error(exc))\n            }\n        }\n    }\n}\n```\n\n## File Upload\n\nFile upload merupakan sebuah kasus yang sering dijumpai pada banyak aplikasi seperti sosmed, e-commerse, e-learn dsb. Di Android proses file upload ke server bisa dilakukan dengan dua cara, pertama dengan menggunakan Content-Type: multipart/form-data dengan mengirimkan image dengan cara stream atau binary, sedangkan cara yang kedua adalah dengan menggunakan Base64 Encoder dimana akan menggunakan Content-Type: application/json dan image tadi yang asalnya berupa Byte Array akan diubah menjadi sebuah String yang nantinya akan dikirim ke server dan server akan mendecode string Base64 tadi menjadi sebuah file image.\n\n### `Camera`\n\nAndroid menyediakan akses secara penuh ke hardware kamera sehingga dapat membangun berbagai aplikasi kamera atau aplikasi berbasis penglihatan. Atau jika hanya membutuhkan cara bagi pengguna untuk mengambil foto, aplikasi dapat meminta aplikasi kamera yang ada untuk mengambil foto dan mengembalikan image kepada aplikasi yang dibangun.\n\n### `Infrastructure`\n\nDalam mengakses kamera, perlu beberapa konfigurasi yang dibutuhkan yang kemudian dapat memanggil aplikasi camera.\n\n**file_paths.xml**\n\n```Xml\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n\u003cpaths\u003e\n    \u003cexternal-files-path name=\"images\" path=\"/\" /\u003e\n\u003c/paths\u003e\n```\n\n**AndroidManifest.xml**\n\n```Xml\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n\u003cmanifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"id.refactory.androidmaterial\"\u003e\n\n    \u003cqueries\u003e\n        \u003cintent\u003e\n            \u003caction android:name=\"android.media.action.IMAGE_CAPTURE\" /\u003e\n        \u003c/intent\u003e\n    \u003c/queries\u003e\n\n    \u003cuses-permission android:name=\"android.permission.INTERNET\" /\u003e\n    \u003cuses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" /\u003e\n    \u003cuses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        tools:ignore=\"ScopedStorage\" /\u003e\n\n    \u003cuses-feature\n        android:name=\"android.hardware.camera\"\n        android:required=\"true\" /\u003e\n\n    \u003capplication\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.AndroidMaterial\"\u003e\n        \u003cactivity ... /\u003e\n        \u003cprovider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"com.example.android.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\"\u003e\n            \u003cmeta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/file_paths\" /\u003e\n        \u003c/provider\u003e\n    \u003c/application\u003e\n\n\u003c/manifest\u003e\n```\n\n### `Request Permissions`\n\nDalam mengakses sebuah data sensitive seperti **READ_EXTERNAL_STORAGE** dan **WRITE_EXTERNAL_STORAGE**, perlu ditambahkan sebuah permintaan perizinan akses ke penyimpanan android dari user yang nantinya akan berguna untuk menyimpan data image dari kamera atau dari galeri.\n\n**Fragment.kt**\n\n```Kotlin\nprivate val requestPermissions = 111\nprivate val permissions = arrayOf(\n    Manifest.permission.READ_EXTERNAL_STORAGE,\n    Manifest.permission.WRITE_EXTERNAL_STORAGE\n)\n\noverride fun onStart() {\n    super.onStart()\n\n    requestPermissions(permissions, requestPermissions)\n}\n\noverride fun onRequestPermissionsResult(requestCode: Int, permissions: Array\u003cout String\u003e, grantResults: IntArray) {\n    super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n    if (requestCode == requestPermissions \u0026\u0026 grantResults.size != permissions.size) {\n        requestPermissions(permissions, requestPermissions)\n    }\n}\n```\n\n### `Create Temporary File`\n\nSebelum mengakses file image dari user, akan lebih baik membuat file sementara untuk menampung hasil gambar dari kamera atau galeri dengan cara sebagai berikut ini:\n\n```Kotlin\nprivate var filePath = \"\"\n\nprivate fun deleteFileTemp() {\n    val file = File(filePath)\n    if (file.exists() \u0026\u0026 file.delete()) {\n        println(\"File dengan alamat ${file.absolutePath} berhasil dihapus\")\n    }\n}\n\n@SuppressLint(\"SimpleDateFormat\")\nprivate fun createImageFile(): File {\n    deleteFileTemp()\n\n    val timeStamp: String = SimpleDateFormat(\"yyyy_MM_dd_HH_mm_ss\").format(Date())\n    val storageDir: File? = requireActivity().getExternalFilesDir(Environment.DIRECTORY_PICTURES)\n    return File.createTempFile(\"IMG_${timeStamp}\", \".jpg\", storageDir).apply {\n        filePath = absolutePath\n    }\n}\n```\n\n### `Start Camera Activity`\n\nUntuk memanggil Activity Camera perlu sebuah request code untuk membedakan hasil pengembalian data dari Activity satu dengan Activity yang lainnya, lebih jelasnya dapat menggunakan perintah dibawah ini:\n\n```Kotlin\nprivate val requestImageCamera = 123\n\nprivate fun captureImageFromCamera() {\n    Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent -\u003e\n        takePictureIntent.resolveActivity(requireActivity().packageManager)?.also {\n            val photoFile: File? = try {\n                createImageFile()\n            } catch (ex: IOException) {\n                null\n            }\n\n            photoFile?.also {\n                val photoURI: Uri = FileProvider.getUriForFile(\n                    requireContext(),\n                    \"com.example.android.fileprovider\",\n                    it\n                )\n                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)\n                startActivityForResult(takePictureIntent, requestImageCamera)\n            }\n        }\n    }\n}\n```\n\n### `Compress Image File`\n\nSetelah aplikasi mendapatkan image dari camera, hal yang perlu diperhatikan adalah melakukan kompres image agar user hanya menyimpan file dengan ukuran yang minimal. User juga nantinya tidak perlu menunggu lama untuk upload file karena file yang dikirim memiliki ukuran yang lebih kecil. Dalam melakukan compress file bisa dengan menggunakan perintah dibawah ini:\n\n```Kotlin\nprivate fun compressImageFile() {\n    val file = File(filePath)\n    if (file.exists()) {\n        val bitmap = BitmapFactory.decodeFile(filePath)\n        val bos = ByteArrayOutputStream()\n        bitmap.compress(Bitmap.CompressFormat.JPEG, 50, bos)\n\n        FileOutputStream(filePath, true).apply {\n            write(bos.toByteArray())\n            flush()\n            close()\n        }\n    }\n}\n```\n\n### `Showing Image from Camera`\n\nSetelah user menangkap image melalui camera maka user akan mengembalikan data berupa image dari Camera Activity, untuk menampilkan data image tersebut dapat menggunakan perintah dibawah ini:\n\n```Kotlin\noverride fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n    super.onActivityResult(requestCode, resultCode, data)\n\n    if (requestCode == requestImageCamera \u0026\u0026 resultCode == Activity.RESULT_OK) {\n        Glide.with(this).load(filePath).into(binding.ivContact)\n    } else {\n        deleteFileTemp()\n    }\n}\n```\n\n### `Gallery`\n\nAndroid menyediakan akses untuk mendapatkan image dari galery, artinya user bebas untuk memilih image yang akan diambil. Image dari galeri mendapatkan perlakuan yang berbeda karena image pada galeri sifatnya sudah tersimpan di memori Android tidak seperti kamera.\n\n### `Start Gallery Activity`\n\nSama seperti dengan Camera, untuk memanggil Gallery Activity perlu sebuah request code untuk membedakan return dari hasil Activity tersebut. Lebih detailnya lihat perintah dibawah ini:\n\n```Kotlin\nprivate fun captureImageFromGallery() {\n    val gallery = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI)\n    startActivityForResult(gallery, requestImageGallery)\n}\n```\n\n### `Copy Image to Temporary File`\n\nSama seperti dengan Camera, untuk menaruh image perlu sebuah temporary file untuk menempatkan file image dari galeri, namun di galeri hanya akan melakukan copy dari file image yang sudah tersimpan ke temporary file dengan perintah dibawah ini:\n\n```Kotlin\n@Throws(IOException::class)\nfun copyStream(input: InputStream, output: OutputStream) {\n    val buffer = ByteArray(1024)\n    var bytesRead: Int\n    while (input.read(buffer).also { bytesRead = it } != -1) {\n        output.write(buffer, 0, bytesRead)\n    }\n}\n```\n\n### `Showing Image from Gallery`\n\nSama seperti dengan Camera, setelah user menangkap image melalui gallery maka user akan mengembalikan data berupa image dari Gallery Activity, untuk menampilkan data image tersebut dapat menggunakan perintah dibawah ini:\n\n```Kotlin\noverride fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n    super.onActivityResult(requestCode, resultCode, data)\n\n    if (requestCode == requestImageGallery \u0026\u0026 resultCode == Activity.RESULT_OK) {\n        val image = try {\n            createImageFile()\n        } catch (exc: Exception) {\n            null\n        }\n\n        image?.let {\n            val bitmap = data?.data\n            bitmap?.let { imageBitmap -\u003e\n                val inputStream =\n                    requireActivity().contentResolver.openInputStream(imageBitmap)\n                val fileOutputStream = FileOutputStream(filePath)\n\n                inputStream?.let { input -\u003e\n                    try {\n                        copyStream(input, fileOutputStream)\n                    } catch (exc: Exception) {\n                        exc.printStackTrace()\n                    }\n                }\n\n                fileOutputStream.close()\n                inputStream?.close()\n\n                Glide.with(this).load(filePath).into(binding.ivContact)\n            }\n        }\n    } else {\n        deleteFileTemp()\n    }\n}\n```\n\n### `Upload Image with Retrofit`\n\nRetrofit dapat digunakan untuk mengupload image dengan menggunakan multipart/form-data, lebih detailnya bisa lihat kode dibawah ini:\n\n**Service.kt**\n\n```Kotlin\ninterface ContactService {\n    @Multipart\n    @POST(\"v1/contacts\")\n    suspend fun insertContact(\n        @Header(\"Authorization\") token: String,\n        @PartMap map: HashMap\u003cString, RequestBody\u003e,\n        @Part image: MultipartBody.Part?\n    ): BaseModel\u003cContactModel\u003e\n}\n```\n\n**Client.kt**\n\n```Kotlin\nclass ContactClient {\n    companion object {\n        val service: ContactService by lazy {\n            val httpLoggingInterceptor = HttpLoggingInterceptor { message -\u003e\n                if (BuildConfig.DEBUG) Log.e(\"LOG_API\", message)\n            }\n            httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)\n\n            val okHttpClient = OkHttpClient()\n                .newBuilder()\n                .addInterceptor(httpLoggingInterceptor)\n                .build()\n\n            val retrofit = Retrofit\n                .Builder()\n                .baseUrl(ConstantUtil.BASE_URL_API)\n                .client(okHttpClient)\n                .addConverterFactory(\n                    GsonConverterFactory.create(GsonBuilder().setLenient().create())\n                )\n                .build()\n            retrofit.create(ContactService::class.java)\n        }\n    }\n}\n```\n\n**ViewModel.kt**\n\n```Kotlin\nfun insertContact(phone: String, name: String, image: String) {\n    mutableState.value = ContactState.Loading()\n\n    viewModelScope.launch(Dispatchers.IO) {\n        try {\n            val body = hashMapOf\u003cString, RequestBody\u003e()\n            body[\"phone\"] = phone.toRequestBody(MultipartBody.FORM)\n            body[\"name\"] = name.toRequestBody(MultipartBody.FORM)\n\n            val file = File(image)\n            val fileBody = file.asRequestBody(MultipartBody.FORM)\n            val photo = if (image.isNotEmpty() \u0026\u0026 file.exists()) MultipartBody.Part.createFormData(\n                \"image\",\n                file.name,\n                fileBody\n            ) else null\n\n            val contactModel = contactService.insertContact(token, body, photo).data\n            mutableState.postValue(ContactState.Create(contactModel))\n        } catch (exc: Exception) {\n            exc.printStackTrace()\n            mutableState.postValue(ContactState.Error(exc))\n        }\n    }\n}\n```\n\n## Parallel Network Calls\n\nCoroutines mendukung untuk melakukan pemanggilan REST API secara parallel dan Multi Thread yang memungkinkan user untuk melakukan pemanggilan API tanpa harus menunggu satu per satu untuk selesai.\n\nSebelum melakukan parallel network calls, pastikan bahwa ada berbagai jenis Coroutines Scope yang perlu dipahami seperi viewModelScope, runBlocking, coroutinesScope dan GlobalScope (hindari penggunaan coroutines scope ini) dan salah satu scope yang cocok dalam menangani parallel network call adalah coroutinesScope karena coroutines scope ini dapat mengembalikan exception ketika salah satu job atau coroutines mengalami error. Lebih jelasnya bisa lihat pada kode dibawah ini:\n\n**ViewModel.kt**\n\n```Kotlin\nfun getAllUserPost() {\n    jobs.add(\n        viewModelScope.launch(Dispatchers.IO) {\n            try {\n                coroutineScope {\n                    // Parallel Network Calls\n                    val jobGetAllUser = async { userService.getAllUser() }\n                    val jobGetAllPost = async { postService.getAllPost() }\n\n                    val users = jobGetAllUser.await()\n                    val posts = jobGetAllPost.await()\n\n                    val userPosts = mutableListOf\u003cUserPostModel\u003e()\n\n                    posts.forEach {\n                        val user = users.find { userModel -\u003e it.userId == userModel.id }\n                        user?.let { userModel -\u003e\n                            userPosts.add(\n                                UserPostModel(\n                                    it.userId,\n                                    it.id,\n                                    it.title,\n                                    it.body,\n                                    userModel\n                                )\n                            )\n                        }\n                    }\n\n                    mutableData.postValue(userPosts)\n                }\n            } catch (exc: Exception) {\n                exc.printStackTrace()\n            }\n        }\n    )\n}\n```\n\n## Firebase\n\nFirebase merupakan salah satu layanan dari Google yang memudahkan para app developer dalam mengembangkan aplikasi dan memonitoring aplikasi. Firebase memiliki banyak layanan dimulai dari Google Analytics, Cloud Messaging, Crashlytics, Performance Monitoring, Realtime Database, Cloud Storage, Hosting, Cloud Firestore.\n\n### `Create a Firebase Project`\n\n1. Pada [Firebase Console](https://console.firebase.google.com/), klik tambahkan proyek, lalu pilih atau masukkan nama Project.\n\n   - Jika sudah memiliki project di [Google Cloud Platform (GCP)](https://console.cloud.google.com/), maka dapat memilih project tersebut dari menu drop-down untuk menambahkan resource Firebase ke project tersebut.\n\n2. (Opsional) Jika membuat project baru, maka dapat mengedit ID Project di lain waktu.\n\n   - Firebase secara otomatis menetapkan ID unik ke project Firebase.\n\n3. Klik tombol lanjutkan.\n\n4. (Opsional) Siapkan Google Analytics untuk proyek Firebase, yang memungkinkan untuk mendapatkan pengalaman yang optimal menggunakan salah satu produk Firebase berikut ini:\n\n   - Firebase Crashlytics\n   - Firebase Predictions\n   - Firebase Cloud Messaging\n   - Firebase In-App Messaging\n   - Firebase Remote Config\n   - Firebase A/B Testing\n\n5. Klik buat project (atau tambahkan firebase, jika menggunakan proyek GCP yang sudah ada).\n\n### `Register your app with Firebase`\n\n1. Buka konsol Firebase.\n\n2. Di tengah halaman overview project, klik ikon Android atau Tambahkan aplikasi untuk meluncurkan alur kerja penyiapan penambahan aplikasi di project Firebase.\n\n3. Masukkan nama paket aplikasi di kolom nama paket Android yang didapatkan pada file AndroidManifest.xml.\n\n4. (Opsional) Masukkan informasi aplikasi lainnya: seperti Nama aplikasi dan sertifikat penandatanganan Debug SHA-1. Untuk mendapatkan SHA-1 dengan mudah bisa lihat gambar dibawah ini: ![image](assets/16.png)\n\n5. Klik tombol daftarkan aplikasi.\n\n### `Add a Firebase configuration file`\n\n1. Tambahkan file konfigurasi firebase android pada aplikasi.\n\n   a. Download file `google-services.json` yang mengandung file konfigurasi firebase Android.\n\n   b. Masukkan file `google-services.json` pada folder app dari project android.\n\n2. Untuk mengaktifkan produk layanan Firebase di aplikasi, tambahkan plugin layanan google ke file Gradle aplikasi, seperti berikut ini:\n\n   a. Pada file `build.gradle` project tambahkan beberapa baris berikut ini:\n\n   ```Groovy\n   buildscript {\n\n       repositories {\n           // Check that you have the following line (if not, add it):\n           google()  // Google's Maven repository\n       }\n\n       dependencies {\n           ...\n           // Ad","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frefactory-id%2Ftraining-sekolahdesain","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frefactory-id%2Ftraining-sekolahdesain","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frefactory-id%2Ftraining-sekolahdesain/lists"}