320x100

Android MVVM 패턴 완벽 입문 가이드 📱
목차
1. MVVM 패턴 이해하기
1.1 MVVM이란?
MVVM (Model-View-ViewModel) 은 UI와 비즈니스 로직을 분리하는 아키텍처 패턴입니다.
┌──────────┐ ┌─────────────┐ ┌───────┐
│ View │ ────▶│ ViewModel │ ────▶│ Model │
│ (UI) │ ◀──── │ (로직) │ ◀──── │(데이터)│
└──────────┘ └─────────────┘ └───────┘
각 계층의 역할:
- View (Activity/Fragment): UI 표시, 사용자 입력 전달만 담당
- ViewModel: UI 로직, 데이터 가공, 상태 관리
- Model: 데이터 저장/조회 (Room, Repository)
1.2 왜 MVVM을 사용할까?
❌ 기존 코드의 문제점:
// 모든 것이 Activity에 섞여 있음
class MainActivity : AppCompatActivity() {
fun onCreate() {
// UI 초기화
// USB 연결
// 데이터 전송
// 로직 처리
}
}
✅ MVVM 적용 후:
// Activity: UI만 담당
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
fun onCreate() {
observeData() // 데이터 관찰만
}
}
// ViewModel: 로직 담당
class MainViewModel : ViewModel() {
fun connectUsb() { /* 연결 로직 */ }
fun sendData() { /* 전송 로직 */ }
}
2. 프로젝트 준비
2.1 필요한 의존성 추가
app/build.gradle.kts 파일을 열고 다음을 추가하세요:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt") // Room과 DataBinding에 필요
}
android {
// ...
buildFeatures {
viewBinding = true
dataBinding = true // DataBinding 활성화
}
}
dependencies {
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.activity:activity-ktx:1.8.0")
// Room Database
implementation("androidx.room:room-runtime:2.6.0")
implementation("androidx.room:room-ktx:2.6.0")
kapt("androidx.room:room-compiler:2.6.0")
// Coroutines (비동기 처리)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
Sync Now 버튼을 클릭하여 의존성을 동기화하세요.
3. 단계별 구현
📌 Step 1: LiveData 이해하기
LiveData는 관찰 가능한 데이터 홀더입니다. 데이터가 변경되면 자동으로 UI가 업데이트됩니다.
간단한 예제:
// ViewModel
class CounterViewModel : ViewModel() {
// MutableLiveData: 읽기/쓰기 가능
private val _count = MutableLiveData<Int>(0)
// LiveData: 읽기만 가능 (외부에 노출)
val count: LiveData<Int> = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
}
// Activity
class CounterActivity : AppCompatActivity() {
private lateinit var binding: ActivityCounterBinding
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCounterBinding.inflate(layoutInflater)
setContentView(binding.root)
// LiveData 관찰
viewModel.count.observe(this) { count ->
binding.textCount.text = count.toString()
}
// 버튼 클릭
binding.buttonIncrement.setOnClickListener {
viewModel.increment()
}
}
}
코틀린 문법 설명:
by viewModels(): 델리게이트 패턴으로 ViewModel 자동 생성observe(this) { }: 람다식으로 데이터 변경 감지_count와count: 캡슐화 (외부에서 직접 수정 방지)
📌 Step 2: 현재 코드를 MVVM으로 리팩토링
Before (현재 코드):
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Activity에서 직접 처리 ❌
val usbConnManager = UsbCommunicationManager(this)
usbConnManager.connect()
val packet = basicParameterPacket.createPacket(...)
usbConnManager.sendData(packet)
}
}
After (MVVM 적용):
MainViewModel.kt 생성
package com.elkaphoto.printertesttools.ui.main
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.elkaphoto.printertesttools.usbcommunicator.UsbCommunicationManager
import com.elkaphoto.printertesttools.usbcommunicator.packets.BasicParameterPacket
import com.elkaphoto.printertesttools.usbcommunicator.packets.DataInterface
/**
* MainActivity의 ViewModel
* - USB 연결 및 데이터 전송 로직 담당
* - UI 상태를 LiveData로 관리
*/
class MainViewModel(application: Application) : AndroidViewModel(application) {
// USB 통신 매니저
private val usbManager = UsbCommunicationManager(application)
// 연결 상태 (UI에 노출)
private val _connectionStatus = MutableLiveData<String>()
val connectionStatus: LiveData<String> = _connectionStatus
// 전송 데이터 로그 (UI에 노출)
private val _sentData = MutableLiveData<String>()
val sentData: LiveData<String> = _sentData
/**
* USB 연결
*/
fun connectUsb() {
try {
usbManager.connect()
_connectionStatus.value = "연결 성공"
} catch (e: Exception) {
_connectionStatus.value = "연결 실패: ${e.message}"
}
}
/**
* 기본 파라미터 쿼리 전송
*/
fun sendBasicParameterQuery() {
val basicParameterPacket = BasicParameterPacket()
val packet = basicParameterPacket.createPacket(
DataInterface.INTERFACE_BASIC_PARAMETER,
0x01,
0x11
)
val hexString = BasicParameterPacket.byteArrayToHexString(packet)
Log.d("MainViewModel", "전송데이터: $hexString")
usbManager.sendData(packet)
_sentData.value = hexString
}
/**
* ViewModel이 제거될 때 리소스 정리
*/
override fun onCleared() {
super.onCleared()
// USB 연결 해제 등
}
}
MainActivity.kt 수정
package com.elkaphoto.printertesttools.ui.main
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.elkaphoto.printertesttools.databinding.ActivityMainBinding
import com.google.android.material.snackbar.Snackbar
/**
* MVVM 패턴이 적용된 MainActivity
* - View: UI 표시 및 사용자 입력 처리만 담당
* - ViewModel의 데이터를 관찰하고 UI 업데이트
*/
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// ViewModel 초기화 (by viewModels()로 자동 생성)
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// ViewModel의 데이터 관찰
observeViewModel()
// USB 연결 시작
viewModel.connectUsb()
// 데이터 전송
viewModel.sendBasicParameterQuery()
}
/**
* ViewModel의 LiveData 관찰
*/
private fun observeViewModel() {
// 연결 상태 관찰
viewModel.connectionStatus.observe(this) { status ->
Snackbar.make(binding.root, status, Snackbar.LENGTH_SHORT).show()
}
// 전송 데이터 관찰
viewModel.sentData.observe(this) { data ->
// UI에 표시 (예: TextView에 출력)
// binding.textSentData.text = data
}
}
}
개선 포인트:
- ✅ UI 로직과 비즈니스 로직 분리
- ✅ 테스트 가능한 구조
- ✅ 화면 회전 시 데이터 유지 (ViewModel은 생명주기 독립적)
4. 최종 프로젝트: 일기장 앱
4.1 프로젝트 구조
app/src/main/java/com/elkaphoto/diary/
├── data/
│ ├── local/
│ │ ├── DiaryDatabase.kt # Room Database
│ │ └── DiaryDao.kt # 데이터 접근 객체
│ ├── model/
│ │ └── Diary.kt # 일기 데이터 모델
│ └── repository/
│ └── DiaryRepository.kt # 데이터 저장소
├── ui/
│ ├── list/
│ │ ├── DiaryListFragment.kt # 일기 목록 (왼쪽)
│ │ └── DiaryListViewModel.kt
│ ├── detail/
│ │ ├── DiaryDetailFragment.kt # 일기 뷰어 (오른쪽)
│ │ └── DiaryDetailViewModel.kt
│ └── MainActivity.kt
└── util/
└── DateUtil.kt # 날짜 유틸리티
📌 Step 3: Room Database 설정
Diary.kt (데이터 모델)
package com.elkaphoto.diary.data.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.Date
/**
* 일기 데이터 모델
* @Entity: Room 테이블로 사용됨을 표시
*/
@Entity(tableName = "diary_table")
data class Diary(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String, // 일기 제목
val content: String, // 일기 내용
val createdAt: Long, // 작성 시간 (타임스탬프)
val modifiedAt: Long // 수정 시간
)
// 코틀린 문법 설명:
// - data class: equals, hashCode, toString 자동 생성
// - val: 읽기 전용 속성
// - 기본값 설정: id = 0
DiaryDao.kt (데이터 접근 객체)
package com.elkaphoto.diary.data.local
import androidx.lifecycle.LiveData
import androidx.room.*
import com.elkaphoto.diary.data.model.Diary
/**
* Room DAO (Data Access Object)
* - 데이터베이스 쿼리 메서드 정의
*/
@Dao
interface DiaryDao {
/**
* 모든 일기 조회 (최신순 정렬)
* @return LiveData로 자동 업데이트
*/
@Query("SELECT * FROM diary_table ORDER BY createdAt DESC")
fun getAllDiaries(): LiveData<List<Diary>>
/**
* 특정 일기 조회
*/
@Query("SELECT * FROM diary_table WHERE id = :diaryId")
fun getDiaryById(diaryId: Long): LiveData<Diary?>
/**
* 일기 추가
* @return 생성된 일기의 ID
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(diary: Diary): Long
/**
* 일기 수정
*/
@Update
suspend fun update(diary: Diary)
/**
* 일기 삭제
*/
@Delete
suspend fun delete(diary: Diary)
/**
* 모든 일기 삭제
*/
@Query("DELETE FROM diary_table")
suspend fun deleteAll()
}
// 코틀린 문법 설명:
// - suspend: 코루틴에서 사용 가능한 중단 함수
// - interface: 구현은 Room이 자동 생성
DiaryDatabase.kt
package com.elkaphoto.diary.data.local
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.elkaphoto.diary.data.model.Diary
/**
* Room Database
* - 싱글톤 패턴으로 구현
*/
@Database(
entities = [Diary::class],
version = 1,
exportSchema = false
)
abstract class DiaryDatabase : RoomDatabase() {
abstract fun diaryDao(): DiaryDao
companion object {
@Volatile
private var INSTANCE: DiaryDatabase? = null
fun getDatabase(context: Context): DiaryDatabase {
// 이미 생성된 인스턴스가 있으면 반환
return INSTANCE ?: synchronized(this) {
// 없으면 새로 생성
val instance = Room.databaseBuilder(
context.applicationContext,
DiaryDatabase::class.java,
"diary_database"
).build()
INSTANCE = instance
instance
}
}
}
}
// 코틀린 문법 설명:
// - companion object: 자바의 static과 유사
// - @Volatile: 멀티스레드 환경에서 안전
// - synchronized: 동시 접근 방지
// - ?: (엘비스 연산자): null이면 우측 실행
📌 Step 4: Repository 패턴
DiaryRepository.kt
package com.elkaphoto.diary.data.repository
import androidx.lifecycle.LiveData
import com.elkaphoto.diary.data.local.DiaryDao
import com.elkaphoto.diary.data.model.Diary
/**
* Repository 패턴
* - ViewModel과 데이터 소스 사이의 중재자
* - 여러 데이터 소스를 하나로 통합 가능
*/
class DiaryRepository(private val diaryDao: DiaryDao) {
// 모든 일기 (LiveData로 자동 업데이트)
val allDiaries: LiveData<List<Diary>> = diaryDao.getAllDiaries()
/**
* 특정 일기 조회
*/
fun getDiaryById(diaryId: Long): LiveData<Diary?> {
return diaryDao.getDiaryById(diaryId)
}
/**
* 일기 추가
* suspend: 백그라운드 스레드에서 실행
*/
suspend fun insert(diary: Diary): Long {
return diaryDao.insert(diary)
}
/**
* 일기 수정
*/
suspend fun update(diary: Diary) {
diaryDao.update(diary)
}
/**
* 일기 삭제
*/
suspend fun delete(diary: Diary) {
diaryDao.delete(diary)
}
}
// 왜 Repository가 필요할까?
// - ViewModel은 데이터 출처를 몰라도 됨
// - Room, 서버 API 등 여러 소스를 통합 관리
// - 테스트 시 Mock Repository로 교체 가능
📌 Step 5: ViewModel 구현
DiaryListViewModel.kt
package com.elkaphoto.diary.ui.list
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.elkaphoto.diary.data.local.DiaryDatabase
import com.elkaphoto.diary.data.model.Diary
import com.elkaphoto.diary.data.repository.DiaryRepository
import kotlinx.coroutines.launch
/**
* 일기 목록 ViewModel
*/
class DiaryListViewModel(application: Application) : AndroidViewModel(application) {
private val repository: DiaryRepository
val allDiaries: LiveData<List<Diary>>
init {
val diaryDao = DiaryDatabase.getDatabase(application).diaryDao()
repository = DiaryRepository(diaryDao)
allDiaries = repository.allDiaries
}
/**
* 새 일기 추가
*/
fun insertDiary(title: String, content: String) {
val diary = Diary(
title = title,
content = content,
createdAt = System.currentTimeMillis(),
modifiedAt = System.currentTimeMillis()
)
// viewModelScope: ViewModel 생명주기에 맞춘 코루틴 스코프
viewModelScope.launch {
repository.insert(diary)
}
}
/**
* 일기 삭제
*/
fun deleteDiary(diary: Diary) {
viewModelScope.launch {
repository.delete(diary)
}
}
}
// 코틀린 문법 설명:
// - init: 클래스 생성 시 자동 실행
// - viewModelScope.launch: 백그라운드 작업 (코루틴)
DiaryDetailViewModel.kt
package com.elkaphoto.diary.ui.detail
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import com.elkaphoto.diary.data.local.DiaryDatabase
import com.elkaphoto.diary.data.model.Diary
import com.elkaphoto.diary.data.repository.DiaryRepository
import kotlinx.coroutines.launch
/**
* 일기 상세보기/수정 ViewModel
*/
class DiaryDetailViewModel(application: Application) : AndroidViewModel(application) {
private val repository: DiaryRepository
// 현재 선택된 일기 ID
private val _currentDiaryId = MutableLiveData<Long>()
// 선택된 일기 (자동으로 업데이트)
val currentDiary: LiveData<Diary?> = _currentDiaryId.switchMap { id ->
repository.getDiaryById(id)
}
init {
val diaryDao = DiaryDatabase.getDatabase(application).diaryDao()
repository = DiaryRepository(diaryDao)
}
/**
* 일기 선택
*/
fun selectDiary(diaryId: Long) {
_currentDiaryId.value = diaryId
}
/**
* 일기 수정
*/
fun updateDiary(diary: Diary, newTitle: String, newContent: String) {
val updatedDiary = diary.copy(
title = newTitle,
content = newContent,
modifiedAt = System.currentTimeMillis()
)
viewModelScope.launch {
repository.update(updatedDiary)
}
}
/**
* 일기 삭제
*/
fun deleteDiary(diary: Diary) {
viewModelScope.launch {
repository.delete(diary)
}
}
}
// 코틀린 문법 설명:
// - switchMap: LiveData 변환 (diaryId가 바뀌면 자동으로 새 일기 조회)
// - copy(): data class의 복사본 생성 (일부 속성만 변경)
📌 Step 6: UI 구현 (Fragment)
activity_main.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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 왼쪽: 일기 목록 -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_list"
android:name="com.elkaphoto.diary.ui.list.DiaryListFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/divider"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<!-- 구분선 -->
<View
android:id="@+id/divider"
android:layout_width="1dp"
android:layout_height="0dp"
android:background="#CCCCCC"
app:layout_constraintStart_toEndOf="@id/fragment_list"
app:layout_constraintEnd_toStartOf="@id/fragment_detail"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_weight="0" />
<!-- 오른쪽: 일기 상세보기 -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_detail"
android:name="com.elkaphoto.diary.ui.detail.DiaryDetailFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toEndOf="@id/divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
DiaryListFragment.kt
package com.elkaphoto.diary.ui.list
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.elkaphoto.diary.databinding.FragmentDiaryListBinding
import com.elkaphoto.diary.ui.detail.DiaryDetailViewModel
/**
* 일기 목록 Fragment
*/
class DiaryListFragment : Fragment() {
private var _binding: FragmentDiaryListBinding? = null
private val binding get() = _binding!!
// 같은 Activity의 ViewModel 공유
private val listViewModel: DiaryListViewModel by activityViewModels()
private val detailViewModel: DiaryDetailViewModel by activityViewModels()
private lateinit var adapter: DiaryListAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDiaryListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
observeViewModel()
setupAddButton()
}
/**
* RecyclerView 설정
*/
private fun setupRecyclerView() {
adapter = DiaryListAdapter { diary ->
// 일기 클릭 시 상세보기로 전달
detailViewModel.selectDiary(diary.id)
}
binding.recyclerViewDiaries.apply {
layoutManager = LinearLayoutManager(context)
adapter = this@DiaryListFragment.adapter
}
}
/**
* ViewModel 데이터 관찰
*/
private fun observeViewModel() {
listViewModel.allDiaries.observe(viewLifecycleOwner) { diaries ->
adapter.submitList(diaries)
}
}
/**
* 추가 버튼 설정
*/
private fun setupAddButton() {
binding.fabAdd.setOnClickListener {
// 다이얼로그 또는 새 화면으로 이동
showAddDiaryDialog()
}
}
private fun showAddDiaryDialog() {
// 다이얼로그 구현 (생략)
listViewModel.insertDiary("새 일기", "내용을 입력하세요")
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 메모리 누수 방지
}
}
// 코틀린 문법 설명:
// - by activityViewModels(): Activity 범위의 ViewModel (Fragment 간 공유)
// - _binding과 binding: null 안전성
// - apply { }: 객체에 여러 설정 적용
DiaryListAdapter.kt (RecyclerView 어댑터)
package com.elkaphoto.diary.ui.list
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.elkaphoto.diary.data.model.Diary
import com.elkaphoto.diary.databinding.ItemDiaryBinding
import java.text.SimpleDateFormat
import java.util.*
/**
* 일기 목록 어댑터
*/
class DiaryListAdapter(
private val onItemClick: (Diary) -> Unit
) : ListAdapter<Diary, DiaryListAdapter.DiaryViewHolder>(DiaryDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DiaryViewHolder {
val binding = ItemDiaryBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return DiaryViewHolder(binding, onItemClick)
}
override fun onBindViewHolder(holder: DiaryViewHolder, position: Int) {
holder.bind(getItem(position))
}
/**
* ViewHolder
*/
class DiaryViewHolder(
private val binding: ItemDiaryBinding,
private val onItemClick: (Diary) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(diary: Diary) {
binding.textTitle.text = diary.title
binding.textPreview.text = diary.content.take(50) // 미리보기 50자
binding.textDate.text = formatDate(diary.createdAt)
binding.root.setOnClickListener {
onItemClick(diary)
}
}
private fun formatDate(timestamp: Long): String {
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
return sdf.format(Date(timestamp))
}
}
/**
* DiffUtil: 리스트 변경 최적화
*/
class DiaryDiffCallback : DiffUtil.ItemCallback<Diary>() {
override fun areItemsTheSame(oldItem: Diary, newItem: Diary): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Diary, newItem: Diary): Boolean {
return oldItem == newItem
}
}
}
// 코틀린 문법 설명:
// - (Diary) -> Unit: 함수 타입 (파라미터로 함수 전달)
// - take(50): 문자열 앞 50자만 추출
📌 Step 7: 상세보기 화면
DiaryDetailFragment.kt
package com.elkaphoto.diary.ui.detail
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.elkaphoto.diary.databinding.FragmentDiaryDetailBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* 일기 상세보기 Fragment
*/
class DiaryDetailFragment : Fragment() {
private var _binding: FragmentDiaryDetailBinding? = null
private val binding get() = _binding!!
private val viewModel: DiaryDetailViewModel by activityViewModels()
private var isEditMode = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDiaryDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeViewModel()
setupButtons()
}
/**
* ViewModel 관찰
*/
private fun observeViewModel() {
viewModel.currentDiary.observe(viewLifecycleOwner) { diary ->
diary?.let {
displayDiary(it)
} ?: showEmptyState()
}
}
/**
* 일기 표시
*/
private fun displayDiary(diary: com.elkaphoto.diary.data.model.Diary) {
binding.editTitle.setText(diary.title)
binding.editContent.setText(diary.content)
setEditMode(false)
}
/**
* 빈 상태 표시
*/
private fun showEmptyState() {
binding.editTitle.setText("")
binding.editContent.setText("")
binding.textEmptyState.visibility = View.VISIBLE
}
/**
* 버튼 설정
*/
private fun setupButtons() {
// 수정 버튼
binding.buttonEdit.setOnClickListener {
if (isEditMode) {
saveChanges()
} else {
setEditMode(true)
}
}
// 삭제 버튼
binding.buttonDelete.setOnClickListener {
showDeleteConfirmation()
}
}
/**
* 수정 모드 전환
*/
private fun setEditMode(enabled: Boolean) {
isEditMode = enabled
binding.editTitle.isEnabled = enabled
binding.editContent.isEnabled = enabled
binding.buttonEdit.text = if (enabled) "저장" else "수정"
}
/**
* 변경사항 저장
*/
private fun saveChanges() {
val diary = viewModel.currentDiary.value ?: return
val newTitle = binding.editTitle.text.toString()
val newContent = binding.editContent.text.toString()
viewModel.updateDiary(diary, newTitle, newContent)
setEditMode(false)
}
/**
* 삭제 확인 다이얼로그
*/
private fun showDeleteConfirmation() {
MaterialAlertDialogBuilder(requireContext())
.setTitle("일기 삭제")
.setMessage("정말로 삭제하시겠습니까?")
.setPositiveButton("삭제") { _, _ ->
deleteDiary()
}
.setNegativeButton("취소", null)
.show()
}
/**
* 일기 삭제
*/
private fun deleteDiary() {
val diary = viewModel.currentDiary.value ?: return
viewModel.deleteDiary(diary)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
// 코틀린 문법 설명:
// - ?. (safe call): null이 아닐 때만 실행
// - ?: (엘비스): null이면 우측 실행
// - let { }: null이 아닐 때 블록 실행
5. 정리 및 학습 포인트
✅ MVVM 핵심 원칙
- View는 데이터를 관찰만 한다
viewModel.data.observe(this) { data -> // UI 업데이트 }- ViewModel은 View를 모른다
class ViewModel { // ❌ binding.textView.text = "..." // ✅ _liveData.value = "..." }- Repository가 데이터 출처를 숨긴다
// ViewModel은 데이터가 Room인지, API인지 몰라도 됨 repository.getData()
📚 추가 학습 자료
- 코루틴: 비동기 처리
- Flow: LiveData의 진화형
- Hilt/Dagger: 의존성 주입
- Navigation Component: Fragment 전환 관리
🎯 연습 과제
- 일기 검색 기능 추가
- 카테고리별 분류
- 이미지 첨부 기능
- 백업/복원 기능
MVVM 패턴에 익숙해지면 더 큰 프로젝트도 체계적으로 관리할 수 있습니다! 🚀
반응형
'개발 이야기 > Android (안드로이드)' 카테고리의 다른 글
| [Android] 확 바뀐 안드로이드 스튜디오 프로파일러로 메모리 릭 & CPU 점유율 잡는 법 (0) | 2026.01.22 |
|---|---|
| Gemini가 말아주는 MVVM 시작 가이드 (0) | 2025.12.08 |
| ChatGpt가 말아주는 MVVM 시작 가이드 (0) | 2025.12.08 |
| ViewModel, DataBinding 으로 ViewPager2/RecyclerView 제대로 쓰는 방법 정리 (1) | 2025.11.19 |
| [Android] MVVM 다음 스텝: Flow, StateFlow, Hilt 초보자 완벽 가이드 (1) | 2025.11.18 |
| [Android] 3년 만에 복귀한 개발자를 위한 MVVM, ViewModel, DataBinding 완벽 정복 가이드 (0) | 2025.11.18 |
| Jetpack Compose 초보자 가이드 (0) | 2024.12.23 |
| Android DataBinding과 ViewModel 적용하기 (6) | 2024.12.12 |