Android/Kotlin

[Kotlin][Android] ์•ˆ๋“œ๋กœ์ด๋“œ MVVM ํŒจํ„ด์— ๋Œ€ํ•ด์„œ - Retrofit์œผ๋กœ ์ „์†ก๋ฐ›์€ ๊ฐ’์„ MVVM๊ตฌ์กฐ ์ ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜๊ธฐ

๋ށ์š” 2022. 11. 10. 15:00

์•ˆ๋…•ํ•˜์„ธ์š” ๐Ÿ‘‹

์˜ค๋Š˜์€ MVVM ํŒจํ„ด์— ๋Œ€ํ•ด์„œ ๊ณต๋ถ€ํ•œ ๋‚ด์šฉ์„

ํฌ์ŠคํŒ…ํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

MVVM ํŒจํ„ด์ด๋ž€?

MVVM ํŒจํ„ด์€ Model, View, ViewModel์„ ๋ถ„๋ฆฌํ•ด View์™€ Model ๊ฐ„์˜

์˜์กด์„ฑ์„ ์ค„์—ฌ์ฃผ๋Š” ํŒจํ„ด์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

MVVMํŒจํ„ด์˜ ๊ตฌ์กฐ

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 ํฌ์ŠคํŒ…์— ๋Œ€ํ•ด ๋งˆ์น˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์˜ค๋Š˜๋„ ์ฆ์ฝ”ํ•˜์„ธ์š” :)