Android ViewModel - LiveData & Sample get API Data (TheMovieDB)

Rivaldy
4 min readOct 14, 2021

Pada tulisan kali ini, tentang bagaimana mengimplementasikan ViewModel pada project.

Project kali ini, kita menggunakan sample data API dari TheMovieDb

Apa itu ViewModel ?

Class ViewModel didesain untuk menyimpan dan mengelola data terkait UI dengan cara yang berbasis siklus proses. Class ViewModel memungkinkan data bertahan saat terjadi perubahan konfigurasi seperti pada saat rotasi layar. Untuk penjelasan detail, kunjungi dokumentasinya. https://developer.android.com/topic/libraries/architecture/viewmodel

pic credit : Android Jetpack - ViewModel

Step 1. Setup Project

Sebelum mengimplementaskan ViewModel pada project, dependencies viemodel, livedata dan libary pendukung untuk fetch API telah tersetup pada project, jika belum silahkan tambahkan terlebih dahulu.

// Core KTX
implementation "androidx.core:core-ktx:1.6.0"
// Lifecycle KTX
implementation "android.arch.lifecycle:extensions:1.1.1"
// Activity & Fragment KTX
implementation "androidx.activity:activity-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.3.6"

// For API
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.9.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"

Atau kunjungi dokumentasinya https://developer.android.com/kotlin/ktx
Dan setup View Binding terlebih dahulu. Disini kita menggunakan viewBinding untuk memudahkan kita dalam pemanggilan View, Cek dokumentasinya untuk penjelasan lebih lanjut https://developer.android.com/topic/libraries/view-binding

buildFeatures {
viewBinding true
}

Step 2. Let’s coding!

1. Struktur Package

Agar mudah dipahami berikut strukur package pada project :

structure package

2. Setup Data Classes (Model class untuk data API)

  • MovieResponse.kt
data class MovieResponse(
@SerializedName("page")
var page: Int? = null,
@SerializedName("results")
var results: MutableList<MovieResult>? = null,
@SerializedName("total_pages")
var totalPages: Int? = null,
@SerializedName("total_results")
var totalResults: Int? = null
)
  • MovieResult.kt
data class MovieResult(
@SerializedName("adult")
var adult: Boolean? = null,
@SerializedName("backdrop_path")
var backdropPath: String? = null,
@SerializedName("genre_ids")
var genreIds: List<Int>? = null,
@SerializedName("id")
var id: Int? = null,
@SerializedName("original_language")
var originalLanguage: String? = null,
@SerializedName("original_title")
var originalTitle: String? = null,
@SerializedName("overview")
var overview: String? = null,
@SerializedName("popularity")
var popularity: Double? = null,
@SerializedName("poster_path")
var posterPath: String? = null,
@SerializedName("release_date")
var releaseDate: String? = null,
@SerializedName("title")
var title: String? = null,
@SerializedName("video")
var video: Boolean? = null,
@SerializedName("vote_average")
var voteAverage: Double? = null,
@SerializedName("vote_count")
var voteCount: Int? = null
)

3. Setup Retrofit (Untuk menangani API)

  • ApiClient.kt
class ApiClient {
companion object {
const val PATH_API_KEY = "api_key"
const val PATH_DEFAULT_LANG = "language"
const val DEFAULT_LANG = "en-US"
}
private fun doRequest(): Retrofit {
val gson = GsonBuilder().create()
val loggingInterceptor =
if (BuildConfig.DEBUG) HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
else HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.NONE)
val okHttpClient = OkHttpClient.Builder()
.readTimeout(120, TimeUnit.SECONDS)
.connectTimeout(120, TimeUnit.SECONDS)
.addInterceptor { chain ->
val url = chain
.request()
.url
.newBuilder()
.addQueryParameter(PATH_API_KEY, BuildConfig.API_KEY)
.addQueryParameter(PATH_DEFAULT_LANG, DEFAULT_LANG)
.build()
chain.proceed(chain.request().newBuilder().url(url).build())
}
.addInterceptor(loggingInterceptor)
.build()

return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}

fun apiTheMovieDb(): ApiTMDB {
return doRequest().create(ApiTMDB::class.java)
}
}
  • ApiTMDB.kt
interface ApiTMDB {
@GET("discover/movie")
fun getMovies(): Call<MovieResponse>
}

4. Setup Repository

Pada project ini kita menuggunakan repository pattern.

  • MovieRepository.kt
class MovieRepository(
private val apiClient: ApiClient
) {
fun getMovies(): Call<MovieResponse> {
return apiClient.apiTheMovieDb().getMovies()
}
}

5. Setup ViewModel

  • MainViewModel.kt
class MainViewModel(
private val repository: MovieRepository
) : ViewModel() {
private val _loading: MutableLiveData<Boolean> = MutableLiveData()
private val _getMovies = MutableLiveData<MutableList<MovieResult>>()
val loading: LiveData<Boolean> = _loading
val getMovies: LiveData<MutableList<MovieResult>> = _getMovies


fun getMovies() = viewModelScope.launch {
_loading.value = true
repository.getMovies().enqueue(object : Callback<MovieResponse> {
override fun onFailure(call: Call<MovieResponse>, t: Throwable) {
_loading.value = false
}

override fun onResponse(call: Call<MovieResponse>, response: Response<MovieResponse>) {
_loading.value = false
if (response.isSuccessful) {
val movieList = response.body()?.results ?: return
_getMovies.value = movieList
}
}
})
}
}

Pada viewModel MainViewModel di atas memiliki sebuah constructor yaitu MovieRepository, karena pada project ini kita belum menggunakan Depedendecy Injection (DI) maka kita membutuhkan ViewModelFacotry.

  • MainViewModelFactory.kt
class MainViewModelFactory(
private val repository: MovieRepository
) : ViewModelProvider.NewInstanceFactory() {

override fun <T : ViewModel?> create(modelClass: Class<T>): T {
try {
val constructor = modelClass.getDeclaredConstructor(MovieRepository::class.java)
return constructor.newInstance(repository)
} catch (err: Exception) {
Log.e("ERROR", err.toString())
}
return super.create(modelClass)
}
}

6. Setup View

Pada tahap ini, kita akan menampilkan data ke View (UI) yang kita hit dari API.

  • Layout XML - activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
tools:context=".ui.MainActivity">

<TextView
android:id="@+id/resultTV"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/click_me"
app:layout_constraintEnd_toEndOf="@+id/actionMB"
app:layout_constraintStart_toStartOf="@+id/actionMB"
app:layout_constraintTop_toTopOf="parent" />

<com.google.android.material.button.MaterialButton
android:id="@+id/actionMB"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginStart="24dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="24dp"
android:text="@string/get_api_data"
android:textAllCaps="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/resultTV" />

<ProgressBar
android:visibility="gone"
android:id="@+id/loadingPB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/actionMB"
app:layout_constraintStart_toStartOf="@+id/actionMB"
app:layout_constraintTop_toBottomOf="@+id/actionMB" />

</androidx.constraintlayout.widget.ConstraintLayout>
preview activity_main.xml
  • Menampilkan data ke View - MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initObservers()
initClick()
}

private fun initObservers() {
// Not recommended, but this is just example (best step using DI)
val apiClient = ApiClient()
val repository = MovieRepository(apiClient)
val factory = MainViewModelFactory(repository)

viewModel = ViewModelProvider(this, factory)[MainViewModel::class.java]
viewModel.loading.observe(this, {
binding.loadingPB.visibility = if (it == true) View.VISIBLE else View.GONE
})
viewModel.getMovies.observe(this, {
val listData = mutableListOf<String>()
it.map { movie -> listData.add(movie.title.toString()) }
binding.resultTV.text = listData.joinToString()
})
}

private fun initClick() {
binding.actionMB.setOnClickListener {
viewModel.getMovies()
}
}
}

Selanjutnya running project.

hasil setelah project di run

Pada tahap ini kita telah selesai mengimplementasi viewModel dengan sederhana pada project.

Untuk full kodenya silahkan kunjugi repository github saya.

Saya juga mempunyai tulisan tentang MVVM (Room Database with MVVM Architecture | Android Jetpack | CRUD)

Done ✅ 👏

--

--

Rivaldy

Make it fun | Find me on my GitHub page at https://github.com/im-o. And let's connect over coffee! ☕ You can reach me on any social media at @rivaldy_o."