Download file with Ktor and show progress with Flow/CallbackFlow - android

I was creating a method to download files using Ktor using the following logic:
Sealed class to trace the steps
sealed class DownloadType {
object Started : DownloadType()
class Progress(
val value: String
) : DownloadType()
class Finished(
val file: File
) : DownloadType()
class Failed(
val throwable: Throwable
) : DownloadType()
}
Hilt Module for Ktor
#Module
#InstallIn(SingletonComponent::class)
object KtorModule {
#Provides
#Singleton
fun provideHttpClient(): HttpClient = HttpClient(Android)
}
Interface to facilitate future maintenance
interface DownloadFileUseCase {
suspend operator fun invoke(
url: String,
name: String
): Flow<DownloadType>
}
Ktor implementation class (with flow)
class DownloadFileUseCaseImplFlow #Inject constructor(
#ApplicationContext private val context: Context,
private val httpClient: HttpClient
) : DownloadFileUseCase {
override suspend fun invoke(
url: String,
name: String
): Flow<DownloadType> = flow {
emit(DownloadType.Started)
val path = File("${context.filesDir}/example")
if (!path.exists()) path.mkdirs()
val bytesChannel = httpClient.get(url) {
onDownload { bytesSentTotal, contentLength ->
// problem here
emit(DownloadType.Progress(value = "$bytesSentTotal of $contentLength"))
}
}.bodyAsChannel()
val file = File(path, name)
bytesChannel.copyAndClose(file.writeChannel())
emit(DownloadType.Finished(file = file))
}.catch { error ->
error.printStackTrace()
emit(DownloadType.Failed(throwable = error))
}
}
This code only works if we don't use an emit inside onDownload.
Researching this problem I found another post here, where it was suggested to use the callbackFlow instead of the flow. The problem is that the answer only had the theoretical part, without the example, so I created a new implementation following luck, because until then I had never used a callbackFlow.
Ktor implementation class (with callback flow)
class DownloadFileUseCaseImplCallbackFlow #Inject constructor(
#ApplicationContext private val context: Context,
private val httpClient: HttpClient
) : DownloadFileUseCase {
override suspend fun invoke(
url: String,
name: String
): Flow<DownloadType> = callbackFlow {
send(DownloadType.Started)
val path = File("${context.filesDir}/example")
if (!path.exists()) path.mkdirs()
val bytesChannel = httpClient.get(url) {
onDownload { bytesSentTotal, contentLength ->
// big difference between these two lines of code (examples attached below)
// case 1:
// send(DownloadType.Progress(value = "$bytesSentTotal - $contentLength"))
// case 2:
// send(DownloadType.Progress(value = "Downloading file size $contentLength, please wait..."))
}
}.bodyAsChannel()
val file = File(path, name)
bytesChannel.copyAndClose(file.writeChannel())
send(DownloadType.Finished(file = file))
close()
}.catch { error ->
error.printStackTrace()
emit(DownloadType.Failed(throwable = error))
}
}
Now I came across the following situation: if send is used for all bytesSentTotal on onDownload, the process is much longer, if only one send is used, the process is faster.
With this comparison above, it seems that a send is only sent when the last send has already been collected, hence the difference in time, as the bytesSentTotal is emitted many times...
Note:
Both examples were made with the same file;
I collected the flow with collectLatest in ViewModel.

Related

How should I structure my code if I am using Moshi to parse form a local JSON file in Kotlin for Android?

So I have a local json that is basically a survey object that contains a survey and some details/attributes(List, Header, Footer, ids etc) relating to the survey. I created a Data Class model using Moshi code gen adapters and it works but I want to know what is the best way to achieve this. Currently this is my thought process. I'm a bit confused on what should be in my DI Module vs what should my repository look like, do I need a DI concrete class, interface etc?
Here is where I am currently at
Hilt Module:
#Provides
#Singleton
fun provideModelAdapter(#ApplicationContext context: Context) : JsonAdapter<Model> {
val moshi: Moshi = Moshi.Builder().build()
return moshi.adapter(Model::class.java)
}
Then would my repository use a DAO interface? If so what would that look like or would it get the json model from in the repo?
Repository:
class Repository #Inject constructor(
#ApplicationContext private val context: Context,
private val adapter: JsonAdapter<ChltSurveyModel>
) {
suspend fun getModel() {
val json: String = context.readFromAssets(MODEL_FILENAME)
adapter.fromJson()
}
}
ViewModel:
#HiltViewModel
class ViewModel #lInject constructor(
private val repository: Repository
) : ViewModel() {
private val _model = MutableLiveData<Model>()
val model: LiveData<Model> = _model
init {
getModel()
}
fun getModel() = viewModelScope.launch {
try {
_model.value = repository.getModel()
} catch (e: JsonDataException) {
e.printStackTrace()
}
}
}
Util Class to Read JSON
fun Context.readFromAssets(filename: String): String = try {
val reader = BufferedReader(InputStreamReader(assets.open(filename)))
val sb = StringBuilder()
var line = reader.readLine()
while (line != null) {
sb.append(line)
line = reader.readLine()
}
reader.close()
sb.toString()
} catch (exp: Exception) {
println("Failed reading line from $filename -> ${exp.localizedMessage}")
""
}

Unable to get data from Retrofit Call to a Direct JSON File

I am working on small project on a Jetpack Compose.
I am trying to data from a static JSON File from this url using Retrofit.
https://firebasestorage.googleapis.com/v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84
Network Module
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule {
#Provides
#Singleton
fun proveidesCurrencyService() : CurrencyService{
return Retrofit.Builder()
.baseUrl("https://firebasestorage.googleapis.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CurrencyService::class.java)
}
}
Service Class
interface CurrencyService {
//#Streaming
#GET
suspend fun getCurrencyFile(#Url fileUrl:String): Response<ResponseBody>
}
data class CurrencyAPIResponse(
#SerializedName("data") var data: MutableState<List<CurrencyRates>>
)
data class CurrencyRates (
#SerializedName("code") var code : String,
#SerializedName("value") var value : String
)
ViewModel
#HiltViewModel
class CurrencyDataViewModel
#Inject
constructor(private val currencyService: CurrencyService
) : ViewModel()
{
init {
getCurrencyFileData()
}
}
fun getCurrencyFileData() = viewModelScope.launch(Dispatchers.IO) {
val url: String =
"https://firebasestorage.googleapis.com/v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84"
val responseBody = currencyService.getCurrencyFile(url).body()
if (responseBody != null) {
Log.d("\nresponsebody", responseBody.string())
val gson = GsonBuilder().setPrettyPrinting().create()
val currencyAPIResponse = gson.fromJson(responseBody.string(), CurrencyAPIResponse::class.java)
val data: MutableState<List<CurrencyRates>> = currencyAPIResponse.data
Log.d("Data", data.value[0].code)
}
}
Everytime, I am getting the below error,
Attempt to invoke virtual method 'androidx.compose.runtime.MutableState com.tuts.playlite.network.response.CurrencyAPIResponse.getData()' on a null object reference
Not sure, where I am failing, I have tried to convert this to JSON Object, but still failing. Is it right way to get the data?
Another thing noticed that even though the JSON file is complete in the url, the response body log is showing the JSON with some other content.
{
"code": "IMP",
"value": "0.722603"
},
{
"code": "INR",
[ 1631385414.170 12452:12478 D/
responsebody]
"value": "72.99465"
},
{
"code": "IQD",
"value": "1458.61356"
},
As a result, the GSON might not able to form the json and hence could be getting null exception.
Not sure why random text is getting added!
You are already providing a Gson converter to retrofit, so retrofit should already be able to do the json to object conversion for you. That's the beauty of retrofit!
Try rewriting your CurrencyService like this:
interface CurrencyService {
#GET("v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84")
suspend fun getCurrencyFile(): CurrencyAPIResponse
}
Your ViewModel also has some issues. Not sure if you actually meant MutableState but I guess you're looking for MutableLiveData or MutableStateFlow. Below an example using MutableLiveData.
#HiltViewModel
class CurrencyDataViewModel #Injectconstructor(
private val currencyService: CurrencyService
) : ViewModel() {
private val _currencyData = MutableLiveData<List<CurrencyRates>>()
private val currencyData: LiveData = _currencyData
init {
getCurrencyFileData()
}
fun getCurrencyFileData() = viewModelScope.launch(Dispatchers.IO) {
_currencyData.postValue(currencyService.getCurrencyFile().data)
}
}
Use Kotin Coroutines for retorfit, Try something like below
interface CurrencyService {
#GET("v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84")
suspend fun getCurrencyFile(): Response<CurrencyAPIResponse>
}
and if you are using MVVM use this is repository class
suspend fun getCurrencyFile:Response<CurrencyAPIResponse>{
return currencyService.getCurrencyFile()
}
then in your view model class
Coroutines.main {
try{
val response = repository.getCurrencyFile()
if(response.isSuccessful){
//response.body is your data
}
}catch(Exception){}
}
if you are not using the repository class you can skip the second step and directly call the service in viewmodel class,
The Coroutines code is
object Coroutines {
fun main(work:suspend (()->Unit)) =
CoroutineScope(Dispatchers.Main).launch {
work()
}
}
Finally, the culprit seems to be responseBody Stream. After I changed the code with below, It seems to be working. I assume, it was unable to get the proper complete JSON earlier and throwing the null object reference error.
val responseBody = currencyService.getCurrencyFile(url).body()
val resData = responseBody?.byteStream()!!.bufferedReader().use {
it.readText()
}
val gson = GsonBuilder().setPrettyPrinting().create()
val currencyAPIResponse = gson.fromJson(resData, CurrencyAPIResponse::class.java)
Thank you for all support!

Passing errors coming from the API call

I am using 2 separate liveData exposed to show the error coming from the API. I am basically checking if there is an exception with the API call, pass a failure status and serverErrorLiveData will be observed.
So I have serverErrorLiveData for error and creditReportLiveData for result without an error.
I think I am not doing this the right way. Could you please guide me on what is the right way of catching error from the API call. Also, any concerns/recommendation on passing data from repository on to view model.
What is the right way of handing loading state?
CreditScoreFragment
private fun initViewModel() {
viewModel.getCreditReportObserver().observe(viewLifecycleOwner, Observer<CreditReport> {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
})
viewModel.getServerErrorLiveDataObserver().observe(viewLifecycleOwner, Observer<Boolean> {
if (it) {
showScoreUI(false)
showToastMessage()
}
})
viewModel.getCreditReport()
}
MainActivityViewModel
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
var creditReportLiveData: MutableLiveData<CreditReport>
var serverErrorLiveData: MutableLiveData<Boolean>
init {
creditReportLiveData = MutableLiveData()
serverErrorLiveData = MutableLiveData()
}
fun getCreditReportObserver(): MutableLiveData<CreditReport> {
return creditReportLiveData
}
fun getServerErrorLiveDataObserver(): MutableLiveData<Boolean> {
return serverErrorLiveData
}
fun getCreditReport() {
viewModelScope.launch(Dispatchers.IO) {
val response = dataRepository.getCreditReport()
when(response.status) {
CreditReportResponse.Status.SUCCESS -> creditReportLiveData.postValue(response.creditReport)
CreditReportResponse.Status.FAILURE -> serverErrorLiveData.postValue(true)
}
}
}
}
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCreditReport(): CreditReportResponse {
return try {
val creditReport = apiServiceInterface.getDataFromApi()
CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
} catch (e: Exception) {
CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
}
}
}
ApiServiceInterface
interface ApiServiceInterface {
#GET("endpoint.json")
suspend fun getDataFromApi(): CreditReport
}
CreditScoreResponse
data class CreditReportResponse constructor(val creditReport: CreditReport?, val status: Status) {
enum class Status {
SUCCESS, FAILURE
}
}
It's creates complexity and increased chances for a coding error to have two LiveData channels for success and failure. You should have a single LiveData that can offer up the data or an error so you know it's coming in orderly and you can observe it in one place. Then if you add a retry policy, for example, you won't risk somehow showing an error after a valid value comes in. Kotlin can facilitate this in a type-safe way using a sealed class. But you're already using a wrapper class for success and failure. I think you can go to the source and simplify it. You can even just use Kotlin's own Result class.
(Side note, your getCreditReportObserver() and getServerErrorLiveDataObserver() functions are entirely redundant because they simply return the same thing as a property. You don't need getter functions in Kotlin because properties basically are getter functions, with the exception of suspend getter functions because Kotlin doesn't support suspend properties.)
So, to do this, eliminate your CreditReportResponse class. Change your repo function to:
suspend fun getCreditReport(): Result<CreditReport> = runCatching {
apiServiceInterface.getDataFromApi()
}
If you must use LiveData (I think it's simpler not to for a single retrieved value, see below), your ViewModel can look like:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
val _creditReportLiveData = MutableLiveData<Result<CreditReport>>()
val creditReportLiveData: LiveData<Result<CreditReport>> = _creditReportLiveData
fun fetchCreditReport() { // I changed the name because "get" implies a return value
// but personally I would change this to an init block so it just starts automatically
// without the Fragment having to manually call it.
viewModelScope.launch { // no need to specify dispatcher to call suspend function
_creditReportLiveData.value = dataRepository.getCreditReport()
}
}
}
Then in your fragment:
private fun initViewModel() {
viewModel.creditReportLiveData.observe(viewLifecycleOwner) { result ->
result.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
viewModel.fetchCreditReport()
}
Edit: the below would simplify your current code, but closes you off from being able to easily add a retry policy on failure. It might make better sense to keep the LiveData.
Since you are only retrieving a single value, it would be more concise to expose a suspend function instead of LiveData. You can privately use a Deferred so the fetch doesn't have to be repeated if the screen rotates (the result will still arrive and be cached in the ViewModel). So I would do:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
private creditReportDeferred = viewModelScope.async { dataRepository.getCreditReport() }
suspend fun getCreditReport() = creditReportDeferred.await()
}
// In fragment:
private fun initViewModel() = lifecycleScope.launch {
viewModel.getCreditReport()
.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
}

How to inject Kotlin extentsion properties using Dagger-Hilt

I'm trying to implement Proto Datastore using Kotlin serialization and Hilt.
Reference: https://medium.com/androiddevelopers/using-datastore-with-kotlin-serialization-6552502c5345
I couldn't inject DataStore object using the new DataStore creation syntax.
#InstallIn(SingletonComponent::class)
#Module
object DataStoreModule {
#ExperimentalSerializationApi
#Singleton
#Provides
fun provideDataStore(#ApplicationContext context: Context): DataStore<UserPreferences> {
val Context.dataStore: DataStore<UserPreferences> by dataStore(
fileName = "user_pref.pb",
serializer = UserPreferencesSerializer
)
return dataStore
}
}
I get the lint message Local extension properties are not allowed
How can inject this Kotlin extension property? Or is there any way to inject dataStore object?
You can't use extensions in a local context, you should call this way:
#InstallIn(SingletonComponent::class)
#Module
object DataStoreModule {
#ExperimentalSerializationApi
#Singleton
#Provides
fun provideDataStore(#ApplicationContext context: Context): DataStore<UserPreferences> =
DataStoreFactory.create(
serializer = UserPreferencesSerializer,
produceFile = { context.dataStoreFile("user_pref.pb") },
)
}
Found a way to do this using Predefined qualifers in Hilt
Now no DataStoreModule class. I directly inject the application context into a Datastore Manager class. Below is the code.
#Singleton
class DataStoreManager #Inject constructor(
#ApplicationContext private val context: Context,
) {
private val Context.userPreferencesDataStore: DataStore<UserPreferences> by dataStore(
fileName = "user_pref.pb",
serializer = UserPreferencesSerializer
)
val userPreferencesFlow: Flow<UserPreferences> =
context.userPreferencesDataStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Timber.e("Error reading sort order preferences. $exception")
emit(UserPreferences())
} else {
throw exception
}
}
suspend fun updateUid(uid: String) {
context.userPreferencesDataStore.updateData { userPreferences ->
userPreferences.copy(uid = uid)
}
}
suspend fun getUid(): String {
return userPreferencesFlow.first().uid
}
}
This works like a charm.

kotlin + retrofit + coroutine + androidTest

How can create androidTest for sample retrofit request?
Sample
data class TestDataClass(
val id: String,
val employee_name: String,
val employee_salary: String,
val employee_age: String,
val profile_image: String)
enum class NetworkState { LOADING, ERROR, DONE }
private const val BASE_URL = "http://dummy.restapiexample.com/api/v1/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(BASE_URL)
.build()
interface TestApiService {
#GET("employees")
fun getPropertiesAsync():
Deferred<List<TestDataClass>>
}
object TestApi {
val retrofitTest : TestApiService by lazy { retrofit.create(TestApiService::class.java) }
}
You can use the MockWebServer library by Square.
Create a resources in your tests source set (src/test/resources), and put in it a JSON file containing a sample response from your API. Let's say it looks like this:
src/test/resources/sample_response.json
[
{
"id": "1",
"employee_name": "John Doe",
"employee_salary": "60000",
"employee_age": 37,
"profile_image": "https://dummy.sample-image.com/johndoe"
}
]
You may then write your tests as:
class ApiTest {
private lateinit var server: MockWebServer
private lateinit var retrofit: Retrofit
private lateinit var service: TestApiService
#Before
fun setup() {
server = MockWebServer()
retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(get<Moshi>()))
.addCallAdapterFactory(get<CoroutinesNetworkResponseAdapterFactory>())
.baseUrl(mockWebServer.url("/"))
.build()
service = retrofit.create(TestApi::class.java)
}
#After
fun teardown() {
server.close()
}
#Test
fun simpleTest() = runBlocking<Unit> {
val sampleResponse = this::class.java.getResource("/sample_response.json").readText()
server.enqueue(
MockResponse()
.setBody(sampleResponse)
)
val response = service.getPropertiesAsync().await()
assertTrue(1, response.size)
assertTrue(response[0].employee_name = "John Doe"
// Make as many assertions as you like
}
}
You have to ask yourself though, what exactly is it that you're trying to test? There's no need to test Retrofit's functionality. Nor should you test functionality of other well known libraries like Moshi.
These tests best serve the purpose of validating that the data models you have created for API responses are indeed correct, and that your parser (in this case, Moshi) can correctly handle unexpected values (such as null) gracefully. It is therefore important that the sample responses that you pick are actual responses from your API, so that your data models can be validated against real data in tests before being used in the app.

Categories

Resources