[Android] Retrofit - MVVM ๊ตฌ์กฐ
#1 ์ด์ ๊ธ
[Android] Retrofit - ๊ธฐ์ด
#1 ์ด์ ๊ธ [Android] Retrofit - ๋ฐฐ๊ฒฝ๊ณผ ๊ตฌ์กฐ#1 Restrofit์ ๋ฐฐ๊ฒฝ#1-1 REST API REST API (REpresentational State Transfer Application Programming Interface)#1 ๋ฌด์(What)์ ๋ํ API์ธ๊ฐ?#1-1 ๊ฐ์REST(REpresentational State Transfer) ๋๋
kenel.tistory.com
์ด์ ๊ฒ์๊ธ์์๋ userId๋ฅผ ํ๋ ์ฝ๋ฉํ์ง๋ง, ์ฌ๊ธฐ์์๋ ์ฌ์ฉ์๊ฐ ์ํ๋ userId๋ฅผ ์์ฒญํ๊ณ ๊ทธ์ ์๋ตํ ์ ์๊ฒ ๋ง๋ค์ด๋ณธ๋ค. ํ๋ ๊น์ MVVM์ ์ธ ๊ตฌ์กฐ๋ก ๋ง๋ ๋ค.
#2 MVVM ํจํด
[Android] MVVM ๊ตฌ์กฐ ํ๋์ ๋ณด๊ธฐ
#1 ์๋๋ก์ด๋ ์ฑ์ '์ ํต์ ์ธ' ๋ฐฉ์ vs MVVM ํจํด#1-1 ๋์๋์ ์ข ์์ฑํ์ดํ๋ ํด๋์ค ๊ฐ์ ์ข ์์ฑ์ ๋ํ๋ธ๋ค. ์๋ฅผ ๋ค์ด, View๋ ViewModel์ ์ข ์๋๋ค. ์ข ์์ ์ฌ์ ์ ์๋ฏธ๋ '์์ฃผ์ฑ์ด ์์ด ์ฃผ๊ฐ
kenel.tistory.com
์ด์ ๊ฒ์๊ธ์ ์์ฑ๋ ์ฑ(์์ค ์ฝ๋)๋ฅผ์ ์ง๊ธ๊น์ง ๊ณต๋ถํ๋ MVVM ํจํด์ ์ ๋ชฉ์์ผฐ๋ค.
#3 ์ฝ๋ ์์
#3-1 ๊ฐ์
์์ ํ ์ ์ฒด ์์ค ์ฝ๋๋ #5์์ ํ์ธํ์. ์ฌ๊ธฐ์๋ ๊ฐ๋ตํ ์ค๋ช
๋ง์ ๊ณ๋ค์ธ๋ค.
#3-2 ํ๋ก์ ํธ ์์ค build.gradle ์์
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
...
id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
}
#3-3 ๋ชจ๋ ์์ค build.gradle ์์
plugins {
...
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.kapt")
}
android {
...
buildFeatures {
dataBinding = true
}
}
dependencies {
...
// Retrofit
...
// Coroutines
...
// LiveData, ViewModel
...
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
}
#3-4 Repository ํด๋์ค ๋ง๋ค๊ธฐ
// package com.example.mvvmbasics
import com.example.mvvmbasics.retrofit.AlbumService
import com.example.mvvmbasics.retrofit.Albums
import kotlinx.coroutines.delay
import retrofit2.Response
class MyRepository(private val retService: AlbumService) {
suspend fun getAlbums(): Response<Albums> {
delay(1500) // ํต์ ๋๋ ์ด๋ฅผ ์ฌํํ๊ธฐ ์ํ delay
return retService.getAlbums()
}
suspend fun getSortedAlbums(userId: Int): Response<Albums> {
delay(1500) // ํต์ ๋๋ ์ด๋ฅผ ์ฌํํ๊ธฐ ์ํ delay
return retService.getSortedAlbums(userId)
}
}
์ฐ์ , Repository ํด๋์ค๋ฅผ ๋ง๋ค๊ณ , ์์ฑ์์ Retofit Service๋ฅผ ์๊ตฌํ๊ฒ ๋ง๋ค์๋ค. ์ด Repository Class๋ ๋์ค์ ๋ณธ ์ฑ์ ๋ ์
๊ทธ๋ ์ด๋ํด์ Room๊น์ง ์ฌ์ฉํ๊ฒ ๋๋ ๊ฒฝ์ฐ, ์์ฑ์์ Room์ DAO๋ ์๊ตฌํ๊ฒ ๋ ๊ฒ์ด๋ค. ์ฆ, Repository๋ ์๊ฒฉ(Retrofit)์ด๋ ๋ก์ปฌ(Room)์ด๋ ๋ฐ์ดํฐ ์ ์ฅ์๋ฅผ ํ ๋ฐ ๋ชจ์ ํตํฉํ๊ณ ์ฝ๊ฒ ๊ด๋ฆฌํ๋ ์ญํ ์ ํ๋ค.
#3-5 ViewModel
// package com.example.mvvmbasics
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.mvvmbasics.retrofit.Albums
import kotlinx.coroutines.launch
import retrofit2.Response
class MainViewModel(private val repository: MyRepository) : ViewModel() {
private var _userId: MutableLiveData<Int> = MutableLiveData()
val userId: MutableLiveData<Int> get() = _userId
private var _albums: MutableLiveData<Response<Albums>> = MutableLiveData()
val albums: LiveData<Response<Albums>> get() = _albums
init {
_userId.value = 0
_albums.value = Response.success(Albums()) // ๋น Albums ๋ฃ๊ธฐ
}
fun onSubmitClicked(newUserId: Int) {
_userId.value = newUserId
updateAlbums()
}
private fun updateAlbums() {
viewModelScope.launch {
val response = repository.getSortedAlbums(_userId.value!!)
_albums.value = response
}
}
}
Model์ด '์ฌ๋ฃ'๊ณ View๊ฐ '์๋ฆฌ'๋ผ๋ฉด, View Model์ 'ํ์ํ ์ฌ๋ฃ๊ฐ ์ฌ๋ผ๊ฐ์๋ ๋๋ง'๋ค. ๋ฐ๋ผ์, Model์ ํด๋นํ๋ Repository ์ดํ ๋ง๋ค ๊ฒ์ View Model์ด๋ค. ์์ฑ์๋ก Repository๋ฅผ ์๊ตฌํ๊ฒ ๋ง๋ ๋ค (๋ฐ๋ผ์ ์ดํ์ ViewModelFactory๋ ๋ง๋ค์ด์ฃผ์ด์ผ ํ๋ค). ๊ทธ๋ฆฌ๊ณ ViewModel๊ฐ ๊ฐ์ง๊ณ ์์ ํ๋กํผํฐ์ธ userId์ albums๋ฅผ ์ ์ํ๋ค. ์๋ ์ชฝ์ ์๋ ํจ์๋ ์ด ํ๋กํผํฐ์ ๊ด๋ จ๋ ๋ก์ง์ ์ฒ๋ฆฌํ๋ ํจ์๋ค์ด๋ค.
#3-6 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>
<import type="com.example.mvvmbasics.MyConverter" />
<variable
name="viewModel"
type="com.example.mvvmbasics.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<EditText
android:id="@+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="User id"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="User id"
android:onClick="@{() -> viewModel.onSubmitClicked(MyConverter.stringToInt(editText, editText.getText().toString()))}"
android:text="Submit"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/editText"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/constraintLayout">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text=""
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ์ฌ์ฉํ ๊ฒ์ด๋ฏ๋ก, <layout> ํ๊ทธ๋ก ์ฝ๋๋ฅผ ๊ฐ์ผ๋ค. ์ ์ฝ๋๋ฅผ ์ฒ์๋ถํฐ ๋ง๋ ๊ฒ์ ์๋๊ณ ํ๋ง ์ก์๋์ ์ฑ๋ก ์ดํ ๋ค๋ฅธ ์์
์ ํ๋ฉด์ ๋์์ ๋ค๋ฌ์ด๋๊ฐ๋ค. Stringํ์ธ EditText์ ๋ฐ์ดํฐ์ Intํ์ธ ViewModel.userId ๊ฐ ๋ฐ์ดํฐํ ์ฐจ์ด๋ฅผ ๋ฉ๊ฟ์ฃผ๊ธฐ ์ํด์ Converter๋ฅผ ์ฌ์ฉํ๋ค (์๋ฐฉํฅ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ด ์๋์ด๋ ๊ทธ๋ฅ ์ฌ์ฉํ ์ ์๋ค).
#3-7 ์กํฐ๋นํฐ ์์
// package com.example.mvvmbasics
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.example.mvvmbasics.databinding.ActivityMainBinding
import com.example.mvvmbasics.retrofit.AlbumService
import com.example.mvvmbasics.retrofit.Albums
import com.example.mvvmbasics.retrofit.RetrofitInstance
import retrofit2.Response
class MainActivity : AppCompatActivity() {
private lateinit var repository: MyRepository
private lateinit var viewModelFactory: MainViewModelFactory
private lateinit var viewModel: MainViewModel
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
repository = MyRepository(RetrofitInstance.getRetrofitInstance().create(AlbumService::class.java))
viewModelFactory = MainViewModelFactory(repository)
viewModel = ViewModelProvider(this, viewModelFactory).get(MainViewModel::class.java)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.userId.observe(this, Observer {
binding.textView.text = ""
})
viewModel.albums.observe(this, Observer {
appendListOnTextView(binding.textView, it)
})
}
private fun appendListOnTextView(textView: TextView, list: Response<Albums>) {
val albumsList = list.body()?.listIterator()
if (albumsList != null) {
while (albumsList.hasNext()) {
val albumsItem = albumsList.next()
val result =
"User id : ${albumsItem.userId}" + "\n" +
"Album id : ${albumsItem.id}" + "\n" +
"Album Title : ${albumsItem.title}" + "\n\n\n"
textView.append(result)
}
}
}
}
UI๋ฅผ ์ง์ ์ ์ผ๋ก ๋ค๋ฃจ๋ ์ฝ๋๋ฅผ Activity๊ฐ ์๋ ViewModel์ ๋ฃ์ง ์๊ฒ ์ฃผ์ํด์ผ ํ๋ค. ViewModel์ #3-5์์ ๋งํ๋ฏ Repository์์ ๊ฐ์ ธ์จ ์ฌ๋ฃ๋ค ์ค์์ ํ์ํ ๊ฒ๋ค๋ง ์ฑ๊ฒจ๋์ ๋๋ง์ ๋ถ๊ณผํ๋ค.
#4 ์๋ ํ์ธ
#5 ์์ฑ๋ ์ฑ
android-practice/retrofit/MVVMBasics at master · Kanmanemone/android-practice
Contribute to Kanmanemone/android-practice development by creating an account on GitHub.
github.com