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:
//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)
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:
//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)
})
Hopefully this helps out anyone who has just started out learning LiveData and using HTTP requests.
Related
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.
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")
}
}
}
PROBLEM STATEMENT
: When i press register button for register new user it show register success response in toast from live data, but when i tried to do same button trigger it show again register success response message from API & then also show phone number exist response from API in toast. It means old response return by live data too. So how can i solve this recursive live data response return issue?
HERE is the problem video link to understand issue
Check here https://drive.google.com/file/d/1-hKGQh9k0EIYJcbInwjD5dB33LXV5GEn/view?usp=sharing
NEED ARGENT HELP
My Api Interface
interface ApiServices {
/*
* USER LOGIN (GENERAL USER)
* */
#POST("authentication.php")
suspend fun loginUser(#Body requestBody: RequestBody): Response<BaseResponse>
}
My Repository Class
class AuthenticationRepository {
var apiServices: ApiServices = ApiClient.client!!.create(ApiServices::class.java)
suspend fun UserLogin(requestBody: RequestBody) = apiServices.loginUser(requestBody)
}
My View Model Class
class RegistrationViewModel : BaseViewModel() {
val respository: AuthenticationRepository = AuthenticationRepository()
private val _registerResponse = MutableLiveData<BaseResponse>()
val registerResponse: LiveData<BaseResponse> get() = _registerResponse
/*
* USER REGISTRATION [GENERAL USER]
* */
internal fun performUserLogin(requestBody: RequestBody, onSuccess: () -> Unit) {
ioScope.launch {
isLoading.postValue(true)
tryCatch({
val response = respository.UserLogin(requestBody)
if (response.isSuccessful) {
mainScope.launch {
onSuccess.invoke()
isLoading.postValue(false)
_registerResponse.postValue(response.body())
}
} else {
isLoading.postValue(false)
}
}, {
isLoading.postValue(false)
hasError.postValue(it)
})
}
}
}
My Registration Activity
class RegistrationActivity : BaseActivity<ActivityRegistrationBinding>() {
override val layoutRes: Int
get() = R.layout.activity_registration
private val viewModel: RegistrationViewModel by viewModels()
override fun onCreated(savedInstance: Bundle?) {
toolbarController()
viewModel.isLoading.observe(this, {
if (it) showLoading(true) else showLoading(false)
})
viewModel.hasError.observe(this, {
showLoading(false)
showMessage(it.message.toString())
})
binding.registerbutton.setOnClickListener {
if (binding.registerCheckbox.isChecked) {
try {
val jsonObject = JSONObject()
jsonObject.put("type", "user_signup")
jsonObject.put("user_name", binding.registerName.text.toString())
jsonObject.put("user_phone", binding.registerPhone.text.toString())
jsonObject.put("user_password", binding.registerPassword.text.toString())
val requestBody = jsonObject.toString()
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
viewModel.performUserLogin(requestBody) {
viewModel.registerResponse.observe(this){
showMessage(it.message.toString())
//return old reponse here then also new reponse multiple time
}
}
} catch (e: JSONException) {
e.printStackTrace()
}
} else {
showMessage("Please Accept Our Terms & Conditions")
}
}
}
override fun toolbarController() {
binding.backactiontoolbar.menutitletoolbar.text = "Registration"
binding.backactiontoolbar.menuicontoolbar.setOnClickListener { onBackPressed() }
}
override fun processIntentData(data: Uri) {}
}
your registerResponse live data observe inside button click listener, so that's why it's observing two times! your registerResponse live data should observe data out side of button Click listener -
override fun onCreated(savedInstance: Bundle?) {
toolbarController()
viewModel.isLoading.observe(this, {
if (it) showLoading(true) else showLoading(false)
})
viewModel.registerResponse.observe(this){
showMessage(it.message.toString())
}
viewModel.hasError.observe(this, {
showLoading(false)
showMessage(it.message.toString())
})
binding.registerbutton.setOnClickListener {
if (binding.registerCheckbox.isChecked) {
try {
val jsonObject = JSONObject()
jsonObject.put("type", "user_signup")
jsonObject.put("user_name", binding.registerName.text.toString())
jsonObject.put("user_phone", binding.registerPhone.text.toString())
jsonObject.put("user_password", binding.registerPassword.text.toString())
val requestBody = jsonObject.toString()
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
viewModel.performUserLogin(requestBody) {
}
} catch (e: JSONException) {
e.printStackTrace()
}
} else {
showMessage("Please Accept Our Terms & Conditions")
}
}
}
LiveData is a state holder, it's not really meant to be used as an event stream. There is a number of articles however about the topic like this one which describe the possible solutions, including SingleLiveEvent implementation taken from google samples.
But as of now kotlin coroutines library provides better solutions. In particular, channels are very useful for event streams, because they implement fan-out behaviour, so you can have multiple event consumers, but each event will be handled only once. Channel.receiveAsFlow can be very convenient to expose the stream as flow. Otherwise, SharedFlow is a good candidate for event bus implementation. Just be careful with replay and extraBufferCapacity parameters.
My goal is to capture or get a response from my viewModel into my signUp-Fragment when I click the signUp-Button and navigate to verification-Fragment if response.status is true.
When I click on my signUp button in my signUpFragment, a POST request retrofit call is made and a response is received like this :
UserResponse(message=Sign up successful. A verfication code has been sent to your email address, payload=UserPayload(country=Nigeria, createdAt=2020-04-10T10:55:06.220Z, email=osehiproductengineer#gmail.com, id=5e90508a455f70002f19b42e, isVerified=false, name=osehiase ehilen, phone=07083372454, updatedAt=2020-04-10T10:55:06.220Z, v=0), status=200, token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1ZTkwNTA4YTQ1NWY3MDAwMmYxOWI0MmUiLCJpYXQiOjE1ODY1MTYxMDYsImV4cCI6MTU4NjYwMjUwNn0.H_JhBQY-3PQ6Kqk7SS0cm8RP_1mzYlD987M66_LT0PU)
I saw this response using Log; the response does not get to my signUp-Fragment.
Here is my Repository code below:
class NetworkRepository(): BaseRepository() {
val userApi = UserAPI()
val authAPI = AuthAPI()
val treeAPI = TreeAPI()
val paymentAPI = PaymentAPI()
val loginAPI = LoginAPI()
val TAG = "NETWORK REPOSITORY"
private val _networkState = MutableLiveData<NetworkState>()
val networkState: LiveData<NetworkState>
get() = _networkState
//User
suspend fun createUser(userBody: UserBody): UserResponse {
var status = UserResponse()
// Log.d("SIGNUP_RESPONSE2", "inside status:$status")
withContext(Dispatchers.IO){
try {
status = userApi.addUserAsync(userBody).await()
// Log.d("signup_Response3", "after the call:$status")
}catch (t: Throwable){
Log.e(TAG, t.message.toString())
}
}
Log.d("SIGNUP_RESPONSE", "here is the $status")
return status
}
}
Here is my viewModel code:
class UserViewModel : ViewModel(){
private val repository = NetworkRepository()
private val job = Job()
private val scope = CoroutineScope(job + Dispatchers.Main)
fun createUser(userBody: UserBody):UserResponse {
var userPayload: UserResponse = UserResponse()
// Log.d("USERVIEWMODEL_TOP", "the first response:$userPayload")
scope.launch {
// userPayload = repository.createUser(userBody)
userPayload = repository.createUser(userBody)
// Log.d("USERVIEWMODELCHCK", "speak now:$userPayload")
}
// Log.d("USERVIEWMODEL_RESPONSE", "check this userViewModelRes:$userPayload")
return userPayload
}
}
Here is my SignUp-Fragment Code:
class SignUpFragment : Fragment() {
private lateinit var viewModel: UserViewModel
private lateinit var userBody: UserBody
var captureStatus:UserResponse = UserResponse()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
return inflater.inflate(R.layout.fragment_sign_up, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
signup_submit_btn.setOnClickListener {
val response = sendUser()
// Log.d("SIGNUP_FRAGMENTRES", "where is this response:$response")
if (response.status == 200) {
Log.d("SIGNUP_FRAGMENT", "wat is here:${response}")
saveUserInfo(response)
findNavController().navigate(R.id.action_signUpFragment_to_verificationFragment)
} else {
Toast.makeText(
this.context,
"${response.status}, ${response.message}",
Toast.LENGTH_SHORT
).show()
}
}
signup_have_an_account.paintFlags = Paint.UNDERLINE_TEXT_FLAG
signup_have_an_account.setOnClickListener {
findNavController().navigate(R.id.action_signUpFragment_to_loginFragment)
}
signup_back_btn.setOnClickListener {
findNavController().popBackStack()
}
}
private fun sendUser(): UserResponse {
var userBody: UserBody? = null
//verification
when {
signup_email_input.editText!!.text.isEmpty() -> {
signup_email_input.editText!!.error = "Email cannot be empty"
}
signup_phone_input.editText!!.text.isEmpty() -> {
signup_phone_input.editText!!.error = "Phone cannot be empty"
}
signup_country_input.editText!!.text.isEmpty() -> {
signup_country_input.editText!!.error = "Country cannot be empty"
}
signup_password_input.editText!!.text.isEmpty() -> {
signup_password_input.editText!!.error = "Password cannot be empty"
}
signup_password_input.editText!!.text.length < 6 -> {
signup_password_input.editText!!.error = "Password cannot be less than 6 characters"
}
signup_name_input.editText!!.text.isEmpty() -> {
signup_name_input.editText!!.error = "Name cannot be empty"
}
else -> {
val email = signup_email_input.editText!!.text.toString()
val country = signup_country_input.editText!!.text.toString()
val name = signup_name_input.editText!!.text.toString()
val password = signup_password_input.editText!!.text.toString()
val phone = signup_phone_input.editText!!.text.toString()
userBody = UserBody(country, email, false, name, password, phone)
}
}
// Log.d("USER REG", userBody.toString())
return viewModel.createUser(userBody!!)
}
private fun saveUserInfo(userResponse: UserResponse) {
this.activity?.let { Preferences.setEmail(it, userResponse.payload!!.email) }
this.activity?.let { Preferences.saveAuthToken(it, userResponse.token!!) }
}
}
userPayload = repository.createUser(userBody)
This line in your ViewModel will execute in background thread and hence is asynchronus, In order to publish "userPayload" object to your fragment, you need to make use of LiveData like this
//Define a mutablelivedata property in your ViewModel
public var userPayloadLiveData = MutableLiveData<UserResponse>()
From the co-routine inside your ViewModel you need to post your response to the livedata as such
userPayload = repository.createUser(userBody)
userPayloadLiveData.postValue(userPayload)
From your fragment you need to observe the "userPayloadLiveData" for async changes in value.
viewModel.userPayloadLiveData.observe(this, Observer { userResponse ->
//this code will run after network call and now safe to do UI stuff.
//userResponse is your response object
})
To understand more about how LiveData or MutableLiveData works please refer to androidx docs MutableLiveData
It's bad approach to instantiate variable (like: status, userPayload) with some Dummy Object, and then changing it with response from other method, then returning it via return function. You should better instantiate it with null and return the response to calling function via callback. Then if you have null you immediately now, something goes wrong.
The above approach is source of your problem. Because the createUser function is returning DummyObject, not actual object from Retrofit. To fix this, you need to delete return function from createUser method. And then add callback or higher-order function as second parameter.
Here is example of higher-order function, which is used as callback, when user is created:
createUser in ViewModel
fun createUser(userBody: UserBody, onUserCreated: (UserResponse) -> Unit) {
//Log.d("USERVIEWMODEL_TOP", "the first response:$userPayload")
scope.launch {
//userPayload = repository.createUser(userBody)
val userPayload: UserResponse = repository.createUser(userBody)
onUserCreated(userPayload)
//Log.d("USERVIEWMODELCHCK", "speak now:$userPayload")
}
}
Why this way? Because scope.launch{...} is something like closed environment and you have to get somehow userPayload from inside curly brackets scope.launch{ ... }. In your solution, you are returning userPayload from outisde of scope.launch{} which knows nothing about what happens inside {}
Also modify createUser function in your repository:
createUser in repository:
suspend fun createUser(userBody: UserBody): UserResponse {
return withContext(Dispatchers.IO) {
try {
val status = userApi.addUserAsync(userBody).await()
//Will be used as returend value.
status
} catch (t: Throwable) {
//Will be used as returned value - in case of error.
UserResponse()
}
}
}
Why? The same reason as above. You were returning status from outside of withContext(Dispatchers.IO) {}. So due to we want to return something from inside withContext(Dispatchers.IO) {} - we need to add return statement just before withContext. In this case last line from try {} or last line from catch{}(in case of error) will be used as return value for return statement before withContext(Dispatchers.IO) {}
And now you should be able to receive response in your fragment, by calling createUser function this way:
fragment
viewModel.createUser(userBody) { response ->
if (response.status == 200) {
Log.d("SIGNUP_FRAGMENT", "wat is here:${response}")
saveUserInfo(response)
findNavController().navigate(R.id.action_signUpFragment_to_verificationFragment)
} else {
Toast.makeText(
this.context,
"${response.status}, ${response.message}",
Toast.LENGTH_SHORT
).show()
}
}
I'm new to kotlin so this maybe a very easy issue to resolve.
What I'm trying to do is filter the json response that I receive using Retrofit2 before I display the images in a grid with a RecyclerView.
instagram.com/explore/tags/{hashtag}/?__a=1&max_id= Using Retrofit2 I'm able to get the data response fine and also display the given url images in a RecyclerView.
I have not been successful in using the filter, map, loops and conditions to remove elements from the Arraylist. I do not understand these to the fullest extent but I have searched looking for solutions and those are what I came apon.
Interface
interface InstagramDataFetcher
{
#GET("tags/{tag}/?__a=1&max_id=")
fun getInstagramData(#Path("tag") hashtag: String) : Call <InstagramResponse>
}
Where I get my response from and also get StringIndexOutOfBoundsException
class InstagramFeedFragment : Fragment()
{
private fun onResponse()
{
val service = RestAPI.retrofitInstance?.create(InstagramDataFetcher::class.java)
val call = service?.getInstagramData("hashtag")
call?.enqueue(object : Callback<InstagramResponse>
{
override fun onFailure(call: Call<InstagramResponse>, t: Throwable)
{
Log.d("FEED", " $t")
}
override fun onResponse(
call: Call<InstagramResponse>, response: Response<InstagramResponse>
)
{
//for ((index, value) in data.withIndex())
if (response.isSuccessful)
{
var data: ArrayList<InstagramResponse.InstagramEdgesResponse>? = null
val body = response.body()
data = body!!.graphql.hashtag.edge_hashtag_to_media.edges
for ((index, value) in data.withIndex())
{
if(value.node.accessibility_caption[index].toString().contains("text") ||
value.node.accessibility_caption[index].toString().contains("person"))
{
data.drop(index)
}
}
recyclerView.adapter = InstagramGridAdapter(data, parentFragment!!.context!!)
}
}
})
}
}
This is my model class
data class InstagramResponse(val graphql: InstagramGraphqlResponse)
{
data class InstagramGraphqlResponse(val hashtag: InstagramHashtagResponse)
data class InstagramHashtagResponse(val edge_hashtag_to_media: InstagramHashtagToMediaResponse)
data class InstagramHashtagToMediaResponse(
val page_info: InstagramPageInfo,
val edges: ArrayList<InstagramEdgesResponse>
)
data class InstagramPageInfo(
val has_next_page: Boolean,
val end_cursor: String
)
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val __typename: String,
val shortcode: String,
val display_url: String,
val thumbnail_src: String,
val thumbnail_resources: ArrayList<InstagramThumbnailResourceResponse>,
val is_video: Boolean,
val accessibility_caption: String
)
data class InstagramThumbnailResourceResponse(
val src: String,
val config_width: Int,
val config_height: Int
)
}
Simply again, I want to just remove elements from the arraylist that match certain things what I don't want. For instance. the "is_video" value that comes from the json. I want to go through the arraylist and remove all elements that have "is_video" as true.
Thanks
If you asking how to filter the list then below is the demo.
You just need to use filter on your data which is an ArrayList. I've tried keeping the same structure for the models so that you can get a better understanding.
fun main() {
val first = InstagramNodeResponse(
title = "first",
is_video = true
)
val second = InstagramNodeResponse(
title = "second",
is_video = false
)
val list: ArrayList<InstagramEdgesResponse> = arrayListOf(
InstagramEdgesResponse(node = first),
InstagramEdgesResponse(node = second)
)
val itemsWithVideo = list.filter { it.node.is_video == true }
val itemsWithoutVideo = list.filter { it.node.is_video == false }
println(itemsWithVideo.map { it.node.title }) // [first]
println(itemsWithoutVideo.map { it.node.title }) // [second]
}
// Models
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val title: String,
val is_video: Boolean
)