How can I check to see if JSON data is null without an infinite loop? - android

I have a viewmodel and data classes that fetch the NASA api for photos of Mars. The user should be displayed images from a random date queried. I always need an image url (imgSrc in Photo class) returned. If no url (imgSrc) is found, refresh data until one is found and display it. This logic would need to return an imgSrc following launch of the application as well as after swiperefreshlayout if the user chooses to swipe to refresh. I have been stuck on this for a week with no resolve. What is the best way to handle this? Even if I have to refactor my code I would like to be pointed in the right direction.
Here is the actual project on github.
JSON that I want to fetch
JSON returning no imgSrc
viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dev20.themarsroll.models.MarsPhotos
import com.dev20.themarsroll.models.Photo
import com.dev20.themarsroll.repository.MarsPhotoRepository
import com.dev20.themarsroll.util.Resource
import kotlinx.coroutines.launch
import retrofit2.Response
class MarsPhotoViewModel(
private val marsPhotoRepository: MarsPhotoRepository
): ViewModel() {
val marsPhotos: MutableLiveData<Resource<MarsPhotos>> = MutableLiveData()
init {
getRandomPhotos()
}
fun getCuriosityPhotos(solQuery: Int, roverQuery: Int, camera: String) = viewModelScope.launch {
marsPhotos.postValue(Resource.Loading())
val response = marsPhotoRepository.getCuriosityPhotos(solQuery, roverQuery, camera)
marsPhotos.postValue(handlePhotosResponse(response))
}
private fun handlePhotosResponse(response: Response<MarsPhotos> ) : Resource<MarsPhotos> {
if(response.isSuccessful) {
response.body()?.let { resultResponse ->
return Resource.Success(resultResponse)
}
}
return Resource.Error(response.message())
}
fun getRandomPhotos() {
getCuriosityPhotos((1..2878).random(), 5, "NAVCAM")
}
fun savePhoto(photo: Photo) = viewModelScope.launch {
marsPhotoRepository.upsert(photo)
}
fun getSavedPhotos() = marsPhotoRepository.getSavedPhotos()
fun deletePhoto(photo: Photo) = viewModelScope.launch {
marsPhotoRepository.deletePhoto(photo)
}
}
CuriosityFragment
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.View
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.dev20.themarsroll.R
import com.dev20.themarsroll.adapters.MarsPhotoAdapter
import com.dev20.themarsroll.util.Resource
import com.dev20.ui.MarsActivity
import com.dev20.ui.MarsPhotoViewModel
import kotlinx.android.synthetic.main.fragment_curiosity.*
class CuriosityFragment : Fragment(R.layout.fragment_curiosity) {
lateinit var viewModel: MarsPhotoViewModel
lateinit var marsPhotoAdapter: MarsPhotoAdapter
val TAG = "CuriosityFragment"
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as MarsActivity).viewModel
setupRecyclerView()
swipeLayout.setOnRefreshListener {
viewModel.getRandomPhotos()
swipeLayout.isRefreshing = false
}
marsPhotoAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("photo", it)
}
findNavController().navigate(
R.id.action_curiosityFragment_to_cameraFragment,
bundle
)
}
viewModel.marsPhotos.observe(viewLifecycleOwner, { response ->
when(response) {
is Resource.Success -> {
hideProgressBar()
response.data?.let { curiosityResponse ->
marsPhotoAdapter.differ.submitList(curiosityResponse.photos)
}
}
is Resource.Error -> {
hideProgressBar()
response.message?.let { message ->
Log.e(TAG, "An Error occurred: $message")
}
}
is Resource.Loading -> {
showProgressBar()
}
}
})
}
private fun hideProgressBar() {
curiosityPaginationProgressBar.visibility = View.INVISIBLE
}
private fun showProgressBar() {
curiosityPaginationProgressBar.visibility = View.VISIBLE
}
private fun setupRecyclerView() {
marsPhotoAdapter = MarsPhotoAdapter()
rvCuriosityPhotos.apply {
adapter = marsPhotoAdapter
layoutManager = LinearLayoutManager(activity)
}
}
}
MarsPhoto data class
data class MarsPhotos(
val photos: MutableList<Photo>,
val camera: MutableList<Camera>
)
Photo data class
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.google.gson.annotations.SerializedName
import java.io.Serializable
#Entity(
tableName = "photos"
)
#TypeConverters
data class Photo(
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
#SerializedName("earth_date")
val earthDate: String,
#SerializedName("img_src")
val imgSrc: String,
val sol: Int,
#SerializedName("rover_id")
val rover: Int,
) : Serializable

There are many potential solutions here that I can think of. However, given the app needs to have predictable and reasonable user experience, herein I'm scoping out the issues first.
Since a random resource is being requested each time, there's always a chance of it being null. Hence, multiple round-trips cannot be done away with (but can be reduced).
Multiple HTTP round-trips, that too with the unpredictability of returning null several times, can be really frustrating user experience.
Below are the potential ways (in increasing order of complexity) this can be dealt with.
The simplest solution is to implement logic on the repository level, wherein the function getCuriosityPhotos is responsible to request the api resource indefinitely till it responds with a not null data. This will solve the core issue that the user will eventually be shown something (but it might take a hell lot of time).
(PS- You'll also need to delegate the random number generation as a potential service available to the repository.)
To reduce the number of requests and hence wait-time for the user, you can save to the in-app database, the request params as well as the response. Thus, your database can act as a single source of truth. Hence, before making a request, you can query the database to check if the app had previously requested the same params earlier. If it did not, dispatch the request else if it did, then there's no need to request again and you can use the previous result. If it was null, regenerate another random number and try again. If it was not null, serve the data from the database. (This is a good enough solution and as more and more requests & responses are saved, user wait time would continually reduce)
(Note: In case the endpoints do not respond with static data and the data keeps changing, prefer using an in-memory database than a persistent database such as SQLite)
The app can run a background service that continually (by iterating over all possible combinations of the request params) requests and saves the data into the database. When the user requests random data, the app should display a random set of data from within the database. If the database is empty/does not meet a threshold of having at least n number of rows in the database, the app can perhaps show an initialization setup UI.
Pro-tip: Ideally (and in case you are building a product/service), mobile apps are meant to be very very predictable and have to be mindful of a user's time. Hence, the very task of requesting data from such resources should be a task of a backend server and database which operate some sort of service to fetch and store data and in-turn the app would request this server to fetch data amongst this subset which does not have any null values.
I've answered this question from a perspective of solving the problem with varying granularity. In case you need help/advice on the technical implementation part, let me know, I'll be happy to help!

Related

How to call different api resource from paging source or remote mediator Kotlin

Hey I want to call two different api for my Paging Library 3. I want to ask what is best suit for me to use Paging Source or Remote Mediator?. What is the use case of both? Can someone please explain me.
For 1st api call only for single time
#GET("/movie?min=20")
Above api call returns this response
data class movie(
var id: Int?,
var name: String?,
var items : List<Genre>?
}
Now for 2nd api call its loop to call again and again
#GET("/movie?count=20&&before={time}")
Above api call retrun this
data class movie(
var items : List<Genre>?
}
Genre
data class Genre(
var type: String?,
var date: String?,
var cast: String?
}
Genre have data in both api call. I tried to google this and found this Example. But inside this both api return same data. But in my case both returns little bit different. Also id, name is only used in UI component else list will go to adapter. But I didn't understand how to achieved this. I am new in Flow, it too difficult to understand, to be honest I am trying to learning CodeLab. Another important thing when 1st time api call, in which the last item contains date will send to 2nd api call in time parameter and then 2nd api last item date call again 2nd api, this will go in loop. So how can I track this again in loop condition. Third I want to update data at top of list, can we store data in memory than we can update value on that list? Thanks for advance. Sorry for my wrong english.
UPDATE
After #dlam suggestion, I tried to practice some code
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel by viewModels<ActivityViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launchWhenCreated {
viewModel.getMovie().collectLatest {
// setupAdapter()
}
}
}
}
ActivityViewModel
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
fun getMovie(): Flow<PagingData<Genre>> {
return Pager(
config = PagingConfig(
pageSize = 20
),
pagingSourceFactory = {
MultiRequestPagingSource(DataSource())
}
).flow
}
}
MultiRequestPagingSource
class MultiRequestPagingSource(private val dataSource: DataSource) : PagingSource<String, Genre>() {
override fun getRefreshKey(state: PagingState<String, Genre>): String? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.nextKey
}
}
override suspend fun load(params: LoadParams<String>): LoadResult<String, Genre> {
val key = params.key ?: ""
return try {
val data = when (params) {
is LoadParams.Refresh -> {
dataSource.fetchInitialMovie()
}
is LoadParams.Append -> {
dataSource.fetchMovieBefore(key)
}
is LoadParams.Prepend -> null
}
LoadResult.Page(
data = data.result,
prevKey = null,
nextKey = data?.nextKey,
)
} catch (exception: IOException) {
LoadResult.Error(exception)
}
}
}
I am getting error on data = data.result
Type mismatch.
Required:
List<TypeVariable(Value)>
Found:
ArrayDeque<Genre>?
DataSource
package com.example.multirequestpaging
class DataSource {
data class MovieResult(
val result: ArrayDeque<Genre>?,
val nextKey: String?
)
fun fetchInitialMovie(): MovieResult {
val response = ApiInterface.create().getMovieResponse(20)
return MovieResult(
addInArrayDeque(response),
response.items?.last()?.date
)
}
fun fetchMovieBefore(key: String): MovieResult {
val response = ApiInterface.create().getMovieResponseBefore(20, key)
return MovieResult(
addInArrayDeque(response),
response.items?.last()?.date
)
}
private fun addInArrayDeque(response: MovieResponse): ArrayDeque<Genre> {
val result: ArrayDeque<Genre> = ArrayDeque()
response.items?.forEach {
result.add(it)
}
return result
}
}
For Full code Project Link
1. I want to add an item to the top of the list. How can I use invalidate function? Sorry I didn't understand where I can use.
2. I want to use id,name in other place so how can i get those variable value in my activity class.
3. Is my code structure is good?. Do I need to improved, please give an example. It will also help beginner, who is learning Paging Library.
Thanks
PagingSource is the main driver for Paging, it's responsible for loading items that get displayed and represents the single source of truth of data.
RemoteMediator is for layered sources, it is essentially a callback which triggers when PagingSource runs out of data, so you can fetch from a secondary source. This is primarily useful in cases where you fetching from both DB + Network, where you want locally cached data to power Paging, and then use RemoteMediator as a callback to fetch more items into the cache from network.
In this scenario you have two APIs, but they both fetch from the same Network source, so you only need PagingSource here. If I'm understanding correctly, you essentially want to call the first API on initial load and the second API on subsequent prepend / append page loads, which you can check / switch on by the type of LoadParams you get. See the subtypes here: https://developer.android.com/reference/kotlin/androidx/paging/PagingSource.LoadParams

Android Paging 3: How to change parameters of RemoteMediator

I am struggling with the Paging 3 Library of Jetpack.
I setup
Retrofit for the network API calls
Room to store the retrieved data
A repository that exposes the Pager.flow (see code below)
A RemoteMediator to cache the network results in the room database
The PagingSource is created by Room.
I understand that the RemoteMediators responsibility is to fetch items from the network and persist them into the Room database. By doing so, we can use the Room database as single point of truth. Room can easily create the PagingSource for me as long as I am using Integers as nextPageKeys.
So far so good. Here is my ViewModel to retrieve a list of Sources:
private lateinit var _sources: Flow<PagingData<Source>>
val sources: Flow<PagingData<Source>>
get() = _sources
private fun fetchSources() = viewModelScope.launch {
_sources = sourcesRepository.getSources(
selectedRepositoryUuid,
selectedRef,
selectedPath
)
}
val sources is collected in the Fragment.
fetchSources() is called whenever one of the three parameters change (selectedRepositoryUuid, selectedRef or selectedPath)
Here is the Repository for the Paging call
fun getSources(repositoryUuid: String, refHash: String, path: String): Flow<PagingData<Source>> {
return Pager(
config = PagingConfig(50),
remoteMediator = SourcesRemoteMediator(repositoryUuid, refHash, path),
pagingSourceFactory = { sourcesDao.get(repositoryUuid, refHash, path) }
).flow
}
Now what I experience is that Repository.getSources is first called with correct parameters, the RemoteMediator and the PagingSource are created and all is good. But as soon as one of the 3 parameters change (let's say path), neither the RemoteMediator is recreated nor the PagingSource. All requests still try to fetch the original entries.
My question: How can I use the Paging 3 library here in cases where the paging content is dependent on dynamic variables?
If it helps to grasp my use-case: The RecyclerView is displaying a paged list of files and folders. As soon as the user clicks on a folder, the content of the RecyclerView should change to display the files of the clicked folder.
Update:
Thanks to the answer of dlam, the code now looks like this. The code is a simplification of the real code. I basically encapsulate all needed information in the SourceDescription class.:
ViewModel:
private val sourceDescription = MutableStateFlow(SourceDescription())
fun getSources() = sourceDescription.flatMapConcat { sourceDescription ->
// This is called only once. I expected this to be called whenever `sourceDescription` emits a new value...?
val project = sourceDescription.project
val path = sourceDescription.path
Pager(
config = PagingConfig(30),
remoteMediator = SourcesRemoteMediator(project, path),
pagingSourceFactory = { sourcesDao.get(project, path) }
).flow.cachedIn(viewModelScope)
}
fun setProject(project: String) {
viewModelScope.launch {
val defaultPath = Database.getDefaultPath(project)
val newSourceDescription = SourceDescription(project, defaultPath)
sourceDescription.emit(newSourceDescription)
}
}
In the UI, the User first selects a project, which is coming from the ProjectViewModel via LiveData. As soon as we have the project information, we set it in the SourcesViewModel using the setProject method from above.
Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// load the list of sources
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
sourcesViewModel.getSources().collectLatest { list ->
sourcesAdapter.submitData(list) // this is called only once in the beginning
}
}
projectsViewModel.projects.observe(viewLifecycleOwner, Observer { project ->
sourcesViewModel.setProject(project)
})
}
The overall output of Paging is a Flow<PagingData>, so typically mixing your signal (file path) into the flow via some flow-operation will work best. If you're able to model the path the user clicks on as a Flow<String>, something like this might work:
ViewModel.kt
class MyViewModel extends .. {
val pathFlow = MutableStateFlow<String>("/")
val pagingDataFlow = pathFlow.flatMapLatest { path ->
Pager(
remoteMediator = MyRemoteMediator(path)
...
).flow.cachedIn(..)
}
}
RemoteMediator.kt
class MyRemoteMediator extends RemoteMediator<..> {
override suspend fun load(..): .. {
// If path changed or simply on whenever loadType == REFRESH, clear db.
}
}
The other strategy if you have everything loaded is to pass the path directly into PagingSource, but it sounds like your data is coming from network so RemoteMediator approach is probably best here.

Gson unbable to invoke no-args construktor for observer ( LiveData) in my Android & Kotlin project

I already read a lot of stackoverflow articles and other blog posts about it trying out different solutions for the similar (but not same) problems they had there.
I will structure this post as follows:
My problem
My code (the part I think is relevant)
What I tried to fix it
1. My Problem
I'm getting the following error message:
Process: com.myapp, PID: 23553
java.lang.RuntimeException: Unable to invoke no-args constructor for androidx.arch.core.internal.SafeIterableMap$SupportRemove<androidx.lifecycle.Observer<? super java.lang.Integer>, androidx.lifecycle.LiveData$ObserverWrapper>. Registering an InstanceCreator with Gson for this type may fix this problem.
at com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:228)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:212)
at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:41)
at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:186)
at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:145)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:131)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:222)
at com.google.gson.Gson.fromJson(Gson.java:932)
at com.google.gson.Gson.fromJson(Gson.java:897)
at com.google.gson.Gson.fromJson(Gson.java:846)
at com.myapp.ui.customGame.CustomGameViewModel.initialize(CustomGameViewModel.kt:46)
Description of what I am trying to achieve:
I'm writing an app as a project for learning programming in Kotlin and making apps in general, I'm relatively new to making apps and Kotlin, bear that in mind if you see me making stupid mistakes, please ;).
In my app, I have an activity that contains a fragment that lets you choose the settings for a game of Volleyball (the CustomGameSetupFragment). The settings include simple things like the final score a team wins at, the names etc. After the settings are chosen and saved, the app creates an object of the Game class with the settings applied, saves them to a Room database. An entity in the table of my database basically contains an ID, some other information, and a JSON string of the game object (created via Google's Gson package). The activity then replaces the fragment with the fragment that lets you count the score of the game and see the names and stuff (the CustomGameFragment). The new fragment creates a ViewModel object which then again reads the games from the database, picks the last saved entities and then tries to recreate the game object from the JSON string saved.
This is done by executing:
val gameType = object: TypeToken<Game>() {}.type
this.game = Gson().fromJson<Game>(
gameRepo.ongoingGameData[0].gameJson,
gameType
//Game::class.java // this was an other method I tried. Also didnt work
)
Before, the Game class contained no LiveData/MutableLiveData but that resulted in the necessity to cast the attributes into LiveData/MutableLiveData in the ViewModel class and that resulted in a lot of bugs. But it worked!. I refactored the Game class so it mostly LiveData/MutableLiveData attributes (the ones I need to be LiveData), since in the CustomGameFragment and its ViewModel that would allow me to simply observe the attributes of the game directly. But after I refactored the class, Gson is not able to load it anymore.
I'm not sure it's simply because I use LiveData, and they somehow need the context or viewLifeCylceOwner that they get implicitly in the ViewModel or something.
2. My Code
a) The Room database (with Repository, Entity, Dao, Database)
Entity:
package com.myapp.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
#Entity(tableName = "gameData_table")
data class GameData(
#PrimaryKey(autoGenerate = true) val id: Int?,
#ColumnInfo(name = "gid") val gid: String?,
#ColumnInfo(name = "is_over") val isOver : Boolean?,
#ColumnInfo(name = "team1_name") val team1Name: String?,
#ColumnInfo(name = "team2_name") val team2Name: String?,
#ColumnInfo(name = "team1_sets") val team1Sets: Int?,
#ColumnInfo(name = "team2_sets") val team2Sets: Int?,
#ColumnInfo(name = "total_sets") val totalSets: Int?,
#ColumnInfo(name = "game_json") val gameJson : String?
)
The Dao:
package com.myapp.data
import androidx.room.*
#Dao
interface GameDao {
#Query("SELECT * FROM gameData_table")
suspend fun getAll(): List<GameData>
#Query("SELECT * FROM gameData_table WHERE id = (:id)")
fun loadAllByIds(id: Array<Int>): List<GameData>
#Query("SELECT * FROM gameData_table WHERE is_over = 0")
suspend fun getOngoing() : List<GameData>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(vararg game: GameData)
#Delete
suspend fun delete(game: GameData)
#Query("DELETE FROM gameData_table WHERE is_over = 0")
suspend fun deleteOngoing()
#Query("UPDATE gameData_table SET game_json = (:json) WHERE gid = (:gameId)")
suspend fun updateJSON(json: String, gameId : String)
}
The Database:
package com.myapp.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
#Database(entities = [GameData::class], version = 1, exportSchema = false)
abstract class GameDatabase : RoomDatabase() {
abstract fun gameDao() : GameDao
companion object {
//Singleton pattern to prevent multiple instances of the database
#Volatile
private var INSTANCE: GameDatabase? = null
fun getDatabase(context: Context) : GameDatabase {
return INSTANCE ?: synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
GameDatabase::class.java,
"game_database"
).build()
INSTANCE = instance
return instance
}
}
}
The Repository:
package com.myapp.data
import kotlinx.coroutines.runBlocking
class GameRepository (private val gameDao: GameDao){
val allGameData: List<GameData> = runBlocking { gameDao.getAll()}
val ongoingGameData : List<GameData> = runBlocking { gameDao.getOngoing() }
suspend fun insert(gameData : GameData){
gameDao.insertAll(gameData)
}
suspend fun deleteOngoing() {
gameDao.deleteOngoing()
}
suspend fun updateGame(gameData : GameData){
gameDao.updateJSON(gameData.gameJson!!, gameData.gid!!)
}
}
b) The Game class
And now a very short version of the game, since most of the methods are not really relevant for my problem I think:
package com.myapp.game
import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.myapp.data.GameData
import com.myapp.values.Values
import com.google.gson.Gson
class Game {
/*
No live data needed or possible?
*/
private var sets : MutableList<Set>
private val pointGap : Int = Values.DEFAULT_POINT_GAP
private val gid : String = this.toString()
/*
Live data needed or possible
*/
// private MutableLiveData
private var _team1Name : MutableLiveData<String> = MutableLiveData(Values.DEFAULT_TEAM1_NAME)
(more strings ...)
private var _setWinScore : MutableLiveData<Int> = MutableLiveData(Values.DEFAULT_WIN_SCORE)
(...)
// public LiveData
val team1Name : LiveData<String>
(more strings ...)
val setWinScore : LiveData<Int>
(...)
init{
team1Name = _team1Name
(more strings ...)
setWinScore = _setWinScore
(...)
}
constructor(gameSettings: GameSettings = GameSettings()){
this._team1Name.value = gameSettings.team1Name
(more strings...)
this._setWinScore.value = gameSettings.setWinScore
(...)
}
}
3. Approaches to fix it#
I tried to use a InstanceCreator. But after I read some stuff about it, I found that this is neccessary if the object you want to recreate has an argument of something the Gson class needs to know to put it in (context for example). I don't have that, I think (?).
I tried it anyway, which of course didn't work.
Also I tried several variations of using TypeToken which I also have shown at the beginning.
Another thing I read often, was to use the newest version of the package Gson, Room and LiveData or to use kapt instad of implement keywords in the grandle.build at project level.
I tried both -> same Exception
So, do you have any ideas?
Or am I doing something stupidly wrong?
Thanks in advance for sacrificing your time!
PS: I'm not an English native speaker, so sorry for bad grammar and spelling.
The following shows how to deserialize LiveData, however maybe in your use case it would be more appropriate to share the Game data as ViewModel? See Android Developers page.
When no custom or built-in type adapter matches, Gson uses a reflection-based one. The problem is that you are asking Gson to deserialize JSON as LiveData. If you look at the source code of LiveData you will see that is has multiple private fields and for the type of one of them Gson cannot create instances.
In general it is discouraged to use Gson's reflection-based serialization or deserialization for any third party classes (here LiveData) because you then rely on their internal implementation details which could change at any point.
This can be solved by creating a custom TypeAdapterFactory.
I am not familiar with Kotlin, but hopefully the following Java code is useful for you as example nonetheless:
class LiveDataTypeAdapterFactory implements TypeAdapterFactory {
public static final LiveDataTypeAdapterFactory INSTANCE = new LiveDataTypeAdapterFactory();
private LiveDataTypeAdapterFactory() { }
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<?> rawType = type.getRawType();
// Only handle LiveData and MutableLiveData
if (rawType != LiveData.class && rawType != MutableLiveData.class) {
return null;
}
// Assumes that LiveData is never used as raw type but is always parameterized
Type valueType = ((ParameterizedType) type.getType()).getActualTypeArguments()[0];
// Get the adapter for the LiveData value type `T`
// Cast TypeAdapter to simplify adapter code below
#SuppressWarnings("unchecked")
TypeAdapter<Object> valueAdapter = (TypeAdapter<Object>) gson.getAdapter(TypeToken.get(valueType));
// Is safe due to `type` check at start of method
#SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new TypeAdapter<LiveData<?>>() {
#Override
public void write(JsonWriter out, LiveData<?> liveData) throws IOException {
// Directly write out LiveData value
valueAdapter.write(out, liveData.getValue());
}
#Override
public LiveData<?> read(JsonReader in) throws IOException {
Object value = valueAdapter.read(in);
return new MutableLiveData<>(value);
}
};
return adapter;
}
}
(Note that this does not retain the observers of the LiveData)
You can then create the Gson instance using a GsonBuilder and register the factory:
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(LiveDataTypeAdapterFactory.INSTANCE)
.create();
There is no need to use TypeToken when deserializing Game, using the class directly will work as well. TypeToken is intended for generic types.
Ideally you would also create a TypeAdapterFactory for MutableList to not rely on its internal implementation.

Android MVVM multiple API Call

I'm on my way to make an MVVM example project without the complexity of injection dependency library and RX ( because I think it's better to understand how it works fundamentally for people without all this very efficient stuff ) but its harder to make :)
I'm in trouble, I use the CatApi here: https://thecatapi.com/
I'm trying to do a spinner that contains breeds name and also a picture of a cat to the left ( for each breed ) but in this API you can only get this result in two calls ( one for breeds, one for images for a breed ), I don't push the research on the API far because even if the API can solve my problem, I want to face the problem because it can happen later in my life :)
So there is my probleme i've made the following code :
BreedEntity :
package com.example.mvvm_kitty.data.local.entities
//Entity was used to be stored into a local DB so no use here
data class BreedEntity (
val adaptability: Int,
val affection_level: Int,
val description: String,
val id: String,
var name: String,
val life_span: String,
val origin: String,
var iconImage : BreedImageEntity?,
var images: List<BreedImageEntity>?
){
}
the call into the BreedActivity :
private fun subscribeToModel(breedsViewModel: BreedsViewModel) {
//Todo: Gerer les erreurs reseau
breedsViewModel.getBreeds().observe(this, Observer {
breedEntities ->
mBinding.catSelected = breedEntities[0]
breedSpinnerAdapter = BreedsSpinnerAdapter(this, breedEntities)
mBinding.breedSelector.adapter = breedSpinnerAdapter
breedEntities.forEach {breedEntity ->
breedsViewModel.getBreedImages(breedEntities.indexOf(breedEntity)).observe(this, Observer {
breedEntity.iconImage = it[0]
})
}
})
}
yeah I think made a foreach it's very dirty ( and also it doesn't work because don't run on the same time so when I set the images in the observer the "it" value is on the last item
there is my BreedsViewModel :
package com.example.mvvm_kitty.viewmodels
import android.app.Application
import android.util.Log
import android.view.animation.Transformation
import androidx.lifecycle.*
import com.example.mvvm_kitty.BasicApp
import com.example.mvvm_kitty.data.local.entities.BreedEntity
import com.example.mvvm_kitty.data.local.entities.BreedImageEntity
import com.example.mvvm_kitty.data.repositories.CatRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class BreedsViewModel(application: Application, private val catRepository: CatRepository) : AndroidViewModel(application) {
private val mObservableBreeds: LiveData<List<BreedEntity>> = catRepository.getBreeds()
/**
* Expose the product to allow the UI to observe it
*/
fun getBreeds(): LiveData<List<BreedEntity>> {
return mObservableBreeds
}
fun getBreedImages(index : Int): LiveData<List<BreedImageEntity>> {
val breed = mObservableBreeds.value?.get(index)
return catRepository.getBreedImages(breed!!.id)
}
/**
* Factory is used to inject dynamically all dependency to the viewModel like reposiroty, or id
* or whatever
*/
class Factory(private val mApplication: Application) :
ViewModelProvider.NewInstanceFactory() {
private val mRepository: CatRepository = (mApplication as BasicApp).getCatRepository()
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return BreedsViewModel(mApplication, mRepository) as T
}
}
}
and to finish the CatRepository method to get the images :
private fun getBreedImagesFromApi(id: String) : LiveData<List<BreedImageEntity>>{
mObservableBreedImages.addSource(catService.getAllImages(id, 10)){
mObservableBreedImages.postValue(it.resource?.map { breedDto ->
breedDto.toEntity()})
}
return mObservableBreedImages
}
My problem is the following how can I get my images for each item in a clean way ( because I think my code is good but the foreach observer part is very dirty )
If someone can help me it would be very nice :D, Thanks in advance for your time.
Seems like instead of fetching the data separately, you should be fetching them at the same time and combining the result into one response.
Generally speaking:
Remove getBreedImagesFromApi.
Update your getBreedsFromApi (I
assume that exists and you're using coroutines) to fetch both pieces
of data in the one call you already have for getting breeds. You can
use async and await() for this to fire off two requests to the two different endpoints, have them run concurrently, and wait for both requests to finish.
Remove the "foreach" because the images will now exist by the time you get the list of breeds.
Hope that helps!
I guess the view should receive a completed object data from your view-model,
which mean you must do all this logic in view model.so
hitting both APIs & observe them in view-model.
create one more Live Data responsible for passing data to your View(Activity/Fragment)
update the ViewLiveData from both APIs observers.
and to do this you will need Transformation or MediatorLiveData in your view-model.
also you can use Rxjava, check fetch every item on the list

Need help Kotlin Coroutines, Architecture Component and Retrofit

I'm trying to wrap my head around the mentioned components and I can't get it right. I want to do something very simple: Fetch data from the network and present it to the user. Currently am not yet caching it as am still learning new Coroutine features in Architecture components. Every time app loads I get an empty model posted, which seems weird.
My API is get hit fine and response is 200 which is OK.
Below is what I have attempted:
POJO
data class Profile(#SerializedName("fullname") val fullName : String.....)
Repository
class UserRepo(val context: Context, val api: Api) {
suspend fun getProfile(): Profile
{
val accessToken = ....
return api.getUserProfile(accessToken)
}
}
API
interface GatewayApi {
#GET("users/profile")
suspend fun getUserProfile(#Query("access-token") accessToken: String?): Profile
}
ViewModel
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val usersRepo = UserRepo(application.applicationContext, Apifactory.Api)
val userProfileData = liveData{
emit(usersRepo.getProfile())
}
fun getProfile() = viewModelScope.launch {
usersRepo.getProfile()
}
}
Finally my fragment's relevant code
val viewModel = ViewModelProviders.of(activity!!).get(UserViewModel::class.java)
viewModel.userProfileData.observe(this, Observer<UserProfile> {
//it is having nulls
})
//trigger change
viewModel.getProfile()
So I added HTTP requests and responses (thanks to #CommonsWare for pointing that out) and it happened I had used a different model than I was supposed to use. The correct model that mapped the JSON response was ProfileResponse and as you can see in my posted code, I used Profile instead. So all fields were empty as Gson could not correctly serialize JSON into Profile object.
All the credit goes to #CommonsWare for pointing that out in comment.

Categories

Resources