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
Related
I have this observer for my viewmodel so that I can setup my adapter but, when I run the app, it gives me the NullPointerException error on this line:
japaneseAdapter = it.data?.let { it1 -> JapaneseAdapter(it1) }!!
This is the activity with that line:
#AndroidEntryPoint
class JapaneseActivity : AppCompatActivity() {
private lateinit var binding: ActivityJapaneseBinding
private val japaneseViewModel by viewModels<JapaneseViewModel>()
private lateinit var japaneseAdapter: JapaneseAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityJapaneseBinding.inflate(layoutInflater)
setContentView(binding.root)
japaneseViewModel.japaneseResponse.observe(this, {
when(it.status){
Resource.Status.LOADING -> { }
Resource.Status.SUCCESS -> {
japaneseAdapter = it.data?.let { it1 -> JapaneseAdapter(it1) }!!
binding.rvNews.adapter = japaneseAdapter
}
Resource.Status.ERROR -> { Log.d("ERROR","ERROR RAISED") }
}
})
}
}
This is the adapter:
class JapaneseAdapter(private var japaneseResponse: List<JapaneseResponse>) :
RecyclerView.Adapter<JapaneseAdapter.ViewHolder>() {
inner class ViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
private val binding = NewsItemsBinding.bind(view)
private val itemTitle: TextView = binding.tvTitle
private val itemImage: ImageView = binding.ivNews
private val itemDescription: TextView = binding.tvDescription
fun bind(response: JapaneseResponse) {
Picasso.get().load(response.urlToImage).into(itemImage)
itemTitle.text = response.Title
itemDescription.text = response.Description
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.news_items, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(japaneseResponse[position])
}
override fun getItemCount(): Int {
return japaneseResponse.size
}
}
Generic data source:
abstract class BaseDataSource {
protected suspend fun <T> getResult(call: suspend () -> Response<ApiResponse<T>>): Resource<T> {
try {
val response = call()
// if(response.isSuccessful) {
// val body = response.body()?.data
// if(body != null) return Resource.success(body)
// }
val body = response.body()?.data
return Resource.success(body)
//return Resource.error("${response.code()}: ${response.message()}")
} catch (e: Exception) {
return Resource.error(e.message ?: "Generic error")
}
}
}
data class Resource<out T>(val status: Status, val data: T?, val message: String?) : Serializable {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(
Status.SUCCESS,
data,
null
)
}
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,
null
)
}
}
fun isSuccessful() = status == Status.SUCCESS
fun isError() = status == Status.ERROR
fun isLoading() = status == Status.LOADING
}
The data source for the Japanese news:
class JapaneseDataSource #Inject constructor(private val japaneseService: JapaneseService) :
BaseDataSource() {
suspend fun getJpNews() = getResult { japaneseService.jpNews() }
}
Repository:
class JapaneseRepository #Inject constructor(
private val remote: JapaneseDataSource
) {
suspend fun jpNews() =
remote.getJpNews()
}
The service:
interface JapaneseService {
#GET("/v2/top-headlines?country=jp&apiKey=77acc490875643c5b2328fb615e0cf83")
suspend fun jpNews(): Response<ApiResponse<List<JapaneseResponse>>>
}
I can see that the response is there since I have okhttp logging it for me but for some reason it seems to be null and I am not sure why...
Any help?
japaneseAdapter = it.data?.let { it1 -> JapaneseAdapter(it1) }!!
!! is Kotlin's "crash the app" operator. It says that you want to crash the app if the value you are applying !! to is null. Your objective, as a Kotlin programmer, is to avoid using !!.
In this case, the fact that you are crashing on that line with that error means that !! is being applied to null. That will occur if it.data evaluates to null.
it.data appears to be a Resource object with a status of SUCCESS. So, presumably, you are calling success() with a value of null.
With all that in mind, you will need to use your debugger and see why val body = response.body()?.data is evaluating to null, or see where else you are getting a Resource with null data.
And, please, try to avoid using !!.
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
I am trying to show json data using retrofit library in kotlin
This is my Json:
[
{
"login": "mojombo",
"id": 1,
},
{
"login": "defunkt",
"id": 2,
}
]
My Main activity
call.enqueue(object : Callback<UserResponse> {
override fun onResponse(call: Call<UserResponse>, response: Response<UserResponse>) {
Log.e("list","list")
val countrylist = response.body()
for (size in response.body()) {
System.out.println(size.toString())
}
// var listOfMovies: List<UserResponse> = response.body()?.results!!
// myCustomAdapter = UserListAdapter(applicationContext, listOfMovies)
// recyclerView.setAdapter(myCustomAdapter)
progressBar.visibility = View.GONE
}
override fun onFailure(call: Call<UserResponse>?, t: Throwable?) {
progressBar.visibility = View.GONE
Log.e("list", t.toString())
}
})
This an app that I build in kotlin using retrofit and rxjava in the best way possible using a test API.
Model
data class Post( val userID:Int, val title:String, val body: String)
Retrofit Package
IMyApi interface
interface IMyApi {
#get:GET("posts")
val posts: Observable<List<Post>>
}
RetrofitClient Object class
object RetrofitClient {
val instance: Retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
Adapter Package
PostAdapter class
class PostAdapter(private val context: Context, private val postList: List<Post>)
:RecyclerView.Adapter<PostViewHolder>()
{
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
PostViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.post_item, parent, false)
return PostViewHolder(itemView)
}
override fun getItemCount(): Int {
return postList.size
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int)
{
holder.userId.text = postList[position].userID.toString()
holder.title.text = postList[position].title
holder.body.text = StringBuilder(postList[position].body.substring(0,20))
.append("...").toString()
}
}
PostViewHolder class
class PostViewHolder(itemView: View):RecyclerView.ViewHolder(itemView) {
var userId = itemView.txtID
var title = itemView.txtTitle
var body = itemView.txtBody
}
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var jsonApi: IMyApi
private var compositeDisposable: CompositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Init api
val retrofit = RetrofitClient.instance
jsonApi = retrofit.create(IMyApi::class.java)
// View
recycler_posts.layoutManager = LinearLayoutManager(this)
recycler_posts.setHasFixedSize(true)
fetchData()
}
private fun fetchData() {
compositeDisposable.add(jsonApi.posts
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { posts->displayData(posts)})
}
private fun displayData(posts: List<Post>?) {
val adapter = PostAdapter(this, posts!!)
recycler_posts.adapter = adapter
}
}
Using this as displayed above should help you solve your issue hopefully. Also when in the code you come across "recycler_posts". This is a id to the recycler added in activity_main. If you need me to include that let me know
That's what we have on our app
object GetFAQsAPI {
private val LOG_TAG = GetFAQsAPI.javaClass.simpleName
interface ThisCallback {
fun onSuccess(getFAQs: GetFAQs)
fun onFailure(failureMessage: String)
fun onError(errorMessage: String)
}
/* POST */
fun postData(jo: JsonObject, callback: GetFAQsAPI.ThisCallback) {
val call = Service.getService().get_faqs(jo)
call.enqueue(object : Callback<JsonObject> {
override fun onResponse(call: Call<JsonObject>, response: Response<JsonObject>) {
//Log.e(LOG_TAG, response.body().toString());
try {
if (response.body()?.get("success")!!.asBoolean) {
val gson = GsonBuilder().setPrettyPrinting().create()
val getFAQs = gson.fromJson(response.body(), GetFAQs::class.java)
callback.onSuccess(getFAQs)
} else {
Log.e(LOG_TAG, "else")
val error = response.body()!!.get("err").asString
callback.onError(error)
}
} catch (e: Exception) {
Log.e(LOG_TAG, "exception" + e.localizedMessage)
callback.onFailure(e.message!!)
}
}
override fun onFailure(call: Call<JsonObject>, t: Throwable) {
Log.e(LOG_TAG, "onFailure: " + t.message)
callback.onFailure(t.message!!)
}
})
}
}
That's how we call it from our fragment - getFAQs is the object parsed.
private fun getFAQsAPI() {
showLoading(true)
val jo = JsonObject().apply {
addProperty("faq_category", "admin")
}
GetFAQsAPI.postData(jo, object : GetFAQsAPI.ThisCallback {
override fun onSuccess(getFAQs: GetFAQs) {
Log.i(LOG_TAG, "onSuccess")
showLoading(false)
updateUI(getFAQs)
}
override fun onFailure(failureMessage: String) {
Log.e(LOG_TAG, failureMessage)
}
override fun onError(errorMessage: String) {
Log.e(LOG_TAG, errorMessage)
}
})
}
Hope that helps.
I`m developing a gallery app in kotlin using pixabay api, implementing Android Architecture components and retrofit to interact with the server.
the app works fine but when i change the configuration the view model returns null! what is the problem ?
interface PhotoApi {
#GET(context.getString(R.string.api_key))
fun getPhotos(): Call<PhotoList>
}
Retrofit setup
object PhotoRetriever {
val BASE_URL = "https://pixabay.com/api/"
val service: PhotoApi
init {
val retrofit = Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()).build()
service = retrofit.create(PhotoApi::class.java)
}
}
class MainActivity : AppCompatActivity() {
//var photoList: List<Photo>? = null
lateinit var viewModel: MainActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this#MainActivity).get(MainActivityViewModel::class.java)
viewModel.getList().observe(this, object :Observer<List<Photo>> {
override fun onChanged(t: List<Photo>?) {
textview.setText(t?.get(1)?.webFormatUrl)
}
})
the view model class
class MainActivityViewModel : ViewModel() {
private val repository: Repository
lateinit var photoList: LiveData<List<Photo>>
val itemsListObservable: MediatorLiveData<List<Photo>>
init {
repository = Repository()
itemsListObservable = MediatorLiveData()
}
fun getList(): LiveData<List<Photo>> {
photoList = repository.getNetworkData()
Log.i("RECEIVED","size is ${photoList.value?.size} from viewModel ")
return photoList
}
}
the Repository class
class Repository {
val photoApi: PhotoApi
var photoList: List<Photo>?
val retlist = MutableLiveData<List<Photo>>()
init {
photoApi = PhotoRetriever.service
photoList = ArrayList<Photo>()
}
fun getItemsListFromWeb() {
photoApi.getPhotos().enqueue(object : Callback<PhotoList> {
override fun onFailure(call: Call<PhotoList>?, t: Throwable?) {
// Log.i("RECEIVED","${t?.message} from repository ")
// retlist.value = null
}
override fun onResponse(call: Call<PhotoList>?, response: Response<PhotoList>?) {
if (response!!.isSuccessful) {
retlist.value = response.body()?.hits
} else {
retlist.value = null
}
Log.i("RECEIVED", "size is ${retlist.value?.size} from repository ")
}
})
}
fun getNetworkData(): LiveData<List<Photo>> {
getItemsListFromWeb()
return retlist
}
}
i have solved the problem, the problem was in retrofit onResponse callback method
i remove the else block and everything works fine
before
override fun onResponse(call: Call<PhotoList>?, response: Response<PhotoList>?) {
if (response!!.isSuccessful) {
retlist.value = response.body()?.hits
} else {
retlist.value = null
}
Log.i("RECEIVED", "size is ${retlist.value?.size} from repository ")
}
after
override fun onResponse(call: Call<PhotoList>?, response: Response<PhotoList>?) {
if (response!!.isSuccessful) {
retlist.value = response.body()?.hits
}
Log.i("RECEIVED", "size is ${retlist.value?.size} from repository ")
}
I'm using Room + LiveData in my Android project. Following to Google Blueprints, I've implemented data layer of my application.
This is how my Dao looks like:
#Query("SELECT * FROM events WHERE id=:arg0")
fun loadSingle(id: String): LiveData<Event>
I'm calling it from my EventRepository:
fun loadSingle(eventId: String): LiveData<RequestReader<Event>> {
return object: NetworkManager<Event, Event>(appExecutors!!) {
override fun loadLocal(): LiveData<Event> {
val item = eventLocal!!.loadSingle("Title 1")
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::loadLocal=$item")
return item
}
override fun isUpdateForced(data: Event?): Boolean {
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::isUpdateForced")
return data == null || requestTimeout.isAllowed(UNDEFINED_KEY.toString())
}
override fun makeRequest(): LiveData<ApiResponse<Event>> {
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::makeRequest")
return Database.createService(EventRemote::class.java).load(eventId)
}
override fun onSuccess(item: Event) {
eventLocal?.save(item)
}
override fun onFail() {
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::onFail")
requestTimeout.reset(UNDEFINED_KEY.toString())
}
}.liveData
}
Where NetworkManager class is (has been "taken" from here):
abstract class NetworkManager<ResultType, RequestType> #MainThread constructor(val appExecutors: AppExecutors) {
companion object {
private val TAG = "TAG_NETWORK_MANAGER"
}
val liveData: MediatorLiveData<RequestReader<ResultType>> = MediatorLiveData()
init {
liveData.value = RequestReader.loading(null)
val localSource: LiveData<ResultType> = loadLocal()
Log.d(TAG, "before add::localSource=${localSource.value}")
liveData.addSource(localSource, { data ->
Log.d(TAG, "data=$data")
liveData.removeSource(localSource)
if (isUpdateForced(data)) {
loadRemote(localSource)
} else {
liveData.addSource(localSource, { reusedData -> liveData.value = RequestReader.success(reusedData)})
}
})
}
private fun loadRemote(localSource: LiveData<ResultType>) {
val remoteSource = makeRequest()
liveData.addSource(localSource, {
liveData.value = RequestReader.success(it)
})
liveData.addSource(remoteSource) { response ->
liveData.removeSource(localSource)
liveData.removeSource(remoteSource)
if (response!!.isSuccessful) {
appExecutors.diskIO.execute {
onSuccess(processResponse(response))
appExecutors.mainThread.execute {
liveData.addSource(localSource, {
liveData.value = RequestReader.success(it)
})
}
}
} else {
onFail()
liveData.addSource(localSource, {
liveData.value = RequestReader.error("Error: ${response.errorMessage}", it)
})
}
}
}
#MainThread
protected abstract fun loadLocal(): LiveData<ResultType>
#MainThread
protected abstract fun isUpdateForced(data: ResultType?): Boolean
#MainThread
protected abstract fun makeRequest(): LiveData<ApiResponse<RequestType>>
#WorkerThread
protected abstract fun onSuccess(item: RequestType)
#MainThread
protected abstract fun onFail()
#WorkerThread
protected fun processResponse(response: ApiResponse<RequestType>): RequestType {
return response.body!!
}
}
And after i expect to get my LiveData in ViewModel:
open class EventSingleViewModel: ViewModel(), RepositoryComponent.Injectable {
companion object {
private val TAG = "TAG_EVENT_SINGLE_VIEW_MODEL"
}
#Inject lateinit var eventRepository: EventRepository
var eventSingle: LiveData<RequestReader<Event>>? = null
override fun inject(repositoryComponent: RepositoryComponent) {
repositoryComponent.inject(this)
eventSingle = MutableLiveData<RequestReader<Event>>()
}
fun load(eventId: String) {
Crashlytics.log(Log.VERBOSE, TAG, "starts to loadList::eventId=$eventId")
eventSingle = eventRepository.loadSingle(eventId)
}
}
The problem.
I'm getting a list of events the same way (it works!) I've described above, but with a single event (this event is already in database) it doesn't work. I've found out that localSource.value is null (in NetworkManager). Maybe my query is bad or.. something else.
Check again your DAO implementation, the argument must be the same in both, the function parameter and the annotation arg.
Change this:
#Query("SELECT * FROM events WHERE id=:arg0")
fun loadSingle(id: String): LiveData<Event>
To:
#Query("SELECT * FROM events WHERE id=:id ")
fun loadSingle(id: String): LiveData<Event>