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

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