How to create calculated properties in data class with GSON implemented? - android

Background: GSON, Kotlin, Retrofit
I am writing a restaurant app. In the home page, the user is able to load a list of restaurant brands. Each brand can have up to 3 cuisine types, the first one is non-null and the next two are nullable. Each cuisine type is within the CuisineType enum class.
What I would like to do is to create a joined string like this:
cuisineType1.title + cuisineType2?.title + cuisineType3?.title = combinedCuisines. This can make all cuisines shown within a textView in Chinese. In order to do this I created a helper class. In this helper class, if the CuisineType from Brand cannot map any of the enum members, it will display the raw name from the Brand JSON(incase of server error). I tried the three solutions commented out below and non of them work. Much help would be appreciated. Thanks in advance!
data class Brand(
#SerializedName("id")
val id: Int,
#SerializedName("name_en")
val nameEN: String?,
#SerializedName("cuisine_1")
val cuisineType1: String,
#SerializedName("cuisine_2")
val cuisineType2: String?,
#SerializedName("cuisine_3")
val cuisineType3: String?,
/*Solution 1(not working):
val combinedCuisines = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
***java.lang.IllegalArgumentException: Unable to create converter for class
*/
/*Solution 2(not working):
#Transient
val combinedCuisines = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
***combinedCuisines = null after network call in fragment
*/
) {
/* Solution 3(not working):
val combinedCuisines: String
get() = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
***problem with GSON, I can only map the #SerializedName from the Cuisine enum class and will only run the illegal argument solution from the CombineCuisineHelper. For example, get hong_kong_style from the JSON brand but it will not convert to HongKongStyle and map to its title.
*/
}
//It should be a long list but I shortened it.
enum class CuisineType {
#SerializedName("chinese")
Chinese,
#SerializedName("hong_kong_style")
HongKongStyle,
#SerializedName("cantonese")
Cantonese,
val title: Double
get() {
return when (this) {
Chinese -> "中菜"
HongKongStyle -> "港式"
Cantonese -> "粵式"
}
class CombineCuisineHelper {
companion object {
fun combineCuisines(cuisineSubtype1: String, cuisineSubtype2: String?, cuisineSubtype3: String?): String {
val combinedSubtypes = ArrayList<String?>()
combinedSubtypes += try {
CuisineSubtype.valueOf(cuisineSubtype1).title
} catch (e: IllegalArgumentException) {
cuisineSubtype1
}
if (cuisineSubtype2 != null) {
combinedSubtypes += try {
CuisineSubtype.valueOf(cuisineSubtype2).title
} catch (e: IllegalArgumentException) {
cuisineSubtype2
}
}
if (cuisineSubtype3 != null) {
combinedSubtypes += try {
CuisineSubtype.valueOf(cuisineSubtype3).title
} catch (e: IllegalArgumentException) {
cuisineSubtype3
}
}
}

The first and second solutions are not good, because the data might not be ready at the initialization time. The third solution is the one we can continue on:
val combinedCuisines: String
get() = CombineCuisineHelper.combineCuisines(cuisineType1, cuisineType2, cuisineType3)
The SerializedNames are useless for enum constants and won't work for you as you are expecting. So the valueOf method for enum won't find a value for literals like "hong_kong_style" and will throw an exception.
You can create your own helper method in your enum class like this:
enum class CuisineType {
Chinese,
HongKongStyle,
Cantonese;
val title: String
get() {
return when (this) {
Chinese -> "中菜"
HongKongStyle -> "港式"
Cantonese -> "粵式"
}
}
companion object {
//Note this helper method which manually maps string values to enum constants:
fun enumValue(title: String): CuisineType = when (title) {
"chinese" -> Chinese
"hong_kong_style" -> HongKongStyle
"cantonese" -> Cantonese
else -> throw IllegalArgumentException("Unknown cuisine type $title")
}
}
}
And then use this new method instead of the enum's own valueOf method:
val combinedSubtypes = ArrayList<String?>()
combinedSubtypes += try {
CuisineType.enumValue(cuisineSubtype1).title
} catch (e: IllegalArgumentException) {
cuisineSubtype1
}
//continued...

Related

I keep getting the error "E/Network: searchBooks: Failed Getting books" but I am not sure why

So I am using the Google's API and for some reason, I'm getting a generic error:
E/Network: searchBooks: Failed Getting books
When it initially loads up, the hard coded query "android" shows up with a list of books associated with the book topic. But when I search up a different topic like "shoes" for example, the error shows up. Even when you hard code a different topic other than "android", it still shows the error. I have checked the API and it is working properly with the different query searches.
Here's the Retrofit Interface:
#Singleton
interface BooksApi {
#GET(BOOK_EP)
suspend fun getAllBooks(
//don't initialize the query, so that the whole api is available to the user
#Query("q") query: String
): Book
#GET("$BOOK_EP/{bookId}")
suspend fun getBookInfo(
#Path("bookId") bookId: String
): Item
}
The Repo
class BookRepository #Inject constructor(private val api: BooksApi) {
suspend fun getBooks(searchQuery: String): Resource<List<Item>> {
return try {
Resource.Loading(data = true)
val itemList = api.getAllBooks(searchQuery).items
if(itemList.isNotEmpty()) Resource.Loading(data = false)
Resource.Success(data = itemList)
}catch (exception: Exception){
Resource.Error(message = exception.message.toString())
}
}
suspend fun getBookInfo(bookId: String): Resource<Item>{
val response = try {
Resource.Loading(data = true)
api.getBookInfo(bookId)
}catch (exception: Exception){
return Resource.Error(message = "An error occurred ${exception.message.toString()}")
}
Resource.Loading(data = false)
return Resource.Success(data = response)
}
The ViewModel:
class SearchViewModel #Inject constructor(private val repository: BookRepository): ViewModel(){
var list: List<Item> by mutableStateOf(listOf())
var isLoading: Boolean by mutableStateOf(true)
init {
loadBooks()
}
private fun loadBooks() {
searchBooks("android")
}
fun searchBooks(query: String) {
viewModelScope.launch(Dispatchers.Default) {
if (query.isEmpty()){
return#launch
}
try {
when(val response = repository.getBooks(query)){
is Resource.Success -> {
list = response.data!!
if (list.isNotEmpty()) isLoading = false
}
is Resource.Error -> {
isLoading = false
Log.e("Network", "searchBooks: Failed Getting books", )
}
else -> {isLoading = false}
}
}catch (exception: Exception){
isLoading = false
Log.d("Network", "searchBooks: ${exception.message.toString()}")
}
}
}
}
I'll leave the project public so you guys can check it out for more of an understanding
Github Link: https://github.com/OEThe11/ReadersApp
P.S. you would have to create a login (takes 30 sec), but once you do, you'll have access to the app immediately.
This issue is occurring because of JsonSyntaxException java.lang.NumberFormatException while the JSON response is getting parsed from the API. This is because the averageRating field in the VolumeInfo data class is declared as Int but the response can contain floating point values.
If you change averageRating field type from Int to Double in the VolumeInfo data class, the exception would no longer occur.
I suggest you to debug your code in such cases.

How to handle two data type in the same API android

I have an API response. When data is purchased it gives as a JSONObject else a null string.
How do I process both the data types.
If I try to specify Any as the data type is model class, I am not able to retrieve the data in the JSONObject.
If it's just a null, you can simply check if the key exists before calling getString:
private fun JSONObject.getStringOrNull(key: String): String? {
return when {
this.has(key) -> try { getString(key) } catch (e: Exception) { null }
else -> null
}
}
#Test
fun runTest() {
val json = """
{ "name":"Bob Ross", "email":null }
""".trimIndent()
val obj = JSONObject(json)
val name = obj.getStringOrNull("name")
val email = obj.getStringOrNull("email")
println(name)
println(email)
}
You can use JsonElement to store the data in the model class.Since primitive data types also extend JsonElement

Type mismatch: inferred type is Unit when Weather was expected

I am working on an Android Weather application and I am getting the error described in the title. The function that is causing this error is a repository that is calling some weather data. I have a helper class called DataOrException which is:
class DataOrException<T, Boolean, E>(
var data: T? = null,
var loading: Kotlin.Boolean? = null,
var e: E? = null
)
The function that is calling this class is a coroutine that is getting the weather information from repository which is using Injection to return the class. Here's the function:
suspend fun getWeather(cityQuery: String, units: String): DataOrException<Weather, Boolean, Exception> {
val response = try {
api.getWeather(query = cityQuery, units = units)
} catch (e: Exception) {
Log.d("REX", "getWeather: $e")
return DataOrException(e = e)
}
return DataOrException(data = response) //Error occurs here.
Any ideas on how to fix this error?
Your getWeather function in WeatherApi is not returning anything so it's basically a kotlin Unit. But, In your repository return DataOrException(data = response) here data is expected to be of type Weather. That's why the error.
Solution:
Return Weather from WeatherApi function getWeather & keep everything else as it was.
interface WeatherApi {
#GET(value = "/cities/cityID=Chelsea")
suspend fun getWeather(#Query("q") query: String, #Query("units") units: String = "imperial") : Weather
}
OR
================
Change data type to Unit by changing to : DataOrException<Unit, Boolean, Exception>
suspend fun getWeather(
cityQuery: String,
units: String
): DataOrException<Unit, Boolean, Exception> {
val response = try {
api.getWeather(query = cityQuery, units = units)
} catch (e: Exception) {
Log.d("REX", "getWeather: $e")
return DataOrException(e = e)
}
return DataOrException(data = response)
}

Implementing Google places autoComplete textfield implementation in jetpack compose android

Did anyone implement google autocomplete suggestion text field or fragment in a jetpack compose project? If so kindly guide or share code snippets as I'm having difficulty in implementing it.
Update
Here is the intent that I'm triggering to open full-screen dialog, but when I start typing within it gets closed, and also I'm unable to figure out what the issue is and need a clue about handling on activity result for reading the result of the predictions within this compose function.
Places.initialize(context, "sa")
val fields = listOf(Place.Field.ID, Place.Field.NAME)
val intent = Autocomplete.IntentBuilder(
AutocompleteActivityMode.FULLSCREEN,fields).build(context)
startActivityForResult(context as MainActivity,intent, AUTOCOMPLETE_REQUEST_CODE, Bundle.EMPTY)
I am using the MVVM architecture and this is how I implemented it:
GooglePlacesApi
I've created an api for reaching google api named GooglePlacesApi
interface GooglePlacesApi {
#GET("maps/api/place/autocomplete/json")
suspend fun getPredictions(
#Query("key") key: String = <GOOGLE_API_KEY>,
#Query("types") types: String = "address",
#Query("input") input: String
): GooglePredictionsResponse
companion object{
const val BASE_URL = "https://maps.googleapis.com/"
}
}
The #Query("types") field is for specifiying what are you looking for in the query, you can look for establishments etc.
Types can be found here
Models
So I created 3 models for this implementation:
GooglePredictionsResponse
The way the response looks if you are doing a GET request with postman is:
Google Prediction Response
You can see that we have an object with "predictions" key so this is our first model.
data class GooglePredictionsResponse(
val predictions: ArrayList<GooglePrediction>
)
GooglePredictionTerm
data class GooglePredictionTerm(
val offset: Int,
val value: String
)
GooglePrediction
data class GooglePrediction(
val description: String,
val terms: List<GooglePredictionTerm>
)
I only needed that information, if you need anything else, feel free to modify the models or create your own.
GooglePlacesRepository
And finally we create the repository to get the information (I'm using hilt to inject my dependencies, you can ignore those annotations if not using it)
#ActivityScoped
class GooglePlacesRepository #Inject constructor(
private val api: GooglePlacesApi,
){
suspend fun getPredictions(input: String): Resource<GooglePredictionsResponse>{
val response = try {
api.getPredictions(input = input)
} catch (e: Exception) {
Log.d("Rently", "Exception: ${e}")
return Resource.Error("Failed prediction")
}
return Resource.Success(response)
}
}
Here I've used an extra class I've created to handle the response, called Resource
sealed class Resource<T>(val data: T? = null, val message: String? = null){
class Success<T>(data: T): Resource<T>(data)
class Error<T>(message: String, data:T? = null): Resource<T>(data = data, message = message)
class Loading<T>(data: T? = null): Resource<T>(data = data)
}
View Model
Again I'm using hilt so ignore annotations if not using it.
#HiltViewModel
class AddApartmentViewModel #Inject constructor(private val googleRepository: GooglePlacesRepository): ViewModel(){
val isLoading = mutableStateOf(false)
val predictions = mutableStateOf(ArrayList<GooglePrediction>())
fun getPredictions(address: String) {
viewModelScope.launch {
isLoading.value = true
val response = googleRepository.getPredictions(input = address)
when(response){
is Resource.Success -> {
predictions.value = response.data?.predictions!!
}
}
isLoading.value = false
}
}
fun onSearchAddressChange(address: String){
getPredictions(address)
}
}
If you need any further help let me know
I didn't include UI implementation because I assume it is individual but this is the easier part ;)
#Composable
fun MyComponent() {
val context = LocalContext.current
val intentLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
when (it.resultCode) {
Activity.RESULT_OK -> {
it.data?.let {
val place = Autocomplete.getPlaceFromIntent(it)
Log.i("MAP_ACTIVITY", "Place: ${place.name}, ${place.id}")
}
}
AutocompleteActivity.RESULT_ERROR -> {
it.data?.let {
val status = Autocomplete.getStatusFromIntent(it)
Log.i("MAP_ACTIVITY", "Place: ${place.name}, ${place.id}")
}
}
Activity.RESULT_CANCELED -> {
// The user canceled the operation.
}
}
}
val launchMapInputOverlay = {
Places.initialize(context, YOUR_API_KEY)
val fields = listOf(Place.Field.ID, Place.Field.NAME)
val intent = Autocomplete
.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields)
.build(context)
intentLauncher.launch(intent)
}
Column {
Button(onClick = launchMapInputOverlay) {
Text("Select Location")
}
}
}

Get Class By String Name In Android To Avoid Repeating Myself

So in my code, I'm getting a list of strings, which correspond to movie genres in an enum. I'm then getting a metadata file, and going through each of those genres, seeing if the date the server updated that genre is newer than the date I retrieved those movies and stored them in the database. Long story short, the metadata file has classes for each of the genres, and I'm doing a TON of repeating myself, simply for the fact that I can't think of how to programtically let my app know that, for instance, if you're evaluating the genre ACTION you should look at the member variable metadata.action. Here's my code, I've truncated it but there are a couple dozen genres.
TwMetadata
data class TwMetadata (
#SerializedName("action") var action : Action,
#SerializedName("adventure") var adventure : Adventure,
#SerializedName("animation") var animation : Animation,
#SerializedName("comedy") var comedy : Comedy,
)
In the following code, I've taken the list of strings I've pulled from the server, converted them into genre enums, and am checking the date last updated in the sharedpreferences metadata file (spObj), vs the date last updated in the newly pulled metadata file(metadata.value). If there is new data, I'm changing the date in the sharedpreferences metadata file, which I commit at the end of this completable. Then I'm sending it to a function which either pulls the data from the database or the web, depending on the value of the boolean i'm sending it. Ideally I'd be able to get rid of this entire 'when' block and just run a single function on each genre, but for that to work they'd need to know what class they correspond to in the metadata file.
private fun checkGenreDate(genreList: MutableList<GENRE>) : Completable {
return Completable.create { emitter ->
var getNew = json.isNullOrEmpty()
genreList.forEach {
when (it) {
GENRE.NULL -> {
}
GENRE.ACTION -> {
if (spObj.action.updated < metadata.value!!.action.updated) {
getNew = true
spObj.action = metadata.value!!.action
}
loadFromTwApi(it, getNew)
}
GENRE.ADVENTURE -> {
if (spObj.adventure.updated < metadata.value!!.adventure.updated) {
getNew = true
spObj.adventure = metadata.value!!.adventure
}
loadFromTwApi(it, getNew)
}
GENRE.ANIMATION -> {
if (spObj.animation.updated < metadata.value!!.animation.updated) {
getNew = true
spObj.animation = metadata.value!!.animation
}
loadFromTwApi(it, getNew)
}
GENRE.COMEDY -> {
if (spObj.comedy.updated < metadata.value!!.comedy.updated) {
getNew = true
spObj.comedy = metadata.value!!.comedy
}
loadFromTwApi(it, getNew)
}
GENRE.RANDOM -> {
}
GENRE.THEATER -> {
loadTheaterData()
}
}
}
emitter.onComplete()
}
}
my enum class
enum class GENRE {
NULL,
ACTION,
ADVENTURE,
ANIMATION,
COMEDY,
RANDOM,
THEATER;
companion object {
fun getGenreByName(name: String) = try {
(name.toUpperCase(Locale.getDefault()))
} catch (e: IllegalArgumentException) {
null
}
}
I suggest a generic sealed class with abstract metadata get and set methods, and a static lookup method included on the sealed class. Pretty much Kotlin magic for turning enums into a class hierarchy so you can both share and enforce functionality amongst them.
So for just Action and Adventure, here's a function updateAllGenres which will update all the metadata.
fun updateAllGenres( spObj: TwMetadata, metadata: MetadataFile ) {
Genre.forEach {
it.maybeUpdateSharedMetadata( spObj, metadata )
}
}
sealed class Genre < T : GenreMetadata > {
companion object {
fun forEach( callback: ( Genre < * > ) -> Unit ) {
Genre::class.sealedSubclasses.forEach {
callback( it.objectInstance as Genre < * > )
}
}
}
fun maybeUpdateSharedMetadata( spObj: TwMetadata, metadata: MetadataFile ): Boolean {
val genreMetadataInFile = getGenreMetadata( metadata.value!! )
return getGenreMetadata( spObj ).updated < genreMetadataInFile.updated
.also { setGenreMetadataInDictionary( spObj, genreMetadataInFile )
}
}
protected abstract fun getGenreMetadata( metadata: TwMetadata ): T
protected abstract fun setGenreMetadataInDictionary( metadata: TwMetadata, genreMetadata: T )
}
object ActionGenre : Genre < Action >() {
override fun getGenreMetadata( metadata: TwMetadata ): Action {
return metadata.action
}
override fun setGenreMetadataInDictionary( metadata: TwMetadata, genreMetadata: Action ) {
metadata.action = genreMetadata
}
}
object AdventureGenre : Genre < Adventure >() {
override fun getGenreMetadata( metadata: TwMetadata ): Adventure {
return metadata.adventure
}
override fun setGenreMetadataInDictionary( metadata: TwMetadata, genreMetadata: Adventure ) {
metadata.adventure = genreMetadata
}
}
A couple more tweaks are needed to get exactly your function, but I'm sure you get the idea. Also, you might want to go further than this and look at combining the sealed subclasses and the classes Action, Adventure etc that hold the metadata, but that's a bit more work.
For reference the interfaces in the above are:
interface Action : GenreMetadata {}
interface Adventure : GenreMetadata {}
interface GenreMetadata {
val updated: Long
}
interface TwMetadata {
var action: Action
var adventure: Adventure
}
interface MetadataFile {
val value: TwMetadata?
}

Categories

Resources