🚨 ISSUE
프로젝트를 진행하면서 서버에 저장된 매장 정보를 사용자에게 보여주는 과정에서 UI를 바로 렌더링 하지 못하는 issue가 발생했습니 다. SplashScreen이나 Loading 화면을 만들어 해결할 수 도 있겠지만 그러면 저 이거 못해요~~라고 인정하는 거 같아서 🐌 이를 개선하기 위해 기존 코드의 문제점을 분석해 보았습니다.
📌Activity
class MyStoreActivity : AppCompatActivity(){
private lateinit var binding: ActivityMyStoreBinding
private val viewModel: MyPageViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMyStoreBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.myStore.observe(this){ data ->
val bitmap = StringToBitmaps(data.image)
binding.mainImage.setImageBitmap(bitmap)
binding.storeNameTextView.text = data.storeName
binding.storeCprTextView.text = data.crn
binding.storePhoneTextView.text = data.contact
binding.storeRepreNameTextView.text = data.ceoName
binding.storeKindTextView.text = data.kind
binding.storeAddrTextView.text = data.address
}
}
}
📌ViewModel
class MyPageViewModel: ViewModel() {
private val networkRepository = NetworkRepository()
private val _myStore = MutableLiveData<MyStoreInfoResponseModel>()
val myStore: LiveData<MyStoreInfoResponseModel>
get() = _myStore
fun getMyStoreInfo() = viewModelScope.launch(Dispatchers.IO) {
try {
MyDataStore().getCrn().collect{ crn->
val response = networkRepository.getMyStoreInfo(crn)
_myStore.postValue(response)
}
}catch (e: Exception){
Log.d("MyPageViewModel","MyPageViewModel: $e")
}
}
}
🔎 원인 분석
구글링이나 다른 자료를 찾아보기에 앞서 제 스스로 원인에 대해 생각해보고 개선해보기로 하였습니다.
Retrofit을 사용해 아래와 같은 흐름으로 서버에서 데이터를 가져와 LiveData로 저장하고 Activity에서 관찰하며 UI를 그려 주었고 원인을 3가지로 추측했습니다.
1️⃣ ViewBinding을 사용해 Activity를 걸쳐서 XML까지 도달하는 과정
- XML 레이아웃에서 직접 ViewModel과을 걸쳐 데이터를 바인딩하는 게 더 나을 거라고 생각했습니다.
2️⃣ 생명주기를 따르는 LiveData
- 안드로이드 컴포넌트의 생명주기를 따르는 LiveData 대신 Coroutine의 비동기 데이터 스트림인 StateFlow가 비동기적으로 동작하기 때문에 더 좋다고 생각했습니다. 또한 Android Clean Architecture에서는 LiveData를 StateFlow로 대체할 것을 권장하기 때문에 개선해야 합니다. 자세한 내용은 이 글을 참조해 주세요.
3️⃣ setImageBitmap() 메소드의 사용
- 가장 큰 원인이라고 생각한 부분입니다. 이 메소드는 메인 스레드에서 직접 이미지를 메모리에 로딩하므로 가장 큰 원인이라고 생각했고 내부적으로 비동기 처리되는 Glide를 사용해 개선할 것입니다.
🔑ViewBinding -> DataBinding
- databinding 을 사용하는 XML Layout은 <layout> 루트 태그로 시작하여야 합니다.
- <data> 엘리먼트는 데이터 바인딩을 위한 섹션입니다.
- <variable> 엘리먼트를 사용하여 뷰모델(VM) 변수를 정의하고 있습니다.
- name 속성은 변수의 이름을 나타내고, type 속성은 ViewModel 클래스를 나타냅니다.
- ViewModel에서 바인딩할 Data를 " @{ } " 를 사용해 지정해 주었습니다.
- binding : Activity에서 Data Binding 연결을 설정합니다.
- binding.vm : 바인딩된 뷰모델을 설정하여 레이아웃에서 뷰모델의 데이터를 사용할 수 있도록 합니다.
class MyStoreActivity : AppCompatActivity() {
private lateinit var binding: ActivityMyStoreBinding
private val viewModel: MyPageViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_my_store)
binding.vm = viewModel
}
}
🔑 LiveData -> StateFlow
- 반드시 초기값을 줘야 하는 StateFlow의 특성 때문에 빈 매장 정보 객체를 생성하는 createDefaultMyStoreInfo( ) 함수를 생성하고 MutableStateFlow를 생성했습니다.
class MyPageViewModel: ViewModel() {
private val networkRepository = NetworkRepository()
private var _myStore = MutableStateFlow(createDefaultMyStoreInfo())
val myStore: StateFlow<MyStoreInfoResponseModel> get() = _myStore
fun getMyStoreInfo() = viewModelScope.launch(Dispatchers.IO) {
try {
MyDataStore().getCrn().collect{ crn->
val response = networkRepository.getMyStoreInfo(crn)
_myStore.value = response
}
}catch (e: Exception){
Log.d("MyPageViewModel","MyPageViewModel: $e")
}
}
private fun createDefaultMyStoreInfo() =
MyStoreInfoResponseModel("","","","", "", "", "", "", "")
}
🔑 setImageBitmap -> Glide
Data Binding과 Glide를 함께 사용하기 위해선 별도의 Binding Adapter가 필요합니다. Binding Adapter는 Data Binding을 사용할 때 특정한 이벤트를 수행하거나 View의 속성을 지정하는 데 사용되며 @BindingAdapter( ) annotation을 사용합니다.
- BindingAdapter( ) 안에 들어가는 매개변수는 XML Layout에서 메소드를 호출하는 데 사용됩니다.
- BindingAdapter 구현 메서드의 첫 번째 매개 변수는 메소드를 적용할 View를 전달합니다.
- 두 번째 매개변수는 View에 전달할 데이터를 전달합니다.
@BindingAdapter("myStoreImage")
fun ImageBindingAdapter(view: ImageView, imageUrl: String?){
if (imageUrl != null) {
val decodedBytes = Base64.decode(imageUrl, Base64.DEFAULT)
val bitmap = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
Glide.with(view.context)
.asBitmap()
.load(bitmap)
.into(view)
}
}
- 저는 서버에서 Image를 Base64로 인코딩하여 전송하였습니다.
- Base64 형식을 Glide에서 사용하기 위해서 Bitmap으로 Decoding 한 객체를 asBitmap()을 통해 Glide에 전달해 주었습니다.
- Binding Adapter Annotation에 지정한 이름을 View에서 지정하고 @{ 전달할 데이터 } 를 지정해 줍니다.
📕 결과
제가 원하던 대로 Activity가 전환 됐을 때 화면을 바로 렌더링 하는 데 성공했습니다. 사실 다 하고 찾아보니까 viewBinding이 DataBinding보다 빠르다고 합니다. 하지만 DataBinding을 사용하니까 Activity Class File에 작성해야 하는 코드를 줄일 수 있고 코드가 좀 더 모듈화 되어 유지 보수가 편리했습니다. DataBinding은 ViewBinding 보다 사용하기 어려운 느낌이라 사용을 안 했는데 이번 기회에 기존 ViewBinding을 모두 DataBinding으로 변경해 봐야겠겠습니다.
'PROJECT' 카테고리의 다른 글
안드로이드 클린 아키텍처 도메인 레이어 설계 (0) | 2024.07.13 |
---|---|
[PROJECT] MulterError: Unexpected field (1) | 2024.02.06 |
[PROJECT] Fused Location Provider (0) | 2024.01.28 |
[PROJECT] 프로젝트에 DataBinding & @BindingAdapter 사용해보기 (2) | 2024.01.24 |