I used this CodeLabs tutorial to learn how to make an HTTP request from the Google Books API https://codelabs.developers.google.com/codelabs/kotlin-android-training-internet-data/#4
Right now, I'm trying to access a nested JSON object that the Google Books API spits out I.e
"items": [{
"kind": "books#volume",
"id": "mOsbHQAACAAJ",
"volumeInfo" : {
"description": "Young wizard Harry Potter finds himself back at the miserable Hogwarts School of Witchcraft and Wizardry. He doesn't realize the difficulty of the task that awaits him. Harry must pull out all the stops in order to find his missing friend. No Canadian Rights for the Harry Potter Series HARRY POTTER and all related characters and elements are trademarks of and (c) Warner Bros. Entertainment Inc. Harry Potter publishing rights (c) J. K. Rowling. (s05)",
"imageLinks": {
"smallThumbnail": "http://books.google.com/books/content?id=mOsbHQAACAAJ&printsec=frontcover&img=1&zoom=5&source=gbs_api",
"thumbnail": "http://books.google.com/books/content?id=mOsbHQAACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api"
}
},
I just want the description and thumbnail property.
My interface for the API service is
package com.example.customapp.network
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
//Code from https://codelabs.developers.google.com/codelabs/kotlin-android-training-internet-data/#3
private const val BASE_URL = "https://www.googleapis.com/books/v1/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface BookApiService {
//Get annotation specifies the endpoint for this web service method.
//when getProperties() method is invoked, Retrofit appends the endpoint 'book' to the base URL
//And creates a Call object. The Call object is used to start the request.
@GET("volumes?q='harry+potter")
suspend fun getProperties(): BookProperty
}
object BookApi {
val retrofitService: BookApiService by lazy {
retrofit.create(BookApiService::class.java)
}
}
}
My BookProperty.kt is
data class BookProperty(@field:Json(name = "items" ) val bookDetail: List<BookDetail>)
data class BookDetail(@field:Json(name = "volumeInfo") val volumeInfo: VolumeInfo)
data class VolumeInfo(@field:Json(name = "description") val description: String, @field:Json(name= "imageLinks") val imageLink: ImageLink)
data class ImageLink(@field:Json(name = "thumbnail") val thumbnail: String)
I'm calling the API from my ViewModel
val readAllData: LiveData<List<BookItem>>
private val repository: BookRepository
private val _response = MutableLiveData<String>()
val response: LiveData<String>
get() = _response
init {
val bookDao = BookDatabase.getDatabase(application).bookDao()
repository = BookRepository(bookDao)
readAllData = repository.readAllData
}
fun addBook(book: BookItem) {
viewModelScope.launch(Dispatchers.IO) {
repository.addBook(book)
}
}
fun updateBook(book: BookItem) {
viewModelScope.launch(Dispatchers.IO) {
repository.updateBook(book)
}
}
fun getBookDetailProperties() {
viewModelScope.launch {
try {
//calling get properties from the BookApi service creates and starts the network call
//on a background thread
var listResult = BookApi.retrofitService.getProperties()
_response.value = "${
listResult.bookDetail[0].volumeInfo.description} book properties received"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}
I'm trying to make an HTTP request each time I update an item on my CRUD app i.e when I click a button, but I can't seem to get any response back. This is my UpdateFragment where I initiate the API call.
class UpdateFragment : Fragment() {
//Read up on delegation
//https://codelabs.developers.google.com/codelabs/kotlin-bootcamp-classes/#7
//UpdateFragmentArgs is a class that is automatically generated
//when we created an argument for our Update Fragment in the nav graph
//UpdateFragmentArgs will contain our current book
//we can also use bundle
private val args by navArgs<UpdateFragmentArgs>()
private lateinit var mBookViewModel: BookViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_update, container, false)
//So the keyboard doesn't push the EditText fields up
this.activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
Glide
.with(this)
.load(args.currentBook.image)
.into(view.bookImageDetail)
mBookViewModel = ViewModelProvider(this).get(BookViewModel::class.java)
view.apply {
updateInputName.setText(args.currentBook.title)
updateInputAuthor.setText(args.currentBook.author)
updateBookDesc.text = args.currentBook.desc
updateRatingBar.rating = args.currentBook.rating.toFloat()
updateBookCompleted.isChecked = args.currentBook.finished
updateBookCompleted.text =
if (updateBookCompleted.isChecked) getString(R.string.book_completed) else getString(
R.string.book_not_completed
)
updateDateCreated.text = getString(R.string.date_created, args.currentBook.dateCreated)
}
view.updateBtn.setOnClickListener {
updateItem()
}
view.updateBookCompleted.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
view.updateBookCompleted.text = getString(R.string.book_completed)
} else {
view.updateBookCompleted.text = getString(R.string.book_not_completed)
}
}
return view
}
private fun updateItem() {
val bookName = updateInputName.text.toString()
val bookAuthor = updateInputAuthor.text.toString()
val bookRating = updateRatingBar.rating.toDouble()
val bookFinished = updateBookCompleted.isChecked
if (inputCheck(bookName, bookAuthor)) {
//***Initiate API call here ****
mBookViewModel.getBookDetailProperties()
//Get description and image from API
mBookViewModel.response.observe(viewLifecycleOwner, {
println("Get resp " + it)
})
//Create book object
val updatedBook = BookItem(
args.currentBook.id,
bookName,
bookAuthor,
args.currentBook.desc,
args.currentBook.image,
bookRating,
args.currentBook.dateCreated,
bookFinished
)
//update current book
mBookViewModel.updateBook(updatedBook)
Toast.makeText(requireContext(), "Updated book successfully!", Toast.LENGTH_SHORT)
.show()
//navigate back
findNavController().navigate(R.id.action_updateFragment_to_listFragment)
}
}
private fun inputCheck(bookName: String, authorName: String): Boolean {
return !(TextUtils.isEmpty(bookName) && TextUtils.isEmpty(authorName))
}
}
The issue is I can't get any response from the API call - I'm not sure if it's because of the nested objects in the JSON. Please help me shed some light on this, I'm still new to Kotlin programming.
I found out the reason why I was not getting any response. In my UpdateFragment, I'm doing this:
I am navigating back to another fragment before I can observe any changes from the HTTP response. This causes the observer to stop observing any changes, and thus I can't get a response. I just need to put my code inside the callback, so I can do something with the data I received. Like so:
Hopefully this helps out anyone who has just started out learning LiveData and using HTTP requests.