Loading and displaying images from the internet with Retrofit into your android app

In this tutorial, we'll learn how to load and display photos from a web URL. We'll use Glide to display a single image from a web service. Let's get started...

Step 1: Create a new project and call it DisplayDataImage

  • Open Android Studio if it is not already opened.
  • In the main Welcome to Android Studio dialog, click Start a new Android Studio project.
  • The Choose your project dialog appears. Select Empty Activity as shown below, and click Next.
  • In the Configure your project dialog, enter "DisplayDataImage" for the Name, set Minimum SDK to API 21 and click Finish

Step 2: Add various dependencies:

  • Inside the dependencies block, add the Gradle dependency for the Retrofit, Moshi library, Glide library and ViewModel.
//Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"

//Moshi Library
implementation "com.squareup.moshi:moshi-kotlin:1.9.3"

//Glide Library
implementation "com.github.bumptech.glide:glide:4.8.0"

//ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
  • Add the data binding dependency to Gradle
android{...

    buildFeatures {
        dataBinding true
    }
  • Apply the kotlin-kapt plugin at the top of the build.gradle file
id 'kotlin-kapt'
  • Ensure the support for Java 8 language features are added
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
    jvmTarget = '1.8'
}

Click on Sync Now

Step 3: Implement the DataProperty

  • Create a data class, call it DataProperty and add the following code
data class DataProperty(
        val id: String,
        val name: String,
        val propellant: String,
        val destination: String,
        @Json(name = "imageurl") val imgSrcUrl: String,
        val technologyexists: String
)

Each of the variables in the DataProperty data class corresponds to a key name in the JSON object.

Step 4: Setup ViewModel and connect with DisplayDataFragment

  • Create a new Kotlin class called DisplayDataViewModel class and make the class extend the abstract class ViewModel
class DisplayDataViewModel : ViewModel() {
}
  • In DisplayDataViewModel, we create both internal and external LiveData for the status String
private val _status = MutableLiveData<String>()

val status: LiveData<String>
    get() = _status
  • Add an encapsulated LiveData<DataProperty> property:
private val _property = MutableLiveData<DataProperty>()

val property: LiveData<DataProperty>
  get() = _property

Add a Fragment class and create a binding object

  • Select File > New > Fragment > Fragment (Blank).
  • For the Fragment Name, enter DisplayDataFragment.
  • For the Fragment layout name, enter fragment_display_data
  • For source language, select Kotlin and click Finish.
  • Open the DisplayDataFragment.kt fragment file, if it is not already open.
  • Delete the onCreate() method, the fragment initialization parameters, companion object and the code inside onCreateView().
  • Next, create a binding object and inflate the Fragment's view (which is equivalent to using setContentView() for an Activity). Make sure your DisplayDataFragment class looks like the following:
class DisplayDataFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding = FragmentDisplayDataBinding.inflate(inflater)
        return binding.root
    }
}

Add the new fragment to the main layout file

  • Open res > layout > activity_main.xml and select the Code tab to view the layout XML code.
  • Delete the codes Inside the existing main layout file.
  • Add the following code:
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/displayDataFragment"
    android:name="com.example.displaydataimage.DisplayDataFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
  • At the top of DisplayDataFragment class, make the DisplayDataFragment lazily initializes the DisplayDataViewModel.
private val viewModel: DisplayDataViewModel by lazy {
    ViewModelProvider(this).get(DisplayDataViewModel::class.java)
}
  • Inside the fragment_display_data.xml layout, wrap your root layout inside tags. This adds the data binding for the view model. Import the DisplayDataViewModel. The fragment_display_data.xml should look like this:
<layout ...>
    <data>
        <variable
            name="viewModel"
            type="com.example.displaydataimage.DisplayDataViewModel" />
    </data>
</layout>
  • In the fragment_display_data, inside the root layout create the ImageView, and add an app:imageUrl attribute to the ImageView element to use the new image loading binding adapter:
    <ImageView
        android:id="@+id/mars_image"
        android:layout_width="match_parent"
        android:layout_height="170dp"
        android:adjustViewBounds="true"
        android:padding="2dp"
        app:imageUrl="@{viewModel.property.imgSrcUrl}"
        android:scaleType="centerCrop"
        tools:src="@tools:sample/backgrounds/scenic" />
  • Inside the DisplayDataFragment file, import the com.example.displaydataimage.databinding.FragmentDisplayDataBinding and Specify the DisplayDataFragment view as the lifecycle owner of the binding then passes the viewModel into the data binding.
binding.lifecycleOwner = this
binding.viewModel = viewModel

return binding.root

Step 5: Implement the DisplayDataApiService

  • Create DisplayDataApiService.kt file and add a constant Base Url at the top of the file. We will use a fake Online REST API.

      private const val BASE_URL = "https://raw.githubusercontent.com/"
    
  • Below the constant, add the following code to create the Moshi instance.

      private val moshi = Moshi.Builder()
       .add(KotlinJsonAdapterFactory())
       .build()
    
  • Below the Moshi instance, use a Retrofit builder to create a Retrofit object.

private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()
  • Just below the call to the Retrofit builder, define an interface that defines how Retrofit talks to the web server using HTTP requests.
interface DisplayDataApiService {
    @GET("Oclemy/SampleJSON/338d9585/spacecrafts.json")
    suspend fun getProperties():
            List<DataProperty>
}
  • Below the DisplayDataApiService interface, define a public object called DataApi to initialize the Retrofit service.
object DataApi {
    val retrofitService : DisplayDataApiService by lazy { 
       retrofit.create(DisplayDataApiService::class.java) }
}

Step 6: Call the web service in DisplayDataViewModel.

  • In the DisplayDataViewModel, at the bottom of the class we create the init block and call getDetails() method inside.
  • Create the getDetails method
  • Inside getDetails(), launch the coroutine using viewModelScope.
  • Inside the launch block, add a try/catch block to handle exceptions:
  • Inside the try {} block, call getProperties() on the retrofitService object. Calling getProperties() from the DataApi service creates and starts the network call on a background thread.
  • Also inside the try {} block, update getMarsRealEstateProperties() to set _property to the first MarsProperty from listResult.
  • Inside the catch {} block, handle the error response to a status value.

Your code should look like this:

init {
        getDetails()
    }

    private fun getDetails() {
        viewModelScope.launch {
            try {
                val listResult = DataApi.retrofitService.getProperties()
                if (listResult.size > 0) {
                    _property.value = listResult[0]
                }
            } catch (e: Exception) {
                _status.value = "Failure: ${e.message}"
            }
        }
    }

Step 7: Create a binding adapter and call Glide.

In this step, you use a binding adapter to take the URL from an XML attribute associated with an ImageView, and you use Glide to load the image.

Create BindingAdapters.kt to hold the binding adapters.

  • Create a bindImage() function that takes an ImageView and a String as parameters. Annotate the function with @BindingAdapter.
  • Inside the bindImage() function, add a let {} block for the imgUrl argument
  • Inside the let {} block, add the line shown below to convert the URL string (from the XML) to a Uri object.
  • Still inside let {}, call Glide.with() to load the image from the Uri object into the ImageView.
  • of course, add the loading_animation and the ic_broken_image into drawable folder.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = 
           imgUrl.toUri().buildUpon().scheme("https").build()
        Glide.with(imgView.context)
                .load(imgUri)
                .into(imgView)
    }
}

Step 8: Define the internet permission.

<uses-permission android:name="android.permission.INTERNET" />
  • Compile and run the app. If everything is working correctly with your internet connection, you see:

DisplayDataImage2.png

Cover Photo by Markus Winkler on Unsplash

No Comments Yet