In this example, as short and easy as it is, we are going to see how our two options are leading to different results in the code hierarchy, and I'll give you my humble thought about it at the end.
Implementation steps :
//retrofit
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
//Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
//Rx
implementation "io.reactivex.rxjava2:rxkotlin:2.2.0"
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
implementation "io.reactivex.rxjava2:rxjava:2.2.4"
In the graddle module file, we set the Retrofit library as well as Rx ( aren't they the subject matter ?) and Glide so we can convert our image URL to proper images into our ImageView without sweating too much.
Objects :
For our example, we are going to need simple objects, we will use an pixabay api to get a reasonnable number of pictures for this example.
If you wish you can insert this snippet into an ImageResponse file.
data class ImageList(val hits : List<ImageApi>){
fun getPhoto(i : Int) : ImageApi{
return hits[i]
}
}
data class ImageApi(val userImageURL : String)
Interface used for requests
May I say, X's are supposed to be replaced by an API key gotten from https://pixabay.com/.
Observant as we are, these methods are really similar, requesting the exact same file in the @GET annotation, and apart from the name, the difference is their return files.
getNewPicture() returns an Observable<> imported from io.reactivx.Observable we will use subscribe to with Rx within our view model and getPictureCall() returns a Call<>, from the import retrofit2.Call.
interface PixabayApi {
@GET("api/?key=xxxxxxxxxxxxxxxxxxxxxxxxx&image_type=all")
fun getNewPicture() :Observable<ImageList>
@GET("api/?key=xxxxxxxxxxxxxxxxxxxxxxxxx&image_type=all")
fun getPictureCall() : Call<ImageList>
}
Setting up our service class
Two things to consider, the first one is we add methods returning the methods from the pixabayInterface accordingly. The second is the creation of our Retrofit object that will and i quote :
" Create an implementation of the API endpoints defined by the {@code service} interface."
The addConverterFactory(GsonConverterFactory.create()) method is used for serialization and deserialization of objects.
And because we want to return a type different than call (here we return Observable<>) we add addCallAdapterFactory(RxJava2CallAdapterFactory.create()) method.
GsonConverterFactory, RxJava2CallAdapterFactory are both included into Retrofit implementation.
class Service {
var pixabayApi : PixabayApi? = null
companion object {
private val URL = "https://pixabay.com/"
}
init{
val retrofit = Retrofit.Builder()
.baseUrl(URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
pixabayApi = retrofit.create(PixabayApi::class.java)
}
fun getNewPicture() : Observable<ImageList>{
return pixabayApi!!.getNewPicture()
}
fun getPictureCall(callback : Callback<ImageList>) {
val call = pixabayApi!!.getPictureCall()
call.enqueue(callback)
}
}
Rx and ViewModel
From here the real fun begins. From my point of view ( I may be wrong people ), coupling our friendly friends together saves us from the MVP main problem : THE CALLBACK HELL.
Let's see it.
class ImageViewModel : ViewModel() {
private var compositeDisposable = CompositeDisposable()
private var imageList : ImageList? = null
var imagelistMutable = MutableLiveData<ImageList>()
init {
downloadImageList()
}
override fun onCleared() {
compositeDisposable.dispose()
super.onCleared()
}
private fun setImageList(list: ImageList ){
imageList = list
imagelistMutable.value = imageList
}
private fun getNewIndex() : Int{
return Random().nextInt(imageList!!.hits.size)
}
fun getImage() = imageList!!.getPhoto(getNewIndex())
private fun downloadImageList(){
val subscription = Service().getNewPicture()
//1
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
//2
.subscribe(
//on next
{
Log.i("list is ", it.hits.size.toString())
setImageList(it)
},
//on error
{ Log.e("error",it.toString())}
)
compositeDisposable.add(subscription)
}
}
I'm going to break down the downloadImageList() methods only, because others are explaining themselves really well.
1 : We tell our subscription "dude go work somewhere else and leave the main thread alone !", and then as rude as we are we also tell it " But when you're finished with this bring it back here ! ".
The subscription is a polish and you are the boss !
2 : We susbcribe to the ObservableSource so we can get handlers. What handlers ? onNext and onError of course !
onNext will let us deal with the emitted items we've got from the Observable received. and onError will help us handle errors.
Back to the Activity
How flawless it is huh !
class MainActivity : AppCompatActivity() {
private var viewModel : ImageViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//1
viewModel =
ViewModelProvider.AndroidViewModelFactory(application)
.create(ImageViewModel::class.java)
//2
viewModel!!.imagelistMutable?.observe(this, Observer {
if(it.hits.isNotEmpty()){
setImageView()
}
})
next.setOnClickListener {
setImageView()
}
}
//3
private fun setImageView(){
val imageApi : ImageApi = viewModel!!.getImage()
Glide.with(this).load(imageApi.userImageURL).into(image)
}
}
Let's break this down again :
1 : Behind this easy initialization do you remember the downloadImageList() method ?
If not check upward. We set it in the init{} of our viewModel, meaning its been called from the moment it has been initialized.
2 : ViewModel provides observers as well to update the UI when data has changed or has been received ( from API, db etc... ). The only problem is that, it doesn't allow us to handle errors as Rx does, that's why for secure purpose we still check if the live data we are observing is null.
3 : setImageView() provides a new image to the ImageView. Being the same code when we click as from the moment we received the data it's makes more sense to create a small method and Uhoh ! our Glide implementation is doing the rest.
Without Rx and ViewModel
Let' says instead a view model, we decide to create a presenter class, from which as the view model does, we require the data when we initialize it.
//2
class RetrofitPresenter(listener : RetrofitTransfer) {
private val apiservice = Service()
init {
val callback = object : Callback<ImageList> {
override fun onFailure(call: Call<ImageList>, t: Throwable)
{
Log.e("error",t.message!!)
}
override fun onResponse(call: Call<ImageList>, response:
Response<ImageList>)
{
if(response.isSuccessful){
Log.i("response", response.body().toString())
//3
listener.setImageList(response.body()!!)
}
}
}
apiservice.getPictureCall(callback)
}
private fun getNewIndex(imageList: ImageList) : Int{
return Random().nextInt(imageList.hits.size)
}
fun getImage(imageList: ImageList) = imageList.hits[getNewIndex(imageList)]
// 1
interface RetrofitTransfer {
fun setImageList( imageList: ImageList)
}
}
If you've seen numbers on the snippet it means something needs to be explained so let's do it :
1 : We create an interface, as a listener ,we'll be able to manage data later on with in the main activity which will implement it.
2 : As previously said, we 'll use the listener implemented in the main activity, not from the controller class.
3 : We finally call the listener and its method, to set its parameter with the reponse.body(), which is the result we want to use later on.
But daddy, why do you use an interface to set a value in another class instead of a field variable ?
Dear Billy, it's called the null galaxy. Check by yourself, the data you will get in the override onResponse() method, will be null outside of it.
Back to the Main Activity again
//1
class MainActivity : AppCompatActivity() , RetrofitPresenter.RetrofitTransfer
{
private var retrofitController : RetrofitController? = null
private var mImageList : ImageList? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
retrofitController = RetrofitPresenter(this)
next.setOnClickListener { setImage()}
}
private fun setImage(){
val imageApi = retrofitController!!.getImage(mImageList!!)
Glide.with(this).load(imageApi.userImageURL).into(image)
}
//2
override fun setImageList(imageList: ImageList) {
mImageList = imageList
setImage()
}
It's pretty straight forward :
1 : We implement the interface located in the RetrofitPresenter class
2 : we override its method and use the data acquired from the onResponse() method in the controller to set our field variable.
Conclusion
Let's be honest, it's a tiny smally doolly example. But it's big enough to show us the structure I preferably want to avoid.
Using a view model from the main activity, we understand the same view model can be used in a different place somewhere in our code jungle, helping us to trim tree and see where tricky monkeys are. The advantage as small as it is in this example using RxJava is not to have an anonymous class in our class running around and calling its override methods, which make the received data available.
When we use a the presenter class on the other hand, thanks to the null galaxy We first need to create a new interface, resulting in the beginning of the CALLBACK HELL. It will need to be listened to somewhere else to be used and transfer data from the presenter to the activity. Plus, let's say we now have several methods in the interface, our code jungle becomes a JUNGLE CALLBACK HELL ( for sure when using Java). And to add more on the CALLBACK HELL back, retrofit doesn't help, even if it's just two methods added, the anonymous callback makes it kinda ugly.
I like better a straightforward way to do things, but we are different and it's still important to know our options ( but take the first one ! ).
Comentários