Before in other random languages I always returned values from functions and I was so surprised now when I try do like below but got error:
fun getChannels(): List<TblChannel> {
val stringRequest = JsonObjectRequest(
Request.Method.GET, "$baseUrl/api/json/channel_list.json",
null,
{ response ->
try{
val gson = Gson()
val token = TypeToken.getParameterized(ArrayList::class.java,TblChannel::class.java).type
val channels1:JSONArray = response.getJSONArray("groups").getJSONObject(0).getJSONArray("channels")
//got "return isn't allowed here" error
return gson.fromJson(channels1.toString(),token)
} catch (e:Exception){
Log.e(tag,"DkPrintError on getChannels: $e")
}
},
{ error ->
Log.e(tag, "DkPrintError on getChannels: $error")
})
requestQueue.add(stringRequest)
}
How can I convert response body to my class and return them?
This isn't really a kotlin problem, we do have functions that return values, however you cannot return a value from asynch function (which is the case here):
If you perform some calculation asynchronously, you cannot directly return the value, since you don't know if the calculation is finished yet. You could wait it to be finished, but that would make the function synchronous again. Instead, you should work with callbacks
source
what you could do tho (as suggested in the quote), is use callbacks, as shown here
That post will be so helpfull to solve that problem.
In that case I solved the problem with callback method and my code was like below:
fun getChannels(onDataReadyCallback: OnDataReadyCallBack){
val stringRequest = JsonObjectRequest(
Request.Method.GET, "$baseUrl/api/json/channel_list.json",
null,
{ response ->
try{
val gson = Gson()
val token = TypeToken.getParameterized(ArrayList::class.java,TblChannel::class.java).type
val channels1:JSONArray = response.getJSONArray("groups").getJSONObject(0).getJSONArray("channels")
onDataReadyCallback.onDataReady(gson.fromJson(channels1.toString(),token))
} catch (e:Exception){
Log.e(tag,"DkPrintError on getChannels: $e")
}
},
{ error ->
Log.e(tag, "DkPrintError on getChannels: $error")
})
requestQueue.add(stringRequest)
}
and I called that fun like:
private fun getChannels(){
viewModelScope.launch {
channelsLiveData.value=roomRepository.getAllChannels
if (channelsLiveData.value.isNullOrEmpty()){
remoteRepository.getChannels(object :OnDataReadyCallBack{
override fun onDataReady(data: List<TblChannel>) {
viewModelScope.launch {
channelsLiveData.value=data
}
}
})
}
}
}
Related
I've written one function with Flow collector which is as shown below,
private fun callSocket(
eventEmmit: String,
eventOn: String,
request: JSONObject
): Flow<SocketCallback<JSONObject>> =
flow {
try {
if (socket.connected()) {
var response = JSONObject()
Log.e("EMIT", JSONObject(Gson().toJson(request)).toString())
socket.on(
eventOn
) { args ->
response = args[0] as JSONObject
Log.e("ON", response.toString())
**this.emit(SocketCallback.OnSuccess(response))**
}.emit(
eventEmmit,
request
)
emit(SocketCallback.OnSuccess(response))
} else {
Log.e("SOCKET_ERROR", "Socket connection failed")
emit(SocketCallback.OnError("Socket connection failed"))
}
} catch (e: SocketException) {
emit(SocketCallback.OnError(e.toString()))
}
}.flowOn(Dispatchers.IO)
But when I write this.emit(SocketCallback.OnSuccess(response))(enclosed in ** in code) in on method it shows me the error "Suspension functions can be called only within coroutine body".
Any solution for this?
Thanks in advance.
You are trying to emit events to flow outside of coroutineScope. socket.on() function probably has signature:
fun on(ev: String, block: (args: String) -> Unit) {
}
in that case, inside lambda block: (args: String) -> Unit) you are outside of scope and you can not invoke suspending functions.
You have only 2 solutions:
Every time new event approach - create new coroutine with coroutine builder launch:
socket.on(
eventOn
) { args ->
response = args[0] as JSONObject
Log.e("ON", response.toString())
launch {
emit(SocketCallback.OnSuccess(response))
}
}.emit(
eventEmmit,
request
)
Use callbackFlow to avoid creation of new coroutine on each event. Please check especially this post.
Here's how I solved it by using the CallBackFlow
private fun callOnSocket(
eventOn: String
): Flow<SocketCallback<JSONObject>> =
callbackFlow<SocketCallback<JSONObject>> {
try {
if (socket.connected()) {
Log.e("ON", "Started")
var response = JSONObject()
socket.on(
eventOn
) {
response = it[0] as JSONObject
Log.e("ON", response.toString())
trySend(SocketCallback.OnSuccess(response))
}
} else {
Log.e("SOCKET_ERROR", "Socket connection failed")
trySend(SocketCallback.OnError("Socket connection failed"))
}
} catch (e: SocketException) {
trySend(SocketCallback.OnError("Socket connection failed"))
}
awaitClose { cancel() }
}.flowOn(Dispatchers.IO)
I'm trying to understand Kotlin couroutine. So here's my code (based on this tutorial). To keep the code relatively simple, I deliberately avoid MVVM, LiveData, etc. Just Kotlin couroutine and Retrofit.
Consider this login process.
ApiInterface.kt
interface ApiInterface {
// Login
#POST("/user/validate")
suspend fun login(#Body requestBody: RequestBody): Response<ResponseBody>
}
ApiUtil.kt
class ApiUtil {
companion object {
var API_BASE_URL = "https://localhost:8100/testApi"
fun getInterceptor() : OkHttpClient {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
return okHttpClient
}
fun createService() : ApiInterface {
val retrofit = Retrofit.Builder()
.client(getInterceptor())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(OJIRE_BASE_URL)
.build()
return retrofit.create(ApiInterface::class.java)
}
}
fun login(userParam: UserParam): String {
val gson = Gson()
val json = gson.toJson(userParam)
var resp = ""
val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())
CoroutineScope(Dispatchers.IO).launch {
val response = createService().login(requestBody)
withContext(Dispatchers.Main){
if (response.isSuccessful){
val gson = GsonBuilder().setPrettyPrinting().create()
val prettyJson = gson.toJson(
JsonParser.parseString(
response.body()
?.string()
)
)
resp = prettyJson
Log.d("Pretty Printed JSON :", prettyJson)
}
else {
Log.e("RETROFIT_ERROR", response.code().toString())
}
}
}
return resp
}
}
LoginActivity.kt
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
edtUsername = findViewById(R.id.edtUsername)
edtPassword = findViewById(R.id.edtPassword)
btnLogin = findViewById(R.id.btnLogin)
btnLogin.setOnClickListener {
val api = ApiUtil()
val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
val response = JSONObject(api.login(userParam))
var msg = ""
if (response.getString("message").equals("OK")){
msg = "Login OK"
}
else {
msg = "Login failed"
}
Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
When debugging the login activity, the API response is captured properly on prettyJson
The problem is resp is still empty. Guess that's how async process work. What I want is to wait until the API call is completed, then the result can be nicely passed to resp as the return value of login(). How to do that?
Well, you got several things wrong here. We'll try to fix them all.
First, the main problem you described is that you need to acquire resp in login() synchronously. You got this problem only because you first launched an asynchronous operation there. Solution? Don't do that, get the response synchronously by removing launch(). I guess withContext() is also not required as we don't do anything that requires the main thread. After removing them the code becomes much simpler and fully synchronous.
Last thing that we need to do with login() is to make it suspendable. It needs to wait for the request to finish, so it is a suspend function. The resulting login() should be similar to:
suspend fun login(userParam: UserParam): String {
val gson = Gson()
val json = gson.toJson(userParam)
val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())
val response = createService().login(requestBody)
return if (response.isSuccessful){
val gson = GsonBuilder().setPrettyPrinting().create()
gson.toJson(
JsonParser.parseString(
response.body()
?.string()
)
)
}
else {
Log.e("RETROFIT_ERROR", response.code().toString())
// We need to do something here
}
}
Now, as we converted login() to suspendable, we can't invoke it from the listener directly. Here we really need to launch asynchronous operation, but we won't use CoroutineScope() as you did in your example, because it leaked background tasks and memory. We will use lifecycleScope like this:
btnLogin.setOnClickListener {
val api = ApiUtil()
val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
lifecycleScope.launch {
val response = JSONObject(api.login(userParam))
var msg = ""
if (response.getString("message").equals("OK")){
msg = "Login OK"
}
else {
msg = "Login failed"
}
withContext(Dispatchers.Main) {
Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
Above code may not be fully functional. It is hard to provide working examples without all required data structures, etc. But I hope you get the point.
Also, there are several other things in your code that could be improved, but I didn't touch them to not confuse you.
I'm trying to take data from a mySQL database and my code take it correctly.
The problem is that I have the information in a JsonObjectRequest and out of it, I can't use it. My idea was to use variables to save some of the information I need.
Something like this:
val queue=Volley.newRequestQueue(this)
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET,url,null,
{ response ->
emailGet = response.getString("email")
usernameGet = response.getString("name")
}, { error ->
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
)
queue.add(jsonObjectRequest)
As I said the problem here is that emailGet and usernameGet (variables declared before this code bloc) store the values only inside the JsonObjectRequest, out of it the variables are empty.
Example:
val queue=Volley.newRequestQueue(this)
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET,url,null,
{ response ->
emailGet = response.getString("email")
usernameGet = response.getString("name")
Toast.makeText(this, emailGet, Toast.LENGTH_LONG).show()
}, { error ->
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
)
queue.add(jsonObjectRequest)
Toast.makeText(this, usernameGet, Toast.LENGTH_LONG).show()
Here the Toast will print on the screen only the emailGet content because it's inside the JsonObjectRequest, the Toast that have to print the usernameGet value will not do it.
Looking for information I have found that this problem could be because this function is asynchronous and I found a possible solution in Java that I tried to translate to Kotlin.
val queue=Volley.newRequestQueue(this)
val future : RequestFuture<JSONObject> = RequestFuture.newFuture()
val request = JsonObjectRequest(
Request.Method.GET, url, null, future, future)
queue.add(request)
try{
var response = future.get()
} catch (e : InterruptedException){
} catch (e : ExecutionException){
}
I do not really understand this second code, but it still doesn't working, the response variable is always empty and the program stays in an infinite loop inside that try.
If you want to use the emailGet and usernameGet variables, you should do it within the callback:
val queue = Volley.newRequestQueue(this)
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET, url, null,
{ response ->
emailGet = response.getString("email")
usernameGet = response.getString("name")
// TODO do something with the variables here
}, { error ->
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
)
queue.add(jsonObjectRequest)
If instead, you have a method method doSomething() that runs immediately after the response is received:
fun doSomething(email:String, username:String){
// TODO do something with the variables here
}
You can replace the TODO comment in the first code snippet with doSomething(emailGet, usernameGet).
The 4th and 5th parameters for JsonObjectRequest are in fact objects of types Response.Listener and Response.ErrorListener. These two listeners are Single Abstract Method interfaces. If expanded it would look like this:
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET, url, null,
object : Response.Listener {
override fun onResponse(response: JSONObject) {
emailGet = response.getString("email")
usernameGet = response.getString("name")
doSomething(emailGet, usernameGet)
}
},
object : Response.ErrorListener {
override fun onErrorResponse(error: VolleyError) {
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
}
)
The lambda syntax you were using was a short hand of SAM interfaces.
The simplest way is using #ttyip's answer but you could also use live data and observing it! You're calling an asynchronous method and there's going to be some delay(network API calling and this delay depends on user's internet connection etc) So First you'll need to add jetPack's lifeCycle components inside your project:
dependencies {
def lifecycle_version = "2.4.0-alpha02"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Annotation processor
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
// alternately - if using Java8, use the following instead of lifecycle-compiler
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
}
After syncing your project, define global inside your activity/fragment:
val email: MutableLiveData<String> = MutableLiveData<String>()
val username: MutableLiveData<String> = MutableLiveData<String>()
And inside your response:
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET,url,null,
{ response ->
val emailGet = response.getString("email")
val usernameGet = response.getString("name")
email.postValue(emailGet)
username.postValue(usernameGet)
}, { error ->
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
)
And somewhere inside your activity just observe your livedata:
email.observe(this, Observer { string ->
// Do some work
})
username.observe(this, Observer { string ->
// Do some work
})
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.
I made app where user can add server (recycler row) to favorites. It only saves the IP and Port. Than, when user open FavoriteFragment Retrofit makes calls for each server
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Server
So in repository I mix the sources and make multiple calls:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
list.add(server)
}
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
and then in ViewModel I create LiveData object
fun getFavoriteServers() {
viewModelScope.launch {
repository.getFavoriteServersToRecyclerView()
.onEach { dataState ->
_favoriteServers.value = dataState
}.launchIn(viewModelScope)
}
}
And everything works fine till the Favorite server is not more available in the Lobby and the Retrofit call failure.
My question is: how to skip the failed call in the loop without crashing whole function.
Emit another flow in catch with emitAll if you wish to continue flow like onResumeNext with RxJava
catch { cause ->
emitAll(flow { emit(DataState.Errorcause)})
}
Ok, I found the solution:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val list: MutableList<Server> = mutableListOf()
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val job = CoroutineScope(coroutineContext).launch {
getFavoritesServersNotLiveData.forEach { fav ->
val server = getServer(fav.ip, fav.port)
server.collect { dataState ->
when (dataState) {
is DataState.Loading -> Log.d(TAG, "loading")
is DataState.Error -> Log.d(TAG, dataState.exception.message!!)
is DataState.Success -> {
list.add(dataState.data)
Log.d(TAG, dataState.data.toString())
}
}
}
}
}
job.join()
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
when using retrofit you can wrap response object with Response<T> (import response from retrofit) so that,
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Response<Server>
and then in the Repository you can check if network failed without using try-catch
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
if(getFavoritesServersNotLiveData.isSuccessful) {
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.body().forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
// if the above request fails it wont go to the else block
list.add(server)
}
emit(DataState.Success(list))
} else {
val error = getFavoritesServersNotLiveData.errorBody()!!
//do something with error
}
}