Membuat library ekstensi Kotlin

Android KTX adalah kumpulan ekstensi untuk API framework Android yang umum digunakan, library Android Jetpack, dan lainnya. Kami membuat ekstensi ini agar panggilan dari kode Kotlin ke API berbasis bahasa pemrograman Java lebih ringkas dan idiomatis dengan memanfaatkan fitur bahasa seperti fungsi dan properti ekstensi, lambda, parameter bernama dan default, serta coroutine.

Apa yang dimaksud dengan library KTX?

KTX adalah singkatan dari ekstensi Kotlin, dan bukan teknologi khusus atau fitur bahasa dari bahasa Kotlin. Ini hanya nama yang kami gunakan untuk library Kotlin Google yang memperluas fungsi API yang awalnya dibuat dalam bahasa pemrograman Java.

Kelebihan ekstensi Kotlin adalah siapa saja bisa membangun library sendiri untuk API mereka sendiri, atau bahkan untuk library pihak ketiga yang Anda gunakan dalam project.

Codelab ini akan menjelaskan beberapa contoh penambahan ekstensi sederhana yang memanfaatkan fitur bahasa Kotlin. Kita juga akan melihat bagaimana cara mengonversi panggilan asinkron di API berbasis callback menjadi fungsi penangguhan dan Alur - aliran asinkron berbasis coroutine.

Yang akan Anda buat

Dalam codelab ini, Anda akan mengerjakan aplikasi sederhana yang memperoleh dan menampilkan lokasi pengguna saat ini. Aplikasi Anda akan:

  • Mendapatkan lokasi terbaru yang diketahui dari penyedia lokasi.
  • Mendaftar untuk mendapatkan pembaruan langsung terkait lokasi pengguna saat aplikasi sedang berjalan.
  • Menampilkan lokasi di layar dan menangani status error jika lokasi tidak tersedia.

Yang akan Anda pelajari

  • Cara menambahkan ekstensi Kotlin untuk mendukung class yang ada
  • Cara mengonversi panggilan asinkron yang menampilkan hasil tunggal menjadi fungsi penangguhan coroutine
  • Cara menggunakan Alur untuk mendapatkan data dari sumber yang dapat memunculkan nilai beberapa kali

Yang Anda butuhkan

  • Versi terbaru Android Studio (disarankan 3.6+)
  • Android Emulator atau perangkat yang terhubung melalui USB
  • Pengetahuan tingkat dasar tentang pengembangan Android dan bahasa Kotlin
  • Pemahaman dasar tentang coroutine dan fungsi penangguhan

Download Kode

Klik link berikut untuk mendownload semua kode untuk codelab ini:

Download kode sumber

... atau clone repositori GitHub dari command line dengan menggunakan perintah berikut:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

Kode untuk codelab ini ada di direktori ktx-library-codelab.

Dalam direktori project, Anda akan menemukan beberapa folder step-NN yang berisi status akhir yang diinginkan dari setiap langkah codelab ini sebagai referensi.

Kita akan mengerjakan semua tugas coding di direktori work.

Menjalankan aplikasi untuk pertama kali

Buka folder root (ktx-library-codelab) di Android Studio, lalu pilih konfigurasi run work-app di menu drop-down seperti yang ditunjukkan di bawah ini:

79c2a2d2f9bbb388.png

Tekan tombol Run 35a622f38049c660.png untuk menguji aplikasi Anda:

58b6a81af969abf0.png

Aplikasi ini belum melakukan sesuatu yang menarik. Beberapa bagian hilang sehingga tidak dapat menampilkan data. Kita akan menambahkan fungsi yang hilang tersebut pada langkah berikutnya.

Cara yang lebih mudah untuk memeriksa izin

58b6a81af969abf0.png

Meskipun dapat berjalan, aplikasi hanya menampilkan error - aplikasi tidak dapat memperoleh lokasi saat ini.

Hal ini dikarenakan kode untuk meminta izin akses lokasi runtime dari pengguna tidak ada.

Buka MainActivity.kt, dan cari kode yang diberi tanda komentar berikut:

//  val permissionApproved = ActivityCompat.checkSelfPermission(
//      this,
//      Manifest.permission.ACCESS_FINE_LOCATION
//  ) == PackageManager.PERMISSION_GRANTED
//  if (!permissionApproved) {
//      requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 0)
//  }

Jika Anda menghapus tanda komentar dari kode dan menjalankan aplikasi, aplikasi akan meminta izin dan melanjutkan untuk menampilkan lokasi. Namun, kode ini sulit dibaca karena beberapa alasan:

  • Kode menggunakan metode statis checkSelfPermission dari class utilitas ActivityCompat, yang hanya ada untuk menahan metode untuk kompatibilitas mundur.
  • Metode ini selalu menggunakan instance Activity sebagai parameter pertama, karena tidak mungkin menambahkan metode ke class framework dalam bahasa pemrograman Java.
  • Kita selalu memeriksa apakah izin tersebut adalah PERMISSION_GRANTED, sehingga akan lebih baik untuk langsung mendapatkan nilai Boolean true jika izin diberikan dan salah (false) jika sebaliknya.

Kita ingin mengonversi kode panjang yang ditampilkan di atas menjadi kode yang lebih pendek seperti ini:

if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
    // request permission
}

Kita akan mempersingkat kode dengan bantuan fungsi ekstensi di Aktivitas. Dalam project ini, Anda akan menemukan modul lain bernama myktxlibrary. Buka ActivityUtils.kt dari modul tersebut, dan tambahkan fungsi berikut:

fun Activity.hasPermission(permission: String): Boolean {
    return ActivityCompat.checkSelfPermission(
        this,
        permission
    ) == PackageManager.PERMISSION_GRANTED
}

Mari kita bedah apa yang terjadi di sini:

  • fun dalam cakupan terluar (bukan di dalam class) berarti kita menentukan fungsi tingkat atas dalam file.
  • Activity.hasPermission menentukan fungsi ekstensi dengan nama hasPermission pada penerima jenis Activity.
  • Fungsi ini mengambil izin sebagai argumen String dan menampilkan Boolean yang menunjukkan apakah izin telah diberikan.

Jadi, apa sebenarnya yang dimaksud dengan "penerima jenis X"?

Anda akan sering melihatnya saat membaca dokumentasi untuk fungsi ekstensi Kotlin. Ini berarti fungsi ini akan selalu dipanggil pada instance Activity (dalam kasus ini) atau subclass-nya, dan di dalam isi fungsi, kita dapat merujuk ke instance tersebut menggunakan kata kunci this (yang juga dapat bersifat implisit, artinya kita dapat menghilangkannya sepenuhnya).

Ini merupakan inti dari fungsi ekstensi, yaitu menambahkan fungsi baru di atas class yang tidak dapat atau tidak ingin kita ubah.

Mari lihat bagaimana kita memanggilnya di MainActivity.kt. Buka dan ubah kode izin menjadi:

if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
   requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 0)
}

Jika menjalankan aplikasi sekarang, Anda dapat melihat lokasi yang ditampilkan di layar.

c040ceb7a6bfb27b.png

Bantuan untuk memformat teks Lokasi

Meski begitu, teks lokasi tidak tampak cukup bagus. Teks tersebut menggunakan metode Location.toString default, yang tidak ditujukan untuk ditampilkan di UI.

Buka class LocationUtils.kt di myktxlibrary. File ini berisi ekstensi untuk class Location. Lengkapi fungsi ekstensi Location.format untuk menampilkan String yang diformat, lalu modifikasi Activity.showLocation di ActivityUtils.kt untuk memanfaatkan ekstensi.

Anda dapat melihat kode di folder step-03 jika mengalami masalah. Hasil akhirnya akan terlihat seperti ini:

b8ef64975551f2a.png

Penyedia Lokasi Gabungan dari Layanan Google Play

Project aplikasi yang kita kerjakan ini menggunakan Penyedia Lokasi Gabungan dari Layanan Google Play untuk mendapatkan data lokasi. API-nya sendiri cukup sederhana, tetapi karena mendapatkan lokasi pengguna bukanlah operasi yang dapat dijalankan seketika, semua panggilan ke library harus bersifat asinkron. Hal ini mempersulit kode karena adanya callback.

Ada dua langkah untuk mendapatkan lokasi pengguna. Pada langkah ini, kita akan berfokus untuk mendapatkan lokasi terakhir yang diketahui, jika tersedia. Pada langkah berikutnya, kita akan melihat pembaruan lokasi berkala saat aplikasi sedang berjalan.

Mendapatkan lokasi terakhir yang diketahui

Dalam Activity.onCreate, kita melakukan inisialisasi FusedLocationProviderClient yang akan menjadi titik masuk ke library.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
}

Dalam Activity.onStart, kita kemudian memanggil getLastKnownLocation() yang saat ini terlihat seperti ini:

private fun getLastKnownLocation() {
   fusedLocationClient.lastLocation.addOnSuccessListener { lastLocation ->
       showLocation(R.id.textView, lastLocation)
   }.addOnFailureListener { e ->
       findAndSetText(R.id.textView, "Unable to get location.")
       e.printStackTrace()
   }
}

Seperti yang dapat Anda lihat, lastLocation adalah panggilan asinkron yang dapat diselesaikan dengan berhasil atau gagal. Untuk setiap hasil ini, kita harus mendaftarkan fungsi callback yang akan menampilkan lokasi ke UI atau menampilkan pesan error.

Kode ini belum tampak terlalu rumit karena adanya callback saat ini. Namun, dalam project nyata, Anda mungkin ingin memproses lokasi, menyimpannya ke database, atau mengupload ke server. Banyak dari operasi ini juga asinkron, dan menambahkan callback di atas callback akan segera membuat kode kita tidak bisa dibaca dan dapat terlihat seperti ini:

private fun getLastKnownLocation() {
   fusedLocationClient.lastLocation.addOnSuccessListener { lastLocation ->
       getLastLocationFromDB().addOnSuccessListener {
           if (it != location) {
               saveLocationToDb(location).addOnSuccessListener {
                   showLocation(R.id.textView, lastLocation)
               }
           }
       }.addOnFailureListener { e ->
           findAndSetText(R.id.textView, "Unable to read location from DB.")
           e.printStackTrace()
       }
   }.addOnFailureListener { e ->
       findAndSetText(R.id.textView, "Unable to get location.")
       e.printStackTrace()
   }
}

Lebih buruk lagi, kode di atas memiliki masalah dengan kebocoran memori dan operasi, karena pemroses tidak akan dihapus ketika Activity yang memuatnya selesai.

Kita akan mencari cara yang lebih baik untuk menyelesaikan masalah ini dengan coroutine, yang memungkinkan Anda menulis kode asinkron yang terlihat seperti blok kode imperatif biasa dari atas ke bawah, tanpa melakukan pemblokiran panggilan pada thread panggilan. Selain itu, coroutine juga dapat dibatalkan, sehingga kita dapat membersihkannya setiap kali berada di luar cakupan.

Pada langkah berikutnya, kita akan menambahkan fungsi ekstensi yang mengonversi callback API yang ada menjadi fungsi penangguhan yang dapat dipanggil dari cakupan coroutine yang terkait dengan UI Anda. Kita ingin hasil akhir terlihat seperti ini:

private fun getLastKnownLocation() {
    try {
        val lastLocation = fusedLocationClient.awaitLastLocation();
        // process lastLocation here if needed
        showLocation(R.id.textView, lastLocation)
    } (e: Exception) {
        // we can do regular exception handling here or let it throw outside the function
    }
}

Membuat fungsi penangguhan menggunakan suspendCancellableCoroutine

Buka LocationUtils.kt, dan tentukan fungsi ekstensi baru di FusedLocationProviderClient:

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =
   suspendCancellableCoroutine { continuation ->
    TODO("Return results from the lastLocation call here")
}

Sebelum masuk ke bagian implementasi, mari kita bedah tanda tangan fungsi ini:

  • Anda sudah mengetahui fungsi ekstensi dan jenis penerima dari bagian codelab sebelumnya: fun FusedLocationProviderClient.awaitLastLocation
  • suspend berarti fungsi tersebut akan menjadi fungsi penangguhan, yaitu jenis fungsi khusus yang hanya dapat dipanggil dalam coroutine atau dari fungsi suspend lainnya
  • Jenis hasil pemanggilannya akan berupa Location, seolah-olah merupakan cara sinkron untuk mendapatkan hasil lokasi dari API.

Untuk membuat hasil, kita akan menggunakan suspendCancellableCoroutine, blok pembuatan tingkat rendah untuk membuat fungsi penangguhan dari library coroutine.

suspendCancellableCoroutine menjalankan blok kode yang diteruskan ke fungsi tersebut sebagai parameter, lalu menangguhkan eksekusi coroutine saat menunggu hasil.

Mari coba tambahkan callback yang berhasil dan gagal ke isi fungsi, seperti yang kita lihat pada panggilan lastLocation sebelumnya. Sayangnya, seperti yang dapat Anda lihat pada komentar di bawah, hal yang jelas-jelas ingin kita lakukan, yaitu menampilkan hasil, tidak mungkin dilakukan di bagian isi callback:

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =
   suspendCancellableCoroutine { continuation ->
    lastLocation.addOnSuccessListener { location ->
        // this is not allowed here:
        // return location
    }.addOnFailureListener { e ->
        // this will not work as intended:
        // throw e
    }
}

Hal ini karena callback terjadi lama setelah fungsi di sekitarnya selesai, dan sekarang tidak ada tempat untuk menampilkan hasilnya. Di situlah suspendCancellableCoroutine memainkan peran dengan continuation yang disediakan untuk blok kode kita. Kita dapat menggunakannya untuk memberikan hasil kembali ke fungsi yang ditangguhkan di waktu mendatang, menggunakan continuation.resume. Selesaikan kasus error menggunakan continuation.resumeWithException(e) untuk menyebarkan pengecualian ke situs panggilan dengan benar.

Umumnya, Anda harus selalu memastikan bahwa pada masa mendatang, Anda akan menampilkan hasil atau pengecualian agar coroutine tidak ditangguhkan selamanya saat menunggu hasil.

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =
   suspendCancellableCoroutine<Location> { continuation ->
       lastLocation.addOnSuccessListener { location ->
           continuation.resume(location)
       }.addOnFailureListener { e ->
           continuation.resumeWithException(e)
       }
   }

Selesai. Kita baru saja memperlihatkan versi ditangguhkan dari API lokasi terakhir yang diketahui, yang bisa dimanfaatkan dari coroutine di aplikasi.

Memanggil fungsi penangguhan

Mari kita modifikasi fungsi getLastKnownLocation dalam MainActivity untuk memanggil versi coroutine baru dari panggilan lokasi terakhir yang diketahui:

private suspend fun getLastKnownLocation() {
    try {
        val lastLocation = fusedLocationClient.awaitLastLocation()
        showLocation(R.id.textView, lastLocation)
    } catch (e: Exception) {
        findAndSetText(R.id.textView, "Unable to get location.")
        Log.d(TAG, "Unable to get location", e)
    }
}

Seperti yang disebutkan sebelumnya, fungsi penangguhan harus selalu dipanggil dari fungsi penangguhan lain untuk memastikan fungsi tersebut berjalan dalam coroutine, yang berarti kita harus menambahkan pengubah penangguhan ke fungsi getLastKnownLocation itu sendiri, atau kita akan mendapatkan error dari IDE.

Perhatikan bahwa kita dapat menggunakan blok try-catch reguler untuk penanganan pengecualian. Kita dapat memindahkan kode ini dari dalam callback yang gagal karena pengecualian yang berasal dari Location API sekarang disebarkan dengan benar, seperti dalam program wajib yang reguler.

Untuk memulai coroutine, biasanya kita akan menggunakan CoroutineScope.launch, yang memerlukan cakupan coroutine. Untungnya, library Android KTX dilengkapi dengan beberapa cakupan bawaan untuk objek siklus proses umum seperti Activity, Fragment, dan ViewModel.

Tambahkan kode berikut ke Activity.onStart:

override fun onStart() {
   super.onStart()
   if (!hasPermission(ACCESS_FINE_LOCATION)) {
       requestPermissions(arrayOf(ACCESS_FINE_LOCATION), 0)
   }

   lifecycleScope.launch {
       try {
           getLastKnownLocation()
       } catch (e: Exception) {
           findAndSetText(R.id.textView, "Unable to get location.")
           Log.d(TAG, "Unable to get location", e)
       }
   }
   startUpdatingLocation()
}

Anda seharusnya dapat menjalankan aplikasi dan memastikan bahwa aplikasi berfungsi sebelum melanjutkan ke langkah berikutnya. Pada langkah tersebut, kita akan mengenalkan Flow untuk fungsi yang memunculkan hasil lokasi beberapa kali.

Sekarang kita akan fokus pada fungsi startUpdatingLocation(). Dalam kode saat ini, kita mendaftarkan pemroses dengan Penyedia Lokasi Gabungan untuk mendapatkan update lokasi secara berkala setiap kali perangkat pengguna bergerak di dunia nyata.

Untuk menampilkan apa yang ingin kita peroleh dengan API berbasis Flow, pertama-tama mari lihat bagian MainActivity yang akan dihapus di bagian ini, dan memindahkannya ke detail implementasi fungsi ekstensi baru.

Dalam kode saat ini, ada variabel untuk pelacakan jika kita mulai memroses pembaruan:

var listeningToUpdates = false

Ada juga subclass dari class callback dasar dan implementasi untuk fungsi callback yang diupdate lokasi:

private val locationCallback: LocationCallback = object : LocationCallback() {
   override fun onLocationResult(locationResult: LocationResult?) {
       if (locationResult != null) {
           showLocation(R.id.textView, locationResult.lastLocation)
       }
   }
}

Kita juga memiliki pendaftaran awal pemroses (yang bisa gagal jika pengguna tidak memberikan izin yang diperlukan), beserta callback karena panggilan tersebut asinkron:

private fun startUpdatingLocation() {
   fusedLocationClient.requestLocationUpdates(
       createLocationRequest(),
       locationCallback,
       Looper.getMainLooper()
   ).addOnSuccessListener { listeningToUpdates = true }
   .addOnFailureListener { e ->
       findAndSetText(R.id.textView, "Unable to get location.")
       e.printStackTrace()
   }
}

Terakhir, saat layar tidak aktif lagi, kita menghapus:

override fun onStop() {
   super.onStop()
   if (listeningToUpdates) {
       stopUpdatingLocation()
   }
}

private fun stopUpdatingLocation() {
   fusedLocationClient.removeLocationUpdates(locationCallback)
}

Anda dapat melanjutkan dan menghapus semua cuplikan kode tersebut dari MainActivity, dan hanya menyisakan fungsi startUpdatingLocation() kosong yang nantinya akan kita gunakan untuk mulai mengumpulkan Flow.

callbackFlow: Builder Alur untuk API berbasis callback

Buka LocationUtils.kt lagi, lalu tentukan fungsi ekstensi lainnya di FusedLocationProviderClient:

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    TODO("Register a location listener")
    TODO("Emit updates on location changes")
    TODO("Clean up listener when finished")
}

Ada beberapa hal yang perlu kita lakukan di sini untuk membuat replika fungsi yang baru saja kita hapus dari kode MainActivity. Kita akan menggunakan callbackFlow(), fungsi builder yang menampilkan Flow, yang cocok untuk memunculkan data dari API berbasis callback.

Blok yang diteruskan ke callbackFlow() ditentukan dengan ProducerScope sebagai penerimanya.

noinline block: suspend ProducerScope<T>.() -> Unit

ProducerScope mengenkapsulasi detail implementasi callbackFlow, seperti fakta bahwa ada Channel yang mencadangkan Flow yang dibuat. Tanpa perlu membahas detailnya, Channels digunakan secara internal oleh beberapa builder dan operator Flow, dan kecuali Anda menulis builder/operator sendiri, Anda tidak perlu khawatir dengan detail tingkat rendah ini.

Kita hanya akan menggunakan beberapa fungsi yang ditampilkan ProducerScope untuk menghasilkan data dan mengelola status Flow.

Mari mulai dengan membuat pemroses untuk location API:

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    TODO("Register a location listener")
    TODO("Clean up listener when finished")
}

Kita akan menggunakan ProducerScope.offer untuk mengirim data lokasi ke Flow begitu data tersebut masuk.

Selanjutnya, daftarkan callback dengan FusedLocationProviderClient, yang dengan cermat akan menangani error:

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    requestLocationUpdates(
       createLocationRequest(),
       callback,
       Looper.getMainLooper()
    ).addOnFailureListener { e ->
       close(e) // in case of error, close the Flow
    }

    TODO("Clean up listener when finished")
}

FusedLocationProviderClient.requestLocationUpdates adalah fungsi asinkron (seperti lastLocation) yang menggunakan callback untuk memberi sinyal saat berhasil atau gagal menyelesaikan.

Di sini, kita dapat mengabaikan status berhasil, karena artinya di masa mendatang, onLocationResult akan dipanggil, dan kita akan mulai menampilkan hasilnya ke Flow.

Jika terjadi kegagalan, kita akan langsung menutup Flow dengan Exception.

Hal terakhir yang harus selalu Anda panggil di dalam blok yang diteruskan ke callbackFlow adalah awaitClose. Fungsi ini menyediakan tempat yang sesuai untuk meletakkan kode pembersihan apa pun untuk melepaskan resource jika terjadi penyelesaian atau pembatalan Flow (terlepas dari apakah itu terjadi dengan Exception atau tidak):

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    requestLocationUpdates(
       createLocationRequest(),
       callback,
       Looper.getMainLooper()
    ).addOnFailureListener { e ->
       close(e) // in case of exception, close the Flow
    }

    awaitClose {
       removeLocationUpdates(callback) // clean up when Flow collection ends
    }
}

Setelah mengerjakan semua bagian (mendaftarkan pemroses, memroses pembaruan, dan membersihkan), mari kembali ke MainActivity untuk memanfaatkan Flow guna menampilkan lokasi.

Mengumpulkan Alur

Mari kita modifikasi fungsi startUpdatingLocation di MainActivity untuk memanggil builder Flow dan mulai mengumpulkannya. Implementasi asli mungkin terlihat seperti ini:

private fun startUpdatingLocation() {
    lifecycleScope.launch {
        fusedLocationClient.locationFlow()
        .conflate()
        .catch { e ->
            findAndSetText(R.id.textView, "Unable to get location.")
            Log.d(TAG, "Unable to get location", e)
        }
        .collect { location ->
            showLocation(R.id.textView, location)
            Log.d(TAG, location.toString())
        }
    }
}

Flow.collect() adalah operator terminal yang memulai operasi Flow yang sebenarnya. Di dalamnya, kita akan menerima semua update lokasi yang ditransmisi dari builder callbackFlow kami. Karena collect adalah fungsi penangguhan, fungsi tersebut harus berjalan di dalam coroutine, yang kita luncurkan menggunakan lifecycleScope.

Anda juga dapat melihat operator perantara conflate() dan catch() yang dipanggil di Flow. Ada banyak operator yang disertakan dengan library coroutine, yang memungkinkan Anda memfilter dan mentransformasikan alur secara deklaratif.

Menggabungkan alur berarti kita hanya ingin menerima update terbaru, setiap kali update dihasilkan lebih cepat daripada pemrosesan yang dilakukan kolektor. Ini sesuai dengan contoh, karena kita hanya ingin menampilkan lokasi terbaru di UI.

catch, seperti namanya, akan memungkinkan Anda menangani pengecualian apa pun yang ditampilkan di upstream, dalam hal ini di builder locationFlow. Anda dapat menganggap upstream sebagai operasi yang diterapkan sebelum operasi saat ini.

Jadi, apa masalahnya dengan cuplikan di atas? Meskipun tidak akan menyebabkan aplikasi error, dan akan dihapus dengan benar setelah aktivitas DIBERSIHKAN (berkat lifecycleScope), cuplikan tersebut tidak memperhitungkan saat aktivitas dihentikan (misalnya saat tidak terlihat).

Artinya, kita tidak hanya akan memperbarui UI jika tidak diperlukan, tetapi Alur juga akan membuat langganan ke data lokasi tetap aktif serta menghabiskan baterai dan siklus CPU!

Salah satu cara untuk memperbaikinya adalah dengan mengonversi Alur ke LiveData menggunakan ekstensi Flow.asLiveData dari library LiveData KTX. LiveData mengetahui kapan harus mengamati dan kapan harus menjeda langganan, dan akan memulai ulang Alur dasar sesuai kebutuhan.

private fun startUpdatingLocation() {
    fusedLocationClient.locationFlow()
        .conflate()
        .catch { e ->
            findAndSetText(R.id.textView, "Unable to get location.")
            Log.d(TAG, "Unable to get location", e)
        }
        .asLiveData()
        .observe(this, Observer { location ->
            showLocation(R.id.textView, location)
            Log.d(TAG, location.toString())
        })
}

lifecycleScope.launch eksplisit tidak diperlukan lagi karena asLiveData akan menyediakan cakupan yang diperlukan untuk menjalankan Alur masuk. Panggilan observe sebenarnya berasal dari LiveData dan tidak berkaitan dengan coroutine atau Alur. Ini hanyalah cara standar untuk mengamati LiveData dengan LifecycleOwner. LiveData akan mengumpulkan Alur dasar dan metransmisikan lokasi ke pengamatnya.

Karena pembuatan ulang dan pengumpulan alur akan ditangani secara otomatis sekarang, kita harus memindahkan metode startUpdatingLocation() dari Activity.onStart (yang dapat dijalankan berulang kali) ke Activity.onCreate:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
   startUpdatingLocation()
}

Sekarang Anda dapat menjalankan aplikasi dan memeriksa bagaimana reaksinya terhadap rotasi, dengan menekan tombol Beranda dan Kembali. Periksa logcat untuk melihat apakah lokasi baru ditampilkan saat aplikasi berada di latar belakang. Jika implementasinya benar, aplikasi harus menjeda dengan benar dan memulai ulang koleksi Alur saat Anda menekan Beranda lalu kembali ke aplikasi.

Anda berhasil membuat library KTX pertama Anda!

Selamat! Apa yang telah Anda capai dalam codelab ini sangat mirip dengan apa yang biasanya terjadi saat membuat library ekstensi Kotlin untuk API berbasis Java yang sudah ada.

Untuk meringkas hal-hal yang telah kita lakukan:

  • Anda menambahkan fungsi praktis untuk memeriksa izin dari Activity.
  • Anda memberikan ekstensi format teks pada objek Location.
  • Anda mengekspos versi coroutine Location API untuk mendapatkan lokasi terakhir yang diketahui dan update lokasi berkala menggunakan Flow.
  • Jika ingin, Anda dapat membersihkan kode lebih lanjut, menambahkan beberapa pengujian, dan mendistribusikan library location-ktx ke developer lain di tim Anda agar mereka dapat memanfaatkannya.

Untuk membuat file AAR untuk distribusi, jalankan tugas :myktxlibrary:bundleReleaseAar.

Anda dapat mengikuti langkah serupa untuk API lainnya yang dapat memanfaatkan ekstensi Kotlin.

Menyempurnakan arsitektur aplikasi dengan Alur

Sebelumnya telah disebutkan bahwa meluncurkan operasi dari Activity seperti yang kita lakukan dalam codelab ini tidak selalu menjadi pilihan terbaik. Anda dapat mengikuti codelab ini untuk mempelajari cara mengamati alur dari ViewModels di UI, bagaimana alur dapat berinteroperasi dengan LiveData, dan bagaimana Anda dapat mendesain aplikasi menggunakan aliran data.