๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ“ฑ| Android/๐Ÿ“˜ | ๊ธฐ๋ก

[Android, Kotlin] Android MVVM ๋‹ค๋ค„๋ณด๊ธฐ

by immgga 2023. 3. 18.

์ด๋ฒˆ์— ํ•„์ž๊ฐ€ ๊ณต๋ถ€ํ•ด๋ณธ ๊ฒƒ์€ MVVM์ด๋‹ค.

MVVM์€ View - Model - ViewModel์˜ ์•ฝ์ž์ธ๊ฑด ๋‹ค ์•Œ๊ณ  ์žˆ๋Š” ์‚ฌ์‹ค์ด๋‹ˆ ๊ทธ๋ƒฅ ๋„˜๊ธฐ๊ณ (?)

ํ•„์ž๋Š” ์˜ค๋ž˜์ „๋ถ€ํ„ฐ ์‚ฌ์šฉํ•ด์™”๋˜ ์•ˆ๋“œ๋กœ์ด๋“œ ์•„ํ‚คํ…์ฒ˜์ด์ง€๋งŒ

์“ฐ๋Š” ์ด์œ ์™€ ๊ฐ ํŒŒ์ผ๋“ค์ด ์ •ํ™•ํžˆ ๋ฌด์—‡์„ ์œ„ํ•œ ํŒŒ์ผ๋“ค์ธ์ง€ ์ž˜ ์•Œ์ง€ ๋ชปํ–ˆ๊ธฐ์—

์ด๋ฒˆ ๊ธฐํšŒ์— ์ œ๋ฐ๋กœ ์•Œ์•„๋ณด๊ณ ์ž ํ•œ๋‹ค.


1. MVVM์˜ ๊ตฌ์กฐ

MVVM์€ ์ด๋ฆ„์—์„œ๋„ ์•Œ ์ˆ˜ ์žˆ๋“ฏ์ด View, Model, ViewModel๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๋‹ค.

 

 

  • View: ์ด๋ฒคํŠธ ๋ฐœ์ƒ, liveData ๊ฐ์ง€ํ•ด ๋ทฐ์— ์ถœ๋ ฅํ•œ๋‹ค(์ถœ๋ ฅ์‹ธ๊ฐœ).
  • Model: ์‹ค์ง์ ์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ณณ(DB, Api ๋“ฑ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜(?)ํ•œ๋‹ค.)
  • ViewModel: ํ”„๋กœ์ ํŠธ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ด€๋ฆฌํ•œ๋‹ค(Api ํ˜ธ์ถœ, DBํ˜ธ์ถœ ์ด๋Ÿฐ๊ฑฐ).

์ฃผ์˜ํ•  ์ ์€ View์™€ Model์˜ ์˜์กด์„ฑ์ด ์—†์–ด์•ผ ํ•œ๋‹ค.

 

 

๊ทธ๋Ÿผ ๊ถ๊ธˆํ•  ๊ฒƒ์ด๋‹ค

์™œ MVVM์„ ์จ์•ผ ํ•˜๋Š”๊ฐ€? ์“ฐ๋ฉด ํŒŒ์ผ๋„ ๋งŽ์ด ๋งŒ๋“ค์–ด์•ผ ํ•˜๊ณ  ์กด๋‚˜๊ท€์ฐฎ์•„์งˆ๊ฒƒ ๊ฐ™์€๋ฐ...

 

์™œ๋ƒ๋ฉด(์ดํ•ด๋ฅผ ์œ„ํ•ด MVC์™€ MVP ์•„ํ‚คํ…์ฒ˜๋ฅผ ์˜ˆ๋กœ ๋“ค์–ด๋ณด๊ฒ ๋‹ค)

  • MVC๋Š” ์ดˆ๋ณด์ž๋ฅผ ์œ„ํ•œ ์‰ฌ์šด ํŒŒ์ผ ๊ตฌ์กฐ์ด์ง€๋งŒ ์ฝ”๋“œ๋ฅผ ๋ชจ๋‘ activity์— ๊ฝ‚์•„ ๋„ฃ๊ธฐ(?) ๋•Œ๋ฌธ์— ์•ฑ์ด ๋ฌด๊ฑฐ์›Œ์งˆ ์ˆ˜ ์žˆ๋‹ค(MVVM์€ activity์— ๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ๋„ฃ๋Š” ๋ฐฉ์‹์ด ์•„๋‹Œ ์—ญํ• ๋ณ„๋กœ ํŒŒ์ผ ๊ตฌ์กฐ๋ฅผ ๋ถ„๋ฆฌํ•ด๋†จ๋‹ค).
  • MVP๋Š” View์™€ Model์ด Presenter๋ฅผ ํ†ตํ•ด์„œ๋งŒ ๋™์ž‘ํ•˜๊ฒŒ ํ•˜๋Š” ๋””์ž์ธ ํŒจํ„ด์ด์ง€๋งŒ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด View์™€ Model์ด Presenter์™€์˜ ์˜์กด์„ฑ์ด ์ปค์ง„๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค(์˜์กด์„ฑ์ด ์ปค์ง€๊ฒŒ ๋˜๋ฉด ์•ฑ์„ ์œ ์ง€๋ณด์ˆ˜ ํ•˜๊ธฐ๊ฐ€ ๋งค์šฐ ๊ป„๋„๋Ÿฌ์›Œ์ง„๋‹ค.).
    MVVM์€ View์™€ Presenter๊ฐ€ 1:1์˜ ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง€๋Š”๊ฒŒ ์•„๋‹Œ, ํ•˜๋‚˜์˜ ViewModel์„ ์—ฌ๋Ÿฌ activity๊ฐ€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค(1:ๅคš ๊ด€๊ณ„).

 

์ด์ œ MVVM์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์ด์œ ๋„ ์•Œ์•˜์œผ๋‹ˆ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊นƒํ—ˆ๋ธŒ open api ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด ๊ณต๋ถ€ํ•ด๋ณด๋„๋ก ํ•˜์ž.

 

ํŒŒ์ผ ๊ตฌ์กฐ

 

1. activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    tools:context=".view.MainActivity"
    android:orientation="vertical">

    <ImageView
        android:layout_width="250dp"
        android:layout_height="250dp"
        android:src="@drawable/github_icon"
        android:layout_gravity="center"/>

    <EditText
        android:id="@+id/github_user_name"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_marginTop="15dp"
        android:layout_marginStart="35dp"
        android:layout_marginEnd="35dp"
        android:hint="github username" />

    <Button
        android:id="@+id/result_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="find"
        android:backgroundTint="@color/black"
        android:layout_marginEnd="55dp"
        android:layout_marginStart="55dp"
        android:layout_marginTop="40dp"/>
</LinearLayout>

 

2. activity_user_info.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=".view.UserInfoActivity">

    <ImageView
        android:id="@+id/profile_img"
        android:layout_width="140dp"
        android:layout_height="140dp"
        android:layout_marginStart="20dp"
        android:layout_marginTop="40dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="@+id/profile_img"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/profile_img"
        app:layout_constraintTop_toTopOf="@+id/profile_img">

        <TextView
            android:id="@+id/user_name_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="name(subname)"
            android:textSize="23sp"
            android:textStyle="bold"
            android:textColor="@color/black"/>

        <TextView
            android:id="@+id/follow_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="follower: 47, following: 49"
            android:textSize="17sp"
            android:autoSizeTextType="uniform"/>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:orientation="vertical"
        app:layout_constraintStart_toStartOf="@+id/profile_img"
        app:layout_constraintTop_toBottomOf="@+id/profile_img">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/company_icon" />

            <TextView
                android:id="@+id/company_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:text="company"
                android:textSize="17sp" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:orientation="horizontal">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/baseline_location_on_24" />

            <TextView
                android:id="@+id/location_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:text="location"
                android:textSize="17sp" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:orientation="horizontal">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/link_icon" />

            <TextView
                android:id="@+id/link_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:text="link"
                android:textSize="17sp" />
        </LinearLayout>
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

 

1. models

GithubUser.kt

import com.google.gson.annotations.SerializedName

data class GithubUser(
    @SerializedName("login") val username: String,
    @SerializedName("name") val name: String,
    @SerializedName("avatar_url") val avatarUrl: String,
    @SerializedName("company") val company: String,
    @SerializedName("blog") val blog: String,
    @SerializedName("location") val location: String,
    @SerializedName("followers") val followers: Int,
    @SerializedName("following") val following: Int
)

 

GithubService.kt

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path

interface GithubService {
    @GET("users/{owner}")
    fun getUser(
        @Path("owner") owner: String
    ): Call<GithubUser>
}

 

GithubObject.kt

import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object GithubObject {
    private val gson = GsonBuilder()
        .setLenient()
        .create()

    private val okHttp = OkHttpClient.Builder()
        .addInterceptor(HttpLoggingInterceptor())
        .build()

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .client(okHttp)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build()

    val githubService: GithubService = retrofit.create(GithubService::class.java)
}

model ๊ณ„์ธต์—์„œ ํ•„์ž๋Š” ๋ถˆ๋Ÿฌ์˜ฌ ๋ฐ์ดํ„ฐ๋“ค, api service, api builder๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

 

 

2. ViewModel

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.day2.model.GithubObject
import com.example.day2.model.GithubUser
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class GithubViewModel: ViewModel() {
    private val _getGithub = MutableLiveData<GithubUser>()
    val getGithub: LiveData<GithubUser> = _getGithub

    fun getGithubUser(userName: String) {
        GithubObject.githubService.getUser(userName).enqueue(object : Callback<GithubUser> {
            override fun onResponse(call: Call<GithubUser>, response: Response<GithubUser>) {
                if (response.isSuccessful) {    // 400์—๋Ÿฌ๊ฐ€ ์•ˆ๋œธ
                    _getGithub.value = response.body()
                } else Log.d("TAG", "onResponse error: $response")
            }

            override fun onFailure(call: Call<GithubUser>, t: Throwable) {
                Log.d("TAG", "onFailure response fail: $call")
                Log.e("TAG", "onFailure response fail: ${t.printStackTrace()}", t.cause)
            }
        })
    }
}

viewmodel ๊ณ„์ธต์—์„œ๋Š” retrofit ํ˜ธ์ถœ์„ ๋กœ์ง์„ ์ƒ์„ฑํ–ˆ๋‹ค.

 

 

3. View

MainActivity.kt

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.day2.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.resultBtn.setOnClickListener {
            startActivity(Intent(this, UserInfoActivity::class.java)
                .putExtra("name", binding.githubUserName.text.toString()))
        }
    }
}

 

UserInfoActivity.kt

import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import coil.load
import coil.transform.CircleCropTransformation
import com.example.day2.databinding.ActivityUserInfoBinding
import com.example.day2.viewmodel.GithubViewModel

class UserInfoActivity : AppCompatActivity() {
    private val viewModel by viewModels<GithubViewModel>()
    private lateinit var binding: ActivityUserInfoBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityUserInfoBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val name = intent.getStringExtra("name")

        if (name != null) {
            viewModel.getGithubUser(name)
            viewModel.getGithub.observe(this) { data ->
                binding.profileImg.load(data.avatarUrl) {
                    transformations(CircleCropTransformation())
                }
                binding.userNameText.text = "${data.username}(${data.name})"
                binding.followInfo.text = "followers: ${data.followers}, following: ${data.following}"
                binding.companyText.text = data.company
                binding.locationText.text = data.location
                binding.linkText.text = data.blog

            }
        }
    }
}

view ๊ณ„์ธต์—์„œ๋Š” viewmodel์—์„œ ์ƒ์„ฑํ•œ ๋กœ์ง์„ ์ด์šฉํ•ด ๋ทฐ๋ฅผ ํ™”๋ฉด์— ์ถœ๋ ฅํ–ˆ๋‹ค.


์ •๋ฆฌ

์ด๋ฒˆ์— MVVM์„ ๋‹ค์‹œ ๊ณต๋ถ€ํ•ด ๋ณด๋ฉด์„œ ๊ฐ๊ฐ์˜ ๊ณ„์ธต๋“ค์˜ ์—ญํ• ์„ ๋” ์ •ํ™•ํ•˜๊ฒŒ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค.

728x90

๋Œ“๊ธ€