Building a Kotlin project 2/2
Ciro RizzoPart 2
In the previous post we started a new project from scratch, and adjusted the build.gradle
for the Kitten App purpose.
The next steps are going to be the programming of the elements of the app.
Data Model
One of the main features of the app is to retrieve data through Internet via the http://thecatapi.com/
The complete API call will be i.e.
http://thecatapi.com/api/images/get?format=xml&results_per_page=10
The API sends back an XML
file like this one
It needs to deserialize in order to get the url
property containing the location of the Kitten image.
Kotlin has a very useful class called data class
that is perfect for the purpose.
Let's starting to create a new class file in the model.cats
package using right click on it and New->Kotlin File/Class
and call it Cats
and choose Class
as kind.
In order to structure the class as the XML
file received the Cats.kt
will be as the following
data class Cats(var data: Data? = null)
data class Data(var images: ArrayList<Image>? = null)
data class Image(var url: String? = "", var id: String? = "", var source_url: String? = "")
So far very simple... The same class in Java is much longer!
The Kotlin Data Class has several benefits, some of them are that the compiler generates getter()
, setter()
and toString()
methods, and many more like equals()
hashCode()
and copy()
. So it's the perfect class to use to deserialize data
API Call
There are many ways to retrieve data through the network, and different libraries to handle it. One of these libraries is the Retrofit2 library from Square.
This is a very powerful HTTPClient
and easy to set up.
Let's start with the interface
and create it under the network
package.
Call it CatAPI
as shown
interface CatAPI {
@GET("/api/images/get?format=xml&results_per_page=" + BuildConfig.MAX_IMAGES_PER_REQUEST)
fun getCatImageURLs(): Observable<Cats>
}
The interface
will manage the Get
request to the API Endpoint /api/images/get?format=xml&results_per_page=
.
In this case the param results_per_page
retrieve its numerical value from the constant defined in the build.gradle
called MAX_IMAGES_PER_REQUEST
that will have different values depending on the buildTypes
used.
buildTypes {
debug {
buildConfigField("int", "MAX_IMAGES_PER_REQUEST", "10")
...
This method is very useful to have different value of constants in case we're using
debug
type orrelease
one i.e. especially in case you need to access to the debug API instead of the production one
About the interface CatAPI
is very interesting the function called to manage the callback from the API fun getCatImageURLs(): Observable<Cats>
So the next step is its implementation.
Let's create a new class under the same package (network
) and call it CatAPINetwork
as this
class CatAPINetwork {
fun getExec(): Observable<Cats> {
val retrofit = Retrofit.Builder()
.baseUrl("http://thecatapi.com")
.addConverterFactory(SimpleXmlConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
val catAPI: CatAPI = retrofit.create(CatAPI::class.java)
return catAPI.getCatImageURLs().
subscribeOn(Schedulers.io()).
observeOn(AndroidSchedulers.mainThread())
}
}
The fun getExec(): Observable<Cats>
is implicitly public
so that means available to be called by outside this class.
The line .addConverterFactory(SimpleXmlConverterFactory.create())
indicates to use the XML
converter to deserialize the result form the API call.
Then the .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
is the call adapter to use on the API Callback
The return
lines referring to the RxJava
Observable
return catAPI.getCatImageURLs().
subscribeOn(Schedulers.io()).
observeOn(AndroidSchedulers.mainThread())
Presenter
The Presenter
modules are in charge of managing the logic of the app and to bind data between the View
and the Model
.
In our case it will implement the method called by the View
to retrieve the API data and send them to the Adapter
in charge to show up.
In order to communicate with the View
we're starting to create its interface
called MasterPresenter
under the package presenter
as the followed
interface MasterPresenter {
fun connect(imagesAdapter: ImagesAdapter)
fun getMasterRequest()
}
The first function fun connect(imagesAdapter: ImagesAdapter)
will be used to connect the Adapter interface
to show data, and the fun getMasterRequest()
will be the one to start the API request.
Let's the implementations in a new class under the same presenter
package and call it MasterPresenterImpl
class MasterPresenterImpl : MasterPresenter {
lateinit private var imagesAdapter: ImagesAdapter
override fun connect(imagesAdapter: ImagesAdapter) {
this.imagesAdapter = imagesAdapter
}
override fun getMasterRequest() {
imagesAdapter.setObservable(getObservableMasterRequest(CatAPINetwork()))
}
private fun getObservableMasterRequest(catAPINetwork: CatAPINetwork): Observable<Cats> {
return catAPINetwork.getExec()
}
}
Interesting line at lateinit private var imagesAdapter: ImagesAdapter
where Kotlin give us the chance to declare a Non-Nullable mutable object without initialization thanks to lateinit
keyword. So it will be initialize at the first time it will be used at runtime; in our case on calling the fun connect(imagesAdapter: ImagesAdapter)
.
The function fun getMasterRequest()
is in charge of starting the API call, just set the Observable
in order to be subscribed by the Adapter
(i.e. imagesAdapter
) after starts the catAPINetwork.getExec()
that executes the API call
View section
In the view
package are collected the classes to manage the UI.
Basically are the View
and the Adapter
ones; in our case MainActivity
and ImagesAdapter
.
Layouts
Before starting with their implementation, let's go through the Layout
designing.
To design it we need basically of two elements the main container and the item container.
The main container is the element containing the list of the item and we're going to place it in the activity_main.xml
contained in the res->layout
folder of the project; this one has been automatically created during the initial phase of Project Creation.
For our app we need to inject in a RecyclerView
component (a very powerful and optimized component for listing views).
The activity_main.xml
will be like shown
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:context=".view.MainActivity"
android:gravity="center">
<android.support.v7.widget.RecyclerView
android:id="@+id/containerRecyclerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:layout_centerInParent="true" />
</RelativeLayout>
The component containerRecyclerView
represents the main container of the list of item
The row_card_view.xml
is the item container of our list and basically it appears like this
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/card_view"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
card_view:cardCornerRadius="4dp"
android:layout_margin="16dp"
android:background="@android:color/transparent"
android:layout_centerInParent="true"
android:elevation="4dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:foregroundGravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imgVw_cat"
android:padding="4dp"
android:layout_centerInParent="true"
android:scaleType="fitCenter"
android:contentDescription="@string/cat_image" />
</RelativeLayout>
</android.support.v7.widget.CardView>
As you can see the item container is the card_view
that basically is composed by a RelativeLayout
containing an ImageView
(imgVw_cat
)
Adapter
Now we've got basic elements of the Layout
, so let's move on implementing the MainActivity
and the Adapter
.
Starting on Adapter
the first thing to create is its interface
in order to be invoked by the previous MasterPresenterImpl
so let's create a new file in the view
package and called it ImagesAdapter
and write it down like this
interface ImagesAdapter {
fun setObservable(observableCats: Observable<Cats>)
fun unsubscribe()
}
The function setObservable(observableCats: Observable<Cats>)
is called by MasterPresenterImpl
to set the Observable
and give the Adapter
to subscribe to it.
The unsubscribe()
function will be invoked by the MainActivity
to unsubscribe the Adapter
from the Observable
when the activity is destroyed.
Now let's implement them in a new class under the same package called ImagesAdapterImpl
that appears like that
class ImagesAdapterImpl : RecyclerView.Adapter<ImagesAdapterImpl.ImagesURLsDataHolder>(), ImagesAdapter {
private val TAG = ImagesAdapterImpl::class.java.simpleName
private var cats: Cats? = null
private val subscriber: Subscriber<Cats> by lazy { getSubscribe() }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImagesURLsDataHolder {
return ImagesURLsDataHolder(
LayoutInflater.from(parent.context).inflate(R.layout.row_card_view, parent, false))
}
override fun getItemCount(): Int {
return cats?.data?.images?.size ?: 0
}
override fun onBindViewHolder(holder: ImagesURLsDataHolder, position: Int) {
holder.bindImages(cats?.data?.images?.get(position)?.url ?: "")
}
private fun setData(cats: Cats?) {
this.cats = cats
}
override fun setObservable(observableCats: Observable<Cats>) {
observableCats.subscribe(subscriber)
}
override fun unsubscribe() {
if (!subscriber.isUnsubscribed) {
subscriber.unsubscribe()
}
}
private fun getSubscribe(): Subscriber<Cats> {
return object : Subscriber<Cats>() {
override fun onCompleted() {
Log.d(TAG, "onCompleted")
notifyDataSetChanged()
}
override fun onNext(cats: Cats) {
Log.d(TAG, "onNextNew")
setData(cats)
}
override fun onError(e: Throwable) {
//TODO : Handle error here
Log.d(TAG, "" + e.message)
}
}
}
class ImagesURLsDataHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindImages(imgURL: String) {
Glide.with(itemView.context).
load(imgURL).
placeholder(R.mipmap.document_image_cancel).
diskCacheStrategy(DiskCacheStrategy.ALL).
centerCrop().
into(itemView.imgVw_cat)
}
}
}
This is the class inflate the row_card_view.xml
, basically the item container as you can see at function onCreateViewHolder
The function getSubscribe()
provide to subscribe the Adapter
to the Observable
used at line private val subscriber: Subscriber<Cats> by lazy { getSubscribe() }
where you can notice the lazy
initialization, this is a way to declare an unmutable object (i.e. subscriber
) and it'll be created through the function enclosed in the braces (i.e. getSubscribe()
) at the first invocation at runtime.
The Subscriber and Observable concepts comes from RxJava; We can dig deeper in some next posts
At the end, a very interesting piece of code is the inner class called ImagesURLsDataHolder
used to fill up the imgVw_cat
using the Glide
library, that helps to retrieve the image from the passed URL
extracted by the API call. This part is wrapped in the function bindImages(imgURL: String)
and invoked by the method onBindViewHolder
in the same file.
Activity
Last but not least the the Activity
(i.e. MainActivity
)
class MainActivity : AppCompatActivity() {
private val imagesAdapterImpl: ImagesAdapterImpl by lazy { ImagesAdapterImpl() }
private val masterPresenterImpl: MasterPresenterImpl
by lazy {
MasterPresenterImpl()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initRecyclerView()
connectingToMasterPresenter()
getURLs()
}
override fun onDestroy() {
imagesAdapterImpl.unsubscribe()
super.onDestroy()
}
private fun initRecyclerView() {
containerRecyclerView.layoutManager = GridLayoutManager(this, 1)
containerRecyclerView.adapter = imagesAdapterImpl
}
private fun connectingToMasterPresenter() {
masterPresenterImpl.connect(imagesAdapterImpl)
}
private fun getURLs() {
masterPresenterImpl.getMasterRequest()
}
}
Notice the functions
initRecyclerView()
connectingToMasterPresenter()
getURLs()
Respectively used to
- initialize the main container (i.e.
RecyclerView
) - connecting the
MainActivity
to theMasterPresenterImpl
and pass to it theinterface
of theImagesAdapterImpl
(akaAdapter
) getURLs()
starts the API request to retrieve theXML
data, and so executing the followed tasks (deserialize data, retrieving images throughAdapter
).
The Kitten App is now ready to run. In any case you can find the entire project on my Github Repository of the KShow project. The same project has been written in Java as well JShows so you can comparing them
Translations
This post is also available in Chinese on gold.xitu.io A special thanks for the translation to the gold.xitu.io Team