I'm new in kotlin and I'm trying to use retrofit with Rxjava and live data in MVVM architecture.
I config retrofit, and also use observable and subscribe in ViewModel to make observable variable to use in activity binding layout.
I have a button in my view and when I click on it, the request method gets to start and subscription write a log of its own data. but my variable gets null at first and after seconds, when retrofit completed its task, my variable gets data but log value doesn't update.
this is my retrofit initialize class
class ApiService {
private val INSTANCE =
Retrofit.Builder()
.baseUrl("http://www.janbarar.ir/App/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(IRetrofitMethods::class.java)
private fun <T> callBack(iDataTransfer: IDataTransfer<T>) =
object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val data = response.body()
if (data != null)
iDataTransfer.onSuccess(data)
else
try {
throw Exception("data is empty")
} catch (ex: Exception) {
iDataTransfer.onError(ex)
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
iDataTransfer.onError(t)
}
}
fun getCategories(iDataTransfer: IDataTransfer<List<Category>>) =
INSTANCE.getCategories().enqueue(callBack(iDataTransfer))
this is an interface for retrofit
#GET("GetCategories")
fun getCategories(): Call<List<Category>>
this is my model class. I think the problem is here. because the observable send null data before retrofit finish its work.
fun getCategories(): Observable<ArrayList<Category>> {
val result = arrayListOf<Category>()
api.getCategories(object : IDataTransfer<List<Category>> {
override fun onSuccess(data: List<Category>) {
result.addAll(data)
}
override fun onError(throwable: Throwable) {
Log.e("Model", throwable.message!!)
}
})
return Observable.just(result)
}
and this is also my ViewModel class
class ProductViewModel(private val model: ProductModel) : ViewModel() {
var isLoading = ObservableField(false)
var categoryList = MutableLiveData<ArrayList<Category>>()
private var compositeDisposable = CompositeDisposable()
fun getCategories() {
isLoading.set(true)
compositeDisposable +=
model.getCategories()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
categoryList.value = it
}, {
Log.e("ViewModel", it.message.toString())
})
isLoading.set(false)
}
finally, it's my activity
lateinit var binding: ActivityMainBinding
private val vm: ProductViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.vm = vm
vm.categoryList.observe(this, Observer {
if (it != null)
Log.e("activity", it.toString())
})
}
As ExpensiveBelly mentioned in a comment, Retrofit provides a call adapter for RxJava, so you can let your API return Observable<List<Category>> directly. To do this, you will need to add the RxJava call adapter dependency to your app module's build.gradle:
implementation 'com.squareup.retrofit2:adapter-rxjava2:(version)'
Add the call adapter factory when constructing your Retrofit instance:
private val INSTANCE =
Retrofit.Builder()
.baseUrl("http://www.janbarar.ir/App/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // add this
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(IRetrofitMethods::class.java)
And then just let your service return an Observable directly:
#GET("GetCategories")
fun getCategories(): Observable<List<Category>>
If ApiService needs to do some handling before the rest of the app gets the response, you can use RxJava operators like map.
It would be illustrative to see why your code doesn't work and how to fix it. When you call api.getCategories(someCallback), one of your callback methods will be called at some point in the future. In the meantime, the model.getCategories() method will return immediately.
When you subscribe to the returned Observable, it emits the result variable, which is currently an empty list. result will eventually have some data in it, but your code will not be informed of this at all.
What you really want to do is emit the list of categories when it becomes available. The standard way to get an Observable from a callback API is with Observable.create:
fun getCategories(): Observable<ArrayList<Category>> {
return Observable.create { emitter ->
api.getCategories(object : IDataTransfer<List<Category>> {
override fun onSuccess(data: List<Category>) {
emitter.onNext(data)
emitter.onComplete()
}
override fun onError(throwable: Throwable) {
emitter.onError(throwable)
}
})
}
}
Of course, it's better to just use RxJava2CallAdapterFactory if possible, since this work has already been done there.
Related
We have been discussing about this but we don't know the reason of creating a viewmodel factory to create a viewmodel instead of instantiate the viewmodel directly. What is the gain of creating a factory that just creates the viewmodel?
I just put a simple example of how I did it without Factory
here is the kodein module:
val heroesRepositoryModel = Kodein {
bind<HeroesRepository>() with singleton {
HeroesRepository()
}
bind<ApiDataSource>() with singleton {
DataModule.create()
}
bind<MainViewModel>() with provider {
MainViewModel()
}
}
The piece of the Activity where I instantiate the viewmodel without using the factory
class MainActivity : AppCompatActivity() {
private lateinit var heroesAdapter: HeroAdapter
private lateinit var viewModel: MainViewModel
private val heroesList = mutableListOf<Heroes.MapHero>()
private var page = 0
private var progressBarUpdated = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this)
.get(MainViewModel::class.java)
initAdapter()
initObserver()
findHeroes()
}
The ViewModel where I instantiate the usecase directly without having it in the constructor
class MainViewModel : ViewModel(), CoroutineScope {
private val heroesRepository: HeroesRepository = heroesRepositoryModel.instance()
val data = MutableLiveData<List<Heroes.MapHero>>()
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = uiContext + job
fun getHeroesFromRepository(page: Int) {
launch {
try {
val response = heroesRepository.getHeroes(page).await()
data.value = response.data.results.map { it.convertToMapHero() }
} catch (e: HttpException) {
data.value = null
} catch (e: Throwable) {
data.value = null
}
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
So here a example using factory
class ListFragment : Fragment(), KodeinAware, ContactsAdapter.OnContactListener {
override val kodein by closestKodein()
private lateinit var adapterContacts: ContactsAdapter
private val mainViewModelFactory: MainViewModelFactory by instance()
private val mainViewModel: MainViewModel by lazy {
activity?.run {
ViewModelProviders.of(this, mainViewModelFactory)
.get(MainViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_list, container, false)
}
The viewmodelfactory:
class MainViewModelFactory (private val getContacts: GetContacts) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(getContacts) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
And the viewmodel:
class MainViewModel(private val getContacts: GetContacts) : BaseViewModel() {
lateinit var gamesList: LiveData<PagedList<Contact>>
var contactsSelectedData: MutableLiveData<List<Contact>> = MutableLiveData()
var contactsSelected: ArrayList<Contact> = ArrayList()
private val pagedListConfig by lazy {
PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(PAGES_CONTACTS_SIZE)
.setPageSize(PAGES_CONTACTS_SIZE)
.setPrefetchDistance(PAGES_CONTACTS_SIZE*2)
.build()
}
Here is the complete first example:
https://github.com/ibanarriolaIT/Marvel/tree/mvvm
And the complete second example:
https://github.com/AdrianMeizoso/Payment-App
We can not create ViewModel on our own. We need ViewModelProviders utility provided by Android to create ViewModels.
But ViewModelProviders can only instantiate ViewModels with no arg constructor.
So if I have a ViewModel with multiple arguments, then I need to use a Factory that I can pass to ViewModelProviders to use when an instance of MyViewModel is required.
For example -
public class MyViewModel extends ViewModel {
private final MyRepo myrepo;
public MyViewModel(MyRepo myrepo) {
this.myrepo = myrepo;
}
}
To instantiate this ViewModel, I need to have a factory which ViewModelProviders can use to create its instance.
ViewModelProviders Utility can not create instance of a ViewModel with argument constructor because it does not know how and what objects to pass in the constructor.
In short,
if we need to pass some input data to the constructor of the viewModel , we need to create a factory class for viewModel.
Like example :-
class MyViewModelFactory constructor(private val repository: DataRepository): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MyViewModel::class.java!!)) {
MyViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}
}
Reason
We cannot directly create the object of the ViewModel as it would not be aware of the lifecyclerOwner. So we use :-
ViewModelProviders.of(this, MyViewModelFactory(repository)).get(MyViewModel::class.java)
We have been discussing about this but we don't know the reason of creating a viewmodel factory to create a viewmodel instead of instantiate the viewmodel directly. What is the gain of creating a factory that just creates the viewmodel?
Because Android will only give you a new instance if it's not yet created for that specific given ViewModelStoreOwner.
Let's also not forget that ViewModels are kept alive across configuration changes, so if you rotate the phone, you're not supposed to create a new ViewModel.
If you are going back to a previous Activity and you re-open this Activity, then the previous ViewModel should receive onCleared() and the new Activity should have a new ViewModel.
Unless you're doing that yourself, you should probably just trust the ViewModelProviders.Factory to do its job.
(And you need the factory because you typically don't just have a no-arg constructor, your ViewModel has constructor arguments, and the ViewModelProvider must know how to fill out the constructor arguments when you're using a non-default constructor).
When we are simply using ViewModel, we cannot pass arguments to that ViewModel
class GameViewModel() : ViewModel() {
init {
Log.d(TAG, "GameViewModel created")
}
}
However, in some cases, we need to pass our own arguments to ViewModel. This can be done using ViewModelFactory.
class ScoreViewModel(finalScore: Int) : ViewModel() {
val score = finalScore
init {
Log.d(TAG, "Final score: $finalScore")
}
}
And to instantiate this ViewModel, we need a ViewModelProvider.Factory as simple ViewModel cannot instantiate it.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
return ScoreViewModel(finalScore) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
When it comes to instantiating object of this ViewModel i.e with ViewModelProvider, we pass ViewModelFactory as an argument which contains information about our custom arguments which we want to pass. It goes like:
viewModelFactory = ScoreViewModelFactory(score)
viewModel = ViewModelProvider(this,viewModelFactory).get(ScoreViewModel::class.java)
That is why factory methods are there.
class MainActivity : AppCompatActivity() {
private val fizzUrl = "https://s3-us-west-1.amazonaws.com/fizzup/files/public/"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
//Instance Retrofit
val retrofit = Retrofit.Builder()
.baseUrl(fizzUrl)
.addConverterFactory(MoshiConverterFactory.create())
.build()
//Instance Api
val service = retrofit.create(ExerciseService::class.java)
//Get Request
val call = service.listExercises()
//Get execution
call.enqueue(object: Callback<List<Exercise>> {
override fun onResponse(call: Call<List<Exercise>>, response: Response<List<Exercise>>) {
val allExercise = response.body()
if (allExercise!= null) {
println("All exercises are loaded :")
for (c in allExercise)
println(" one exercise : ${c.name}")
}
}
override fun onFailure(call: Call<List<Exercise>>, t: Throwable) {
error("KO")
}
})
}
Here is my main activity. I have a data model : Exercise
and i have an interface ExerciceService.
But i have an issue with the enqueue method that tell me : Expecting member declaration
Therefore I have an other issue on the object : name expected.
But I've looked how to use enqueue and all I see is what I've done.
Thank for reading me and sorry for bad english.
#Xetiam,
Check if have imported correctly Callback class.
You can also check here. They have great documentation.
https://site-valley.com/2021/02/17/retrofit-android-tutorial-in-kotlin/
I want to get input from the user using EditText and pass it to server and show the response to the user. I do this simply without any architecture but I would like to implement it in MVVM.
this is my repository code:
class Repository {
fun getData(context: Context, word: String): LiveData<String> {
val result = MutableLiveData<String>()
val request = object : StringRequest(
Method.POST,
"https://jsonplaceholder.typicode.com/posts",
Response.Listener {
result.value = it.toString()
},
Response.ErrorListener {
result.value = it.toString()
})
{
#Throws(AuthFailureError::class)
override fun getParams(): MutableMap<String, String> {
val params = HashMap<String, String>()
params["word"] = word
return params
}
}
val queue = Volley.newRequestQueue(context)
queue.add(request)
return result
}
}
and these are my View Model codes:
class ViewModel(application: Application) : AndroidViewModel(application) {
fun getData(word: String): LiveData<String> {
val repository = Repository()
return repository.getData(getApplication(), word)
}
}
and my mainActivity would be like this:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val model = ViewModelProviders.of(this).get(ViewModel::class.java)
model.getData("test").observe(this, Observer {
Log.i("Log", "activity $it")
})
}
}
My layout has an EditText which I want to get user input and pass it to the server, how should i do that?
Here how i did it in my projet.
You can probably use android annotations.
It's gonna requiere you to put some dependencies and maybe change the class a bit, but then you gonna link your Viewmodel with your repository and then you gonna have to program the setter of the variable to do a notifyChange() by making the class herited from BaseObservable. Then in the xml, if you did the correctly, you should be able to do a thing like text:"#={model.variable}" and it should be updating at the same time.
A bit hard and explain or to show for me sorry, but i would look into Android Annotations with #DataBinding, #DataBound :BaseObservable
https://github.com/androidannotations/androidannotations/wiki/Data-binding-support
Hope that can help a bit!
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
}
}
})
}
So I have a method that binds to the service.
fun bindService() {
val intent = Intent(this, BluetoothService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
Inside onCreate method I use this code:
bindService()
launch {
delay(500L)
service = serviceConnection.serviceBinder?.getService() as BluetoothService
}
Is there more elegant way to wait for the service to be bound than using delay()?
I wrote this just now, and haven't tried it, but hopefully something like it could work. The magic is in suspendCoroutine, which pauses the current coroutine and then gives you a continuation thingy you can use to resume it later. In our case we resume it when the onServiceConnected is called.
// helper class which holds data
class BoundService(
private val context: Context,
val name: ComponentName?,
val service: IBinder?,
val conn: ServiceConnection) {
fun unbind() {
context.unbindService(conn)
}
}
// call within a coroutine to bind service, waiting for onServiceConnected
// before the coroutine resumes
suspend fun bindServiceAndWait(context: Context, intent: Intent, flags: Int) = suspendCoroutine<BoundService> { continuation ->
val conn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
continuation.resume(BoundService(context, name, service, this))
}
override fun onServiceDisconnected(name: ComponentName?) {
// ignore, not much we can do
}
}
context.bindService(intent, conn, flags)
}
// just an example
suspend fun exampleUsage() {
val bs = bindServiceAndWait(context, intent, Context.BIND_AUTO_CREATE)
try {
// ...do something with bs.service...
} finally {
bs.unbind()
}
}