What is the best way to get data from an API using retrofit when something needs to wait on that data? - kotlin

I have a retrofit API call but am having trouble getting the data out of it:
This is in a class file that's not a viewModel or Fragment. It's called from the apps main activity view model. I need to be able to get the data from the API and wait for some processing to be done on it before returning the value back the view model. Newer to kotlin and struggling with all the watchers and async functions. The result of this an empty string is the app crashes, because it's trying to access data before it has a value.
From class getData which is not a fragment
private lateinit var data: Data
fun sync(refresh: Boolean = false): List<String> {
var info = emptyList<String>
try {
getData(::processData, ::onFailure)
info = data.info
} catch(e: Throwable){
throw Exception("failed to get data")
}
}
}
return info
}
fun getData(
onSuccess: KFunction1<ApiResponse>?, Unit>,
onFailed: KFunction1<Throwable, Unit>
) {
val client = ApiClient().create(Service.RequestData)
val request = client.getData()
request.enqueue(object : Callback<ApiResponse> {
override fun onResponse(
call: Call<ApiResponse>,
response: Response<ApiResponse>
) {
onSuccess(response.body())
}
override fun onFailure(call: Call<RegistryResponse<GlobalLanguagePack>>, t: Throwable) {
onFailed(Exception("failed to get data"))
}
})
}
private fun processData(body: ApiResponse?) {
requireNotNull(body)
data = body.data
}
```
From appViewModel.kt:
```
fun setUpStuff(context: Context, resources: AppResources) = viewModelScope.launch {
val stuff = try {
getData.sync()
} catch (e: Exception) {
return#launch
}
if (stuff.isEmpty()) return#launch
}
```

Related

How to handle Kotlin Jetpack Paging 3 exceptions?

I am new to kotlin and jetpack, I am requested to handle errors (exceptions) coming from the PagingData, I am not allowed to use Flow, I am only allowed to use LiveData.
This is the Repository:
class GitRepoRepository(private val service: GitRepoApi) {
fun getListData(): LiveData<PagingData<GitRepo>> {
return Pager(
// Configuring how data is loaded by adding additional properties to PagingConfig
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false
),
pagingSourceFactory = {
// Here we are calling the load function of the paging source which is returning a LoadResult
GitRepoPagingSource(service)
}
).liveData
}
}
This is the ViewModel:
class GitRepoViewModel(private val repository: GitRepoRepository) : ViewModel() {
private val _gitReposList = MutableLiveData<PagingData<GitRepo>>()
suspend fun getAllGitRepos(): LiveData<PagingData<GitRepo>> {
val response = repository.getListData().cachedIn(viewModelScope)
_gitReposList.value = response.value
return response
}
}
In the Activity I am doing:
lifecycleScope.launch {
gitRepoViewModel.getAllGitRepos().observe(this#PagingActivity, {
recyclerViewAdapter.submitData(lifecycle, it)
})
}
And this is the Resource class which I created to handle exceptions (please provide me a better one if there is)
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
As you can see I am using Coroutines and LiveData. I want to be able to return the exception when it occurs from the Repository or the ViewModel to the Activity in order to display the exception or a message based on the exception in a TextView.
Your GitRepoPagingSource should catch retryable errors and pass them forward to Paging as a LoadResult.Error(exception).
class GitRepoPagingSource(..): PagingSource<..>() {
...
override suspend fun load(..): ... {
try {
... // Logic to load data
} catch (retryableError: IOException) {
return LoadResult.Error(retryableError)
}
}
}
This gets exposed to the presenter-side of Paging as LoadState, which can be reacted to via LoadStateAdapter, .addLoadStateListener, etc as well as .retry. All of the presenter APIs from Paging expose these methods, such as PagingDataAdapter: https://developer.android.com/reference/kotlin/androidx/paging/PagingDataAdapter
You gotta pass your error handler to the PagingSource
class MyPagingSource(
private val api: MyApi,
private val onError: (Throwable) -> Unit,
): PagingSource<Int, MyModel>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, YourModel> {
try {
...
} catch(e: Exception) {
onError(e) // <-- pass your error listener here
}
}
}

How to ignore empty database result for the first time and wait for server result in application?

My app using room as a database and retrofit as a network calling api.
i am observing database only as a single source of truth. every thing is working fine. But i am not finding solution of one scenario.
Like for the first time when user open app it do following operations
fetch data from db
fetch data from server
because currently database is empty so it sends empty result to observer which hide progress bar . i want to discard that event and send result to observer when server dump data to database. even server result is empty. so progress bar should always hide once their is confirmation no data exists.
in other words application should always rely on database but if it empty then it should wait until server response and then notify observer.
this is my code
observer
viewModel.characters.observe(viewLifecycleOwner, Observer {
Log.e("status is ", "${it.message} at ${System.currentTimeMillis()}")
when (it.status) {
Resource.Status.SUCCESS -> {
binding.progressBar.visibility = View.GONE
if (!it.data.isNullOrEmpty()) adapter.setItems(ArrayList(it.data))
}
Resource.Status.ERROR -> {
Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show()
binding.progressBar.visibility = View.GONE
}
Resource.Status.LOADING ->
binding.progressBar.visibility = View.VISIBLE
}
})
ViewModel
#HiltViewModel
class CharactersViewModel #Inject constructor(
private val repository: CharacterRepository
) : ViewModel() {
val characters = repository.getCharacters()
}
Repository
class CharacterRepository #Inject constructor(
private val remoteDataSource: CharacterRemoteDataSource,
private val localDataSource: CharacterDao
) {
fun getCharacters() : LiveData<Resource<List<Character>>> {
return performGetOperation(
databaseQuery = { localDataSource.getAllCharacters() },
networkCall = { remoteDataSource.getCharacters() },
saveCallResult = { localDataSource.insertAll(it.results) }
)
}
}
Utility function for all api and database handling
fun <T, A> performGetOperation(databaseQuery: () -> LiveData<T>,
countQuery: () -> Int,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val source = databaseQuery().map { Resource.success(it,"database") }.distinctUntilChanged()
emitSource(source)
val responseStatus = networkCall()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Resource.error(responseStatus.message!!))
}
}
LocalDataSource
#Dao
interface CharacterDao {
#Query("SELECT * FROM characters")
fun getAllCharacters() : LiveData<List<Character>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(characters: List<Character>)
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(character: Character)
}
DataSource
class CharacterRemoteDataSource #Inject constructor(
private val characterService: CharacterService
): BaseDataSource() {
suspend fun getCharacters() = getResult { characterService.getAllCharacters() }}
}
Base Data Source
abstract class BaseDataSource {
protected suspend fun <T> getResult(call: suspend () -> Response<T>): Resource<T> {
try {
Log.e("status is", "started")
val response = call()
if (response.isSuccessful) {
val body = response.body()
if (body != null) return Resource.success(body,"server")
}
return error(" ${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}
private fun <T> error(message: String): Resource<T> {
Timber.d(message)
return Resource.error("Network call has failed for a following reason: $message")
}
}
Character Service
interface CharacterService {
#GET("character")
suspend fun getAllCharacters() : Response<CharacterList>
}
Resource
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T,message : String): Resource<T> {
return Resource(Status.SUCCESS, data, message)
}
fun <T> error(message: String, data: T? = null): Resource<T> {
return Resource(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(Status.LOADING, data, "loading")
}
}
}
CharacterList
data class CharacterList(
val info: Info,
val results: List<Character>
)
What is the best way by that i ignore database if it is empty and wait for server response and then notify observer

My first observer called correctly but another was not called after inserted data to room database in kotlin android

In the application, I am fetching data from the web and from the observer change method, Insert that data to local db. that's fine. but after inserted to db, My second observer not called so my UI will not update.
ManActivity.class
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var adapter: MainAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layout.activity_main)
setupViewModel()
setupUI()
setupObservers()
setupObservers2()
}
private fun setupViewModel() {
viewModel = ViewModelProviders.of(
this,
ViewModelFactory(ApiHelper(RetrofitBuilder.apiService))
).get(MainViewModel::class.java)
}
private fun setupUI() {
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = MainAdapter(arrayListOf())
recyclerView.addItemDecoration(
DividerItemDecoration(
recyclerView.context,
(recyclerView.layoutManager as LinearLayoutManager).orientation
)
)
recyclerView.adapter = adapter
}
private fun setupObservers() {
viewModel.getUsers().observe(this, Observer {
//viewModel.getUserFromWeb()
it?.let { resource ->
when (resource.status) {
SUCCESS -> {
Log.d("MYLOG","MyAPIChange success")
recyclerView.visibility = View.VISIBLE
progressBar.visibility = View.GONE
resource.data?.let {
users -> viewModel.setUserListToDB(this,users)
//sleep(1000)
}
}
ERROR -> {
recyclerView.visibility = View.VISIBLE
progressBar.visibility = View.GONE
Log.d("MYLOG","MyAPIChange error")
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
}
LOADING -> {
Log.d("MYLOG","MyAPIChange loading")
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
}
}
})
}
private fun setupObservers2() {
viewModel.getUserFromDB(this).observe(this, Observer {
users -> retrieveList(users)
Log.d("MYLOG","..MyDBChange")
})
}
private fun retrieveList(users: List<User>) {
adapter.apply {
addUsers(users)
notifyDataSetChanged()
}
}
}
MyViewModel.class
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {
//lateinit var tempUser : MutableLiveData<List<User>>
fun getUsers() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
}
//emit(mainRepository.getUsers()) //direct call
}
fun getUserFromDB(context: Context) = liveData(Dispatchers.IO) {
emit(mainRepository.getUserList(context))
}
fun setUserListToDB(context: Context, userList: List<User>) {
/*GlobalScope.launch {
mainRepository.setUserList(context, userList)
}*/
CoroutineScope(Dispatchers.IO).launch {
mainRepository.setUserList(context, userList)
}
}
}
MyRepository.class
class MainRepository(private val apiHelper: ApiHelper) {
suspend fun getUsers() = apiHelper.getUsers() // get from web
companion object {
var myDatabase: MyDatabase? = null
lateinit var userList: List<User>
fun initializeDB(context: Context): MyDatabase {
return MyDatabase.getDataseClient(context)
}
/*fun insertData(context: Context, username: String, password: String) {
myDatabase = initializeDB(context)
CoroutineScope(Dispatchers.IO).launch {
val loginDetails = User(username, password)
myDatabase!!.myDao().InsertData(loginDetails)
}
}*/
}
//fun getUserList(context: Context, username: String) : LiveData<LoginTableModel>? {
suspend fun getUserList(context: Context) : List<User> {
myDatabase = initializeDB(context)
userList = myDatabase!!.myDao().getUserList()
Log.d("MYLOG=", "DBREAD"+userList.size.toString())
return userList
}
fun setUserList(context: Context,userList: List<User>){
myDatabase = initializeDB(context)
/*CoroutineScope(Dispatchers.IO).launch {
myDatabase!!.myDao().InsertAllUser(userList)
Log.d("MYLOG","MyDBInserted")
}*/
myDatabase!!.myDao().InsertAllUser(userList)
Log.d("MYLOG","MyDBInserted")
/*val thread = Thread {
myDatabase!!.myDao().InsertAllUser(userList)
}
Log.d("MYLOG","MyDBInserted")
thread.start()*/
}
}
DAO class
#Dao
interface DAOAccess {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun InsertAllUser(userList: List<User>)
// #Query("SELECT * FROM User WHERE Username =:username")
// fun getLoginDetails(username: String?) : LiveData<LoginTableModel>
#Query("SELECT * FROM User")
suspend fun getUserList() : List<User>
}
RetrofitBuilder
object RetrofitBuilder {
private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
Please can you know that what I am doing wrong here and why the second observer was not called after insert to db
Actually It was called when the screen launched but that time data not inserted so list size was 0 and after insert data this method will not call again. But Once I close app and again start then data will display bcoz at launch time, this method call and data got
I don't have enough reputation to commet, therefore I just bring a suggestion in this answer:
Suggestion/Solution
Room supports LiveData out of the box. So in your DAO you can change
suspend fun getUserList() : List<User>
to
suspend fun getUserList() : LiveData<List<User>>
Then in your repository adjust to
suspend fun getUserList(context: Context) : LiveData<List<User>> {
myDatabase = initializeDB(context)
userList = myDatabase!!.myDao().getUserList()
Log.d("MYLOG=", "DBREAD"+userList.value.size.toString())
return userList
}
and in the ViewModel
fun getUserFromDB(context: Context) = mainRepository.getUserList(context))
With these adjustments I think it should work.
Explaination
You used the liveData couroutines builder here
fun getUserFromDB(context: Context) = liveData(Dispatchers.IO) {
emit(mainRepository.getUserList(context))
}
As far as I understand this builder, it is meant to execute some asynchronous/suspend task and as soon as this task finishes the liveData you created will emit the result. That means that you only once receive the state of the user list an emidiately emit the list to the observer one single time and then this liveData is done. It does not observe changes to the list in the DB the whole time.
That is why it works perfectly for observing the API call (you want to wait until the call is finished and emit the response one single time), but not for observing the DB state(you want to observe the user list in the DB all the time and emit changes to the observer whenever the list is changed)

How to return an object from an api-call-function in Kotlin? [duplicate]

How can I return a value after a callback in kotlin, I tried using Thread.sleep but it doesn't work
fun searchColorFromAPI(): Colors {
val service: RetrofitService = ServiceGenerator.createService(RetrofitService::class.java)
val result: MutableList<String> = arrayListOf()
val call: Call<Colors?>? = service.unityConverter(result)
call?.enqueue(object : Callback<Colors?> {
override fun onResponse(call: Call<Colors?>?, response: Response<Colors?>) {
//switchProgressVisibility()
if (response.isSuccessful) {
val serviceResponse: Colors? = response.body()
if (serviceResponse != null) {
mColors = serviceResponse
}
else {
//buildToast(getString(R.string.null_response))
}
}
else {
//buildToast(getString(R.string.response_unsuccessful))
val errorBody: ResponseBody = response.errorBody()
Log.e(TAG, errorBody.toString())
}
}
override fun onFailure(call: Call<Colors?>?, t: Throwable?) {
/* buildToast(getString(R.string.error_calling_service))
Log.e(TAG, t?.message)*/
}
})
return mColors
}
Always, the mColors is returned before the onFailure or onResponse because they're asynchronous. Before this code was in MainActivity but I was advised to take off, but now when I try get mColors I get the empty value before and after the onResponse is executed, please I'm still learning Kotlin and Android.
Your problem stems from the fact that Retrofit call is asynchronous, so as soon as you call searchColorFromAPI it returns you mColors but the API call may not have been made yet, so you get the mColors value before API call.
To solve this issue, you can do
Use callback, this will require little modification in your current setup, but the 2nd option is preferable over this. Using callback your function should look like this.
/* Now instead of returning a value, your function takes a function (named callback)
as parameter. when your api call finishes, you can call the callback function and
pass the api response.
*/
fun searchColorFromAPI(callback: (Colors?) -> Unit) {
val service: RetrofitService = ServiceGenerator.createService(RetrofitService::class.java)
val result: MutableList<String> = arrayListOf()
val call: Call<Colors?>? = service.unityConverter(result)
call?.enqueue(object : Callback<Colors?> {
override fun onResponse(call: Call<Colors?>?, response: Response<Colors?>) {
//switchProgressVisibility()
if (response.isSuccessful) {
val serviceResponse: Colors? = response.body()
/** pass API response to callback */
callback(serviceResponse)
}
else {
val errorBody: ResponseBody = response.errorBody()
Log.e(TAG, errorBody.toString())
callback(null)
}
}
override fun onFailure(call: Call<Colors?>?, t: Throwable?) {
callback(null)
}
})
}
And in your activity declare a function as follows.
// This function will be called when your api call finishes
// and it will give you the api response
fun apiCallback(colors: Colors?){
if(colors == null){
// API Call failed
}
else{
// use colors as returned by API
}
}
And now call to searchColorFromApi should look like this
searchColorFromApi(apiCallback)
Use Live Data, declare following field in your viewmodel, if you are not using viewmodel then declare it in the class which has searchColorFromApi function.
var colors: MutableLiveData<Colors> = MutableLiveData()
and modify your searchColorFromAPI function as follows
fun searchColorFromAPI() {
val service: RetrofitService = ServiceGenerator.createService(RetrofitService::class.java)
val result: MutableList<String> = arrayListOf()
val call: Call<Colors?>? = service.unityConverter(result)
call?.enqueue(object : Callback<Colors?> {
override fun onResponse(call: Call<Colors?>?, response: Response<Colors?>) {
//switchProgressVisibility()
if (response.isSuccessful) {
val serviceResponse: Colors? = response.body()
if (serviceResponse != null) {
colors.postValue(response.body)
}
}
else {
colors.postValue(null)
val errorBody: ResponseBody = response.errorBody()
Log.e(TAG, errorBody.toString())
}
}
override fun onFailure(call: Call<Colors?>?, t: Throwable?) {
colors.postValue(null)
}
})
}
and in your activity do following
fun setupObservers(){
yourApiCallingClass.colors.observe(this, Observer {
// this code is called when ever value of color field changes
})
}
You can use live data ,that gets updated once the callback receives ,the same live data is observed by the caller fragment/activity
You can use coroutines to return a value from function which has asyn calls in it.
You can use interface callbacks to activity/ fragment to trigger the updates received from retrofit calls.

Why am I getting a NullPointerException when fetching JSON?

I keep getting a NullPointerException at return place.
When I was debugging the app, the code skips the onFailure() and onResponse() methods.
Previously, this worked but I refactored it into the current classes.
class Repository private constructor() {
private val baseUrl: String = "http://api.openweathermap.org/"
val client = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY))
.build()
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
val networkApi = retrofit.create(NetworkApi::class.java)
private object Holder { val INSTANCE = Repository() }
companion object {
val instance: Repository by lazy { Holder.INSTANCE }
}
fun fetchWeatherData(placeName: String): Place {
var place: Place? = null
val call: Call<Place> = networkApi.getPlaceWeather(placeName)
call.enqueue(object : Callback<Place> {
override fun onFailure(call: Call<Place>?, t: Throwable?) {
println(t?.message)
}
override fun onResponse(call: Call<Place>?, response: Response<Place>?) {
if (response != null && response.isSuccessful && response.body() != null) {
place = response.body() as Place
println(place.toString())
}
}
})
return place!!
}
}
class MainPresenter(private val view: MainContract.View, val context: Context) : MainContract.Presenter {
val repository = Repository.instance
...
override fun updateListOfPlaces() {
var places = mutableListOf<Place>()
for (index in 0 until favPlaceStrings.size) {
places.add(repository.fetchWeatherData(favPlaceStrings.elementAt(index)))
}
view.showFavouritePlaces(places)
}
}
The way you're using retrofit makes it have an asynchronous behaviour, meaning the code within onFailure and onResponse might run after or before you have a chance to return from fetchWeatherData. In other words, you cannot assume that place will have a value when you return from fetchWeatherData and this is actually what's happening, place is still null and calling !! will cause the null pointer exception you're having.
To fix this you either change the way you're using retrofit to be synchronous, or you use an approach like callbacks.
Personally, I prefer the callback approach / reactive streams and this you can check here.
Making the code synchronous will most likely lead to other issues such as network calls on the main thread, which are not allowed and crash the app.
You need to invert your logic. You cannot simply "return data" from a network call that you're waiting for
Instead, loop over the list, make requests, then show/update the view
for (index in 0 until favPlaceStrings.size) {
val call: Call<Place> = networkApi.getPlaceWeather(favPlaceStrings.elementAt(index))
call.enqueue(object : Callback<Place> {
override fun onFailure(call: Call<Place>?, t: Throwable?) {
println(t?.message)
}
override fun onResponse(call: Call<Place>?, response: Response<Place>?) {
if (response != null && response.isSuccessful && response.body() != null) {
val place: Place = response.body() as Place
places.add(place) // move this list to a field
println(place.toString())
view.showFavouritePlaces(places) // this is fine that it's inside a loop
}
}
})
}