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!
Related
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.
I'm trying to do something really simple (at least, should be IMO): create a function that receives a string containing some json and turns that into a Gson object. I've created the following function in my class:
class EasyJson(val url: String, private val responseDataClass: Class<*>) {
var text: String
var json: Gson
init {
text = getJsonFromUrl(URL(url)) //another function does this and is working fine
json = stringToJson(text, responseDataClass::class.java) as Gson
}
private fun <T> stringToJson(data: String, model: Class<T>): T {
return Gson().fromJson(data, model)
}
And here is the calling class:
class CallingClass() {
val url="https://api"
init {
test()
}
data class ApiResponse(
val count: Number,
val next: Number?,
val previous: Number?
)
private fun test(){
val json = EasyJson(url, ApiResponse::class.java)
}
}
However, when I do this I get the following exception:
java.lang.UnsupportedOperationException: Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?
How can I use a generic data class as a parameter for Gson here?
The issue here is most likely the following line:
stringToJson(text, responseDataClass::class.java)
This will use the class of the class, which is always Class<Class>. You should just call your stringToJson function with responseDataClass (without ::class.java):
stringToJson(text, responseDataClass)
Additionally it looks like you treat the stringToJson result incorrectly. The result is an instance of T (actually T? because Gson.fromJson can return null). However, you are casting it to Gson.
The correct code would probably look like this:
class EasyJson<T>(val url: String, private val responseDataClass: Class<T>) {
var text: String
var json: T?
init {
text = getJsonFromUrl(URL(url)) //another function does this and is working fine
json = stringToJson(text, responseDataClass)
}
private fun <T> stringToJson(data: String, model: Class<T>): T? {
return Gson().fromJson(data, model)
}
}
(Though unless your code contains more logic in EasyJson, it might make more sense to move this JSON fetching and parsing to a function instead of having a dedicated class for this.)
I have a sealed class WebSocketMessage which has some subclasses. The WebSocketMessage has a field named type which is used for differentiating between subclasses.
All of the subclasses have their own field named payload which is of different type for each subclass.
Currently I am using Moshi's PolymorphicJsonAdapterFactory so that these classes can be parsed from JSON and encoded to JSON.
This all works, but what I need is to encode the the payload field to stringified JSON instead of JSON object.
Is there any possibility to write a custom adapter class to help me with this problem? Or is there any other solution so that I will not have to do this stringification manually?
I have tried looking into custom adapters but I can't find how I could pass moshi instance to adapter so that I can encode the given field to JSON and then stringify it, nor did I find anything else that could help me.
The WebSocketMessage class with its subclasses:
sealed class WebSocketMessage(
val type: Type
) {
enum class Type(val type: String) {
AUTH("AUTH"),
PING("PING"),
FLOW_INITIALIZATION("FLOW_INITIALIZATION")
}
class Ping : WebSocketMessage(Type.PING)
class InitFlow(payload: InitFlowMessage) : WebSocketMessage(Type.FLOW_INITIALIZATION)
class Auth(payload: Token) : WebSocketMessage(Type.AUTH)
}
The Moshi instance with PolymorphicJsonAdapterFactory:
val moshi = Moshi.Builder().add(
PolymorphicJsonAdapterFactory.of(WebSocketMessage::class.java, "type")
.withSubtype(WebSocketMessage.Ping::class.java, WebSocketMessage.Type.PING.type)
.withSubtype(
WebSocketMessage.InitFlow::class.java,
WebSocketMessage.Type.FLOW_INITIALIZATION.type
)
.withSubtype(WebSocketMessage.Auth::class.java, WebSocketMessage.Type.AUTH.type)
)
// Must be added last
.add(KotlinJsonAdapterFactory())
.build()
How I encode to JSON:
moshi.adapter(WebSocketMessage::class.java).toJson(WebSocketMessage.Auth(fetchToken()))
I currently get the JSON in the next format:
{
"type":"AUTH",
"payload":{
"jwt":"some_token"
}
}
What I would like to get:
{
"type":"AUTH",
"payload":"{\"jwt\":\"some_token\"}"
}
In the second example the payload is a stringified JSON object, which is exactly what I need.
You can create your own custom JsonAdapter:
#Retention(AnnotationRetention.RUNTIME)
#JsonQualifier
annotation class AsString
/////////////////////
class AsStringAdapter<T>(
private val originAdapter: JsonAdapter<T>,
private val stringAdapter: JsonAdapter<String>
) : JsonAdapter<T>() {
companion object {
var FACTORY: JsonAdapter.Factory = object : Factory {
override fun create(
type: Type,
annotations: MutableSet<out Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
val nextAnnotations = Types.nextAnnotations(annotations, AsString::class.java)
return if (nextAnnotations == null || !nextAnnotations.isEmpty())
null else {
AsStringAdapter(
moshi.nextAdapter<Any>(this, type, nextAnnotations),
moshi.nextAdapter<String>(this, String::class.java, Util.NO_ANNOTATIONS)
)
}
}
}
}
override fun toJson(writer: JsonWriter, value: T?) {
val jsonValue = originAdapter.toJsonValue(value)
val jsonStr = JSONObject(jsonValue as Map<*, *>).toString()
stringAdapter.toJson(writer, jsonStr)
}
override fun fromJson(reader: JsonReader): T? {
throw UnsupportedOperationException()
}
}
/////////////////////
class Auth(#AsString val payload: Token)
/////////////////////
.add(AsStringAdapter.FACTORY)
.add(KotlinJsonAdapterFactory())
.build()
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.
I have an Android application written in Kotlin, that gets data from an API, for now it's just a local hosted JSON file. When I'm trying to get the data, I receive the error that my list, persons, is not initialized thus persons == null and didn't receive the data. I'm not sure what I did wrong and how to fix this.
The model
data class Person (
#Json(name = "personId")
val personId: Int,
#Json(name = "personName")
val name: String,
#Json(name = "personAge")
val age: Int,
#Json(name = "isFemale")
val isFemale: Boolean,
)
The JSON response
{
"persons": [{
"personId": 1,
"personName": "Bert",
"personAge": 19,
"isFemale": "false",
}
]
}
The ApiClient
class ApiClient {
companion object {
private const val BASE_URL = "http://10.0.2.2:3000/"
fun getClient(): Retrofit {
val moshi = Moshi.Builder()
.add(customDateAdapter)
.build()
return Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
}
}
The Retrofit http methods
interface ApiInterface {
#GET("persons")
fun getPersons(): Observable<List<Person>>
}
and finally the call
class PersonActivity: AppCompatActivity(){
private lateinit var jsonAPI: ApiInterface
private lateinit var persons: List<Person>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_person)
val retrofit = ApiClient.getClient()
jsonAPI = retrofit.create(ApiInterface::class.java)
jsonAPI.getPersons()
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ persons = it })
}
}
Expected: Data from the JSON file into the persons list instead of NULL.
Your moshi adapter is expecting the person objects directly but they are nested inside the "persons" Json array.
You should add another model as follow
data class Persons (
#Json(name="persons")
val persons : List<Person>
)
Also change the interface to return the new object.
interface ApiInterface {
#GET("persons")
fun getPersons(): Observable<Persons>
}
Will also need to change the subscription to
.subscribe({ persons = it.persons })
I think there could be one of two issues.
If you are using moshi reflection you will have something like these dependencies in gradle:
//Moshi Core
implementation "com.squareup.moshi:moshi:1.8.0"
//Moshi Reflection
implementation "com.squareup.moshi:moshi-kotlin:1.8.0"
In that case you will need to use the KotlinJsonAdapterFactory:
val moshi = Moshi.Builder()
.add(customDateAdapter)
.add(KotlinJsonAdapterFactory())
.build()
If you are using codegen you'll need this:
apply plugin: 'kotlin-kapt'
...
//Moshi Core Artifact
implementation "com.squareup.moshi:moshi:1.8.0"
//Moshi Codegen
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.8.0"
Additionally, every class you want to deserialise to should be annotated with
#JsonClass(generateAdapter = true) (so the Person and Persons class)