All Projects → utsmannn → store-apps-mvvm

utsmannn / store-apps-mvvm

Licence: Apache-2.0 license
Implementation component of Android Jetpack with clean architecture

Programming Languages

kotlin
9241 projects
shell
77523 projects

Projects that are alternatives of or similar to store-apps-mvvm

Android-MVVM-News-App
MVVM News Application with clean code architecture & android jetpack components.
Stars: ✭ 38 (+18.75%)
Mutual labels:  clean-architecture, retrofit2, navigation-architecture-component
Compose-BreakingBad
🧪 ☠︎ Jetpack Compose - Breaking Bad ☢︎
Stars: ✭ 26 (-18.75%)
Mutual labels:  clean-architecture, jetpack, retrofit2
Android-Assignment
This assignment gives you basically a post list and its detail with comments.🚀
Stars: ✭ 32 (+0%)
Mutual labels:  retrofit2, navigation-architecture-component, coroutines-flow
bitcoin-market-android
Bitcoin Market app shows you the current Bitcoin market price and price chart of different time intervals 💰
Stars: ✭ 284 (+787.5%)
Mutual labels:  clean-architecture, jetpack, retrofit2
Dads
*BA DUM TSSS*
Stars: ✭ 240 (+650%)
Mutual labels:  clean-architecture, jetpack
Clean Marvel Kotlin
This repository contains a detailed sample app that implements Clean architecture and MVP in Kotlin using RxJava2, Retrofit
Stars: ✭ 27 (-15.62%)
Mutual labels:  clean-architecture, retrofit2
Jetpack Compose News
基于Jetpack Compose实现的一款集新闻、视频、美图、音乐、天气等功能的资讯App,持续完善中...
Stars: ✭ 58 (+81.25%)
Mutual labels:  jetpack, retrofit2
UseCases
This a library that offers a generic implementation of the data layers from the clean architecture by Uncle bob.
Stars: ✭ 23 (-28.12%)
Mutual labels:  clean-architecture, retrofit2
Clean-MVVM-NewsApp
Android News app developed using Clean + MVVM architecture
Stars: ✭ 52 (+62.5%)
Mutual labels:  clean-architecture, retrofit2
flickr-android
A small sample app to showcase architecting app using Clean Architecture and MVVM
Stars: ✭ 25 (-21.87%)
Mutual labels:  clean-architecture, retrofit2
LittleLight
An Android app that uses Destiny's API to interact with in-game inventory.
Stars: ✭ 19 (-40.62%)
Mutual labels:  clean-architecture, retrofit2
Manime
🍱 An anime app, based on single activity and MVVM architecture.
Stars: ✭ 24 (-25%)
Mutual labels:  retrofit2, navigation-architecture-component
JADA
JADA - Just Another Dictionary App
Stars: ✭ 20 (-37.5%)
Mutual labels:  retrofit2, navigation-architecture-component
MoviesApp
A Movie Application uses TheMovie API, MVVM architecture and all jetpack components.
Stars: ✭ 100 (+212.5%)
Mutual labels:  navigation-architecture-component, coroutines-flow
BESTV
Android TV App powered by TMDb. It is a easy way to find the best TV content, the top movies, series... all of that in your TV.
Stars: ✭ 49 (+53.13%)
Mutual labels:  clean-architecture, retrofit2
GithubApp-android-architecture
Let's learn a deep look at the Android architecture
Stars: ✭ 16 (-50%)
Mutual labels:  clean-architecture, retrofit2
Stars
An android application build with a clean architecture approach and Star wars API
Stars: ✭ 54 (+68.75%)
Mutual labels:  clean-architecture, retrofit2
Rick-and-Morty
The Rick And Morty - MVVM with a clean architecture approach using some of the best practices in Android Development.
Stars: ✭ 45 (+40.63%)
Mutual labels:  jetpack, coroutines-flow
FoodApp
Proof of concept for food app [JetPack + Coroutines + Flow + MockK + JaCoCo coverage + SonarQube]
Stars: ✭ 25 (-21.87%)
Mutual labels:  clean-architecture, coroutines-flow
ResDelivery-Hilt-Coroutines-Mvvm-Single-Activity
This is a Sample Single Activity App (Multi Fragments) that uses Dagger-Hilt, Coroutines Flows, Paging 3 & Mvvm Clean Architecture
Stars: ✭ 28 (-12.5%)
Mutual labels:  retrofit2, coroutines-flow

Store Apps - Apk Downloader

License Pull request Twitter Github

MVVM Clean Architecture develop with

  • Modularization
  • UseCase
  • Retrofit
  • Moshi
  • Dagger Hilt Dependencies Injection
  • Kotlin flow (with stateFlow experimental)
  • Paging 3
  • Navigation Component
  • Data Store Preferences
  • Room
  • Work Manager
  • Download Manager
  • ViewBinding (with helper by yogacp)

Features

  • Apk downloader
  • Search app
  • Update app
  • Deep link app market or playstore
  • Auto installer (root required)

Apk debug

Download apk debug


Table of Contents


Home Detail Detail downloading Download monitor
Download manager Auto installer Options auto installer App updated

Architecture

Projek ini berusaha menerapkan Clean Arch yang baik demi pemeliharaan (maintenance) yang baik pula. Penerapan ini juga berusaha mengikuti prinsip-prinsip Clean Arch yang baik yakni SOLID Principles, (baca di sini https://en.wikipedia.org/wiki/SOLID). Untuk lebih jelas mengenai Android Clean Architecture, silahkan baca tulisan om Yoga Clean Architecture Android.

Stream Data Flow

  • Network: Data berasal dari API Aptoide dan dieksekusi pada network threading menggunakan Retrofit.
  • Repository : Route retrofit berasal dari api service yang dipanggil oleh repository dengan fungsi interactor flow yang menghasilkan response yang telah wrap oleh class result.
  • Domain : Result yang berisi state, loading, success atau error di convert ke dalam DTO yang bertindak sebagai data object yang akan diparsing ke dalam view. Result yang berisi state dari data object tersebut menjadi flow variable.
  • ViewModel : Dalam viewmodel, flow variable tersebut di convert menjadi LiveData dengan ViewModelScope agar bisa dibinding oleh lifecycle activity atau fragment untuk dapat diteruskan menuju view.
  • View : LiveData yang berisi result di ekstrak dalam state idle, loading, success dan error. Dimana state success akan menghasilkan data yang dapat diparsing dalam view, sementara error akan menghasilkan throwable.

Modularization

Dalam proyek ini, saya memisahkan fitur dan layer secara modular sehingga mudah untuk menjaga kode dan tanggungjawab setiap class pada layer masing-masing dan ketergantungannya.

tree:

root
--- app
--- data
--- feature
------ detail
------ home
------ listing
--- libraries
------ abstraction
------ network

Layer

Layer yang dimaksud dalam prinsip Clean Arch adalah lapisan-lapisan yang mewakili fungsionalitas dari class-class yang membangun sebuah fitur. Untuk gampang nya bisa lihat gambar berikut.

Lingkaran tersebut berisi macam-macam lapisan atau layer. Layer-layer itulah yang membangun sebuah fitur dengan scope tugas masing-masing.

  • Presentation : Ini adalah layer UI, tempat user berinteraksi, akhir dari semua aliran data (stream data). Activity atau fragment yang berisi view-view ada dalam layer ini
  • UseCase : Layer ini berisi fungsi-fungsi tindakan user yang berasal dari view, inti dari bisnis logic berawal dari sini
  • Data : Berisi semua sumber data. Stream data berawal dari sini kemudian diteruskan oleh bisnis logic class.
  • Domain : Berisi logika bisnis dari sebuah fitur, biasa nya layer ini juga berisi UseCase class.
  • External : Layer diluar bisnis logic dan view, biasa nya berisi abstraction dan helper.

Khusus untuk layer Data, module dipisah, isi nya berupa repository-repository dan class-class yang dibutuhkan pada module lain. Untuk module lain, dibuat perfitur, masing-masing module menghandle fiturnya masing-masing dimulai dengan fungsi yang disediakan repository pada module data, dengan begitu responsibility akan lebih terjaga.

UseCase

UseCase dalam arsitektur ini menjadi jembatan bagi view dan data, satu level dengan Domain layer. Berisi fungsi-fungsi yang dapat dieksekusi atau diteruskan ke Data layer dan bergantung pada user input Presentasion layer. Dalam project ini, UseCase menghasilkan data yang dibungkus oleh ResultState.

Seperti dibahas di awal, layer ini berfungsi untuk menyimpan function-function yang terkait pengambilan data dari layer Data. Dalam project ini, proses tersebut menggunakan sebuah fungsi yang menghasilkan (return) Flow<ResultState>, fungsi ini dapat disebut dengan interactor.

ResultState

ResultState merupakan class "bungkusan" yang berfungsi membawa data dari hulu menuju hilir, dari layer Data sampai ke View. Class ini menggunakan fungsi sealed dari Kotlin yang berisi empat objek, Loading, Idle, Success dan Error.

sealed class ResultState<T: Any>(val payload: T? = null, val throwable: Throwable? = null, val message: String? = null) {
    class Loading<T: Any> : ResultState<T>()
    class Idle<T: Any>: ResultState<T>()
    data class Success<T: Any>(val data: T) : ResultState<T>(payload = data)
    data class Error<T: Any>(val th: Throwable) : ResultState<T>(throwable = th)
}

Yang merupakan data class turunan hanya state Success dan Error karena dua objek tersebut membawa data masing-masing, sementara Loading dan Idle hanya object tanpa membawa data. Dengan begitu, ketika ekstraksi pada view, data atau payload hanya dapat diakses pada state success, pun begitu pada throwable hanya dapat diakses pada state error.

viewModel.getRandomApps()
viewModel.randomList.observe(viewLifecycleOwner, Observer { state ->
    when (state) {
        is ResultState.Idle -> {
            // idle
        }
        is ResultState.Loading -> {
            // loading
        }
        is ResultState.Success -> {
            // success, get data
            val data = state.data
        }
        is ResultState.Error -> {
            // error, get throwable
            val throwable = state.throwable
        }
    }
})

Interactor

Interactor merupakan fungsi yang bertugas mengambil data dan menghasilkan class Flow<ResultState>. Digunakan untuk berinteraksi dengan suspend function yang mengambil data atau response dari repository atau dapat juga langsung dari route services. Baca mengenai Flow di sini https://kotlinlang.org/docs/reference/coroutines/flow.html

suspend fun <T: Any> fetch(call: suspend () -> T): Flow<ResultState<T>> = flow {
    emit(ResultState.Loading<T>())
    try {
        emit(ResultState.Success(data = call.invoke()))
    } catch (e: Throwable) {
        emit(ResultState.Error<T>(th = e))
    }
}

Dapat diperhatikan, fungsi interaktor ini menghasilkan ResultState.Loading, ResultState.Success dan ResultState.Error, lantas dimana ResultState.Idle (kondisi diam gak ngapa2in)?

ResultState.Idle didefiniskan diluar interactor, yakni pada nilai default dari variable sebuah MutableStateFlow<ResultState> pada class UseCase. State ini menandakan bahwa belum terjadi apa-apa, baik itu state loading, sukses ataupun error.

val resultData: MutableStateFlow<ResultState<AppsView>> = MutableStateFlow(ResultState.Idle())

Value resultData diperbarui dengan nilai yang dihasilkan pada interactor. Variable resultData ini lah yang akan diproses ke layer selanjutnya.

class HomeUseCase(private val appsRepository: AppsRepository) {

    // variable result dengan default value ResultState.Idle
    val randomList: MutableStateFlow<List<AppsView>> = MutableStateFlow(ResultState.Idle())

    // suspend function
    suspend fun getRandomApps() {
    
        // interactor
        fetch {
            // function dalam interactor
            val response = appsRepository.getTopApps()
            response.datalist?.list?.map { app ->

                // convert to object view (lihat gambar data flow di atas)                 
                app.toAppsView()
            } ?: emptyList()
        }.collect {
            
            // set value dari variable result
            randomList.value = it
        }
    }
}

Networking

Networking menggunakan Retrofit, Coroutines dan Moshi adapter. Alih-alih menggunakan RxJava, Coroutine terlihat lebih clean dan simple, bukan berarti RxJava tidak bagus, tapi ini cuman pilihan. Tidak begitu dengan Moshi, saya berhenti menggunakan Gson dan beralih dengan Moshi dalam project-project riset seperti ini karena peforma dan kompabilitasnya dirasa lebih baik.

Moshi

Moshi merupakan JSON Library untuk android dan java yang dikembangkan oleh Square, pengembang yang sama untuk Retrofit. Saya sudah lama menggunakan Gson, tapi sepertinya saya harus mempertimbangkan Moshi yang akan jadi pilihan utama kedepan. Setelah baca beberapa artikel, sedikit catatan untuk itu.

  • Gson bukan library yang modern, tidak ditulis dengan kotlin, sementara Moshi lebih ramah kotlin. Yang membuat ramah adalah sebagian kode ditulis dengan kotlin.
  • Moshi memiliki ukuran yang lebih kecil dibanding Gson
  • Moshi dikembangkan oleh developer yang sama dengan Retrofit. Hal ini memastikan update Retrofit kedepan akan kompatible dengan Moshi.

Meski begitu, sulit membuat function converter dari model ke string Json secara generik. Tidak seperti Gson yang hanya butuh type class, Moshi membutuhkan adapter pada tiap class generik dan memerlukan Buffer Reader UTF 8 untuk generate pretty nya. Lihat JsonBeautifier.kt

Dependencies Injection

Dependencies Injection (untuk selanjutnya disebut DI) merupakan library wajib bagi saya untuk mengerjakan projek-projek android. Setidaknya ada tiga poin utama yang di kemukakan oleh Google mengenai kelebihan menerapkan DI dalam arsitektur android.

  • Penggunaan kembali kode
  • Kemudahan dalam pemfaktoran ulang
  • Kemudahan dalam pengujian

Baca selebihnya pada dokumentasi official Google di sini https://developer.android.com/training/dependency-injection?hl=id

Koin

Koin adalah salah satu library dependencies yang populer dikalangan android developer. Selain yang ramah kotlin, Koin juga mudah dalam penggunaan dan implementasi nya yang sederhana membuat orang yang sedang belajar DI jadi cepat mengerti.

Pada starting project ini, saya menggunakan Koin karena tidak terlalu memikirkan bagaimana peforma library DI bekerja dan fokus pada fitur juga komponen arch yang lain. Untuk melihat kemudahan menggunakan Koin dapat dibaca dokumentasi offical Koin, https://start.insert-koin.io/#/quickstart/android. Kemudian saya beralih pada Hilt, library DI offical Google.

Dagger Hilt

Hilt sama fungsinya seperti Koin, sama-sama library DI. Hanya Hilt merupakan library yang dikembangkan sendiri oleh Google dengan memanfaatkan Dagger sebagai basis kode. Library Dagger yang sudah cukup lama dan mapan sebagai library DI, dirasa cukup sulit penerapannya apalagi dimengerti, sehingga muncul banyak library alternatif lain seperti Koin diatas. Namun sekarang, Google telah mengembangkan Hilt yang mudah di implementasikan dan dimengerti tanpa menghilangkan komponen-komponen yang ada pada Dagger.

Dalam kode ini anda dapat menemukan banyak anotasi dan provide Hilt secara singleton. Saya hanya menggunakan singleton dan ApplicationComponent pada keseluhuran karena ketergantungan dependencies tidak sampai pada layer view. Jadi tiap-tiap dependencies dapat diakses pada semua scope project aplikasi tanpa ada batasan.

Handle dependencies across module

Karena semua dependencies di install pada module ApplicationComponent, maka diperlukan teknik mengambil class dependencies yang telah di provide, terutama pada class-class yang tidak memiliki EntryPoint seperti Activity, Service dan lain-lain (https://developer.android.com/training/dependency-injection/hilt-android?hl=id#kotlin).

Untuk keperluan tersebut, perlu dibuat variable global, yang dapat diakses oleh MainApplication sebagai scope utama project aplikasi. Bagusnya, pada kotlin, kita dapat mendeklarasikan variable global tanpa class, hanya ditulis pada file kotlin (*.kt). Tahap selanjutnya adalah menetapkan value dari masing-masing global variable di MainApplication dengan mengambil dependencies yang telah di inject.

Dalam membuat global variable tersebut, saya menggunakan MutableStateFlow sebagai bungkusannya. Sehingga variable tersebut dapat ditetapkan valuenya berdasarkan dependencies yang telah di provide. Skemanya adalah

Module B. Menetapkan global variable dengan nullable

// lateinit.kt module b
val _someVarState: MutableStateFlow<SomeVar?> = MutableStateFlow(null)

Module A. Menetapkan value variable tersebut dengan nilai yang sudah di provide

// MainApplication.kt module a
import module_b._someVarState

@HiltAndroidApp
class MainApplication : Application() {
        
        @Inject
        lateinit var someVar: SomeVar
        
        override fun onCreate() {
            super.onCreate()
            _someVarState.value = someVar
        }
}

Lantas, tinggal buat function helper agar terlihat lebih bagus yang dapat digunakan tiap-tiap class yang membutuhkan.

inline fun <reified T: Any?>getValueOf(instanceState: MutableStateFlow<T?>): T {
    val immutable: StateFlow<T?> = instanceState
    val className = T::class.simpleName
    return try {
        immutable.value!!
    } catch (e: ExceptionInInitializerError) {
        loge("Module not yet initialize. Check module of class : `$className`")
        throw e
    }
}

inline fun <reified T>getValueSafeOf(instanceState: MutableStateFlow<T>): T {
    val immutable: StateFlow<T> = instanceState
    return immutable.value
}

Ada dua function yang dibuat, getValueOf() menghasilkan nilai yang tidak null, karena global variable tersebut bersifat nullable, maka ditambahkan not-null assertion, cara ini dapat menyebabkan NPE jika value gagal di tetapkan pada MainApplication. Sementara getValueSafeOf() mengambil nilai yang nullable. Sehingga pemanggilan dependencies nya dapat dilakukan pada class seperti helper, WorkManager, interface dll.

class DownloadWorkManager(context: Context, workerParameters: WorkerParameters) : CoroutineWorker(context, workerParameters) {

    private val someVarState = getValueSafeOf(_someVarState)

    ...
}

ViewModel

ViewModel berfungsi meneruskan dan menyimpan state variable (jika diperlukan) dari UseCase. Stream data (aliran data) berada disini setelah melewati UseCase. Di ViewModel juga ResultState yang bertipe Flow dikonversi menjadi livedata agar dapat dibinding pada lifecycle sebuah activity/fragment.

class SomeViewModel(private val useCase: SomeUseCase) : ViewModel() {

    val resultLiveData 
            get() = useCase.someResult.asLiveData(viewModelScope.coroutineContext)
}

Paging Library 3

Paging library merupakan komponen dari Jetpack yang dapat menghandle banyak data pada recyclerview. Meski masih versi alpa, paging 3 cukup mapan untuk digunakan pada production, saya tidak menemui bug yang fatal selama riset Paging 3. Kelebihan yang signifikan dibanding paging 2 terletak pada DataSource, viewModel handling dan adapter.

Data Source

Pada data source paging 2, developer perlu menentapkan 3 function, loadInitial, loadAfter dan loadBefore. Pada loadInital dan loadAfter, dapat terjadi redundant karena code implementasi nya sering kali sama, hanya param key yang bertindak sebagai "page" selanjutnya. Sementara pada Paging 3 hanya membutuhkan implementasi pada load function. Dah gitu, data source pada paging 3 berdiri diatas coroutines functions, lihat AppsPagingSource.kt. Hal ini membuat developer lebih mudah implementasi async code.

Adapter

Pada adapter paging 2, mirip seperti RecyclerView.Adapter biasa, yang membedakan hanya tipe data (PagedList dan List) dan beberapa function seperti submit, getItem. Sementara pada Paging 3, ditambahkan listener LoadState juga footer atau header yang dapat di attach pada adapter. Ini membuat developer tidak perlu lagi membuat listener dan membuat network state dengan mutliple view type.

LoadState Listener

Ini adalah fitur yang ditambahkan pada paging 3, memungkinkan developer melihat state pada aliran data. Pada function addLoadStateListener dari adapter, developer bisa kontrol view saat data sedang loading, notloading dan error dengan lima kondisi, yakni:

  • source: LoadState yang asli dari DataSource
  • refresh: LoadState yang sedang memuat data baru pada DataSource
  • prepend: LoadState awal dan belum memuat apa-apa dari aliran data pada DataSource
  • append: LoadState akhir dari aliran data pada DataSource
  • mediator: LoadState yang terdapat pada RemoteMediator jika RemoteMediator dipasang

Lihat dokumentasi CombinedLoadStates

Berdasarkan state-state tersebut, developer dapat memasang ui loading atau not-loading pada activity/fragment sebelum item dimuat oleh adapter.

pagingListAdapter.addLoadStateListener { combinedLoadStates ->

    val refreshState = combinedLoadStates.refresh // mengambil kondisi refresh load state
    progressCircular.isVisible = refreshState is LoadState.Loading // menampikan progress bar jika refresh state sedang loading
}

Paging 3 ini juga mempunyai state adapter dengan viewholder yang berbeda pada adapter utama. Adapter tersebut di pasang sejajar dengan adapter utama, fungsi ui nya dapat menampilkan status loading maupun error pada data yang sejajar dengan item utama yang telah di load sebelumnya. Jika pada paging 2 developer harus membuat network viewholder dengan teknik multiple viewtype dengan satu viewholder untuk menampilkan ui loading atau error, pada Paging 3 tidak perlu melakukan itu.

class PagingStateAdapter : LoadStateAdapter<PagingStateAdapter.PagingStateViewHolder>() {

    // viewholder single line
    class PagingStateViewHolder(view: View) : RecyclerView.ViewHolder(view)

    override fun onBindViewHolder(holder: PagingStateViewHolder, loadState: LoadState) {
        val binding = ItemListLoaderBinding.bind(holder.itemView)
        binding.run {
    
            // jika state loading menampilkan progress bar
            progressCircular.isVisible = loadState is LoadState.Loading

            // jika state error menampilkan textview dan button
            btnRetry.isVisible = loadState is LoadState.Error
            txtMsg.isVisible = loadState is LoadState.Error

            ....
        }
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        loadState: LoadState
    ): PagingStateViewHolder {
        val view = parent.inflate(R.layout.item_list_loader)
        return PagingStateViewHolder(view)
    }
}

Lihat PagingStateAdapter.kt

Kemudian, state adapter tersebut dipasang dengan adapter utama

binding.recyclerView.run {

    val pagingListAdapter = PagingListAdapter() // adapter utama
    val pagingStateAdapter = PagingStateAdapter() // adapter state

    layoutManager = gridLayout
    adapter = pagingListAdapter.withLoadStateFooter(pagingStateAdapter) // pasang state adapter pada footer dari adapter utama
}

WorkManager

WorkManager adalah library yang memungkinkan sebuah fungsi berjalan pada background diluar aplikasi, sehingga ketika aplikasi ditutup, WorkManager akan tetap menjalankan tugasnya. Pada project ini, WorkManager dijalankan untuk menangani tugas download, sehingga ketika aplikasi ditutup, download akan tetap berjalan bahkan ketika restart task download akan dilanjutkan.

Worker Class

Sebelum menjalankan task working, perlu dibuat class yang berisi function task. Saya membuat class ini dengan extend pada class CoroutineWorker karena DownloadManager yang dijalankan akan bersifat asynchronous. Untuk bagaimana membuat task asynchronous download, lihat DownloadManager Asynchronous.

class DownloadAppWorker(context: Context, workerParameters: WorkerParameters) :
    CoroutineWorker(context, workerParameters) {

    override suspend fun doWork() = withContext(Dispatchers.IO) {
        download(this) // download manager async
    }
}

Selengkapnya lihat DownloadAppWorker.kt

WorkRequest and observer WorkManager

Untuk menjalankan task Working, cukup sederhana. Developer perlu membuat WorkRequest dengan class OneTimeWorkRequestBuilder. OneTimeWorkRequestBuilder sendiri bertugas untuk menjalankan Worker dengan sekali perintah hingga selesai tanpa penjadwalan. Lihat WorkManager Basic

val inputData = workDataOf("data" to "data send to worker")

val worker = OneTimeWorkRequestBuilder<DownloadAppWorker>()
    .setInputData(inputData)
    .build()

Lihat DownloadRepositoryImpl.kt

Sementara untuk observing Worker dapat dilakukan dengan liveData

WorkManager.getInstance(applicationContext)
    // requestId is the WorkRequest id
    .getWorkInfoByIdLiveData(requestId)
    .observe(observer, Observer { workInfo: WorkInfo? ->
            if (workInfo != null) {
                val progress = workInfo.progress
                val value = progress.getInt(Progress, 0)
                // Do something with progress information
            }
    })

Implementation with Architecture

Dalam project ini, ada kondisi dimana perlu menyimpan requestId untuk bisa meng-observer jika activity/fragment di create kembali sesuai konteks yang akan di observer. Untuk handle konndisi tersebut, diperlukan penyimpanan data, jadi saya gunakan Room. Untuk implementasi pada arsitektur project, request WorkManager dilakukan pada level repository, begitupun dengan observer worker. Karena untuk mengobserver worker diperlukan requestId, dilakukan flatMap dari file yang sedang didownload.

Observer WorkManager dapat menghasilkan WorkInfo, dan dibungkus oleh state WorkInfoResult, sehingga pada UseCase dapat mengobserver WorkInfo dengan state nya.

class DetailUseCase(private val downloadRepository: DownloadRepository) {

    // variable workInfoResult
    val workInfoState = MutableStateFlow<WorkInfoResult>(WorkInfoResult.Stopped())

    // function observer dalam usecase
    suspend fun observerWorkInfoResult(packageName: String) {

        // manggil function observer dalam repository
        downloadRepository.observerWorkInfo(packageName)
            .collect {

                // tetapkan nilai dari variable workInfoResult
                workInfoState.value = it
            }
    }
}

Selengkapnya lihat:

Externals

Download Manager

Menggunakan API DownloadManager bawaan dari android untuk downloader pada project ini adalah pilihan yang baik. Selain mudah implemntasinya, juga tidak perlu melakukan pengecekan dan request ulang ketika device reboot. Hanya butuh sedikit kode untuk membuat listener downloading dan penerapan async code yang baik sehingga dapat di implementasi pada workmanager. Di sini, saya buat satu class static (object class) untuk menaruh semua function DownloadManager. Lihat DownloadUtils.kt

Data File

Pada download manager di project ini, diperlukan beberapa parameter yang selalu dibawa, seperti nama file, url file sampai package_manager. Data class FileDownload dibuat untuk keperluan tersebut.

data class FileDownload(
    var id: Int? = 0,
    var name: String? = "",
    var url: String? = "",
    var packageName: String? = "",
    var fileName: String? = ""
) {
    companion object {
        fun simple(download: FileDownload.() -> Unit) = FileDownload().apply(download)
    }
}

Request Download

Untuk melakukan request download, code yang ditulis cukup sederhana.

object DownloadUtils {

        ....

        fun startDownload(fileDownload: FileDownload?, showNotificationComplete: Boolean = true): Long? {
            val downloadRequest = DownloadManager.Request(Uri.parse(fileDownload?.url)).apply {
                val notifyComplete = if (showNotificationComplete) {
                    DownloadManager.Request.VISIBILITY_VISIBLE or DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
                } else {
                    DownloadManager.Request.VISIBILITY_VISIBLE
                }
                setNotificationVisibility(notifyComplete)
                setDestinationInExternalFilesDir(getContext(), Environment.DIRECTORY_DOWNLOADS, "${fileDownload?.fileName}.apk")
                setAllowedOverMetered(true)
                setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE or DownloadManager.Request.NETWORK_MOBILE)
                setTitle("Downloading ${fileDownload?.name}")
            }
        
            return downloadManager().enqueue(downloadRequest)
        }

        ....

}

Function startDownload() me-return downloadId yang berasal dari DownloadManager.enqueue(downloadRequest). Id tersebut dibutuhkan untuk membuat listener dan membatalkan download task. Ketika download berjalan pada WorkManager, downloadId tersebut di simpan menggunakan Room dan digunakan kembali untuk observer pada listener.

Download Listener

Untuk membuat download listener, perlukan Query dan Cursor yang didapat dari query DownloadManager. Query inilah yang akan di observer sehingga didapat posisi status download manager nya. Pada observer download status dengan query, tidak serta merta query melakukan observer secara asynchronous, kita perlu membuat sedikit pemaksaan tiap detik agar cursor dari query mampu mengecek posisi download status, hal ini membutuhkan ticker. Ticker adalah channel coroutines yang mampu mengkonsum Unit atau function secara berulang tiap waktu tergantung pada waktu/delay yang ditentukan.

import kotlinx.coroutines.channels.ticker

val ticker = ticker(100)
ticker.consumeAsFlow().collect {
    // loop every 100ms
}

Ticker juga dapat di akhiri dalam kondisi tertentu dengan ticker.cancel().

Sebelum memanfaatkan kemampuan ticker tersebut, perlu membuat function observer query dulu dengan mengekstensi class ReceiveChannel<Unit> yang merupakan tipe class asli dari ticker..

private suspend fun ReceiveChannel<Unit>.observingCursor(downloadId: Long?) {
    val cursor = getCursor(downloadId)

    if (cursor != null && cursor.moveToFirst()) {
        val colStatus = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
       
        when (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))) {
            DownloadManager.STATUS_SUCCESSFUL -> {
                // state success
                // cancel ticker
                cancel()
            }
            DownloadManager.STATUS_RUNNING -> {
                // state running
               
            }
            DownloadManager.STATUS_PAUSED -> {
                // state paused
            }
            DownloadManager.STATUS_PENDING -> {
                // state pending
            }
            DownloadManager.STATUS_FAILED -> {
                // state failed
                // cancel ticker
                cancel()
            }
        }
    } else {
        // state cancel
        cancel()
    }
}

Lihat DownloadUtils.kt#156

Untuk menconsume state agar state dapat diolah, dibutuhkan listener dan dipasang pada tiap-tiap status sesuai function listenernya.

interface DownloadListener {
    suspend fun onSuccess(cursor: Cursor, status: Download.Status)
    suspend fun onRunning(fileSizeObserver: FileSizeObserver?, status: Download.Status)
    suspend fun onPaused(cursor: Cursor, status: Download.Status)
    suspend fun onPending(cursor: Cursor, status: Download.Status)
    suspend fun onFailed(cursor: Cursor, status: Download.Status)
    suspend fun onCancel(status: Download.Status)
}

Lihat DownloadUtils.kt#221

Setelah selesai buat function listenernya, pasang function tersebut pada ticker.

import kotlinx.coroutines.channels.ticker

val ticker = ticker(100)
ticker.consumeAsFlow().collect {
    ticker.observingCursor(downloadId)
}

Root Apk Installer

Tidak lengkap rasanya jika sebuah aplikasi market apk tidak memiliki kemampuan silent install seperti Google PlayStore. Untuk implementasi kemampuan tersebut, dibutuhkan root akses sehingga dapat melakukan command install pada aplikasi.

Check root access

RootBeer merupakan library untuk melakukan pengecekan akses root dengan kemampuan native, artinya library ini secara native akan mencari binary yang berpotensial ada dalam tiap-tiap root device seperti su dan BusyBox. Jika ditemukan binary-binary tersebut, maka diketahui oleh RootBeer sebagai device yang memiliki akses root. Implementasinya pun sederhana.

val rootBeer = RootBeer(context)
val isRooted = rootBeer.checkForRootNative() && rootBeer.isRooted

Lihat

Command

Contoh command nya sebagai berikut

pm install -r /sdcard/download/simontok-terbaru.apk

Pada android, kita perlu lakukan command tersebut dengan class Proccess

val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
process.waitFor()

Untuk mendapatkan message error, dapat menggunakan InputStreamReader(process.errorStream) dengan reader BufferedReader.readLine()

val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
val errorReader = BufferedReader(InputStreamReader(process.errorStream))
val errorLine = errorReader.readLine()

Lihat RootedRepositoryImplement.kt


Copyright 2020 (c) Muhammad Utsman

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].