Not getting callback for mutableLivedata android? - android

In my viewmodel I have 2 api calls which returns same object. However I created 2 different MutableLiveData objects but I am not able to observe the 2nd object.
This is my code in fragment
private fun initObservables() {
holidayViewModel.progressDialog?.observe(this, Observer {
if (it!!) customeProgressDialog?.show() else customeProgressDialog?.dismiss()
})
holidayViewModel.apiResponse?.observe(
viewLifecycleOwner,
androidx.lifecycle.Observer { response ->
if (response.dataList != null) {
response.dataList!!.removeAt(0)
if (requireArguments().getString("file_type")
.equals(NetworkConstant.FILE_TYPE_LOH, ignoreCase = true)
) {
val data = Data()
data.CountryId = "0"
data.CountryName = "Main organisation"
response.dataList!!.add(0, data)
}
val holidayAdapter = CountryAdapter(response.dataList)
binding.holiday.adapter = holidayAdapter
holidayAdapter.notifyDataSetChanged()
holidayAdapter.setListener(this)
}
})
holidayViewModel.pdfLink?.observe(
viewLifecycleOwner,
androidx.lifecycle.Observer { response ->
utils.openPdf(response.dataList!!.get(0)?.filePath)
})
}
This is the viewmodel class
class HolidayViewModel(networkCall: NetworkCall) : ViewModel() {
var progressDialog: SingleLiveEvent<Boolean>? = null
var apiResponse: MutableLiveData<ApiResponse>? = null
var pdfLink: MutableLiveData<ApiResponse>? = null
var networkCall: NetworkCall;
init {
progressDialog = SingleLiveEvent<Boolean>()
apiResponse = MutableLiveData<ApiResponse>()
this.networkCall = networkCall
}
fun countries(username: String?, userId: String?) {
progressDialog?.value = true
val apiPost = ApiPost()
apiPost.userName = username
apiPost.UserId = userId
networkCall.getCountries(apiPost).enqueue(object : Callback<ApiResponse?> {
override fun onResponse(
call: Call<ApiResponse?>,
response: Response<ApiResponse?>
) {
progressDialog?.value = false
apiResponse?.value = response.body()
}
override fun onFailure(
call: Call<ApiResponse?>,
t: Throwable
) {
progressDialog?.value = false
}
})
}
fun fetchPdf(
username: String?,
password: String?,
userId: String?,
countryId: String?,
fileType: String?
) {
progressDialog?.value = true
val apiPost = ApiPost()
apiPost.userName = username
apiPost.password = password
apiPost.UserId = userId
apiPost.CountryId = countryId
apiPost.FileType = fileType
networkCall.getPDF(apiPost).enqueue(object : Callback<ApiResponse?> {
override fun onResponse(
call: Call<ApiResponse?>,
response: Response<ApiResponse?>
) {
progressDialog?.value = false
pdfLink?.value = response.body()
}
override fun onFailure(
call: Call<ApiResponse?>,
t: Throwable
) {
progressDialog?.value = false
}
})
}
}
I am trying to observe pdfLink object , however the API is called but I never get the callback in my fragment for this object.
What is wrong here?

The problem is pdfLink is always null in viewModel.
You've declared var pdfLink: MutableLiveData<ApiResponse>? = null but haven't initialized yet. And since you are null checking it with ?, it never throws exception.
Try this:
init {
progressDialog = SingleLiveEvent<Boolean>()
apiResponse = MutableLiveData<ApiResponse>()
pdfLink = MutableLiveData<ApiResponse>() // Add this line inside init
this.networkCall = networkCall
}

A silly mistake forget to initialize it
pdfLink = MutableLiveData<ApiResponse>()

Related

Cannot Pass output from Retrofit enqueue to MainActivity Kotlin

I am calling the API using retrofit in ApiClient Class. Where I am trying to store the output of successful login either message or responseBody into the output string. I tried using the output string as part of ViewModel Observer type too
But I CANNOT pass the value of output from AplClient to MainActivity
APICLIENT
...
package com.example.services.api
lateinit var service: ApiInterface
object ApiClient {
#JvmStatic
private val BASE_URL = GlobalConstants.SWAGGER
//private val sharedPrefClass = SharedPrefClass()
#JvmStatic
private var mApiInterface: ApiInterface? = null
var output = "Initializing"
// val sharedPrefClass: SharedPrefClass? = null
// #JvmStatic
// fun getApiInterface(): ApiInterface {
// return setApiInterface()
// }
#JvmStatic
fun setApiInterface() : String {
val platform = GlobalConstants.PLATFORM
var mAuthToken = GlobalConstants.SESSION_TOKEN
var companyId = GlobalConstants.COMPANY_ID
var phone = phone
var cCode = cCode
//Here a logging interceptor is created
//Here a logging interceptor is created
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
//The logging interceptor will be added to the Http client
//The logging interceptor will be added to the http client
val httpClient = OkHttpClient.Builder()
httpClient.addInterceptor(logging)
//The Retrofit builder will have the client attached, in order to get connection logs
//The Retrofit builder will have the client attached, in order to get connection logs
val retrofit: Retrofit = Retrofit.Builder()
.client(httpClient.build())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
service = retrofit.create<ApiInterface>(ApiInterface::class.java)
val call: Call<Tree> = service.post(phone, cCode, companyId, platform)
var model: Model
call.enqueue(object : Callback<Tree> {
override fun onResponse(
call: Call<Tree>, response: Response<Tree>)
{
Log.e(TAG, "Success")
if (!response.isSuccessful()) {
model = Model("Success", "91", "8884340404")
model.output += response.body().toString()
return;
}
}
override fun onFailure(
call: Call<Tree>,
t: Throwable
) {
Log.e(TAG, "Json/Network Error")
model = Model("Json/Network Error", "91", "8884340404")
model.output = "Json/Network Error"
// handle execution failures like no internet connectivity
}
})
return output
}
}
VIEWMODEL
package com.example.kotlinloginapi.viewModel
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import androidx.lifecycle.MutableLiveData
class Model {
var output:String? = null
var cCode:String? = null
var phone: String? = null
constructor(output: String?, cCode: String?, phone: String?) {
this.output = output
this.cCode = cCode
this.phone = phone
}
}
...
MAINACTIVTY
...
package com.example.kotlinloginapi.ui.login
lateinit var textView: TextView
class MainActivity : AppCompatActivity() {
lateinit var mainBinding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
textView = findViewById<TextView>(R.id.textView)
val mApiService: ApiService<Tree>
val mApiClient = ApiClient
var model: Model
model = Model("Creating View", "91", "8884340404")
model.output = "Creating View"
Observable.just(setApiInterface())
.subscribe {
println(it)
textView!!.append(setApiInterface())}
// mainBinding.viewModel!!.output = "Creating View"
textView.append("\n" + model.output)
/*fun getLoginData(jsonObject : JsonObject?) {
if (jsonObject != null) {
val mApiService = ApiService<JsonObject>()
mApiService.get(
object : ApiResponse<JsonObject> {
override fun onResponse(mResponse : Response<JsonObject>) {
val loginResponse = if (mResponse.body() != null)
finish()
else {
output = mResponse.body().toString()
}
}
override fun onError(mKey : String) {
Toast.makeText(
applicationContext,
"Error",
Toast.LENGTH_LONG
).show()
}
}, ApiClient.getApiInterface().callLogin(jsonObject)
)
}
}*/
}
}
LOGINACTIVITY
package com.example.kotlinloginapi.ui.login
lateinit var cCode: String
lateinit var phone: String
class LoginActivity : AppCompatActivity() {
lateinit var loginBinding : ActivityLoginBinding
lateinit var eTcCode: EditText
lateinit var eTphone: EditText
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loginBinding = DataBindingUtil.setContentView(this, R.layout.activity_login)
eTcCode = findViewById<EditText>(R.id.etcCode)
eTphone = findViewById<EditText>(R.id.etPhone)
val login = findViewById<Button>(R.id.login)
val loading = findViewById<ProgressBar>(R.id.loading)
var model = Model("Binding Model", "91", "8884340404")
loginBinding.model = model
loginBinding.lifecycleOwner
loginViewModel = ViewModelProviders.of(this, LoginViewModelFactory())
.get(LoginViewModel::class.java)
loginViewModel.loginFormState.observe(this#LoginActivity, Observer {
val loginState = it ?: return#Observer
// disable login button unless both username / password is valid
login.isEnabled = loginState.isDataValid
if (loginState.usernameError != null) {
eTcCode.error = getString(loginState.usernameError)
}
if (loginState.passwordError != null) {
eTphone.error = getString(loginState.passwordError)
}
})
loginViewModel.loginResult.observe(this#LoginActivity, Observer {
val loginResult = it ?: return#Observer
loading.visibility = View.VISIBLE
if (loginResult.error != null) {
showLoginFailed(loginResult.error)
}
if (loginResult.success != null) {
updateUiWithUser(loginResult.success)
}
setResult(Activity.RESULT_OK)
//Complete and destroy login activity once successful
finish()
})
eTcCode.afterTextChanged {
loginViewModel.loginDataChanged(
eTcCode.text.toString(),
eTphone.text.toString()
)
}
eTphone.apply {
afterTextChanged {
loginViewModel.loginDataChanged(
eTcCode.text.toString(),
eTphone.text.toString()
)
}
setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE ->
loginViewModel.login(
eTcCode.text.toString(),
eTphone.text.toString()
)
}
false
}
login.setOnClickListener {
loading.visibility = View.VISIBLE
loginViewModel.login(eTcCode.text.toString(), eTphone.text.toString())
}
}
}
private fun updateUiWithUser(model: LoggedInUserView) {
val welcome = getString(R.string.welcome)
val displayName = model.displayName
// TODO : initiate successful logged in experience
cCode = eTcCode.text.toString()
phone = eTphone.text.toString()
var intent = Intent(this, MainActivity::class.java)
startActivity(intent)
Toast.makeText(
applicationContext,
"$welcome $displayName",
Toast.LENGTH_LONG
).show()
}
private fun showLoginFailed(#StringRes errorString: Int) {
Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show()
}
}
/**
* Extension function to simplify setting an afterTextChanged action to EditText components.
*/
fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
afterTextChanged.invoke(editable.toString())
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
}
...
Ideally, I want to use binding observers but I am unable to pass the output of retrofit to MainActivity even without that.
I moved the enqueue call to main activity which solved the problem. Now I can call text views. Will try to achieve this without moving and live data in future.

Kotlin class is returning false before it has finished

In my main activity i have a function that runs once the login button it pressed. I'm calling a class that attempts to login via an API which takes a second or two to run. However, when i'm calling the login class it seems to be threaded and doesn't wait for the login to complete and returns false which is the default. Example code is as follows:
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun loginBtnClicked(view: View) {
progressBar.visibility = View.VISIBLE
// get domain info
val domain: TextView = findViewById<TextView>(R.id.loginDomain)
val domainUrl = domain.text.toString()
val url = "$domainUrl/api/"
// get username info
val username = loginUsername
// get password info
val password = loginPassword
if (ApiGet(
url = url,
username = username.text.toString(),
password = password.text.toString()
).login()) {
println("It worked")
val dashboard = Intent(this, DashboardActivity::class.java)
Consts.DOMAIN = url
Consts.USERNAME = username.text.toString()
Consts.PASSWORD = password.text.toString()
startActivity(dashboard)
} else {
println("It didn't work")
progressBar.visibility = View.INVISIBLE
runOnUiThread {
Log.i(ContentValues.TAG, "runOnUiThread")
Toast.makeText(
applicationContext,
"Please check the domain, username and password then try again.",
Toast.LENGTH_SHORT
).show()
}
}
}
}
class ApiGet(val url: String, val username: String = Consts.USERNAME, val password: String = Consts.PASSWORD) {
fun login(): Boolean {
var loginAttempt: Boolean = false
var apiData: ApiLogin
val creds = Credentials.basic(username, password)
val request = Request.Builder().url(url).header("Authorization", creds).build()
val client = OkHttpClient()
client.newCall(request).enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
val body: String? = response.body()?.string()
val gson: Gson = GsonBuilder().create()
apiData = gson.fromJson(body, ApiLogin::class.java)
if (apiData.detail == "Invalid username/password.") {
loginAttempt = false
println(loginAttempt)
} else {
loginAttempt = true
println(loginAttempt)
}
}
override fun onFailure(call: Call, e: IOException) {
Toast.makeText(
MainActivity().getApplicationContext(),
"Please check your connection and try again.",
Toast.LENGTH_SHORT
).show()
loginAttempt = false
}
})
return loginAttempt
}
class ApiLogin(val detail: String)
}
It is because your newCall method runs async to the main thread meaning the rest of your code keeps running after you call it while it waits on another thread. To fix this rather than returning your result you can handle it in a callback like so:
class ApiGet(val url: String, val username: String = Consts.USERNAME, val password: String = Consts.PASSWORD) {
fun login(completion: (Boolean)->Unit) {
var loginAttempt: Boolean = false
var apiData: ApiLogin
val creds = Credentials.basic(username, password)
val request = Request.Builder().url(url).header("Authorization", creds).build()
val client = OkHttpClient()
client.newCall(request).enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
val body: String? = response.body()?.string()
val gson: Gson = GsonBuilder().create()
apiData = gson.fromJson(body, ApiLogin::class.java)
if (apiData.detail == "Invalid username/password.") {
println(loginAttempt)
completion(False)
} else {
print(loginAttempt)
completion(True)
}
}
override fun onFailure(call: Call, e: IOException) {
Toast.makeText(
MainActivity().getApplicationContext(),
"Please check your connection and try again.",
Toast.LENGTH_SHORT
).show()
completion(False) }
})
}
You can call the login function like so:
ApiGet(url = url,
username = username.text.toString(),
password = password.text.toString()).login { result ->
if (result) {
// success
} else {
// failure
}
}

Partial pagination

I am trying to implement a paging logic with partial data loading per one page. I have a page with 100 max page_size value but I don't need all 100 values per one request, what I need is to request 30 records in one request, 30 in second, 30 in third 10 (because of max 100) in fourth request and after that I change page number. That how I wrote this now:
Data source class
class NewsDataSource(
private val compositeDisposable: CompositeDisposable,
private val newsRequests: NewsRequests
) : PageKeyedDataSource<PageNewsKey, News.Data?>() {
private val pRequestNewsStatuses = MutableLiveData<LiveDataStatuses>(LiveDataStatuses.IDLE)
val requestNewsStatuses: LiveData<LiveDataStatuses> = pRequestNewsStatuses
override fun loadInitial(
params: LoadInitialParams<PageNewsKey>,
callback: LoadInitialCallback<PageNewsKey, News.Data?>
) {
val pageNewsKey = PageNewsKey()
pRequestNewsStatuses.postValue(LiveDataStatuses.WAITING)
compositeDisposable.add(
newsRequests.getNews(pageNewsKey.newsCount, pageNewsKey.page)
.subscribeOn(Schedulers.io())
.map {
it.data ?: emptyList()
}
.subscribe({
callback.onResult(it, null, pageNewsKey.nextPageKey)
}, {
})
)
}
override fun loadBefore(
params: LoadParams<PageNewsKey>,
callback: LoadCallback<PageNewsKey, News.Data?>
) {
}
override fun loadAfter(
params: LoadParams<PageNewsKey>,
callback: LoadCallback<PageNewsKey, News.Data?>
) {
pRequestNewsStatuses.postValue(LiveDataStatuses.WAITING)
compositeDisposable.add(
newsRequests.getNews(params.key.newsCount, params.key.page)
.subscribeOn(Schedulers.io())
.map {
it.data ?: emptyList()
}
.subscribe({
callback.onResult(it, params.key.nextPageKey)
}, {
})
)
}
Key class:
class PageNewsKey {
var newsCount: Int = NEWS_COUNT_DEFAULT_VALUE
var page: Int = 1
val nextPageKey: PageNewsKey
get() {
return iterate()
}
private fun iterate(): PageNewsKey {
if (newsCount == NEWS_COUNT_MAX_VALUE) {
newsCount = NEWS_COUNT_DEFAULT_VALUE
page = page.inc()
} else {
val newNewsCount = newsCount + NEWS_COUNT_DEFAULT_VALUE
newsCount = if (newNewsCount > NEWS_COUNT_MAX_VALUE) {
NEWS_COUNT_MAX_VALUE
} else {
newNewsCount
}
}
return this
}
}
Data source factory:
class NewsDataSourceFactory(
private val compositeDisposable: CompositeDisposable,
private val newsRequests: NewsRequests
) : DataSource.Factory<PageNewsKey, News.Data?>() {
private val pNewsDataSource = MutableLiveData<NewsDataSource?>(null)
val newsDataSource: LiveData<NewsDataSource?> = pNewsDataSource
override fun create(): DataSource<PageNewsKey, News.Data?> {
val newsDataSource = NewsDataSource(compositeDisposable, newsRequests)
pNewsDataSource.postValue(newsDataSource)
return newsDataSource
}
fun refresh() {
pNewsDataSource.value?.invalidate()
}
}
This code is in view model class:
private val newsDataSourceFactory = NewsDataSourceFactory(compositeDisposable, NewsRequests.getNewsRequest())
private val newsDataSourceFactoryConfig = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPageSize(NEWS_COUNT_DEFAULT_VALUE)
.build()
val news: LiveData<PagedList<News.Data?>> = LivePagedListBuilder(newsDataSourceFactory, newsDataSourceFactoryConfig).build()
Live data observer inside desired fragment:
homeViewModel.news.observe(this, Observer {
it?.let {
newsAdapter.submitList(it)
}
})
Adapter:
class NewsAdapter : PagedListAdapter<News.Data, NewsAdapter.ViewHolder>(News.Data.NEWS_DATA_DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.news_row, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bingView(getItem(position))
}
fun getItemAtPosition(position: Int): News.Data? = getItem(position)
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private var iv_news_image: ImageView? = null
private var tv_news_title: TextView? = null
private var tv_news_subtitle: TextView? = null
private var clpb_news_image: ContentLoadingProgressBar? = null
init {
iv_news_image = itemView.findViewById(R.id.iv_news_image)
tv_news_title = itemView.findViewById(R.id.tv_news_title)
tv_news_subtitle = itemView.findViewById(R.id.tv_news_subtitle)
clpb_news_image = itemView.findViewById(R.id.clpb_news_image)
clpb_news_image?.hide()
}
fun bingView(data: News.Data?) {
iv_news_image?.let {
clpb_news_image?.show()
GlideApp.with(itemView.context)
.load(data?.imageUrl)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transition(DrawableTransitionOptions.withCrossFade(500))
.listener(object : IDoAfterTerminateGlide {
override fun doAfterTerminate() {
clpb_news_image?.hide()
}
})
.dontAnimate()
.into(it)
}
tv_news_title?.text = data?.title
tv_news_subtitle?.text = data?.subtitle
}
}
}
And finally data class with diff callback:
data class News(
#SerializedName("current_page")
val currentPage: Int? = null,
#SerializedName("data")
val data: List<Data?>? = null,
#SerializedName("first_page_url")
val firstPageUrl: String? = null,
#SerializedName("from")
val from: Int? = null,
#SerializedName("last_page")
val lastPage: Int? = null,
#SerializedName("last_page_url")
val lastPageUrl: String? = null,
#SerializedName("next_page_url")
val nextPageUrl: String? = null,
#SerializedName("path")
val path: String? = null,
#SerializedName("per_page")
val perPage: String? = null,
#SerializedName("prev_page_url")
val prevPageUrl: String? = null,
#SerializedName("to")
val to: Int? = null,
#SerializedName("total")
val total: Int? = null
) {
data class Data(
#SerializedName("body")
val body: String? = null,
#SerializedName("date")
val date: String? = null,
#SerializedName("image_url")
val imageUrl: String? = null,
#SerializedName("news_id")
val newsId: String? = null,
#SerializedName("subtitle")
val subtitle: String? = null,
#SerializedName("title")
val title: String? = null
) {
companion object {
val NEWS_DATA_DIFF_CALLBACK = object : DiffUtil.ItemCallback<Data>() {
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.newsId == newItem.newsId
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.body == newItem.body && oldItem.date == oldItem.date
&& oldItem.imageUrl == newItem.imageUrl
&& oldItem.subtitle == newItem.subtitle
&& oldItem.title == newItem.title
}
}
}
}
}
json response
And this setup is working as I expected, it makes requests:
www.example.com?page=1&page_size=30
www.example.com?page=1&page_size=60
www.example.com?page=1&page_size=90
www.example.com?page=1&page_size=10
www.example.com?page=2&page_size=30
But the problem is that diff item callback doesn't make it's work as I am expecting, and I receive duplicated of data. So my question is pretty simple, does android page keyed data source provide such functionality?

Make android MVVM, Kotlin Coroutines and Retrofit 2.6 work asynchronously

I've just finished my first Android App. It works as it should but, as you can imagine, there's a lot of spaghetti code and lack of performance. From what I've learned on Android and Kotlin language making this project (and a lot of articles/tutorials/SO answers) I'm trying to start it again from scratch to realize a better version. For now I'd like to keep it as simple as possible, just to better understand how to handle API calls with Retrofit and MVVM pattern, so no Volley/RXjava/Dagger etc.
I'm starting from the login obviously; I would like to make a post request to simply compare the credentials, wait for the response and, if positive, show a "loading screen" while fetching and processing data to show in the home page. I'm not storing any information so I have realized a singleton class that holds data as long as the app is running (btw, is there another way to do it?).
RetrofitService
private val retrofitService = Retrofit.Builder()
.addConverterFactory(
GsonConverterFactory
.create(
GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.setLenient().setDateFormat("yyyy-MM-dd")
.create()
)
)
.addConverterFactory(RetrofitConverter.create())
.baseUrl(BASE_URL)
.build()
`object ApiObject {
val retrofitService: ApiInterface by lazy {
retrofitBuilder.create(ApiInterface::class.java) }
}
ApiInterface
interface ApiInterface {
#GET("workstation/{date}")
suspend fun getWorkstations(
#Path("date") date: Date
): List<Workstation>
#GET("reservation/{date}")
suspend fun getReservations(
#Path("date") date: Date
): List<Reservation>
#GET("user")
suspend fun getUsers(): List<User>
#GET("user/login")
suspend fun validateLoginCredentials(
#Query("username") username: String,
#Query("password") password: String
): Response<User>
ApiResponse
sealed class ApiResponse<T> {
companion object {
fun <T> create(response: Response<T>): ApiResponse<T> {
return if(response.isSuccessful) {
val body = response.body()
// Empty body
if (body == null || response.code() == 204) {
ApiSuccessEmptyResponse()
} else {
ApiSuccessResponse(body)
}
} else {
val msg = response.errorBody()?.string()
val errorMessage = if(msg.isNullOrEmpty()) {
response.message()
} else {
msg.let {
return#let JSONObject(it).getString("message")
}
}
ApiErrorResponse(errorMessage ?: "Unknown error")
}
}
}
}
class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()
Repository
class Repository {
companion object {
private var instance: Repository? = null
fun getInstance(): Repository {
if (instance == null)
instance = Repository()
return instance!!
}
}
private var singletonClass = SingletonClass.getInstance()
suspend fun validateLoginCredentials(username: String, password: String) {
withContext(Dispatchers.IO) {
val result: Response<User>?
try {
result = ApiObject.retrofitService.validateLoginCredentials(username, password)
when (val response = ApiResponse.create(result)) {
is ApiSuccessResponse -> {
singletonClass.loggedUser = response.data
}
is ApiSuccessEmptyResponse -> throw Exception("Something went wrong")
is ApiErrorResponse -> throw Exception(response.errorMessage)
}
} catch (error: Exception) {
throw error
}
}
}
suspend fun getWorkstationsListFromService(date: Date) {
withContext(Dispatchers.IO) {
val workstationsListResult: List<Workstation>
try {
workstationsListResult = ApiObject.retrofitService.getWorkstations(date)
singletonClass.rWorkstationsList.postValue(workstationsListResult)
} catch (error: Exception) {
throw error
}
}
}
suspend fun getReservationsListFromService(date: Date) {
withContext(Dispatchers.IO) {
val reservationsListResult: List<Reservation>
try {
reservationsListResult = ApiObject.retrofitService.getReservations(date)
singletonClass.rReservationsList.postValue(reservationsListResult)
} catch (error: Exception) {
throw error
}
}
}
suspend fun getUsersListFromService() {
withContext(Dispatchers.IO) {
val usersListResult: List<User>
try {
usersListResult = ApiObject.retrofitService.getUsers()
singletonClass.rUsersList.postValue(usersListResult.let { usersList ->
usersList.filterNot { user -> user.username == "admin" }
.sortedWith(Comparator { x, y -> x.surname.compareTo(y.surname) })
})
} catch (error: Exception) {
throw error
}
}
}
SingletonClass
const val FAILED = 0
const val COMPLETED = 1
const val RUNNING = 2
class SingletonClass private constructor() {
companion object {
private var instance: SingletonClass? = null
fun getInstance(): SingletonClass {
if (instance == null)
instance = SingletonClass()
return instance!!
}
}
//User
var loggedUser: User? = null
//Workstations List
val rWorkstationsList = MutableLiveData<List<Workstation>>()
//Reservations List
val rReservationsList = MutableLiveData<List<Reservation>>()
//Users List
val rUsersList = MutableLiveData<List<User>>()
}
ViewModel
class ViewModel : ViewModel() {
private val singletonClass = SingletonClass.getInstance()
private val repository = Repository.getInstance()
//MutableLiveData
//Login
private val _loadingStatus = MutableLiveData<Boolean>()
val loadingStatus: LiveData<Boolean>
get() = _loadingStatus
private val _successfulAuthenticationStatus = MutableLiveData<Boolean>()
val successfulAuthenticationStatus: LiveData<Boolean>
get() = _successfulAuthenticationStatus
//Data fetch
private val _listsLoadingStatus = MutableLiveData<Int>()
val listsLoadingStatus: LiveData<Int>
get() = _listsLoadingStatus
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String>
get() = _errorMessage
fun onLoginClicked(username: String, password: String) {
launchLoginAuthentication {
repository.validateLoginCredentials(username, password)
}
}
private fun launchLoginAuthentication(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_loadingStatus.value = true
block()
} catch (error: Exception) {
_errorMessage.postValue(error.message)
} finally {
_loadingStatus.value = false
if (singletonClass.loggedUser != null)
_successfulAuthenticationStatus.value = true
}
}
}
fun onLoginPerformed() {
val date = Calendar.getInstance().time
launchListsFetch {
//how to start these all at the same time? Then wait until their competion
//and call the two methods below?
repository.getReservationsListFromService(date)
repository.getWorkstationsListFromService(date)
repository.getUsersListFromService()
}
}
private fun launchListsFetch(block: suspend () -> Unit): Job {
return viewModelScope.async {
try {
_listsLoadingStatus.value = RUNNING
block()
} catch (error: Exception) {
_listsLoadingStatus.value = FAILED
_errorMessage.postValue(error.message)
} finally {
//I'd like to perform these operations at the same time
prepareWorkstationsList()
prepareReservationsList()
//and, when both completed, set this value
_listsLoadingStatus.value = COMPLETED
}
}
}
fun onToastShown() {
_errorMessage.value = null
}
}
LoginActivity
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel
get() = ViewModelProviders.of(this).get(LoginViewModel::class.java)
private val loadingFragment = LoadingDialogFragment()
var username = ""
var password = ""
private lateinit var loginButton: Button
lateinit var context: Context
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginButton = findViewById(R.id.login_button)
loginButton.setOnClickListener {
username = login_username.text.toString().trim()
password = login_password.text.toString().trim()
viewModel.onLoginClicked(username, password.toMD5())
}
viewModel.loadingStatus.observe(this, Observer { value ->
value?.let { show ->
progress_bar_login.visibility = if (show) View.VISIBLE else View.GONE
}
})
viewModel.successfulAuthenticationStatus.observe(this, Observer { successfullyLogged ->
successfullyLogged?.let {
loadingFragment.setStyle(DialogFragment.STYLE_NORMAL, R.style.CustomLoadingDialogFragment)
if (successfullyLogged) {
loadingFragment.show(supportFragmentManager, "loadingFragment")
viewModel.onLoginPerformed()
} else {
login_password.text.clear()
login_password.isFocused
password = ""
}
}
})
viewModel.listsLoadingStatus.observe(this, Observer { loadingResult ->
loadingResult?.let {
when (loadingResult) {
COMPLETED -> {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
setResult(Activity.RESULT_OK)
finish()
}
FAILED -> {
loadingFragment.changeText("Error")
loadingFragment.showProgressBar(false)
loadingFragment.showRetryButton(true)
}
}
}
})
viewModel.errorMessage.observe(this, Observer { value ->
value?.let { message ->
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
viewModel.onToastShown()
}
})
}
Basically what I'm trying to do is to send username and password, show a progress bar while waiting for the result (if successful the logged user object is returned, otherwise a toast with the error message is shown), hide the progress bar and show the loading fragment. While showing the loading fragment start 3 async network calls and wait for their completion; when the third call is completed start the methods to elaborate the data and, when both done, start the next activity.
It seems to all works just fine, but debugging I've noticed the flow (basically network calls start/wait/onCompletion) is not at all like what I've described above. There's something to fix in the ViewModel, I guess, but I can't figure out what

Retrofit LiveDataCallAdapter doesn't call function adapt (call)

Trying to solve this problem about 4 days, help, please.
I'm creating an app with rest API (retrofit), try to implement LiveDataCallAdapter from Google samples
https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample, but retrofit doesn't call adapter method adapt for getting a response from the server.
I'm edited only NetworkBoundResourse (for working without DB)
Trying to put breakpoints, after I start repo (login), LiveDataCallAdapter fun adapt (where call.enequeue don't want start) debugging don't call
Here is my piece of code, thx
Providing my service instance
#Singleton
#Provides
fun provideRetrofit(): BrizSmartTVService {
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(LiveDataCallAdapterFactory())
.build()
.create(BrizSmartTVService::class.java)
}
There is my LiveDataCallAdapterFactory and LiveDataCallAdapter
class LiveDataCallAdapterFactory : Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != LiveData::class.java) {
return null
}
val observableType = getParameterUpperBound(0, returnType as ParameterizedType)
val rawObservableType = getRawType(observableType)
if (rawObservableType != ApiResponse::class.java) {
throw IllegalArgumentException("type must be a resource")
}
if (observableType !is ParameterizedType) {
throw IllegalArgumentException("resource must be parameterized")
}
val bodyType = getParameterUpperBound(0, observableType)
return LiveDataCallAdapter<Any>(bodyType)
}
}
class LiveDataCallAdapter<R>(private val responseType: Type) :
CallAdapter<R, LiveData<ApiResponse<R>>> {
override fun responseType() = responseType
override fun adapt(call: Call<R>): LiveData<ApiResponse<R>> {
return object : LiveData<ApiResponse<R>>() {
private var started = AtomicBoolean(false)
override fun onActive() {
super.onActive()
if (started.compareAndSet(false, true)) {
Log.d("TAG", ": onActive Started ");
call.enqueue(object : Callback<R> {
override fun onResponse(call: Call<R>, response: Response<R>) {
Log.d("TAG", ": $response");
postValue(ApiResponse.create(response))
}
override fun onFailure(call: Call<R>, throwable: Throwable) {
Log.d("TAG", ": ${throwable.localizedMessage}");
postValue(ApiResponse.create(throwable))
}
})
}
}
}
}
}
There is my NetworkBoundResourse (work only with Network)
abstract class NetworkBoundResource<RequestType> {
private val result = MediatorLiveData<Resource<RequestType>>()
init {
setValue(Resource.loading(null))
fetchFromNetwork()
}
#MainThread
private fun setValue(newValue: Resource<RequestType>) {
if (result.value != newValue) {
result.value = newValue
}
}
private fun fetchFromNetwork() {
val apiResponse = createCall()
result.addSource(apiResponse) { response ->
result.removeSource(apiResponse)
when (response) {
is ApiSuccessResponse -> {
setValue(Resource.success(processResponse(response)))
}
is ApiErrorResponse -> {
onFetchFailed()
setValue(Resource.error(response.errorMessage, null))
}
}
}
}
protected fun onFetchFailed() {
}
fun asLiveData() = result as LiveData<Resource<RequestType>>
#WorkerThread
protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body
#MainThread
protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}
My Repo class
#Singleton
class AuthApiRepo #Inject constructor(
val apiService: BrizSmartTVService
) {
fun authLoginPass(login: String, password: String): LiveData<Resource<AuthResponse>> {
return object : NetworkBoundResource<AuthResponse>() {
override fun createCall(): LiveData<ApiResponse<AuthResponse>> {
val authLogPassBody = AuthLogPassBody(login,password,"password")
Log.d("TAG", ": $authLogPassBody");
return apiService.authLoginPass(authLogPassBody)
}
}.asLiveData()
}
}
And my AuthResponse Class
class AuthResponse {
#SerializedName("token_type")
var tokenType: String? = null
#SerializedName("access_token")
var accessToken: String? = null
#SerializedName("refresh_token")
var refreshToken: String? = null
#SerializedName("user_id")
var userId: String? = null
#SerializedName("expires_in")
var expiresIn: Long = 0
#SerializedName("portal_url")
var portalUrl: String? = null
}
My ViewModel class from where i start calling
class AuthViewModel #Inject constructor(private val authApiRepo: AuthApiRepo) : ViewModel() {
private var _isSigned = MutableLiveData<Boolean>()
val isSigned: LiveData<Boolean>
get() = _isSigned
fun signIn(login: String, password: String) {
authApiRepo.authLoginPass(login, password)
val authRespons = authApiRepo.authLoginPass(login, password)
Log.d("TAG", ": " + authRespons.value.toString());
//here will by always data null and status LOADING
}
override fun onCleared() {
super.onCleared()
}
}
So guys, finaly i found a solution. It's very simple for the peaple experienced in MVVM (live data) subject , but im beginer in MVVM and my brain exploded while I came to this.
So , the problem was I subscribed to Repo livedata from ViewModel , not from View (Fragment in my case). After i locked the chain of livedata observers View - ViewModel - Repo - Service - everything worked. Thx to all

Categories

Resources