[TEKHIT] 오늘의 멘토링

2024. 2. 13. 15:13·TEKHIT ANDROID SCHOOL

멋쟁이 사자처럼에서 진행하는 안드로이드 앱 스쿨에서 멘토링을 받고 배운 지식들을 정리하는 글입니다. 

👩‍💻 오늘의 할 말

요즘 프로젝트를 하느라 바빠서 오랜만에 글을 쓰네요.  빨리 프로젝트를 마무리하고 싶습니다. 다음 계획으론 MVVM과 GoF의 디자인 패턴을 공부할 건데 재미있어 보여서 얼렁 빨리 하고 싶어요😭 공부할 책이랑 강의도 미리 정해놔서 빨리 하고싶습니다...!🔥

📕 질문

내가 등록한 매장 정보를 보여주는 프래그먼트에서 정보를 수정 후, 사진첩을 열고 돌아오면 기존의 수정 데이터가 저장되지 않고 수정 이전의 데이터를 보여주며 사진첩에서 이미지를 선택하고 변경해도 이미지가 변경되지 않습니다.

 

📌Layout.xml

Layout은 매장 정보는 Local Database인 Room에서 가져온 정보를 데이터 바인딩을 사용하여 렌더링 하며 이미지는 바인딩 어댑터를 사용해서 보여주도록 구현하였습니다.

<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:bind="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="vm"
            type="com.myproject.cloudbridge.viewModel.StoreManagementViewModel" />
    </data>
				(생략...)

                    <com.google.android.material.textfield.TextInputLayout
                        android:id="@+id/storeName_Layout"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="15dp"
                        app:boxStrokeColor="@color/blue_grey"
                        app:errorEnabled="true"
                        app:helperTextTextColor="@color/orange_red"
                        app:startIconDrawable="@drawable/baseline_store_24"
                        app:startIconTint="#000">

                        <com.google.android.material.textfield.TextInputEditText
                            android:id="@+id/storeName_edit"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:text="@{vm.myStore.storeName}"
                            android:fontFamily="@font/rubik_bold"
                        />

                    </com.google.android.material.textfield.TextInputLayout>
                    
                    (생략...)

📌ViewModel - Model

  • 데이터 바인딩을 위해 사용된 ViewModel Class와 BindingAdapter, Model Class입니다.
  • DataStore에 저장된 나의 사업자 등록번호를 Primary Key로 Room에서 데이터를 가져왔습니다.
  • Room에서 꺼내온 데이터는 Flow Type입니다. 그래서 StateFlow Type으로 변환하기 위해 stateIn 함수를 사용하였습니다.
class StoreManagementViewModel: ViewModel() {
    private lateinit var myStoreInfoRequestModel: MyStoreInfoRequestModel

    private val networkRepository = NetworkRepository()
    private val dbRepository = DBRepository()

    // 나의 매장 정보
    private var _myStore: MutableStateFlow<StoreEntity> = MutableStateFlow(createFirstData())
    val myStore: StateFlow<StoreEntity> get() = _myStore

        fun getMyStoreInfo() = viewModelScope.launch(Dispatchers.IO) {
        try {
            // 1. Datastore에 저장된 사업자 등록번호를 가지고
            MainDataStore.getCrn().collect{ crn->
                // 2. Room에서 나의 매장 정보를 가져와
                val response = dbRepository.getMyStoreInfo(crn)

                // 3. flow로 반환된 데이터를 StateFlow로 변환해 저장
                response.stateIn(viewModelScope).collect{ myStore->
                    _myStore.value = myStore
                }
            }
        }catch (e: Exception){
            Log.d("MyPageViewModel","MyPageViewModel: $e")
        }
    }
}

###################################################################################
object MyStoreBindingAdapter {  
    // 1) 이미지뷰에 새로운 xml 속성 만들기
    // [속성명 : myStoreImage ]
    @JvmStatic
    @BindingAdapter("myStoreImage") //어노테이션 해독기 필요 - 빌드 그래이들에 기능 추가 필요!(kapt)
    fun imageBindingAdapter(view: ImageView, image: Bitmap?){
        if (image != null) {
            Glide.with(view.context).load(image).into(view)
        }
    }
}
###################################################################################
@Entity(tableName = "store_table")
data class StoreEntity (
    @PrimaryKey
    @ColumnInfo(name = "company_registration_number")
    val crn: String,
    @ColumnInfo(name = "store_image")
    val image: Bitmap,
    @ColumnInfo(name = "store_name")
    val storeName: String,
    @ColumnInfo(name = "ceo_name")
    val ceoName: String,
    val contact: String,
    val address: String,
    val latitude: String,
    val longitude: String,
    val kind: String
)

기본적인 나의 매장 정보를 보여주는 로직은 위와 같이 구현해 주었습니다. 이제 저는 View의 수정된 데이터를 받기 위해 새로운 데이터 모델을 만들었습니다.

🤔 왜 새로운 모델을 만들었나요?

viewModel에서는 사용자에게 보여줄 나의 매장 정보를 backing Property로 선언했습니다.

backing Property를 사용한 한 이유는 MVVM에서 다양한 아키텍처적 관점에 대한 장점이 있기 때문입니다.

 

우선 _myStore는 외부에서 관찰할 수 없지만 변경 가능한 타입입니다. myStore는 불변 타입이지만 외부에서 관찰할 수 있으며 _myStore를 통해 데이터를 가져옵니다. 이렇게 하면 ViewModel에서는 데이터 관련한 처리를 하고, UI에서는 데이터를 보여 주기만 할 수 있어 UI와 비즈니스 로직을 분리하고 캡슐화를 제공할 수 있습니다.

 

또한 이를 통해 View와 ViewModel의 역할을 완전히 분리함으로써 단일 책임 원칙을 준수할 수 있습니다.

이외에도 클린 아키텍처적인 장점이 더 많을 것 같은데 아직 제가 클린 아키텍처에 대한 지식이 부족해서 좀 더 공부해야겠습니다😭

 

그래서 저는 UI에서 사용자의 입력값으로 기존 ViewModel에 있던 데이터를 변경하는 것이 불가능하다고 생각해서 새로운 데이터 모델을 만들었지만 이후 이게 문제의 원인이 되었습니다.

data class ModifyStoreStateSaveModel (
    var storeName: String, // 매장명
    var ceoName: String,   // 점주명
    var contact: String,   // 전화번호
    var address: String,   // 주소
    var kind: String       // 업종
)

class StoreManagementViewModel: ViewModel() {
    private lateinit var myStoreInfoRequestModel: MyStoreInfoRequestModel

    private val networkRepository = NetworkRepository()
    private val dbRepository = DBRepository()

    // 나의 매장 수정 정보
    private var _myModifyStoreInfo: MutableStateFlow<ModifyStoreStateSaveModel> = MutableStateFlow(
        ModifyStoreStateSaveModel("", "", "", "", "",))
    val myModifyStoreInfo: StateFlow<ModifyStoreStateSaveModel> get() = _myModifyStoreInfo
		
     fun updateSavedData(modifyData: ModifyStoreStateSaveModel) {
        _myModifyStoreInfo.value = modifyData
    }
}

액티비티에선 이미지 등록 버튼을 눌러 갤러리로 화면이 전환되면 입력되어 있던 매장 정보들을 viewModel에 업데이트합니다. 이후 다시 프래프먼트로 돌아와 LifeCycle이 onstart( ) 상태 일 때, 저장돼있던 데이터들을 화면에 보여주려 했습니다.

 

이렇게 생각한 이유는 Fragment → 갤러리의 화면 전환에서 프래그먼트는 백그라운드 상태가 되며 Fragment LifeCycle은 onStop( ) 상태가 됩니다.  그리고 viewModel은 연결된 Activity나 Fragment가 완전히 제거 (onDestroy) 된 후에 클리어(onCleared) 된다는 점을 이용해서 ViewModel에 데이터를 저장하려 했습니다.

class UpdateFragment2 : Fragment(), View.OnClickListener{

	(생략...)

	override fun onStart() {
        super.onStart()
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.myModifyStoreInfo.collectLatest{

                if (it.storeName.isNotEmpty()){
                    setSavedStateInstance(it)
                }
            }
        }
    }

override fun onClick(v: View?) {
        when(v?.id){

            (생략 ...)

            R.id.img_load_button -> {
                if (isAllPermissionsGranted()){
                    getSavedStateInstance()
                    accessGallery()
                }
                else{
                    launcherForPermission.launch(REQUEST_STORAGE_PERMISSIONS)
                }
            }
        }
    }

	private fun getSavedStateInstance(){
	    with(binding){
	        val modifyData = ModifyStoreStateSaveModel(
	            storeNameEdit.text.toString(),
	            ceoNameEdit.text.toString(),
	            phoneEdit.text.toString(),
	            addrEdit.text.toString(),
	            kindEdit.text.toString()
	        )
	        viewModel.updateSavedData(modifyData)
	    }
	}
}

private fun setSavedStateInstance(state: ModifyStoreStateSaveModel) {
        with(binding){
            storeNameEdit.setText(state.storeName)
            ceoNameEdit.setText(state.ceoName)
            phoneEdit.setText(state.contact)
            addrEdit.setText(state.address)
        }
    }

문제는 여전히 화면 전환 후 데이터가 소실되는 현상이 발생하였습니다. 제가 생각했을 때, 데이터 바인딩에 바인딩된 ViewModel 인스턴스 때문에 새로운 모델에 저장했던 데이터를 보여줄 수 없다고 생각을 했지만 해결 방법을 찾지 못해 멘토링을 신청하게  되었습니다.

📖 이에 대해 멘토링에선 다음과 같은 답변을 얻을 수 있었습니다.

override fun onStart() {
    super.onStart()
    viewLifecycleOwner.lifecycleScope.launch {
        viewModel.myModifyStoreInfo.collectLatest{

            if (it.storeName.isNotEmpty()){
                setSavedStateInstance(it)
            }
        }
    }
}

1️⃣ onStart에서 코루틴을 실행할 필요가 없었습니다. 

 프래그먼트가 화면 전환 후 다시 포그라운드로 전환되면 LifeCycle이 onStart( )부터 동작하기 때문에 이 타이밍에 맞춰서 저장했던 데이터를 렌더링 하려 했습니다. 하지만 제가 위에서 언급했던 것처럼 viewModel은 Fragment가 onDestroy( ) 되기 전까지 살아있기 때문에 굳이 onStart( ) 에서 수행할 필요가 없었습니다. 

 

2️⃣ 데이터 바인딩에 사용된 Model Class를 변경하면 안 됩니다.

  • 이 부분도 제가 생각했던 문젠데 왜 하나만 알고 둘은 몰랐을까요 😭😭😭😭😭😭😭😭

3️⃣ 자자 오늘의 하이라이트 사용자의 입력값으로 ViewModel에서 데이터를 변경하는 방법을 보겠습니다.

class StoreManagementViewModel: ViewModel() {
    // 나의 매장 정보
    private var _myStore: MutableStateFlow<StoreEntity> = MutableStateFlow(createFirstData())
    val myStore: StateFlow<StoreEntity> get() = _myStore
    
	// 사용자 수정 데이터 업데이트
    fun updateSavedData(storeName: String, ceoName: String, contact: String, address: String, kind: String) {
        // 바뀌지 않은 값은 유지
        _myStore.value = _myStore.value.copy(
            storeName = storeName,
            contact = contact,
            ceoName = ceoName,
            address = address,
            kind = kind
        )
    }
}

copy( ) 함수는 얕은 복사(shallow copy)를 수행하며 이 함수는 새로운 객체를 만들어 기존 객체의 필드를 변경하거나 유지하는 데 사용됩니다. 이를 통해 기존의 저장되어 있던 데이터는 유지하면서 사용자가 수정한 값은 변경이 가능해집니다.

 

깨알 팁으로 Named Argument라는 문법도 배울 수 있었습니다. Named Argument는 데이터 클래스 생성 시 필요한 파라미터들의 이름을 지정해 주어 파라미터를 입력 순서와 상관없이 전달할 수 있고 가독성 또한 향상할 수 있습니다. 

 

제가 모르는 코틀린의 유용한 기능들이 정말 많은 것 같습니다. 왜 코틀린이 좋은 언어인지 새삼 깨닫게 되네용... 아직 내용이 한참 남았지만 글이 길어져는 것 같아 다음 글에서 작성하겠슴당 👼

'TEKHIT ANDROID SCHOOL' 카테고리의 다른 글

[TEKHIT] 오늘의 멘토링  (1) 2024.02.16
[TEKHIT] 오늘의 멘토링  (1) 2024.02.14
[TEKHIT] 오늘의 멘토링  (1) 2024.02.07
'TEKHIT ANDROID SCHOOL' 카테고리의 다른 글
  • [TEKHIT] 오늘의 멘토링
  • [TEKHIT] 오늘의 멘토링
  • [TEKHIT] 오늘의 멘토링
빨주노초잠만보
빨주노초잠만보
  • 빨주노초잠만보
    과거의 나를 통해 미래의 나를 성장시키자
    빨주노초잠만보
  • 전체
    오늘
    어제
    • 분류 전체보기 (94)
      • TEKHIT ANDROID SCHOOL (4)
      • Android Architecture (7)
      • Android (40)
      • PROJECT (5)
      • KOTLIN (10)
        • 코루틴의 정석 (3)
      • BACK END (12)
      • CS (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    sealed class
    의존성 주입
    Clean Architecture
    DataSource
    sealed class vs enum class
    android clean architecture
    value class
    saeled class enum class 차이
    flow
    DI
    코틀린 코루틴의 정석
    kotlin interface vs abstract
    kotlin abstract class
    MVI
    Throttle
    STATEFLOW
    repository
    kotlin interface
    kotlin interface abstract 차이
    Repository Pattern
    callbackflow
    orbit
    android Room
    Livedata
    kotlin 타입 소거
    interface vs abstract
    kotlin array
    ThrottleFirst
    리사이클러뷰 풀
    Room
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
빨주노초잠만보
[TEKHIT] 오늘의 멘토링
상단으로

티스토리툴바