Mobile Application

How to Call API’s With Kotlin Coroutines In Right Way

Introduction

API calls are a crucial part of any Android application development, and understanding how they work is essential. API, which stands for Application Programming Interface, connects application services to users, improving their experience. The API is responsible for handling significant changes, making it easy to modify the product. There are various ways to call an API and transfer data between applications in Android development. Asynchronous API calling is the most efficient method, and there are several ways to perform parallel work, including AsyncTask, RxJava, and Coroutine. Coroutine is the best option for efficient parallel work because it is highly recommended by the Kotlin language and Android development community. In this blog, we will discuss how to use Call API’s with Kotlin Coroutines.

Before moving forward, if you are seeking to grasp the fundamentals of Coroutine, then you must check out this blog first: 

Kotlin Coroutines in Android

Let’s start learning API calling with the help of Kotlin Coroutine

We are creating one demo application for the purpose of calling the imdb API to retrieve the movie data. The API information is provided in the following:

Android Custom Library CTA2

https://imdb-API.com/ 

API (Use purpose)

https://imdb-API.com/API/SearchMovie/{API_key}/{expression}

When you sign up in this given API, then the API site will give you an API key, it is necessary to call the API. 

Good to have knowledge for this demo : 

  • Normal API calling and it’s fundamentals 
  • Android development fundamentals 
  • Dependency Injection normal information

Reference : Dagger Hilt in Android Step by Step Guidance

This much knowledge is enough.

Let’s start by creating a project, first take all dependencies which are important for our project.

//gradle(app level)

plugins {
   id 'com.android.application'
   id 'org.jetbrains.Kotlin.android'
   id 'Kotlin-kapt'
   id 'dagger.hilt.android.plugin'
}

android {
   namespace 'com.example.asyncAPIcalling'
   compileSdk 32

   defaultConfig {
       applicationId "com.example.asyncAPIcalling"
       minSdk 22
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

   buildFeatures {
       dataBinding true
   }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
   }
   KotlinOptions {
       jvmTarget = '1.8'
   }
}

dependencies {

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.5.1'
   implementation 'com.google.android.material:material:1.7.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
   implementation 'com.google.android.gms:play-services-dtdi:16.0.0-beta01'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.4'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'


   //retrofit
   implementation "com.squareup.retrofit2:retrofit:2.9.0"
   implementation "com.squareup.okhttp3:okhttp:4.7.2"
   implementation "com.squareup.okhttp3:logging-interceptor:4.7.2"
   implementation "com.squareup.retrofit2:converter-gson:2.9.0"

   //Coroutine
   implementation 'org.jetbrains.Kotlinx:Kotlinx-Coroutines-core:1.4.2'
   implementation 'org.jetbrains.Kotlinx:Kotlinx-Coroutines-android:1.4.1'

   //Coroutine Lifecycle Scope
   implementation "org.jetbrains.Kotlinx:Kotlinx-Coroutines-android:1.4.1"
   implementation "org.jetbrains.Kotlinx:Kotlinx-Coroutines-core:1.4.2"
   implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"

   //sdp-ssp
   implementation 'com.intuit.sdp:sdp-android:1.1.0'
   implementation 'com.intuit.ssp:ssp-android:1.1.0'

   //dagger-hilt
   implementation "com.google.dagger:hilt-android:2.44"
   kapt "com.google.dagger:hilt-compiler:2.44"

   //ok-http
   implementation "com.squareup.okhttp3:okhttp-bom:4.10.0"

   //glide
   implementation 'com.github.bumptech.glide:glide:4.14.2'

}

//allow references to generated code
kapt{
   correctErrorTypes = true
}

//gradle (project level)

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
   id 'com.android.application' version '7.3.1' apply false
   id 'com.android.library' version '7.3.1' apply false
   id 'org.jetbrains.Kotlin.android' version '1.7.20' apply false
   id 'com.google.dagger.hilt.android' version '2.44' apply false
}

After applying all dependencies in the both gradle, take a minute and see the code structure of our project.

using kotlin coroutine

Create one activity_main.xml file for design purpose :

Layout file for main activity :

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

   <data>
       <variable
           name="mainviewmodel"
           type="com.example.asyncAPIcalling.ui.viewmodels.MainViewModel" />
   </data>

   <androidx.constraintlayout.widget.ConstraintLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       tools:context=".ui.activities.MainActivity">

       <TextView
           android:id="@+id/validation_text_for_search"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           app:layout_constraintTop_toTopOf="parent"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintBottom_toBottomOf="parent"
           android:text="@string/search_something_text"
           android:textSize="@dimen/_12sdp"
           android:textColor="@android:color/holo_green_dark"
           />

       <androidx.appcompat.widget.Toolbar
           android:id="@+id/title_toolbar"
           android:layout_width="match_parent"
           android:layout_height="60dp"
           android:background="#3EB489"
           android:textAlignment="center"
           app:layout_constraintTop_toTopOf="parent"
           app:navigationIcon="@drawable/ic_movie"
           app:subtitle="cartoon category"
           app:subtitleTextColor="@color/white"
           app:title="Movies"
           app:titleTextColor="@color/white">

       </androidx.appcompat.widget.Toolbar>

       <EditText
           android:id="@+id/the_search_bar"
           android:layout_width="0dp"
           android:layout_height="wrap_content"
           android:background="@drawable/curved_background"
           android:padding="@dimen/_5sdp"
           android:text="@={mainviewmodel.expressionToSearch}"
           android:layout_marginTop="@dimen/_5sdp"
           android:layout_marginStart="@dimen/_5sdp"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintEnd_toStartOf="@+id/the_search_button"
           app:layout_constraintTop_toBottomOf="@id/title_toolbar"
           />

       <androidx.constraintlayout.utils.widget.ImageFilterView
           android:id="@+id/the_search_button"
           android:layout_width="@dimen/_30sdp"
           android:layout_height="@dimen/_30sdp"
           android:src="@drawable/ic_search"
           app:layout_constraintTop_toTopOf="@id/the_search_bar"
           app:layout_constraintEnd_toEndOf="parent"
           android:layout_marginEnd="@dimen/_5sdp"
           android:padding="@dimen/_5sdp"
           android:paddingEnd="@dimen/_10sdp"
           android:background="@drawable/curved_background_right"
           />

       <ProgressBar
           android:id="@+id/main_progress_bar"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:visibility="gone"
           app:layout_constraintBottom_toBottomOf="parent"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintTop_toTopOf="parent"
           tools:visibility="visible" />

       <androidx.recyclerview.widget.RecyclerView
           android:id="@+id/movie_recycler_view"
           android:layout_width="match_parent"
           android:layout_height="0dp"
           android:layout_marginTop="@dimen/_5sdp"
tools:listitem="@layout/raw_movies_item_layout"
           app:layout_constraintBottom_toBottomOf="parent"
           app:layout_constraintTop_toBottomOf="@id/the_search_bar" />

   </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Then after we will complete the raw item file for our recyclerview, It will show us how our single item will look like this. 

See also  Build your own Cocoapods with help of OneClick IT Consultancy

Layout File for items

raw_movies_item_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto">

   <data>

   </data>

   <RelativeLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content">

       <androidx.constraintlayout.widget.ConstraintLayout
           android:layout_width="match_parent"
           android:layout_height="match_parent">

           <ImageView
               android:id="@+id/image_of_movies"
               android:layout_width="match_parent"
               android:layout_height="@dimen/_125sdp"
               tools:src="@mipmap/ic_launcher"
               android:scaleType="centerCrop"
               app:layout_constraintTop_toTopOf="parent" />

           <TextView
               android:id="@+id/title_of_movie"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               tools:text="Inception"
               android:textSize="@dimen/_18ssp"
               android:textStyle="bold"
               android:textColor="@color/black"
               app:layout_constraintTop_toBottomOf="@id/image_of_movies"
               app:layout_constraintStart_toStartOf="parent" />

           <TextView
               android:id="@+id/description_of_movie"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               tools:text="this is the best movie ever i watched"
               android:textSize="@dimen/_15ssp"
               app:layout_constraintStart_toStartOf="parent"
               app:layout_constraintTop_toBottomOf="@+id/title_of_movie" />


       </androidx.constraintlayout.widget.ConstraintLayout>

   </RelativeLayout>
</layout>


Completing this, Then after see our manifest file: 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools">

   <uses-permission android:name="android.permission.INTERNET"/>

   <application
       android:name=".base.MyApplication"
       android:allowBackup="true"
       android:dataExtractionRules="@xml/data_extraction_rules"
       android:fullBackupContent="@xml/backup_rules"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.AsyncAPICalling"
       android:usesCleartextTraffic="true"
       tools:targetAPI="31">
       <activity
           android:name=".ui.activities.MainActivity"
           android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>

           <meta-data
               android:name="android.app.lib_name"
               android:value="" />
       </activity>
   </application>

</manifest>

After finishing the manifest and its permissions, We will start the coding for dagger-hilt.

For Dagger-hilt first we have to declare one application class and then this class should be put in the manifest file like above. Put the application class package name in the “name” parameter of application tag as shown in the above code.

Application class

MyApplication.kt

package com.example.asyncAPIcalling.base

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApplication : Application() {

}

By using that annotation @HiltAndroidApp we can notify the hilt that this is the application class for our dependency injection operations. And This class will be put in the base package only. 

Then after coming in the injection package, here you have to put the module class which can create the function that helps to put the instance of needed variable and data.

So create one class of module which declares the function of the retrofit instance. 

Module class

RetrofitInstanceModuel.kt

package com.example.asyncAPIcalling.injection

import com.example.asyncAPIcalling.network.APIService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import javax.inject.Singleton

const val BASE_URL = "https://imdb-API.com/"

@Module
@InstallIn(SingletonComponent::class)
class RetrofitInstanceModule {

   @Provides
   @Singleton
   fun theRetrofitInstance() : APIService {
       val API : APIService by lazy {
           Retrofit.Builder()
               .baseUrl(BASE_URL)
               .addConverterFactory(GsonConverterFactory.create())
               .build()
               .create(APIService::class.java)
       }
       return API
   }

}

You can see here that there are some annotations which are used to perform dependency injection. 

@Module 

@InstallIn

@Proivdes 

@Singleton 

@Module annotation will be used to notify the dagger-hilt that this is the module class of our project which can use the function declaration of needed instance class.

And @Proivdes annotation will be used to notify that this function will be put where you want by @Inject.

In this theRetrofitInstance() function, we build the retrofit instance for generating API calling. We have to pass the base url of the API.

For Creating the retrofit instance you first create one interface class of the API calling function.

API service class

APIService.kt

package com.example.asyncAPIcalling.network

import com.example.asyncAPIcalling.network.model.IMDB
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path

interface APIService {
@GET("/API/SearchMovie/{API_key}/{expression}")
  suspend fun imdbFunction(
       @Path("API_key") APIKey : String,
       @Path("expression") searchExpression : String,
   ) : Response<IMDB>

}

So, We will create one interface class which will be used for declaration of all API calling functions.

You can see above code there is one imdbFunction() function which is a suspend function, whenever you have to use Coroutine function then you must have to declare it as suspend function. 

In the above function @GET annotation is used for retrieving the data from API, this annotation changes according to its operation type. 

See also  How Much it Will Cost to Develop Video Editing App like Magisto?

Like : @PATCH, @GET, @POST etc…

And after that the return type of function is IMDB class.

The Model class

IMDB.kt

package com.example.asyncAPIcalling.network.model

import com.google.gson.annotations.SerializedName



data class IMDB(
   @SerializedName("searchType") var searchType: String? = null,
   @SerializedName("expression") var expression: String? = null,
   @SerializedName("results") var results: ArrayList<Results> = arrayListOf(),
   @SerializedName("errorMessage") var errorMessage: String? = null,
)

data class Results(
   @SerializedName("id") var id: String? = null,
   @SerializedName("resultType") var resultType: String? = null,
   @SerializedName("image") var image: String? = null,
   @SerializedName("title") var title: String? = null,
   @SerializedName("description") var description: String? = null,
)

We use the Data class for containing the data which is provided by the server. So we can easily fetch data and communicate with them. 

You see there is one annotation like @SerializedName, This is a normal annotation but creates a great impact to our operation of doing API calling. It will hand-me-down the serialize field with another name rather than its actual name. If API give me the “real_name” field but i have to use “unreal_name” so i will put down the “real_name” in @SearializedName(“real_name”) and then declare that “unreal_name”, then i will able to use that “unreal_name” all over the project and it will work like “real_name”.

After creating all functions, let’s go to the adapters package and quickly create the adapter class for our recycler view. In our adapter class of recyclerview we will use the DiffUtil method to create efficient work from recyclerview. It is the more optimized and structured way to create one adapter class.

Adapter class

MoviesRecyclerAdapter.kt

package com.example.asyncAPIcalling.ui.adapters

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.asyncAPIcalling.R
import com.example.asyncAPIcalling.databinding.RawMoviesItemLayoutBinding
import com.example.asyncAPIcalling.network.model.Results

class MoviesRecyclerAdapter : RecyclerView.Adapter<MoviesRecyclerAdapter.MoviesViewHolder>() {

   inner class MoviesViewHolder(private val rawMoviesItemLayoutBinding: RawMoviesItemLayoutBinding) :
       RecyclerView.ViewHolder(rawMoviesItemLayoutBinding.root) {

           fun bind(results: Results){

               rawMoviesItemLayoutBinding.apply {
                   Glide.with(root.context)
                       .load(results.image)
                       .placeholder(R.drawable.placeholder)
                       .into(imageOfMovies)

                   titleOfMovie.text = results.title
                   descriptionOfMovie.text = results.description
               }

           }

   }

   private val diffCallback = object : DiffUtil.ItemCallback<Results>()
   {
       override fun areItemsTheSame(oldItem: Results, newItem: Results): Boolean {
           return oldItem.id == newItem.id
       }

       override fun areContentsTheSame(oldItem: Results, newItem: Results): Boolean {
           return oldItem == newItem
       }
   }

   private val differ = AsyncListDiffer(this, diffCallback)

   var result : List<Results>
   get() = differ.currentList
   set(value){
       differ.submitList(value)
   }

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MoviesViewHolder {
       return MoviesViewHolder(RawMoviesItemLayoutBinding.inflate(LayoutInflater.from(parent.context),
           parent,
           false))
   }

   override fun onBindViewHolder(holder: MoviesViewHolder, position: Int) {
       holder.bind(result[position])
   }

   override fun getItemCount() = result.size

}

First we will extend the reyclerview class and adapter. Then after creating one inner class called ViewHolder class. 

We are using the dataBinding that’s why we put that one Binding variable. So we can access the view’s id easily. In the inner class we create one function class called bind(). In this apply all data and initialize with the view. 

Succeeding the bind method let’s create an ItemCallback of diffUtils. This will compare the old list and new list, Then maintain the recyclerview in a good way. 

Create one result variable which can contain a list of result class. For that we have to use the getter-setter method in Kotlin. 

Finishing the all processes in this adapter, Import all three methods of adapter class and initialize all things like above code.

Jump to the viewmodel package and start the creating viewmodel class for the main activity.

ViewModel Class

MainViewModel.kt

package com.example.asyncAPIcalling.ui.viewmodels

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.asyncAPIcalling.network.APIService
import com.example.asyncAPIcalling.network.model.IMDB
import com.example.asyncAPIcalling.utils.Const.Companion.API_KEY
import dagger.hilt.android.lifecycle.HiltViewModel
import Kotlinx.Coroutines.*
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(private val retrofitInstance: APIService) : ViewModel() {

   val responseContainer = MutableLiveData<IMDB>()
   val errorMessage = MutableLiveData<String>()
   val isShowProgress = MutableLiveData<Boolean>()
   val expressionToSearch = MutableLiveData("")

   var job: Job? = null
   val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
       onError("Exception handled : ${throwable.localizedMessage}")
   }

   fun getMoviesFromAPI(expression : String) {
       isShowProgress.value = true
       job = viewModelScope.launch {
           val response = retrofitInstance.imdbFunction(API_KEY, expression)
           withContext(Dispatchers.Main) {
               if (response.isSuccessful) {
                   responseContainer.postValue(response.body())
                   isShowProgress.value = false
               } else {
                   onError("Error : ${response.message()}")
               }
           }
       }


   }

   private fun onError(message: String) {
       errorMessage.value = message
       isShowProgress.value = false
   }

   override fun onCleared() {
       super.onCleared()
       job?.cancel()
   }

}

In this class we have to use the annotation of hilt called as @HiltViewModel. By help of this annotation we are able to notify the hilt that this is the view model class. Declare the variables which are observable to the main class. And then create one function for API calling, it is called getMoviesFromAPI(), it has one parameter to give the expression for search. In this use job variable cause job will help to create cancelable Coroutine. Start viewmodel scope and in this initialize one variable to call the interface class’s API function. For that we have to inject one variable instance of retrofit into a constructor of the viewmodel class by help of @Inject annotation. In this put the API key to that API function and that API key will create in the cons class of Util package.

See also  Android vs iOS: Which Platform is Better for You?

Util class of constants

Const.kt

package com.example.asyncAPIcalling.utils

class Const  {

   companion object{
       const val API_KEY = "kindly_put_your_API_key_here"
   }

}

Come back to the viewmodel class, And simply put this API_KEY variable in that parameters of function.  Make one condition that if the response is successful then put all data to the mutable live variable which can be observable by the main activity. And set the isShowProgress false. This condition will false then put the message to the onError() function. 

The onError function simply works to put the message of the server to the errorMessage mutable live data.

Then after override the onCleared() method and candle the created job. If we can not use the job then it will be ok cause it will use the viewmodelscope so when the viewmodel is destroyed it will be automatically canceled by itself. But for knowledge purposes we can use this job’s variable. Also for the knowledge purpose you can see here that one exceptional handling variable is used to help with the Coroutine. It will throw an exception.

Quickly set all the values to the main activity.

Main class

MainActivity.kt

package com.example.asyncAPIcalling.ui.activities

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.asyncAPIcalling.databinding.ActivityMainBinding
import com.example.asyncAPIcalling.network.model.Results
import com.example.asyncAPIcalling.ui.adapters.MoviesRecyclerAdapter
import com.example.asyncAPIcalling.ui.viewmodels.MainViewModel
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

   private lateinit var activityMainBinding: ActivityMainBinding
   private lateinit var moviesAdapter : MoviesRecyclerAdapter
   private lateinit var mainViewModel: MainViewModel

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
       setContentView(activityMainBinding.root)

       setUpViewModel()

       setUpRecyclerView()

       mainViewModel.responseContainer.observe(this, Observer {
           if (it != null){

               moviesAdapter.result = it.results
               activityMainBinding.validationTextForSearch.visibility = View.GONE

           }else{
               Toast.makeText(this, "There is some error!", Toast.LENGTH_SHORT).show()
           }
       })

       mainViewModel.isShowProgress.observe(this, Observer {
           if (it){
               activityMainBinding.mainProgressBar.visibility = View.VISIBLE
               activityMainBinding.validationTextForSearch.visibility = View.GONE
           }
           else{
               activityMainBinding.mainProgressBar.visibility = View.GONE
               activityMainBinding.validationTextForSearch.visibility = View.GONE
           }
       })


       mainViewModel.errorMessage.observe(this, Observer {
           Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
       })

       activityMainBinding.theSearchButton.setOnClickListener { it->
           if(activityMainBinding.theSearchBar.text.isEmpty()){
               Toast.makeText(this, "Please Enter movie type", Toast.LENGTH_SHORT).show()
           }
           else{
               val searchText = activityMainBinding.theSearchBar.text

               mainViewModel.getMoviesFromAPI(searchText.toString())

           }

       }

   }

   private fun setUpRecyclerView() = activityMainBinding.movieRecyclerView.apply {
       moviesAdapter = MoviesRecyclerAdapter()
       adapter = moviesAdapter
       layoutManager = LinearLayoutManager(this@MainActivity)
   }

   private fun setUpViewModel(){
       mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]
   }

}

Use the annotation for hilt notify, @AndoridEntryPoint which gives the idea about main activity to the dagger-hilt.

First we have to declare the binding variable of the xml file of the main activity. Then after declaring the adapter and viewmodel.

Set that binding variable to use and access the view’s id. Then simply create two functions for set-up the recycler view and viewmodel. 

Simply observe the mutable live variable and attach it with the adapter and views of the main activity. By the help of the viewmodel instance. 

In conclusion, we can say that at present time, Kotlin is becoming one of the most popular programming languages for android application development and therefore every developer should learn it.

    Need an IT Experts? Get a Free Consultation!

    lets start your project

    Related Articles