i have more than 5 api calls in one fragment. Doing this, the app get slower in performance loading.
So i planned to make it run parallel with kotlin. How to use executors, threads in my code.
I implemented api in below format.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.callApiOne()
viewModel.callApiTwo()
viewModel.callApiThree()
viewModel.getResponseInViewModel().observe(this.viewLifecycleOwner, Observer {
if (it.errorResponse?.code() == 200) {
}
})
}
ViewModel.kt
fun callApiOne() {
repository.callApiOne()
}
fun getResponseInViewModel(): MutableLiveData<Resource<Model>> {
respp = repository.getResponse()
return respp
}
Repository.kt
private val resp by lazy { MutableLiveData<Resource<Model>>() }
fun callApiOne() {
val api = AppMain.restClient?.services?.callApiOne()
api?.enqueue(object : Callback<Model> {
override fun onFailure(call: Call<Model>, t: Throwable) {
resp.postValue(Resource.failure(t.message!!, null))
}
override fun onResponse(
call: Call<Model>,
response: Response<Model>
) {
if (response.isSuccessful) {
resp.postValue(Resource.successResp(response))
} else {
resp.postValue(Resource.errorresponse(response))
}
}
})
}
fun getResponse(): MutableLiveData<Resource<Model>> = resp
You are supposed to use coroutines in this case.
Example:
MyFragment : Fragment(), CoroutineScope by MainScope() {
...
private fun init() {
launch { // starts a coroutine on main thread
viewModel.fooSuspend()
}
}
}
MyViewModel : ViewModel() {
...
suspend fun fooSuspend() {
withContext(Dispatchers.IO) { //do on IO Thread
doIOHaveOperation()
}
}
}
Related
I'm new to kotlin coroutines. I've been trying to run multiple API calls in parallel and then when all the calls are done update my UI and dismiss the loader, but with no success. This is my code
private fun getScoreForType() {
val job = CoroutineScope(Dispatchers.IO).launch {
types.forEach { type ->
getScore(type)
}
}
runBlocking {
job.join()
// do some ui work
dismissLoader()
}
}
private fun getScore(type: String) {
val call = MyApi.getScores(type)
call.enqueue(object : Callback<Score> {
override fun onResponse(call: Call<Score>, response: Response<Score>) {
setScore(response)
}
override fun onFailure(call: Call<Score>, t: Throwable) {
}
})
}
I've also tried using async and awaitAll but couldn't make it work either. The loader is always dismissed before all the calls are done. Any help on how I could make this work would be much appreciated
Use Flow and collectData it will works as LiveData.
For example:
val myIntFlow = MutableStateFlow(-1)
Try something like;
in ViewModelMethods
private fun getScoreForType() {
It goes first:
CoroutineScope(Dispatchers.IO).launch {
types.forEach { type ->
getScore(type)
}
// it means to change value of flow
myIntFlow.value = 1
}
// Now collect data in fragment to change UI
}
// in fragment like:
CoroutineScope(Dispatchers.Main).launch {
// flow will be triggered, on every changed value
viewModel.myIntFlow.collect {
viewModel.methodFromViewModelToChangeUI()
dissmisloader()
myIntFlow.value = -1
}
}
// try the same here as you wish
private fun getScore(type: String) {
val call = MyApi.getScores(type)
call.enqueue(object : Callback<Score> {
override fun onResponse(call: Call<Score>, response: Response<Score>) {
setScore(response)
}
override fun onFailure(call: Call<Score>, t: Throwable) {
}
})
}
my data is fetched only when it is created...im using viewmodel...when press back button it doesnt update the previous data..onresume is not working in this...
i refered this but none of those helped--> Reacting to activity lifecycle in ViewModel
i need help
thanks in advance
activity:--
class MyAccount : BaseClassActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
var mActionBarToolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable);
setSupportActionBar(mActionBarToolbar);
setEnabledTitle()
val resetbutton=findViewById<Button>(R.id.resetpwd)
resetbutton.setOnClickListener {
val i=Intent(applicationContext,
ResetPasswordActivity::class.java)
startActivity(i)
}
val editbutton=findViewById<Button>(R.id.editdetail)
editbutton.setOnClickListener {
val i=Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
hello()
}
override fun onResume() {
super.onResume()
hello()
}
fun hello(){
val first_name = findViewById<TextView>(R.id.firstname)
val last_name = findViewById<TextView>(R.id.lastname)
val emailuser = findViewById<TextView>(R.id.emailuser)
val phone_no = findViewById<TextView>(R.id.phone_no)
val birthday = findViewById<TextView>(R.id.birthday)
val image=findViewById<ImageView>(R.id.imageprofile)
val model = ViewModelProvider(this)[MyAccountViewModel::class.java]
model.viewmodel?.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext).load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}}
viewmodel:--
class MyAccountViewModel(context: Application) :AndroidViewModel(context),LifecycleObserver{
private var MyAccountViewModels: MutableLiveData<My_account_base_response>? = null
val viewmodel: MutableLiveData<My_account_base_response>?
get() {
if (MyAccountViewModels == null) {
MyAccountViewModels = MutableLiveData<My_account_base_response>()
loadviewmodel()
}
return MyAccountViewModels
}
private fun loadviewmodel(){
val token :String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
MyAccountViewModels!!.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}}
There are bunch of things wrong here, so let me provide you refactored code and explanation as much as I would be able to..
Activity:
class MyAccount : BaseClassActivity() {
private val mActionBarToolbar by lazy { findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbartable) }
private val resetbutton by lazy { findViewById<Button>(R.id.resetpwd) }
private val editbutton by lazy { findViewById<Button>(R.id.editdetail) }
private val first_name by lazy { findViewById<TextView>(R.id.firstname) }
private val last_name by lazy { findViewById<TextView>(R.id.lastname) }
private val emailuser by lazy { findViewById<TextView>(R.id.emailuser) }
private val phone_no by lazy { findViewById<TextView>(R.id.phone_no) }
private val birthday by lazy { findViewById<TextView>(R.id.birthday) }
private val image by lazy { findViewById<ImageView>(R.id.imageprofile) }
lateinit var model: MyAccountViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.myaccount)
setSupportActionBar(mActionBarToolbar)
setEnabledTitle()
model = ViewModelProvider(this)[MyAccountViewModel::class.java]
resetbutton.setOnClickListener {
val i = Intent(applicationContext, ResetPasswordActivity::class.java)
startActivity(i)
}
editbutton.setOnClickListener {
val i = Intent(applicationContext, EditProfile::class.java)
startActivity(i)
}
model.accountResponseData.observe(this, object : Observer<My_account_base_response> {
override fun onChanged(t: My_account_base_response?) {
first_name.setText(t?.data?.user_data?.first_name)
last_name.setText(t?.data?.user_data?.last_name)
emailuser.setText(t?.data?.user_data?.email)
phone_no.setText(t?.data?.user_data?.phone_no).toString()
birthday.setText(t?.data?.user_data?.dob).toString()
Glide.with(applicationContext)
.load(t?.data?.user_data?.profile_pic)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.ic_launcher_foreground)
.into(image)
}
})
}
override fun onResume() {
super.onResume()
model.loadAccountData()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
Few notes on your activity class:
You don't need to findViewById everytime, just do it once during onCreate or do it lazily. (FYI consider using kotlin synthetics or view binding or data binding)
Initialize your viewModel during onCreate method only. (That's the best way to do it)
Also observer your LiveData from ViewModel once, it should be also from the onCreate as it's the entry point to the activity and apart from config changes this method called only once. So, it's safe to observe it over there rather than during onResume which will be called multiple times during activity lifecycle. (The main issue your code wasn't working, so as a fix you only call your API method from ViewModel during resume)
ViewModel:
class MyAccountViewModel(context: Application) : AndroidViewModel(context) {
private val _accountResponseData = MutableLiveData<My_account_base_response?>()
val accountResponseData: MutableLiveData<My_account_base_response?>
get() = _accountResponseData
init {
loadAccountData()
}
fun loadAccountData() {
val token: String = SharedPrefManager.getInstance(getApplication()).user.access_token.toString()
RetrofitClient.instance.fetchUser(token)
.enqueue(object : Callback<My_account_base_response> {
override fun onFailure(call: Call<My_account_base_response>, t: Throwable) {
Log.d("res", "" + t)
_accountResponseData.value = null
}
override fun onResponse(
call: Call<My_account_base_response>,
response: Response<My_account_base_response>
) {
var res = response
if (res.body()?.status == 200) {
_accountResponseData.value = response.body()
} else {
try {
val jObjError =
JSONObject(response.errorBody()!!.string())
Toast.makeText(
getApplication(),
jObjError.getString("user_msg"),
Toast.LENGTH_LONG
).show()
} catch (e: Exception) {
Log.e("errorrr", e.message)
}
}
}
})
}
}
Don't make initial API call along with LiveData creation, it's okay to do in most of cases but if you're updating LiveData on response of that call then it's good to make it separately like during init block.
It's good practice not to allow Ui (Activity/Fragments) to modify LiveDatas of ViewModel directly. So, that's good sign you're following such pattern by having private MutableLiveData exposed as public LiveData, but do it correctly as suggested.
Side note: Your view model doesn't need to be LifecycleObserver. LifecycleObserver is used for some custom class/component which needs to be managed by their self by silently observing/depending on activity lifecycle independently. That's not the use case of ViewModel.
The only thing that I found why your code wasn't working correctly is because you were creating & observing ViewModel & LiveData over & over again as new objects from onResume method where you called hello() method.
Let me know if something don't make sense or missing.
I have this function in Kotlin:
class DictionaryWorker constructor(
context: Context,
private val workerParameters: WorkerParameters,
private val apiInterface: ApiInterface
) :
KneuraWorker(context, workerParameters), BaseDataSource {
private var isJobSuccess: Boolean = false
override suspend fun doWorkerJob(): Result = withContext(Dispatchers.IO) {
val call = apiInterface.downloadDictionaryFille(DICTIONARY_FILE_URL)
call!!.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>?,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
} else {
Log.d("TAG", "server contact failed")
isJobSuccess = false
}
}
override fun onFailure(call: Call<ResponseBody?>?, t: Throwable?) { }
})
return#withContext if (isJobSuccess)
Result.success()
else
Result.failure()
}
}
What is currently happening:
Before this block-1 below
call!!.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>?,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
} else {
Log.d("TAG", "server contact failed")
isJobSuccess = false
}
}
override fun onFailure(call: Call<ResponseBody?>?, t: Throwable?) { }
})
This block-2 executes
return#withContext if (isJobSuccess)
Result.success()
else
Result.failure()
What I am trying to do
Make sure only after block 1 is executed block 2 is executed
Not sure what call!!.enqueue() does, but it's quite likely that it starts another thread and performs it's work asynchronously.
So block 2 is not waiting till block 1 is done.
A really ugly way (which I don't recommend) handling this would be using a CountDownLatch.
But I'd rather add a callback to doWorkerJob():
override fun doWorkerJob(callback: (Result) -> Unit) {
val call = apiInterface.downloadDictionaryFille(DICTIONARY_FILE_URL)
if (call == null) {
callback(Result.failure())
}
call?.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>?,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
callback(Result.success())
} else {
Log.d("TAG", "server contact failed")
callback(Result.failure())
}
}
override fun onFailure(call: Call<ResponseBody?>?, t: Throwable?) {
callback(Result.failure())
}
})
}
I want to make my network request synchronous because the input of second request comes from the output of first request.
override fun onCreate(savedInstanceState: Bundle?) {
retrofit1 =Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/").addConverterFactory(GsonConverterFactory.create()).build()
retrofit2 =Retrofit.Builder()
.baseUrl("https://samples.openweathermap.org/").addConverterFactory(GsonConverterFactory.create()).build()
button.setOnClickListener { view ->
CoroutineScope(IO).launch {
fakeApiRequest()
}}
In my fakeApiRequest(),I am making two network request.
private suspend fun fakeApiRequest() {
val result1 :Geo?= getResult1FromApi()
val result2: Long? = getResult2FromApi(result1)}
Since,this is an asynchronous call,I am getting Null Pointer Exception in my getResult2FromApi(result1) method because the argument passed is null.
In order to fix this issue,I had to add delay(1500) in first call.
private suspend fun getResult1FromApi(): Geo? {
val service:CallService = retrofit1!!.create(CallService::class.java)
val call = service.getUsers()
call.enqueue(object : Callback<List<User>> {
override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
g = users.get(0).address.geo
}
override fun onFailure(call: Call<List<User>>, t: Throwable) {
}
})
delay(1500)
return g
}
-----------------------------------------------------------
private suspend fun getResult2FromApi(result1: Geo?): Long? {
val service2:CallService = retrofit2!!.create(CallService::class.java)
val call2 = service2.getWeather(result1?.lat!!, result1.lng,"b6907d289e10d714a6e88b30761fae22")
call2.enqueue(object : Callback<WeatherData> {
override fun onResponse(call: Call<WeatherData>, response: Response<WeatherData>) {
}
override fun onFailure(call: Call<WeatherData>, t: Throwable) {
}
})
return dt
}
Is there anyway I can make this synchronous, so that I don't have to pass any delay time.
You haven't implemented the suspendable function correctly. You must use suspendCoroutine:
suspend fun getResult1FromApi(): Geo? = suspendCoroutine { continuation ->
val service = retrofit1!!.create(CallService::class.java)
service.getUsers().enqueue(object : Callback<List<User>> {
override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
continuation.resume(response.result.getOrNull(0)?.address?.geo)
}
override fun onFailure(call: Call<List<User>>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
Now your function is synchronous and returns a Geo object.
I want to abstract the retrofit call, so I don't need to write the same boiler plate code when I need a request.
The abstraction
open class NetworkCall<T> {
lateinit var call: Call<T>
var result: MutableLiveData<Resource<T>> = MutableLiveData()
fun makeCall(call: Call<T>) {
this.call = call
val callBackKt = CallBackKt<T>()
callBackKt.result.value = Resource.loading(null)
this.call.enqueue(callBackKt)
result = callBackKt.result
}
class CallBackKt<T> : Callback<T> {
var result: MutableLiveData<Resource<T>> = MutableLiveData()
override fun onFailure(call: Call<T>, t: Throwable) {
result.value = Resource.error()//APIError()
t.printStackTrace()
}
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
result.value = Resource.success(response.body())
}
else {
result.value = Resource.error()
}
}
}
}
Repository
class DetailsRepository(val application: Application) {
private val myListDao =
AppDatabase.getDatabase(application)?.MyListDao()
var movie: MutableLiveData<Resource<DetailsDTO>> = MutableLiveData()
fun getDetails(id: Int) {
val networkCall = NetworkCall<DetailsDTO>()
networkCall.makeCall(Apifactory.tmdbApi.getDetails(id))
movie = networkCall.result
}
}
ViewModel
class DetailsViewModel(application: Application) : AndroidViewModel(application) {
private val repository: DetailsRepository = DetailsRepository(application)
internal var movie: LiveData<Resource<DetailsDTO>> = repository.movie
fun insert(myListItem: MyListItem) {
repository.insert(myListItem)
}
fun fetchDetails(id: Int) {
repository.getDetails(id)
}
}
Activity
viewModel.movie.observe(this, Observer {
it?.apply {
detail_title.text = data?.title
}
})
Log with in every method says it's being called in right order and that the request is working fine. Althoght onChange is not being called.
fetchDetails called
getDetails called
makeCall called
onResponse successful, result.value.data = DetailsDTO(adult=false, backdrop_path=/y8lEIjYZCi2VFP4ixtHSn2klpth.jpg, ...
...
What would be done wrong?