Network calls in android Using retrofit and coroutines
Carlos Okumu
Posted on February 24, 2022
Making network calls is a common task in android.The commonly used Libraries are Retrofit and Volley.I am a huge fan of the retrofit Library because of the good documentation and it is very easy to setup and use.Today we are going to learn how to do network calls using this simple library and coroutines.
Starting from version 2.6.0 Retrofit supports the concept of “suspend” functions.
Before we begin,we are going to understand a simple project that we are going to build.
We will be using mvvm architecture pattern to help you understand our today's topic.If you are not familiar with mvvm or need a refresh of this,please check this nice article..
Set up a new project with Kotlin and other dependencies required
We will be using a single activity for the purpose of this tutorial.By default android studio creates an empty activity for you to begin with.
Add dependencies
Add the following dependencies in your app level build.gradle.
//Lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-alpha03'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0-alpha03'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.0-alpha03'
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'
//Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
//Coil
implementation 'io.coil-kt:coil:1.4.0'
We have added the required dependencies and for the versions you can always change them as versions are being released with time
Setting up the network package
We are going to put our kotlin classes into packages to make things neat.First,let's set up the network package.
Create an Enum class,we will name it Status.Basically this class will represent the three states of the network call.So the Status.kt
will look as below:
enum class Status {
SUCCESS,
ERROR,
LOADING
}
Let's now create a Generic class and name it ApiModel.This class will contain information about our network call so that we can update our views accordingly.So ApiModel.kt
looks like below:
data class ApiModel<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T): ApiModel<T> = ApiModel(status = Status.SUCCESS, data = data, message = null)
fun <T> error(data: T?, message: String): ApiModel<T> =
ApiModel(status = Status.ERROR, data = data, message = message)
fun <T> loading(data: T?): ApiModel<T> = ApiModel(status = Status.LOADING, data = data, message = null)
}
}
Setting up our Model.
We will be fetching our data for the current weather using the name of a city.So go ahead and obtain an api key from here..If you find any problems please don't hesitate to reach out.
Now let's create our Pojo class to represent the data that we will get back after our api call.So again our WeatherResponse.kt
looks like so:
data class WeatherResponse(
var location: Location,
var current: Current
)
Please note that i have not given the full content of the Location and Current classes but you can check them here in the models package.
Setting up Retrofit
Since we will be using retrofit for networking,let us create some classes for this purpose.First we will create an Interface class that will be a retrofit service class.So go ahead and create a class name it ApiService.kt
then it should contain the following code.
interface ApiService {
@GET("v1/current.json")
suspend fun getCurrentWeather(@Query("key") key: String ="94d124c031ba4486b9c81847222402",@Query("q") cityName: String?): WeatherResponse
}
then go ahead and create an object class and name it RetrofitBuilder.kt
.As the name suggests,this class will be responsible for a creating our retrofit instance using the builder pattern.Have the following code in it
object RetrofitBuilder {
private fun getRetrofit(): Retrofit {
val interceptor = HttpLoggingInterceptor()
interceptor.level = when (BuildConfig.BUILD_TYPE) {
"release" -> HttpLoggingInterceptor.Level.NONE
else -> HttpLoggingInterceptor.Level.BODY
}
val okHttp= OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl(Constants.REST_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttp)
.build()
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
Since we will be using a Repository pattern,let's create a class and name it MainRepository.kt
and we will pass it our retrofit instance to the constructor.Have the following code in it also.
class MainRepository(private val retrofit: RetrofitBuilder) {
suspend fun fetchCurrentWeather() = retrofit.apiService.getCurrentWeather()
}
Setting up the ViewModel
We are almost getting to the View now 🙂.Go ahead and create a class and name it MainViewModel.kt
.As you have guessed, have the following code in it also:
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {
fun fetchCurrentWeather() = liveData(Dispatchers.IO) {
emit(ApiModel.loading(data = null))
try {
emit(ApiModel.success(data = mainRepository.fetchCurrentWeather()))
} catch (exception: Exception) {
emit(ApiModel.error(data = null, message = exception.message ?: "Something went Wrong"))
}
}
}
You remember our Apimodel class we created earlier?,this is where we initialize our states.When loading,the data will be null,and if the call is sucessful,we have our data fetched into our value holder and also any error that occurred is tapped.We will also create a Factory for our viewmodel so go ahead and create a class named ViewmodelFactory.kt
and have the following code in it:
class ViewModelFactory(private val retrofit: RetrofitBuilder) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(MainRepository(retrofit)) as T
}
throw IllegalArgumentException("Unknown class name")
}
}
Let's now create the user interface.
Setting up the User Interface
First,let's understand how the interface will work,when a user enters the name of a city and the name is correct,the current weather conditions will be displayed in a CardView.
So activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/appBar"
layout="@layout/app_bar_layout" />
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/margin_normal"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="@dimen/margin_small">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_normal"
android:fontFamily="casual"
android:padding="@dimen/margin_normal"
android:text="Current Weather"
android:textColor="@color/black" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:minWidth="0dp" />
<ImageView
android:id="@+id/imageIcon"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_margin="@dimen/margin_normal" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tempC"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_normal"
android:fontFamily="sans-serif-smallcaps"
android:padding="@dimen/margin_normal"
android:textColor="@color/black"
android:textSize="@dimen/margin_normal"
tools:text="20.3" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:minWidth="0dp" />
<TextView
android:id="@+id/tempF"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_normal"
android:fontFamily="sans-serif-smallcaps"
android:padding="@dimen/margin_small"
android:textColor="@color/black"
android:textSize="@dimen/margin_normal"
tools:text="25.7" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-thin"
android:padding="@dimen/margin_small"
android:textColor="@color/black"
android:textSize="@dimen/margin_normal"
tools:text="Temp(C)" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:minWidth="0dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-thin"
android:padding="@dimen/margin_small"
android:textColor="@color/black"
android:textSize="@dimen/margin_normal"
tools:text="Temp(f)" />
</LinearLayout>
<TextView
android:id="@+id/txtWeather"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/margin_small"
android:fontFamily="sans-serif-thin"
android:textStyle="normal"
tools:text="Cloudy" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
then app_bar_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/app_bar_elevation"
android:gravity="center_vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="@dimen/toolbar_height"
app:contentInsetStart="@dimen/toolbar_inset_start"
app:contentInsetStartWithNavigation="@dimen/toolbar_inset_start">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="start"
android:fontFamily="casual"
android:singleLine="true"
android:textSize="@dimen/toolbar_title_text_size"
tools:text="Toolbar title" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="start"
android:singleLine="true"
android:visibility="gone"
tools:text="Toolbar subtitle"
tools:visibility="visible" />
</LinearLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
And Finally our MainActivity.kt
will look us below:
class MainActivity : AppCompatActivity() {
private lateinit var searchView: SearchView
private lateinit var searchItem: MenuItem
private lateinit var viewModel: MainViewModel
private lateinit var weatherIcon: ImageView
private lateinit var txtWeather: TextView
private lateinit var tempF: TextView
private lateinit var tempC: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Toolbar>(R.id.toolbar).inflateMenu(R.menu.menu_search)
searchItem = findViewById<AppBarLayout>(R.id.appBar).findViewById<Toolbar>(R.id.toolbar).menu.findItem(
R.id.search_item
)
searchView = searchItem.actionView as SearchView
viewModel = ViewModelProviders.of(
this,
ViewModelFactory(RetrofitBuilder)
).get(MainViewModel::class.java)
weatherIcon = findViewById(R.id.imageIcon)
txtWeather = findViewById(R.id.txtWeather)
tempF = findViewById(R.id.tempF)
tempC = findViewById(R.id.tempC)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.fetchCurrentWeather(query).observe(this@MainActivity,::onResponse)
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
})
}
private fun onResponse(apiModel: ApiModel<WeatherResponse>?) {
when(apiModel?.status){
Status.ERROR -> {
Toast.makeText(this, apiModel.message, Toast.LENGTH_SHORT).show()
}
Status.SUCCESS -> {
val data = apiModel.data
weatherIcon.load("https:" + apiModel.data?.current?.condition?.icon)
txtWeather.text = data?.current?.condition?.text
tempC.text = data?.current?.temp_c.toString()
tempF.text = data?.current?.temp_f.toString()
}
Status.LOADING -> {
Toast.makeText(this, "Loading", Toast.LENGTH_SHORT).show()
}
}
}
}
Wrapping up
This was a quick guide on how you can use retrofit and coroutines in android to easily make an api call.You can check out the full code in my repository here.Happy Coding!
Posted on February 24, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.