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.
Related
I am observing live data from the repository to view model but I am not getting any callback. Why is this so?
MyViewModel.kt
fun incrementPoints(userPoints: UserPoints): LiveData<NetworkResource<StatusResponse>> {
var callbackObserver = MutableLiveData<NetworkResource<StatusResponse>>()
IncrementPointsRepository.incrementPoints(userPoints).observeForever {
//not getting any callback here
callbackObserver.value = it
}
return callbackObserver
}
Repository.kt
object IncrementPointsRepository {
fun incrementPoints(userPoints: UserPoints): LiveData<NetworkResource<StatusResponse>> {
val callbackObserver = MutableLiveData<NetworkResource<StatusResponse>>()
val destinationService = ServiceBuilder.buildService(DestinationService::class.java)
val requestCall = destinationService.incrementPoints(userPoints)
requestCall.enqueue(object : Callback<StatusResponse> {
override fun onFailure(call: Call<StatusResponse>, t: Throwable) {
callbackObserver.value = NetworkResource.error(t)
}
override fun onResponse(call: Call<StatusResponse>, response: Response<StatusResponse>) {
//this is getting called
callbackObserver.value = NetworkResource.success(response)
}
})
return callbackObserver
}
}
incrementPoints function was called from another thread so it wasn't getting observed.
activity.runOnUiThread resolved this
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){
I was trying to build a RecyclerView using a response from Retrofit. But, I ran into an issue that my Recycler turns up empty white while my log shows that I have data in my ArrayList from the network response. (I do not want to set up an MVVM yet until I get comfortable with Kotlin.)
PlaylistRecyclerAdapter
class PlaylistRecyclerAdapter (private val playListNames: Array<String>) :
RecyclerView.Adapter<PlaylistRecyclerAdapter.PlayListViewHolder>() {
// Describes an item view and its place within the RecyclerView
class PlayListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val playlistTextView: TextView = itemView.findViewById(R.id.playlist_name_text)
fun bind(word: String) {
playlistTextView.text = word
}
}
// Returns a new ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlayListViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.playlist_name_item, parent, false)
return PlayListViewHolder(view)
}
// Returns size of data list
override fun getItemCount(): Int {
return playListNames.size
}
// Displays data at a certain position
override fun onBindViewHolder(holder: PlayListViewHolder, position: Int) {
holder.bind(playListNames[position])
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
val templist = getPlaylistItems()
//Log.d("RESPONSE", "onCreate: "+templist.get(0).toString())
recyclerView.adapter = PlaylistRecyclerAdapter(templist.toTypedArray())
recyclerView.adapter?.notifyDataSetChanged()
}
private fun getPlaylistItems(): ArrayList<String> {
var playlisttitles = ArrayList<String>()
var BASE_URL = "https://flicastdemo.s3.amazonaws.com/jwplayer/"
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(HomeWebService::class.java)
val call = service.getHomeContent()
var home = HomeRoot()
call.enqueue(object : Callback<HomeRoot> {
override fun onResponse(call: Call<HomeRoot>, response: Response<HomeRoot>) {
if (response.code() == 200) {
home = response.body()
if(!home.equals(null))
{
//Log.e("HOME", "val: " + home.toString())
for (i in 0 until home.content.size){
val BASE_URL = "https://cdn.jwplayer.com/v2/"
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(PlaylistWebService::class.java)
val call = service.getPlayListItem(home.content.get(i).playlistId) //"1QhdrFVq"
call.enqueue(object : Callback<PlaylistRoot> {
override fun onResponse(call: Call<PlaylistRoot>, response: Response<PlaylistRoot>) {
if (response.code() == 200) {
var playlistinfo : PlaylistRoot = response.body();
playlisttitles.add(playlistinfo.title)
Log.e("PlaylistTitle!", "onResponseTitle: "+playlistinfo.title)
}
}
override fun onFailure(call: Call<PlaylistRoot>, t: Throwable) {
Log.d("NO!NO!NO!", "onResponse: "+"NO!")
playlisttitles.add("No Playlist")
}
})
}
}
}
}
override fun onFailure(call: Call<HomeRoot>, t: Throwable) {
Log.d("NO!NO!NO!", "onResponse: "+"NO!")
}
})
return playlisttitles
}
}
Retrofit returns data in a background thread, so the callback to onResponse() is asynchronous to the UI, i.e. it takes some time until the data comes in; and therefore the getPlaylistItems() method will be returned before the retrofit data is up. And therefore it returns an empty list in val templist = getPlaylistItems().
To fix, this you can create a listener interface, or just build-up the RecyclerView within the onResponse callback:
override fun onResponse(call: Call<PlaylistRoot>, response: Response<PlaylistRoot>) {
if (response.code() == 200) {
var playlistinfo : PlaylistRoot = response.body();
playlisttitles.add(playlistinfo.title)
Log.e("PlaylistTitle!", "onResponseTitle: "+playlistinfo.title)
recyclerView.adapter = PlaylistRecyclerAdapter(playlisttitles.toTypedArray())
recyclerView.adapter?.notifyDataSetChanged()
}
}
I have a button which sync from my external api some data to my android device database, that data is get from two different APIs.
Which would be the best way to make the call to both of them and wait for the response from both APIs and then insert the data to the database?
My Sercice looks like this:
#GET("api/prodotti/fornitori")
fun getFornitori(): Call<List<Fornitori>>
#GET("api/prodotti/pv")
fun getPuntiVendita(): Call<List<PuntiVendita>
Till now i was doing two calls one after another, but at that point i was showing "Sync" snackbar twice and saying twice that the data has been saved, while i would do it once for both API calls...
The call for one of the APIs was like this:
val urlServer = preferenceScreen.sharedPreferences.getString("server", "http://127.0.0.1/")!!
val snackSincronizzo = Snackbar.make(
requireView(),
"Sincronizzo le impostazioni...",
Snackbar.LENGTH_INDEFINITE
)
snackSincronizzo.show()
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val retrofit = Retrofit.Builder()
.baseUrl(urlServer)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
val service = retrofit.create(ApiService::class.java)
val callFornitori = service.getFornitori()
callFornitori.enqueue(object : Callback<List<Fornitori>> {
override fun onResponse(
call: Call<List<Fornitori>>,
response: Response<List<Fornitori>>
) {
if (response.isSuccessful) {
val fornitori = response.body()!!
if (fornitori.isNotEmpty()) {
for (fornitore in fornitori) {
corpoViewModel.insertFornitori(fornitore)
}
val callPuntiVendita = service.getPuntiVendita()
callPuntiVendita.enqueue(object : Callback<List<PuntiVendita>> {
override fun onResponse(
call: Call<List<PuntiVendita>>,
response: Response<List<PuntiVendita>>
) {
if (response.isSuccessful) {
val puntiVendita = response.body()!!
if (puntiVendita.isNotEmpty()) {
for (puntoVendita in puntiVendita) {
corpoViewModel.insertPuntiVendita(puntoVendita)
}
}
customSnack(requireView(), "Impostazioni sincronizzati con successo!", false)
snackSincronizzo.dismiss()
}else {
snackSincronizzo.dismiss()
customSnack(requireView(), "Errore durante la sincronizzazione!",true)
}
}
override fun onFailure(
call: Call<List<PuntiVendita>>,
t: Throwable
) {
TODO("Not yet implemented")
}
})
}
snackSincronizzo.dismiss()
}else {
snackSincronizzo.dismiss()
customSnack(requireView(), "Errore durante la sincronizzazione!",true)
}
}
override fun onFailure(call: Call<List<Fornitori>>, t: Throwable) {
snackSincronizzo.dismiss()
customSnack(requireView(), "Errore durante la sincronizzazione!",true)
}
})
So how can i change it to make both getFornitori and getPuntiVendita and get one callback with both data?
In Rx, You can try this.
Change your service class to Single instead of Call like this
#GET("api/prodotti/fornitori")
fun getFornitori(): Single<List<Fornitori>>
#GET("api/prodotti/pv")
fun getPuntiVendita(): Single<List<PuntiVendita>
And call it like this. We can use Zip operator in RxJava to achieve this
private fun getBothData() {
val fornitorList = service.getFornitori()
val puntiVenditaList = service.getPuntiVendita()
val dispose =
Single.zip<List<Fornitori>, List<PuntiVendita>, Pair<List<Fornitori>, List<PuntiVendita>>>(
fornitorList,
puntiVenditaList,
BiFunction { t1, t2 -> Pair(t1, t2) }
).subscribe(
{
val firstApiData = it.first
val secondApiData = it.second
},
{
//Handle Error Part
})
}
Add all RxJava Dependecies & addCallAdapterFactory(RxJava2CallAdapterFactory.create()) in retrofit builder.
Approach 2 in Kotlin using coroutine
Service class
#GET("api/prodotti/fornitori")
suspend fun getFornitori(): List<Fornitori>
#GET("api/prodotti/pv")
suspend fun getPuntiVendita(): List<PuntiVendita>
Change your method
suspend fun getBothData(): Pair<List<Fornitori>, List<PuntiVendita>>> {
var data: Pair<List<Fornitori>, List<PuntiVendita>> = Pair(listOf(), listOf())
coroutineScope {
val firstAPIData = async { service.getFornitori()}.await()
val seconfAPIData = async { service.getPuntiVendita()}.await()
data = Pair(firstAPIData, seconfAPIData)
return#coroutineScope
}
return data
}
Now from calling the place
GlobalScope.launch {
val bothData = getBothData()
val firstApiData = bothData.first
val secondApiData = bothData.second
}
when I use retrofit2 with no coroutine, the result is null. but when using that with coroutine, the result is right. I think it's the problem of syncronization. but I found something strange
using mutablelivedata, the result is right.
retrofit2 with coroutine
override suspend fun getRetrofit(id : Int): DetailEntity {
withContext(ioDispatcher){
val request = taskNetworkSource.searchItem(id)
val response = request.await()
if(response.body !=null){
Log.d("TAG",""+response.toString())
data = response
}
}
return data
}
good result
D/TAG: DetailEntity(body=DetatilItem(oily_score=6, full_size_image=url, price=54840, sensitive_score=76, description=description, id=5, dry_score=79, title=title), statusCode=200)
retrofit2 with no coroutine
override suspend fun getRetrofit(id : Int): DetailEntity {
taskNetworkSource.searchItem(id).enqueue(object: Callback<DetailEntity> {
override fun onFailure(call: Call<DetailEntity>, t: Throwable) {
}
override fun onResponse(call: Call<DetailEntity>, response: Response<DetailEntity>){
if(response.body()!=null) {
Log.d("TAG",response.toString())
data = response.body()!!
}
}
})
return data
}
bad result
D/TAG: Response{protocol=h2, code=200, message=, url=https://6uqljnm1pb.execute-api.ap-northeast-2.amazonaws.com/prod/products/5}
strange result with mutablelivedata(another project code)
lateinit var dataSet : DetailModel
var data = MutableLiveData<DetailModel>()
fun getDetailRetrofit(id:Int) : MutableLiveData<DetailModel>{
Retrofit2Service.getService().requestIndexItem(id).enqueue(object:
Callback<DetailResponse> {
override fun onFailure(call: Call<DetailResponse>, t: Throwable) {
}
override fun onResponse(call: Call<DetailResponse>, response: Response<DetailResponse>) {
if(response.body()!=null) {
var res = response.body()!!.body
dataSet = DetailModel( res.get(0).discount_cost,
res.get(0).cost,
res.get(0).seller,
res.get(0).description+"\n\n\n",
res.get(0).discount_rate,
res.get(0).id,
res.get(0).thumbnail_720,
res.get(0).thumbnail_list_320,
res.get(0).title
)
data.value = dataSet
}
}
})
return data
}
and this another project code result is right. comparing this code to retrofit2 with no coroutine code, the difference is only mutablelivedata or not. do I have to use asyncronouse library or livedata?
added
data class DetailEntity(val body: DetatilItem,
val statusCode: Int = 0)
data class DetatilItem(val oily_score: Int = 0,
val full_size_image: String = "",
val price: String = "",
val sensitive_score: Int = 0,
val description: String = "",
val id: Int = 0,
val dry_score: Int = 0,
val title: String = "")
retrofit with no coroutine it seem to be no problem.
But, respnose at your code written to log are the completely different object.
with coroutine, response is DetailEntity
with no coroutine, response is Response<DetailEntity>
if you want same log print, try as below
override fun onResponse(call: Call<DetailEntity>, response: Response<DetailEntity>){
if(response.body()!=null) {
Log.d("TAG",response.body()!!.toString())
data = response.body()!!
}
}
Reference
Retrofit - Response<T>