๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘/Android

[Android] Retrofit - MVVM ๊ตฌ์กฐ

interfacer_han 2024. 6. 5. 18:15

#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