Android MVVM multiple API Call - android

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

Related

What is the correct usage of Flow in Room?

I am using Room and I have written the Dao class as follows.
Dao
#Dao
interface ProjectDao {
#Query("SELECT * FROM project")
fun getAllProjects(): Flow<List<Project>>
...etc
}
and this Flow is converted to LiveData through asLiveData() in ViewModel and used as follows.
ViewModel
#HiltViewModel
class MainViewModel #Inject constructor(
private val projectRepo: ProjectRepository
) : ViewModel() {
val allProjects = projectRepo.allProjects.asLiveData()
...
}
Activity
mainViewModel.allProjects.observe(this) { projects ->
adapter.submitList(projects)
...
}
When data change occurs, RecyclerView is automatically updated by the Observer. This is a normal example I know.
However, in my project data in Flow, what is the most correct way to get the data of the position selected from the list?
I have already written code that returns a value from data that has been converted to LiveData, but I think there may be better code than this solution.
private fun getProject(position: Int): Project {
return mainViewModel.allProjects.value[position]
}
Please give me suggestion
Room has in built support of flow.
#Dao
interface ProjectDao {
#Query("SELECT * FROM project")
fun getAllProjects(): Flow<List<Project>>
//lets say you are saving the project from any place one by one.
#Insert()
fun saveProject(project :Project)
}
if you call saveProject(project) from any place, your ui will be updated automatically. you don't have to make any unnecessary call to update your ui. the moment there is any change in project list, flow will update the ui with new dataset.
to get the data of particular position, you can get it from adapter list. no need to make a room call.

kotlin - which/how a collection able to store multiple data type?

I have two different data class, eg:
Guitar & Piano.
I wanna create a list to store both data class, eg: instruments,
so that I can add both dataclass into list by:
instruments.add(Guitar())
instruments.add(Piano())
I thinking about using:
val instruments = arrayListOf<Any>()
My question is it any better way to achieve this?
Kotlin does not support multi-typing. However, you can apply workarounds.
First, to help modeling your problem, you could create a super type (interface or abstract class) as suggested in comments, to extract common properties, or just have a "marker" interface. It allows to narrow accepted objects to a certain category, and improve control.
Anyhow, you can filter any list to get back only values of wanted type using filterIsInstance :
enum class InstrumentFamily {
Strings, Keyboards, Winds, Percussions
}
abstract class Instrument(val family : InstrumentFamily)
data class Guitar(val stringCount : Int) : Instrument(InstrumentFamily.Strings)
data class Piano(val year: Int) : Instrument(InstrumentFamily.Keyboards)
fun main() {
val mix = listOf(Guitar(6), Piano(1960), null, Guitar(7), Piano(2010))
val guitars: List<Guitar> = mix.filterIsInstance<Guitar>()
guitars.forEach { println(it) }
val pianos : List<Piano> = mix.filterIsInstance<Piano>()
pianos.forEach { println(it) }
}
However, beware that this operator will scan all list, so it can become slow if used with large lists or many times. So, don't rely on it too much.
Another workaround would be to create an index per type, and use sealed classes to ensure full control over possible types (but therefore, you'll lose extensibility capabilities).
Exemple :
import kotlin.reflect.KClass
enum class InstrumentFamily {
Strings, Keyboards, Winds, Percussions
}
sealed class Instrument(val family : InstrumentFamily)
data class Guitar(val stringCount : Int) : Instrument(InstrumentFamily.Strings)
data class Piano(val year: Int) : Instrument(InstrumentFamily.Keyboards)
/** Custom mapping by value type */
class InstrumentContainer(private val valuesByType : MutableMap<KClass<out Instrument>, List<Instrument>> = mutableMapOf()) : Map<KClass<out Instrument>, List<Instrument>> by valuesByType {
/** When receiving an instrument, store it in a sublist specialized for its type */
fun add(instrument: Instrument) {
valuesByType.merge(instrument::class, listOf(instrument)) { l1, l2 -> l1 + l2}
}
/** Retrieve all objects stored for a given subtype */
inline fun <reified I :Instrument> get() = get(I::class) as List<out I>
}
fun main() {
val mix = listOf(Guitar(6), Piano(1960), null, Guitar(7), Piano(2010))
val container = InstrumentContainer()
mix.forEach { if (it != null) container.add(it) }
container.get<Guitar>().forEach { println(it) }
}

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.

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

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!

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