์๋ ํ์ธ์ ๐
์ค๋์ MVVM ํจํด์ ๋ํด์ ๊ณต๋ถํ ๋ด์ฉ์
ํฌ์คํ ํด๋ณด๋ ค ํฉ๋๋ค.

MVVM ํจํด์ด๋?
MVVM ํจํด์ Model, View, ViewModel์ ๋ถ๋ฆฌํด View์ Model ๊ฐ์
์์กด์ฑ์ ์ค์ฌ์ฃผ๋ ํจํด์ ์๋ฏธํฉ๋๋ค.
View
์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผ ๋ฐ์ดํฐ๋ฅผ ์์ฒญ
โฌ๏ธ
ViewModel
View์์ ์์ฒญํ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ํจ์ ๋ฑ์ ํธ์ถ
โฌ๏ธ
Model
ViewModel์์ ์์ฒญํ๋ ๊ฐ์ ๋ฐํ
โฌ๏ธ
ViewModel
๋ชจ๋ธ์์ ๋ฐํ๋ฐ์ ๊ฐ์ LiveData๋ก ๊ฐ์ธ์ค
โฌ๏ธ
View
๋ผ์ด๋ธ๋ฐ์ดํฐ๋ฅผ ๊ฐ์งํ์ฌ ์ถ๋ ฅ
๊ทธ๋ ๋ค๋ฉด..
์ MVVMํจํด์ ์ฌ์ฉํ ๊น์?
๊ธฐ์กด์ ์ฌ์ฉํ๋ ํจํด์ผ๋ก MVC , MVP ํจํด์ด ์กด์ฌํฉ๋๋ค.
MVC์ ๊ฒฝ์ฐ View์ Controller๋ฅผ ์กํฐ๋นํฐ์์ ๋ชจ๋ ์ฒ๋ฆฌํด์ผ ํ๊ธฐ์ View์ Model์ฌ์ด์ ์์กด์ฑ์ด ๋๊ณ ,
MVP์ ๊ฒฝ์ฐ๋ Presenter๊ฐ View์ 1๋ 1๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ๋ ์ฌ์ด์ ์์กด์ฑ์ด ๊ฐํด์ง๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ณ ํ๋ก์ ํธ์ ๋ณผ๋ฅจ์ด
์ปค์ง ๊ฒฝ์ฐ ํ๋ก์ ํธ์ ๋ก์ง์ด ๋๋ฌด ๊ฑฐ๋ํด์ ธ์ ์ ์ง๋ณด์๊ฐ ํ๋ค๋ค๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
MVVM์ ViewModel 1๊ฐ์ ์ฌ๋ฌ View๊ฐ ๋์ ๊ฐ๋ฅํ๊ธฐ์ ์๋์ ์ผ๋ก ํ๋ก์ ํธ์ ๋ณผ๋ฅจ์ด ์ปค์ ธ๋ ์ ์ง๋ณด์๊ฐ
์ฝ๊ณ , ViewModel์ ํตํด View์ Model์ ์์กด์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ณธ๊ฒฉ์ ์ผ๋ก MVVM ํจํด์ผ๋ก ํ๋ก์ ํธ๋ฅผ ๊ตฌ์ฑํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด๊ฒ ์ต๋๋ค. ๐ค
์ฐ์ ๊ตฌ์ฑ์ ํฌ๊ฒ Model , ViewModel , ViewModelFactory , Repository๋ฅผ ์์ฑํด์ฃผ์ด์ผ ํฉ๋๋ค.
์ฝ๋๋ ์๋์ ๊ฐ์ต๋๋ค.
Model
data class MovieInfo(val name: String, val imageUrl: String, val category: String)
Repository
class MovieRepository(private val retrofitService : RetrofitRestInterface) {
suspend fun getAllMovies() = retrofitService.getAllMovies()
}
ViewModelFactory
class MovieViewModelFactory(private val repository: MovieRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if(modelClass.isAssignableFrom(MovieViewModel::class.java)){
MovieViewModel(repository) as T
} else {
throw java.lang.IllegalArgumentException("ํด๋น ViewModel์ ์ฐพ์์๊ฐ ์์ต๋๋ค!")
}
}
}
ViewModel
class MovieViewModel(private val movieRepository: MovieRepository) : ViewModel() {
val movieList = MutableLiveData<List<MovieInfo>>()
val errorMessage = MutableLiveData<String>()
val isLoading = MutableLiveData<Boolean>()
private var job : Job? = null
private val exceptionHandler = CoroutineExceptionHandler{_ , thrownException ->
thrownException.localizedMessage?.let { onError(it) }
}
fun getAllMoviesFromViewModel() { // ์ ์ฒด ์ํ ์ ๋ณด ๋ถ๋ฌ์ค๊ธฐ
job = CoroutineScope(Dispatchers.IO).launch(exceptionHandler) {
isLoading.postValue(true)
val response = movieRepository.getAllMovies()
withContext(Dispatchers.Main){
if(response.isSuccessful){
movieList.postValue(response.body())
isLoading.postValue(false)
} else {
onError("์๋ฌ๋ด์ฉ : ${response.message()}")
}
}
}
}
private fun onError(message : String) {
errorMessage.value = message
isLoading.value = false
}
override fun onCleared() { //๋ผ์ดํ์ธ์ดํด์ ๋ฐ๋ผ ํด๋น ViewModel์ด ์ฌ๋ผ์ง๋ ํธ์ถ๋จ
super.onCleared()
job?.cancel()
}
}
Repository๋ฅผ ํตํด ViewModel์์ Model์ ๊ฐ์ ๋ถ๋ฌ์ค๊ณ
ViewModelFactory๋ฅผ ํตํด ViewModel์ ์์ฑํฉ๋๋ค.
MainActivity์ Restํต์ , RecyclerView๋ฅผ ๊ตฌ์ฑํ๋ ์ฝ๋๋ ์๋์ ๊ฐ์ต๋๋ค.
interface RetrofitRestInterface {
@GET(MOVIE_LIST_URL) // ์ํ์ ๋ณด json์ ๋ฐ๋ URL
suspend fun getAllMovies() : Response<List<MovieInfo>>
}
class RetrofitService {
companion object{
private lateinit var retrofitService : RetrofitRestInterface
fun getInstance() : RetrofitRestInterface {
if(!::retrofitService.isInitialized){
val retrofit = Retrofit.Builder()
.baseUrl(TARGET_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofitService = retrofit.create(RetrofitRestInterface::class.java)
}
return retrofitService
}
}
}
RecyclerViewAdapter
class MovieRecyclerAdapter : RecyclerView.Adapter<MovieRecyclerAdapter.MovieViewHolder>() {
private var movies = listOf<MovieInfo>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val binding = MovieAdapterLayoutBinding.inflate(LayoutInflater.from(parent.context) , parent , false)
return MovieViewHolder(binding)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.bind(movies[position])
}
override fun getItemCount() = movies.size
fun setList(list : List<MovieInfo>){
movies = list
}
inner class MovieViewHolder(private val binding: MovieAdapterLayoutBinding) : RecyclerView.ViewHolder(binding.root){
fun bind(movie : MovieInfo){
// Glide๋ฅผ ํตํด ์ด๋ฏธ์ง Load
Glide.with(binding.imageview.context).load("$TARGET_URL${movie.imageUrl}").into(binding.imageview)
binding.name.text = movie.name
}
}
}
MainActivity
class MovieMainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var mMovieRecyclerAdapter : MovieRecyclerAdapter
private var mMovieViewModel : MovieViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
mMovieRecyclerAdapter = MovieRecyclerAdapter()
mMovieViewModel = ViewModelProvider(this ,
MovieViewModelFactory(MovieRepository(RetrofitService.getInstance())))[MovieViewModel::class.java]
observeData()
}
override fun onResume() {
super.onResume()
mMovieViewModel?.getAllMoviesFromViewModel() // ์ํ ์ ์ฒด์ ๋ณด๋ฅผ ViewModel์์ ๋ถ๋ฌ์จ๋ค.
}
/**
* Function for observe Live Data
* **/
private fun observeData() { // LiveData์ ๊ฐ์ observeํ๋ ํจ์
mMovieViewModel?.movieList?.observe(this){
Toast.makeText(this , "๋ทฐ๋ชจ๋ธ ์ต์ ๋ฒ ํธ์ถ๋จ!!" , Toast.LENGTH_SHORT).show()
with(binding.movieRV) {
mMovieRecyclerAdapter.setList(it)
adapter = mMovieRecyclerAdapter
mMovieRecyclerAdapter.notifyItemRangeChanged(0, mMovieRecyclerAdapter.itemCount)
}
binding.progressBar.visibility = View.GONE
}
mMovieViewModel?.errorMessage?.observe(this){
binding.progressBar.visibility = View.GONE
Toast.makeText(this@MovieMainActivity , it , Toast.LENGTH_SHORT).show()
}
mMovieViewModel?.isLoading?.observe(this , Observer {
if(it){
binding.progressBar.visibility = View.VISIBLE
} else {
binding.progressBar.visibility = View.GONE
}
})
}
}
ํ๋ก์ ํธ๋ ์์ ๊ฐ์ด ๊ตฌ์ฑ์ ํ๋ฉด ๋๋ฉฐ
์๋ก์ด Model์ด ์ถ๊ฐ๋ ์์๋ Repository์ ์ถ๊ฐ๋ Model์ ๊ฐ์ ๋ถ๋ฌ์ฌ ์ ์๋ ํจ์๋ฅผ ์ถ๊ฐ๋ก ๊ตฌํํด์ฃผ๋ฉด ๋ฉ๋๋ค.
MVVM์ ๋์ ๋ฐฉ์์ ์ ์์ ์ ๊ฐ์ด observer๋ฅผ ํ์ฉ ํ์ฌ LiveData์ ๋ณ๊ฒฝ์ ๊ฐ์งํ๊ณ
๊ทธ ๋ณ๊ฒฝ๋ ์ฌํญ์ View์ ๋ํ๋ด๋ ๊ตฌ์กฐ๋ก ์ด๋ฃจ์ด์ง๋๋ค.
๋์ ํ๋ฉด์ ์๋์ ๊ฐ์ต๋๋ค. ๐
์ด์์ผ๋ก MVVM ํฌ์คํ ์ ๋ํด ๋ง์น๊ฒ ์ต๋๋ค.
์ค๋๋ ์ฆ์ฝํ์ธ์ :)