Android ViewModel - LiveData & Sample get API Data (TheMovieDB)
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
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 :
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>
- 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.
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 ✅ 👏