I often get an error android.os.NetworkOnMainThreadException, when I try get info from some api. I know that this problem is related to the main android thread, but I don't understand how to solve it - coroutines, async okhttp, or both?
P.S I have a bad eng, sorry.
My code:
MainAtivity.kt
class MainActivity: AppCompatActivity(), Alert {
private lateinit var binding: ActivityMainBinding
lateinit var api: ApiWeather
var okHttpClient: OkHttpClient = OkHttpClient()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
api = ApiWeather(okHttpClient)
binding.buttonGetWeather.setOnClickListener {
val cityInput = binding.textInputCity.text.toString()
if (cityInput.isEmpty()) {
errorAlert(this, "...").show()
} else {
val city = "${cityInput.lowercase()}"
val limit = "1"
val appId = "key"
val urlGeocoding = "http://api.openweathermap.org/geo/1.0/direct?" +
"q=$city&limit=$limit&appid=$appId"
var status = false
val coordinates: MutableMap<String, Double> = mutableMapOf()
val job1: Job = lifecycleScope.launch {
val geo = api.getGeo(urlGeocoding)
if (geo != null) {
coordinates["lat"] = geo.lat
coordinates["lon"] = geo.lon
status = true
} else {
status = false
}
}
val job2: Job = lifecycleScope.launch {
job1.join()
when(status) {
false -> {
binding.textviewTempValue.text = ""
errorAlert(this#MainActivity, "...").show()
}
true -> {
val urlWeather = "https://api.openweathermap.org/data/2.5/weather?" +
"lat=${coordinates["lat"]}&lon=${coordinates["lon"]}&units=metric&appid=${appId}"
val weather = api.getTemp(urlWeather)
binding.textviewTempValue.text = weather.main.temp.toString()
}
}
}
}
}
}
}
Api.kt
class ApiWeather(cl: OkHttpClient) {
private val client: OkHttpClient
init {
client = cl
}
suspend fun getGeo(url: String): GeocodingModel? {
val request: Request = Request.Builder()
.url(url)
.build()
val responseStr = client.newCall(request).await().body?.string().toString()
val json = Json {
ignoreUnknownKeys = true
}
return try {
json.decodeFromString<List<GeocodingModel>>(responseStr)[0]
} catch (e: Exception) {
return null
}
}
suspend fun getTemp(url: String): DetailWeatherModel {
val request: Request = Request.Builder()
.url(url)
.build()
val responseStr = client.newCall(request).await().body?.string().toString()
val json = Json {
ignoreUnknownKeys = true
}
return json.decodeFromString<DetailWeatherModel>(responseStr)
}
}
The problem is that api.getGeo(urlGeocoding) runs in the current thread. lifecycleScope.launch {} by default has Dispatchers.Main context, so calling api function will run on the Main Thread. To make it run in background thread you need to switch context by using withContext(Dispatchers.IO). It will look like the following:
lifecycleScope.launch {
val geo = withContext(Dispatchers.IO) { api.getGeo(urlGeocoding) }
if (geo != null) {
coordinates["lat"] = geo.lat
coordinates["lon"] = geo.lon
status = true
} else {
status = false
}
when(status) { ... }
}
You are already using coroutines. The problem is that lifecycleScope is tied to main thread. You want to replace it with GlobalScope or coroutineScope (latter is better in terms of complex project, but I assume you are writing pet-project now, so GlobalScope.launch will be fine)
you should replace
lifecycleScope.launch{
with
lifecycleScope.launch(Dispatchers.IO){
Related
It is recommended to not use GlobalScope and runBlocking.
I have implemented changes in order to this topic:
End flow/coroutines task before go further null issue
However it doesn't work well as previously with runBlocking. In brief icon doesn't change, data is not on time.
My case is to change icon depending on the boolean.
usecase with Flow
class GetNotificationListItemDetailsUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseFlowUseCase<Unit, List<NotificationItemsResponse.NotificationItemData>>() {
override fun create(params: Unit): Flow<List<NotificationItemsResponse.NotificationItemData>> {
return flow{
emit(notificationDao.readAllData())
}
}
}
viewmodel
val actualNotificationList: Flow<List<NotificationItemsResponse.NotificationItemData>> = getNotificationListItemDetailsUseCase.build(Unit)
fragment
private fun getActualNotificationList() : Boolean {
lifecycleScope.launch {
vm.actualNotificationList
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
notificationData.value = response
val notificationDataString = notificationData.value.toString()
val stringToCheck = "isRead=false"
isNotificationNotRead = (notificationDataString.contains(stringToCheck))
}
}
return isNotificationNotRead
}
on method onViewCreated I have initToolbar to check if it's true and make action, with runBlokcing worked.
fun initToolbar{
if (onReceived) {
Log.d("onReceivedGoes", "GOES IF")
} else {
Log.d("onReceivedGoes", "GOES ELSE")
getActualNotificationList()
}
onReceived = false
val item = menu.findItem(R.id.action_notification_list)
when {
isNotificationNotRead && !isOutcomed -> {
item.setIcon(R.drawable.image_icon_change)
}
}
coroutine job before change, it worked well
val job = GlobalScope.launch {
vm.getNotificationListItemDetailsUseCase.build(Unit).collect {
notificationData.value = it
val notificationDataString = notificationData.value.toString()
val stringToCheck = "isRead=false"
isNotificationNotRead = (notificationDataString.contains(stringToCheck))
}
}
runBlocking {
job.join()
}
}
Another question is I have the same thing to do in MainActivity, but I do not use there a flow just suspend function.
UseCase
class UpdateNotificationListItemUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseUpdateBooleanUseCase<Int, Boolean, Boolean, Boolean, Unit>() {
override suspend fun create(itemId: Int, isRead: Boolean, isArchived: Boolean, isAccepted: Boolean){
notificationDao.updateBooleans(itemId, isRead, isArchived, isAccepted)
}
}
MainActivity
val job = GlobalScope.launch { vm.getIdWithUpdate() }
runBlocking {
job.join()
}
MainViewmodel
suspend fun getIdWithUpdate() {
var id = ""
id = notificationAppSessionStorage.getString(
notificationAppSessionStorage.getIncomingKeyValueStorage(),
""
)
if (id != "") {
updateNotificationListItemUseCase.build(id.toInt(), true, false, false)
}
}
}
EDIT1:
collect in fragments works perfectly, thanks
What about MainActivity and using this usecase with suspend fun without flow.
I have read documentation https://developer.android.com/kotlin/coroutines/coroutines-best-practices
val IODispatcher: CoroutineDispatcher = Dispatchers.IO
val externalScope: CoroutineScope = CoroutineScope(IODispatcher)
suspend {
externalScope.launch(IODispatcher) {
vm.getIdWithUpdate()
}.join()
}
Second option, but here I do not wait until job is done
suspend {
withContext(Dispatchers.IO) {
vm.getIdWithUpdate()
}
}
What do you think about it?
You can try to update the icon in the collect block:
private fun getActualNotificationList() = lifecycleScope.launch {
vm.actualNotificationList
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
notificationData.value = response
val notificationDataString = notificationData.value.toString()
val stringToCheck = "isRead=false"
val isNotificationNotRead = (notificationDataString.contains(stringToCheck))
val item = menu.findItem(R.id.action_notification_list)
when {
isNotificationNotRead && !isOutcomed -> {
item.setIcon(R.drawable.image_icon_change)
}
}
}
}
Using runBlocking you are blocking the Main Thread, which may cause an ANR.
I want to use coroutines in my project only when I use coroutines I get the error :Unable to invoke no-args constructor. I don't know why it's given this error. I am also new to coroutines.
here is my apiclient class:
class ApiClient {
val retro = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
Here is my endpoint class:
#GET("v2/venues/search")
suspend fun get(
#Query("near") city: String,
#Query("limit") limit: String = Constants.limit,
#Query("radius") radius: String = Constants.radius,
#Query("client_id") id: String = Constants.clientId,
#Query("client_secret") secret: String = Constants.clientSecret,
#Query("v") date: String
): Call<VenuesMainResponse>
my Repository class:
class VenuesRepository() {
private val _data: MutableLiveData<VenuesMainResponse?> = MutableLiveData(null)
val data: LiveData<VenuesMainResponse?> get() = _data
suspend fun fetch(city: String, date: String) {
val retrofit = ApiClient()
val api = retrofit.retro.create(VenuesEndpoint::class.java)
api.get(
city = city,
date = date
).enqueue(object : Callback<VenuesMainResponse>{
override fun onResponse(call: Call<VenuesMainResponse>, response: Response<VenuesMainResponse>) {
val res = response.body()
if (response.code() == 200 && res != null) {
_data.value = res
} else {
_data.value = null
}
}
override fun onFailure(call: Call<VenuesMainResponse>, t: Throwable) {
_data.value = null
}
})
}
}
my ViewModel class:
class VenueViewModel( ) : ViewModel() {
private val repository = VenuesRepository()
fun getData(city: String, date: String): LiveData<VenuesMainResponse?> {
viewModelScope.launch {
try {
repository.fetch(city, date)
} catch (e: Exception) {
Log.d("Hallo", "Exception: " + e.message)
}
}
return repository.data
}
}
part of activity class:
class MainActivity : AppCompatActivity(){
private lateinit var venuesViewModel: VenueViewModel
private lateinit var adapter: HomeAdapter
private var searchData: List<Venue>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val editText = findViewById<EditText>(R.id.main_search)
venuesViewModel = ViewModelProvider(this)[VenueViewModel::class.java]
venuesViewModel.getData(
city = "",
date = ""
).observe(this, Observer {
it?.let { res ->
initAdapter()
rv_home.visibility = View.VISIBLE
adapter.setData(it.response.venues)
searchData = it.response.venues
println(it.response.venues)
}
})
this is my VenuesMainResponse data class
data class VenuesMainResponse(
val response: VenuesResponse
)
I think the no-args constructor warning should be related to your VenuesMainResponse, is it a data class? You should add the code for it as well and the complete Log details
Also, with Coroutines you should the change return value of the get() from Call<VenuesMainResponse> to VenuesMainResponse. You can then use a try-catch block to get the value instead of using enqueue on the Call.
Check this answer for knowing about it and feel free to ask if this doesn't solve the issue yet :)
UPDATE
Ok so I just noticed that it seems that you are trying to use the foursquare API. I recently helped out someone on StackOverFlow with the foursquare API so I kinda recognize those Query parameters and the Venue response in the code you provided above.
I guided the person on how to fetch the Venues from the Response using the MVVM architecture as well. You can find the complete code for getting the response after the UPDATE block in the answer here.
This answer by me has code with detailed explanation for ViewModel, Repository, MainActivity, and all the Model classes that you will need for fetching Venues from the foursquare API.
Let me know if you are unable to understand it, I'll help you out! :)
RE: UPDATE
So here is the change that will allow you to use this code with Coroutines as well.
Repository.kt
class Repository {
private val _data: MutableLiveData<mainResponse?> = MutableLiveData(null)
val data: LiveData<mainResponse?> get() = _data
suspend fun fetch(longlat: String, date: String) {
val retrofit = Retro()
val api = retrofit.retro.create(api::class.java)
try {
val response = api.get(
longLat = longlat,
date = date
)
_data.value = response
} catch (e: Exception) {
_data.value = null
}
}
}
ViewModel.kt
class ViewModel : ViewModel() {
private val repository = Repository()
val data: LiveData<mainResponse?> = repository.data
fun getData(longLat: String, date: String) {
viewModelScope.launch {
repository.fetch(longLat, date)
}
}
}
api.kt
interface api {
#GET("v2/venues/search")
suspend fun get(
#Query("ll") longLat: String,
#Query("client_id") id: String = Const.clientId,
#Query("client_secret") secret: String = Const.clientSecret,
#Query("v") date: String
): mainResponse
}
MainActivity.kt
private val viewModel by viewModels<ViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.getData(
longLat = "40.7,-74",
date = "20210718" // date format is: YYYYMMDD
)
viewModel.data
.observe(this, Observer {
it?.let { res ->
res.response.venues.forEach { venue ->
val name = venue.name
Log.d("name ",name)
}
}
})
}
}
I am trying to call an API using Retrofit in an Android application using Kotlin. The API requires a
a header and body similar to the following sample input:
Sample Input
Header:
loginName: 65848614-6697-4cf7-a64a-e0b9374c4aee
Body:
clientId: DKOKTrykIQ987yQcLNehT8SWJRMyQLdP
secret: 6Jt1ENlDn9gxPu5f
The content type has to be passed as application/x-www-form-urlencoded.
Currently, I am using the following classes:
YodleeService.kt
interface YodleeService {
#Headers(
"loginName: de5559cc-5375-4aca-8224-990343774c08_ADMIN",
"Api-Version: 1.1",
"Content-Type: application/x-www-form-urlencoded"
)
#POST("auth/token")
fun generateLoginToken(
#Body postBody: PostBody
) : Call<LoginToken>
}
AccountRetriever.kt
class AccountRetriever {
private val service: YodleeService
companion object {
const val BASE_URL = "https://sandbox.api.yodlee.com:443/ysl/"
}
init {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
service = retrofit.create(YodleeService::class.java)
}
fun login(callback: Callback<LoginToken>) {
val postBody = PostBody("TG86ljAk6lt28GYZlRTQWssaLmGpS6jV", "A2P9ZPEqB4uos1nv")
val call = service.generateLoginToken(postBody)
call.enqueue(callback)
}
}
PostBody
data class PostBody(
val clientId: String?,
val secret: String?
)
MainActivity
class MainActivity : AppCompatActivity() {
private val accountRetriever = AccountRetriever()
private val loginCallback = object : Callback<LoginToken> {
override fun onFailure(call: Call<LoginToken>, t: Throwable) {
Log.e("MainActivity", "Problem calling Yodlee API {${t.message}")
}
override fun onResponse(call: Call<LoginToken>?, response: Response<LoginToken>?) {
response?.isSuccessful.let {
Log.i("MainActivity", "errorBody - Content = ${response?.raw()}")
val loginToken = LoginToken(
response?.body()?.accessToken ?: "",
response?.body()?.expiresIn ?: "",
response?.body()?.issuedAt ?: "")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
accountsList.layoutManager = LinearLayoutManager(this)
if (isNetworkConnected()) {
accountRetriever.login(loginCallback)
} else {
AlertDialog.Builder(this).setTitle("No Internet Connection")
.setMessage("Please check your internet connection and try again")
.setPositiveButton(android.R.string.ok) { _, _ -> }
.setIcon(android.R.drawable.ic_dialog_alert).show()
}
}
private fun isNetworkConnected(): Boolean {
val connectivityManager =
getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
return networkCapabilities != null &&
networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
When I debug my application, I am getting a response of [size=213 text=\n {\n "errorCode": "Y303",\n …]. The documentation for the API says that this error code means the clientId or secret is missing.
When I dig through the debugger, I am seeing that the raw call reads as
Request {
method = POST, url = https: //sandbox.api.yodlee.com/ysl/auth/token,
tags = {
class retrofit2.Invocation =
com.example.budgettracker.api.YodleeService.generateLoginToken()[PostBody(
clientId = TG86ljAk6lt28GYZlRTQWssaLmGpS6jV, secret = A2P9ZPEqB4uos1nv)]
}
}
I cannot determine why the API is not seeing the POST body contents.
Any help would be greatly appreciated.
I have an API where I get a list of addresses from postcode. Here is where I make the API call, and return a MutableLiveData object that I then try to observe
fun getAddressFromPostCode(postCode: String): MutableLiveData<List<Address>>{
val trimmedPostCode = postCode.replace("\\s".toRegex(),"").trim()
val dataBody = JSONObject("""{"postcode":"$trimmedPostCode"}""").toString()
val hmac = HMAC()
val hmacResult = hmac.sign(RequestConstants.CSSecretKey, dataBody)
val postData = PostCodeBodyData()
postData.postcode = trimmedPostCode
val body = PostCodeRequestData()
body.dataSignature = hmacResult
body.data = postData
//val myBodyModel = MyBodyModel(data = dataBody, data_signature = hmacResult)
val url = RequestConstants.CS_BASE_URL
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.build()
val api:GetAddressAPIService = retrofit.create(GetAddressAPIService ::class.java)
val myCall: Call<GetAddressResponse> = api.getPostCodeAddress(body)
myCall.enqueue(object : Callback<GetAddressResponse> {
override fun onFailure(call: Call<GetAddressResponse>?, t: Throwable?) {
Log.d("RegistrationInteractor", "Something went wrong", t)
Log.d("RegistrationInteractor", call.toString())
}
override fun onResponse(call: Call<GetAddressResponse>?, response: Response<GetAddressResponse>?) {
// Success response
if(response?.body()?.success == 1){
addressLiveData.postValue(response!!.body()!!.addresses)
}else{
Log.d("", response!!.body()?.serError?.errorDescription)
addressLiveData.postValue(null)
}
}
})
return addressLiveData
}
I then listen for this in my PostCodeFragment:
btn_find_address.setOnClickListener {
val interactor = RegistrationInteractor()
addresses = interactor.getAddressFromPostCode(et_register_postcode.text.toString())
lifecycle.addObserver(interactor)
addresses.observe( viewLifecycleOwner, Observer {
#Override
fun onChanged(newAddresses: List<Address>) {
//Set UI
addressList.addAll(newAddresses)
adapter.notifyDataSetChanged()
}
})
}
However onChanged never gets called. Why is this happening?
You need to do like below
val interactor = RegistrationInteractor()
interactor.addressLiveData.observe( viewLifecycleOwner, Observer {
#Override
fun onChanged(newAddresses: List<Address>) {
//Set UI
addressList.addAll(newAddresses)
adapter.notifyDataSetChanged()
}
})
Update
Do not return your MutableLiveData<List<Address>> object from your method. Just get a direct instance of your addressLiveData and use it to observe.
I have an activity to perform rest API everytime it opened and i use MVVM pattern for this project. But with this snippet code i failed to get updated everytime i open activity. So i debug all my parameters in every line, they all fine the suspect problem might when apiService.readNewsAsync(param1,param2) execute, my postValue did not update my resulRead parameter. There were no crash here, but i got result which not updated from result (postValue). Can someone explain to me why this happened?
Here what activity looks like
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityReadBinding>(this,
R.layout.activity_read).apply {
this.viewModel = readViewModel
this.lifecycleOwner = this#ReadActivity
}
readViewModel.observerRead.observe(this, Observer {
val sukses = it.isSuccess
when{
sukses -> {
val data = it.data as Read
val article = data.article
//Log.d("-->", "${article.toString()}")
}
else -> {
toast("ada error ${it.msg}")
Timber.d("ERROR : ${it.msg}")
}
}
})
readViewModel.getReadNews()
}
Viewmodel
var observerRead = MutableLiveData<AppResponse>()
init {
observerRead = readRepository.observerReadNews()
}
fun getReadNews() {
// kanal and guid i fetch from intent and these value are valid
loadingVisibility = View.VISIBLE
val ok = readRepository.getReadNews(kanal!!, guid!!)
if(ok){
loadingVisibility = View.GONE
}
}
REPOSITORY
class ReadRepositoryImpl private constructor(private val newsdataDao: NewsdataDao) : ReadRepository{
override fun observerReadNews(): MutableLiveData<AppResponse> {
return newsdataDao.resultRead
}
override fun getReadNews(channel: String, guid: Int) = newsdataDao.readNews(channel, guid)
companion object{
#Volatile private var instance: ReadRepositoryImpl? = null
fun getInstance(newsdataDao: NewsdataDao) = instance ?: synchronized(this){
instance ?: ReadRepositoryImpl(newsdataDao).also {
instance = it
}
}
}
}
MODEL / DATA SOURCE
class NewsdataDao {
private val apiService = ApiClient.getClient().create(ApiService::class.java)
var resultRead = MutableLiveData<AppResponse>()
fun readNews(channel: String, guid: Int): Boolean{
GlobalScope.launch {
val response = apiService.readNewsAsync(Constants.API_TOKEN, channel, guid.toString()).await()
when{
response.isSuccessful -> {
val res = response.body()
val appRes = AppResponse(true, "ok", res!!)
resultRead.postValue(appRes)
}
else -> {
val appRes = AppResponse(false, "Error: ${response.message()}", null)
resultRead.postValue(appRes)
}
}
}
return true
}
}
Perhaps this activity is not getting stopped.
Check this out:
When you call readViewModel.getReadNews() in onCreate() your activity is created once, only if onStop is called will it be created again.