I want to retrieve data from Datasource. But turns out it didnt get executed at all. How can i solve it? here is my code
Repository.kt
// all logcat in repository is executed
override fun getDetailGame(id: Int): Flow<Resource<Game>> {
Log.d("Repo", "getDetailGame: called")
return flow {
Log.d("Repo", "getDetailGame: before flow")
remoteDataSource.getDetailGame(id)
Log.d("Repo", "getDetailGame: after flow")
}
}
Datasource.kt
suspend fun getDetailGame(id: Int): Flow<ApiResponse<GameResponse>> =
flow {
try {
// didnt get executed all
Log.d(TAG, "getDetailGame: called")
val response = apiService.getDetailGame(id)
if (response != null) {
emit(ApiResponse.Success(response))
} else {
emit(ApiResponse.Empty)
}
} catch (ex: Exception) {
emit(ApiResponse.Error(ex.message.toString()))
Log.e(TAG, "getDetailGame: ${ex.message} ")
}
}.flowOn(Dispatchers.IO)
Edit: add additional code for other file
ApiResponse.kt (response state management for the datasource)
sealed class ApiResponse<out R> {
data class Success<out T>(val data: T) : ApiResponse<T>()
data class Error(val errorMessage: String) : ApiResponse<Nothing>()
object Empty : ApiResponse<Nothing>()
}
Resource.kt (state management for UI like loading state etc)
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Success<T>(data: T) : Resource<T>(data)
class Loading<T>(data: T? = null) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
}
GameResponse.kt (same like Game but with serializedname for json)
data class GameResponse(
#field:SerializedName("id")
var id: Int,
#field:SerializedName("name")
var title: String,
#field:SerializedName("released")
var released: String? = null,
#field:SerializedName("metacritic")
var metacritic: Int? = null,
#field:SerializedName("metacritic_url")
var metacriticUrl: Int? = null,
#field:SerializedName("background_image")
var bgImage: String? = null,
#field:SerializedName("description")
var description: String? = null,
#field:SerializedName("game_series_count")
var gameSeriesCount: Int? = 0
)
Game.kt (same like GameResponse but the clean version of it)
data class Game(
var id: Int,
var title: String,
var released: String? = null,
var metacritic: Int? = null,
var metacriticUrl: Int? = null,
var bgImage: String? = null,
var description: String? = null,
var gameSeriesCount: Int? = 0
)
Flows are cold streams, this means they wont be executed until you collect them.
If you are trying to convert Flow<ApiResponse> to Flow<Resource>, you should use the map function. If you need a more complex transformation, use transform instead.
override fun getDetailGame(id: Int): Flow<Resource<GameResponse>> {
return remoteDataSource.getDetailGame(id).map { response ->
when (response) {
ApiResponse.Empty -> Resource.Loading()
is ApiResponse.Success -> Resource.Success(response.data)
is ApiResponse.Error -> Resource.Error(response.errorMessage)
}
}
}
Or if you need to emit a value before perform the transformation:
override fun getDetailGame(id: Int): Flow<Resource<GameResponse>> = flow {
emit(Resource.Loading())
emitAll(
remoteDataSource.getDetailGame(id).map { response ->
when (response) {
is ApiResponse.Success -> Resource.Success(response.data)
is ApiResponse.Error -> Resource.Error(response.errorMessage)
ApiResponse.Empty -> Resource.Error("")
}
}
)
}
Related
With retrofit I get response LevelsEntity but if I get error it get me ResponseError, NOTE: I cant merge LevelsEntity and ResponseError together in one entity.
LevelsEntity:
class LevelsEntity : ArrayList<LevelsEntityItem>()
LevelsEntityItem:
data class LevelsEntityItem(
#SerializedName("category")
val category: Int? = null,
#SerializedName("completed")
val completed: Boolean? = null,
#SerializedName("completionhascriteria")
val completionhascriteria: Boolean? = null
)
ResponseError:
data class ResponseError(
#SerializedName("errorcode")
val errorcode: String? = null,
#SerializedName("exception")
val exception: String? = null,
#SerializedName("message")
val message: String? = null
)
And I create bellow class for get multiple data like bellow:
class BaseLevelsEntity<LevelsEntity, ResponseError> {
var levelsEntity: LevelsEntity? = null
var responseError: ResponseError? = null
val isSuccess: Boolean
get() = responseError == null
}
And in my #POST of retrofit is:
#POST("/webservice/rest/server.php")
suspend fun getPopularLevelsInLessonsF(
#Query("mdwsrestformat") mdwsrestformat: String?,
#Field("wsfunction") wsfunction: String?,
#Field("wstoken") wstoken: String?,
#Field("userid") userid: Int?
): Call<BaseLevelsEntity<LevelsEntity, ResponseError>>
But I cant get any result in my impl:
class LessonsRepositoryImpl(
private val lessonsRemoteDatasource: LessonsRemoteDatasource
) : LessonsRepository {
override suspend fun getLevelsInLessonsF(
wstoken: String,
userid: Int
): Resource<BaseLevelsEntity<LevelsEntity, ResponseError>> {
return responseToResource(lessonsRemoteDatasource.getLevelsValueInLessonsF(wstoken, userid).execute())
}
private fun responseToResource(response: Response<BaseLevelsEntity<LevelsEntity, ResponseError>>): Resource<BaseLevelsEntity<LevelsEntity, ResponseError>> {
if (response.isSuccessful) {
if (response.body() != null) {
response.body()?.let { result ->
if (!result.levelsEntity.isNullOrEmpty()) {
if (result.levelsEntity!!.size > 0) {
return Resource.Success(result)
}
} else if (result.responseError != null) {
return Resource.Error(result.responseError?.errorcode ?: "unknown")
}
}
} else {
return Resource.Error("unknown_info")
}
}
return Resource.Error(response.message())
}
}
Normally response should be in common format.
If cannot do this from backend then you can receive response as JsonObject and then check the key manually in repository to decide if it is success or error response. Based on that you can then convert the response to object with gson.
Failed to open APK '/data/app/~~Binr6SVO9dmLCUcPgCpAyA==/com.example.newspaper-yFV7fjvEt5i0gI4--yMcTA==/base.apk': I/O error
Failed to open APK '/data/app/~~Binr6SVO9dmLCUcPgCpAyA==/com.example.newspaper-yFV7fjvEt5i0gI4--yMcTA==/base.apk': I/O error
failed to add asset path '/data/app/~~Binr6SVO9dmLCUcPgCpAyA==/com.example.newspaper-yFV7fjvEt5i0gI4--yMcTA==/base.apk'
java.io.IOException: Failed to load asset path /data/app/~~Binr6SVO9dmLCUcPgCpAyA==/com.example.newspaper-yFV7fjvEt5i0gI4--yMcTA==/base.apk
at android.content.res.ApkAssets.nativeLoad(Native Method)
at android.content.res.ApkAssets.(ApkAssets.java:295)
at android.content.res.ApkAssets.loadFromPath(ApkAssets.java:144)
at android.app.ResourcesManager.loadApkAssets(ResourcesManager.java:454)
at android.app.ResourcesManager.access$000(ResourcesManager.java:72)
at android.app.ResourcesManager$ApkAssetsSupplier.load(ResourcesManager.java:168)
at android.app.ResourcesManager.createAssetManager(ResourcesManager.java:530)
at android.app.ResourcesManager.createResourcesImpl(ResourcesManager.java:612)
at android.app.ResourcesManager.findOrCreateResourcesImplForKeyLocked(ResourcesManager.java:664)
at android.app.ResourcesManager.createResources(ResourcesManager.java:1011)
at android.app.ResourcesManager.getResources(ResourcesManager.java:1114)
at android.app.ActivityThread.getTopLevelResources(ActivityThread.java:2414)
at android.app.ApplicationPackageManager.getResourcesForApplication(ApplicationPackageManager.java:1751)
at android.app.ApplicationPackageManager.getResourcesForApplication(ApplicationPackageManager.java:1737)
at android.app.ApplicationPackageManager.getDrawable(ApplicationPackageManager.java:1506)
at android.app.ApplicationPackageManager.loadUnbadgedItemIcon(ApplicationPackageManager.java:3029)
at android.content.pm.PackageItemInfo.loadUnbadgedIcon(PackageItemInfo.java:290)
at com.android.systemui.toast.SystemUIToast.getBadgedIcon(SystemUIToast.java:284)
at com.android.systemui.toast.SystemUIToast.inflateToastView(SystemUIToast.java:198)
at com.android.systemui.toast.SystemUIToast.(SystemUIToast.java:90)
at com.android.systemui.toast.SystemUIToast.(SystemUIToast.java:77)
at com.android.systemui.toast.ToastFactory.createToast(ToastFactory.java:78)
at com.android.systemui.toast.ToastUI.lambda$showToast$0(ToastUI.java:113)
at com.android.systemui.toast.ToastUI.$r8$lambda$w_gPCh3F8Xxn1jN4lkQZoUci71c(Unknown Source:0)
at com.android.systemui.toast.ToastUI$$ExternalSyntheticLambda0.run(Unknown Source:16)
at com.android.systemui.toast.ToastUI.showToast(ToastUI.java:140)
at com.android.systemui.statusbar.CommandQueue$H.handleMessage(CommandQueue.java:1441)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7842)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
I'm using the News API for my news app. These are the steps until the crash :
Showing a list of news
Click on one of them to show its details into the ArticleFragment
When I click on the floatingactionbutton to save it in the SavedNewsFragment, it crashes with the mentioned error.
The related files :
NewsViewModel
class NewsViewModel(val repo : NewsRepository) : ViewModel()
{
val breakingNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
val breakingNewsPage = 1 // update later
val searchNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
val searchNewsPage = 1 // update later
var savedNews: MutableLiveData<List<Article>> = MutableLiveData()
init {
getBreakingNews()
}
/**For BreakingNewsFragment**/
private fun getBreakingNews(countryString : String = "eg") = viewModelScope.launch {
breakingNews.postValue(Resource.Loading())
val response = repo.getBreakingNews(countryString, breakingNewsPage)
breakingNews.postValue(handleBreakingNewsResponse(response))
}
private fun handleBreakingNewsResponse(response: Response<NewsResponse>) : Resource<NewsResponse>
{
if(response.isSuccessful)
response.body().let {
return Resource.Success(it)
}
else
return Resource.Error(response.message(), response.body())
}
/**For SearchNewsFragment**/
fun searchForNews(text : String) = viewModelScope.launch {
searchNews.postValue(Resource.Loading())
val response = repo.getSearchedNews(text, searchNewsPage)
searchNews.postValue(handleSearchForNewsResponse(response))
}
private fun handleSearchForNewsResponse(response: Response<NewsResponse>) : Resource<NewsResponse>
{
if(response.isSuccessful)
response.body().let {
return Resource.Success(it)
}
else
return Resource.Error(response.message(), response.body())
}
/**Deal with Room**/
fun saveThisArticle(article: Article) = viewModelScope.launch {
repo.addIfNotExist(article)
/*Log.d("NewsViewModel "," => Start saving")
val num = repo.addIfNotExist(article).wait()
Log.d("NewsViewModel => saveThisArticle(article: Article) ", "--------- $num")*/
}
fun getMySavedArticles() = savedNews.postValue(repo.getSavedNews().value)
fun deleteThisArticle(article: Article) = viewModelScope.launch {
repo.deleteArticle(article)
}
class NewsVMFactory(val repo: NewsRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(NewsViewModel::class.java)){
#Suppress("UNCHECKED_CAST")
return NewsViewModel(repo) as T
}
throw IllegalArgumentException("Unknown viewModel class")
}
}
}
NewsRepository
class NewsRepository(val db: ArticleDatabase)
{
/**Remote**/
suspend fun getBreakingNews(country_of_2Letters : String, pageNum : Int) =
RetrofitInstance.newsAPI.getNewsHeadlines()
suspend fun getSearchedNews(text : String, pageNum : Int) =
RetrofitInstance.newsAPI.searchFor(text, pageNum)
/**Local**/
suspend fun addIfNotExist(article : Article) =
withContext(Dispatchers.IO) {
db.newsDatabaseDao.upsert(article)
}
fun getSavedNews() = db.newsDatabaseDao.getAllArticles()
suspend fun deleteArticle(article : Article) = db.newsDatabaseDao.deleteArticle(article)
}
interface NewsDao
#Dao
interface NewsDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(article : Article): Long // TODO ???? Int
#Query("SELECT * FROM articles")
fun getAllArticles(): LiveData<List<Article>> // TODO There will be two tables. one for saved articles and other for cashing.
#Delete
suspend fun deleteArticle(article: Article)
}
ArticleDatabase
#Database(entities = [Article::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class ArticleDatabase : RoomDatabase() {
abstract val newsDatabaseDao: NewsDao
companion object {
#Volatile
var INSTANCE: ArticleDatabase? = null
fun getInstance(context: Context): ArticleDatabase {
synchronized(this) {
return INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
ArticleDatabase::class.java,
"article_database"
)
.fallbackToDestructiveMigration()
.build()
}
}
}
}
The model/data file
data class NewsResponse(
val articles: List<Article>,
val status: String, // ok
val totalResults: Int // 383
)
#Entity(tableName = "articles")
data class Article(
// #PrimaryKey(autoGenerate = true)
// val id : Int? = null, // tried: var, Int, Int = 0, Int?
#ColumnInfo(name = "author")
val author: String? = "",
#ColumnInfo(name = "content")
val content: String? = "",
#ColumnInfo(name = "description")
val description: String? = "",
#ColumnInfo(name = "published_at")
val publishedAt: String,
#ColumnInfo(name = "source")
val source: Source,
#ColumnInfo(name = "title")
val title: String,
#ColumnInfo(name = "url")
val url: String,
#ColumnInfo(name = "url_to_image")
val urlToImage: String?
) : Serializable
{
override fun hashCode(): Int {
var result = id.hashCode()
if(url.isNullOrEmpty()){
result = 31 * result + url.hashCode()
}
return result
}
}
data class Source(
val id: String, // business-insider
val name: String // The Guardian
)
Converters class
class Converters {
// data class Source(
// val id: String,
// val name: String
// )
#TypeConverter
fun fromSource(source: Source): String =
Gson().toJson(source) // set in the database as a string
#TypeConverter
fun toSource(stringSource: String) : Source =
Gson().fromJson(stringSource, Source::class.java) // get it from the database and compose it into its original datatype
}
ArticleFragment
class ArticleFragment : BaseFragment(R.layout.fragment_article) {
lateinit var binding : FragmentArticleBinding
val args : ArticleFragmentArgs by navArgs()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentArticleBinding.inflate(inflater)
val data = args.anArticle
binding.webView.apply {
try{
webViewClient = WebViewClient()
loadUrl(data.url)
}
catch (e: Exception){
loadUrl("https://www.google.co.in/")
Toast.makeText(context, "There is a problem in the url", Toast.LENGTH_LONG).show()
}
}
binding.fab.setOnClickListener {
viewModel.saveThisArticle(data)
Toast.makeText(context, "Saved successfully ✅", Toast.LENGTH_SHORT).show()
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
binding.unbind()
}
}
I'm calling data from Breaking bad API https://www.breakingbadapi.com/api/character/random
I'm unable to get data. I think it's because the main Response file has square brackets that I need to call first. But I don't know how to call it. Can I get some help?
Here's my API interface
interface APIRequest {
#GET("character/random")
suspend fun getInfo() : Response<List<ResponseBB>>
}
ResponseBB Class
data class ResponseBB(
#field:SerializedName("ResponseBB")
val responseBB: List<ResponseBBItem?>? = null
)
data class ResponseBBItem(
#field:SerializedName("birthday")
val birthday: Any? = null,
#field:SerializedName("img")
val img: String? = null,
#field:SerializedName("better_call_saul_appearance")
val betterCallSaulAppearance: Any? = null,
#field:SerializedName("occupation")
val occupation: List<String?>? = null,
#field:SerializedName("appearance")
val appearance: List<Int?>? = null,
#field:SerializedName("portrayed")
val portrayed: String? = null,
#field:SerializedName("name")
val name: String? = null,
#field:SerializedName("nickname")
val nickname: String? = null,
#field:SerializedName("char_id")
val charId: Int? = null,
#field:SerializedName("category")
val category: String? = null,
#field:SerializedName("status")
val status: String? = null
)
Client object
object Client {
val gson = GsonBuilder().create()
val retrofit = Retrofit.Builder()
.baseUrl("https://www.breakingbadapi.com/api/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
val api = retrofit.create(APIRequest::class.java)
}
Here's my function to call result in the main activity
class MainActivity : AppCompatActivity() {
private var TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getCharacterInfo()
linearLayout.setOnClickListener {
getCharacterInfo()
}
}
private fun getCharacterInfo() {
GlobalScope.launch(Dispatchers.IO) {
try {
val response = Client.api.getInfo()
if (response.isSuccessful) {
val data = response.body()
Log.d(TAG, data.toString())
withContext(Dispatchers.Main) {
Picasso.get().load(data!!.img).into(ivImage)
tvName.text = data.name
tvOccupation.text = data.toString()
tvActor.text = data.toString()
tvAppearance.text = data.appearance.toString()
tvStatus.text = data.status
}
}
}
catch (e:Exception){
withContext(Dispatchers.Main){
Toast.makeText(applicationContext, "Cannot Load Data" , Toast.LENGTH_LONG).show()
}
}
}
}
}
I see that you try to use coroutines in retrofit, I recommend that you do not work with Response, change it to call and remove the suspend.
interface APIRequest {
#GET("character/random")
fun getInfo() : Call<List<ResponseBB>>
}
In your Global Scope you can call it this way:
GlobalScope.launch {
try{
val response = Client.api.getInfo().await()
}catch(e:Exception){}
}
you can use the version 2.9.0 in retrofit and gson Converter
So I'm trying to use themoviedb for extracting search results for movies. The url is as follows:
https://api.themoviedb.org/3/search/movie?api_key={apikey}&language=en-US&query={query}
Where in the query I insert the keyword that I want to search. I'm using retrofit library to do this.
This is my code for my ApiService:
interface ApiService {
#GET("3/search/movie?api_key=${BuildConfig.MOVIE_TOKEN}&language=en-US&")
fun getMovies(
#Query("query") query: String
): Call<SearchMovieResponse>
}
This is my code for the ApiConfig object:
class ApiConfig {
companion object {
fun getApiService(): ApiService{
val loggingInterceptor =
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.themoviedb.org/")
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
return retrofit.create(ApiService::class.java)
}
}
}
I also have a RemoteDataSouce class which uses that config to get the movies. I have also generated the data class to using POJO. This is the method in the RemoteDataSource class that uses that the API config.
fun getMovies():List<MoviesItem>?{
val client = ApiConfig.getApiService().getMovies("john")
var listMovies: ArrayList<MoviesItem> = ArrayList<MoviesItem>()
client.enqueue(object: Callback<SearchMovieResponse> {
override fun onResponse(call: Call<SearchMovieResponse>, response: Response<SearchMovieResponse>) {
if (response.isSuccessful){
val rawList = response.body()?.results!!
for (item in rawList){
listMovies.add(item)
}
}
}
override fun onFailure(call: Call<SearchMovieResponse>, t: Throwable) {
return
}
})
return listMovies
}
The json response of the API is this:
The data model that I use for SearchMovieResponse is this:
data class SearchShowResponse(
#field:SerializedName("page")
val page: Int? = null,
#field:SerializedName("total_pages")
val totalPages: Int? = null,
#field:SerializedName("results")
val results: List<ShowsItem?>? = null,
#field:SerializedName("total_results")
val totalResults: Int? = null
)
data class ShowsItem(
#field:SerializedName("first_air_date")
val firstAirDate: String? = null,
#field:SerializedName("overview")
val overview: String? = null,
#field:SerializedName("original_language")
val originalLanguage: String? = null,
#field:SerializedName("genre_ids")
val genreIds: List<Int?>? = null,
#field:SerializedName("poster_path")
val posterPath: String? = null,
#field:SerializedName("origin_country")
val originCountry: List<String?>? = null,
#field:SerializedName("backdrop_path")
val backdropPath: String? = null,
#field:SerializedName("original_name")
val originalName: String? = null,
#field:SerializedName("popularity")
val popularity: Double? = null,
#field:SerializedName("vote_average")
val voteAverage: Double? = null,
#field:SerializedName("name")
val name: String? = null,
#field:SerializedName("id")
val id: Int? = null,
#field:SerializedName("vote_count")
val voteCount: Int? = null
)
However, the listMovies is returning null. I'm not sure what I did wrong here. Can anyone explain? Thanks
Your method getMovies() is returning the list before the Retrofit call is done, you are using enqueue() method that run it asynchronous so your method finish before the onResponse() method is called.
Solution, rewrite your code thinking about this information or use execute()method instead enqueue(), this will execute the call in the main thread so you will have to call it in a new thread or a coroutine.
As, you are using enqueue() that run asynchronous so your function finish before the onResponse() method is called. So you have to return the list after on the complete of the process.
fun getMovies():List<MoviesItem>?{
val client = ApiConfig.getApiService().getMovies("john")
var listMovies: ArrayList<MoviesItem> = ArrayList<MoviesItem>()
client.enqueue(object: Callback<SearchMovieResponse> {
override fun onResponse(call: Call<SearchMovieResponse>, response: Response<SearchMovieResponse>) {
if (response.isSuccessful){
val rawList = response.body()?.results!!
for (item in rawList){
listMovies.add(item)
}
return listMovies
}
}
override fun onFailure(call: Call<SearchMovieResponse>, t: Throwable) {
return
}
})
}
Try to use callback to return your list:
fun getMovies(callback: (List<MoviesItem>) -> Unit) {
val client = ApiConfig.getApiService().getMovies("john")
client.enqueue(object : Callback<SearchMovieResponse> {
override fun onResponse(
call: Call<SearchMovieResponse>,
response: Response<SearchMovieResponse>
) {
var listMovies: ArrayList<MoviesItem> = ArrayList<MoviesItem>()
if (response.isSuccessful) {
val rawList = response.body()?.results!!
for (item in rawList) {
listMovies.add(item)
}
}
callback(listMovies)
}
override fun onFailure(call: Call<SearchMovieResponse>, t: Throwable) {
callback(emptyList()) // or throw error or use Result structure
}
})
}
I have one entity "drinks" which have [id;name;thumb] and I`m using these entities for 2 response calls. One response returns me a NonAlcohol list of drinks, another AlcoholList, I'm using Room for caching the data. But when I run the app, I saw that my lists merged, after some thought, I found a solution to this problem, I added one Boolean field to my entity "alcoholStatus".But I can't understand how to set the data into this variable correctly using this AccessDataStrategy. I'm new to Android, and this is my learning project. Please give me the right way how to solve this problem.
https://github.com/YaroslavSulyma/LetsDrink/tree/master/app/src/main/java/com/example/letsdrink
Thanks a lot!
Entity
#Entity(tableName = "drinks")
data class DrinksModel(
#SerializedName("strDrink")
val strDrink: String,
#SerializedName("strDrinkThumb")
val strDrinkThumb: String?,
#SerializedName("idDrink")
#PrimaryKey
val idDrink: Int,
var alcohol: Boolean
)
DataAccessStrategyCode
fun <T, A> performGetOperation(
databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit
): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val source = databaseQuery.invoke().map { Resource.success(it) }
emitSource(source)
val responseStatus = networkCall.invoke()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Resource.error(responseStatus.message!!))
emitSource(source)
}
}
Resource
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(message: String, data: T? = null): Resource<T> {
return Resource(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
Repository
class CocktailsRepository #Inject constructor(
private val remoteDataSource: CocktailsRemoteDataSource,
private val localDataSource: CocktailsDao
) {
fun getAlcoholicCocktails() = performGetOperation(
databaseQuery = { localDataSource.getAlcoholicCocktails() },
networkCall = { remoteDataSource.getAllAlcoholicCocktails()},
saveCallResult = { localDataSource.insertAllDrinks(it.drinks) }
)
fun getNonAlcoholicCocktails() = performGetOperation(
databaseQuery = { localDataSource.getNonAlcoholicCocktails() },
networkCall = { remoteDataSource.getAllNonAlcoholicCocktails() },
saveCallResult = { localDataSource.insertAllDrinks(it.drinks) }
)
}
DAO
#Dao
interface CocktailsDao {
#Query("SELECT * FROM drinks WHERE alcohol = 'true'")
fun getAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Query("SELECT * FROM drinks WHERE alcohol = 'false'")
fun getNonAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllDrinks(drinks: List<DrinksModel>)
}
RemoteDataSource
class CocktailsRemoteDataSource #Inject constructor(private val iCocktailApisService: ICocktailApisService) :
BaseDataSource() {
suspend fun getAllAlcoholicCocktails() =
getResult { iCocktailApisService.allAlcoholicAndNonAlcoholicCocktails("Alcoholic") }
suspend fun getAllNonAlcoholicCocktails() =
getResult { iCocktailApisService.allAlcoholicAndNonAlcoholicCocktails("Non_Alcoholic") }
}
First: I strongly recommend that you define separate data classes for your remote and local model classes and do the mapping between them when needed, for example:
Remote data model:
data class DrinkRemoteModel(
#SerializedName("idDrink")
val idDrink: Int,
#SerializedName("strDrink")
val strDrink: String,
#SerializedName("strDrinkThumb")
val strDrinkThumb: String?,
#SerializedName("alcohol")
var alcohol: Boolean
)
Local data model:
#Entity(tableName = "drinks")
data class DrinkLocalModel(
#PrimaryKey
#ColumnInfo(name = "idDrink")
val idDrink: Int,
#ColumnInfo(name = "strDrink")
val strDrink: String,
#ColumnInfo(name = "strDrinkThumb")
val strDrinkThumb: String?,
#ColumnInfo(name = "alcohol")
var alcohol: Boolean
)
Back to your implementation: I think what causing the problem is that Room maps Boolean fields in your entity to an integer column, 1 for true, and 0 for false, so try changing your querys in your DAO like following:
#Dao
interface CocktailsDao {
#Query("SELECT * FROM drinks WHERE alcohol = 1")
fun getAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Query("SELECT * FROM drinks WHERE alcohol = 0")
fun getNonAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllDrinks(drinks: List<DrinksModel>)
}
Alternatively: you can substitute your getAlcoholicCocktails and getNonAlcoholicCocktails with one DAO function, like this:
#Query("SELECT * FROM drinks WHERE alcohol = :isAlcoholic")
fun getCocktails(isAlcoholic : Boolean = true): LiveData<List<DrinksModel>>