안드로이드 권장 아키텍처인 UI & Domain & Data Layer로 최대한 나눠봄
여기서 UseCase을 사용하거나 Clean Architecture 방식은 아님
단, AAC(Android Architecture Components)에 MVVM 기반의 Repository 패턴으로 구성을 함
기존에는 투박하게 아키텍처를 고려하지 않았지만 코드 관리나 화면이 추가됨에 따라서 유지보수에 어려움이 생길 것 같아서 AAC와 MVVM 패턴과 Repository를 응용함
이에 따른 AAC 요소를 적극 활용함, 여기서 화면 구성의 경우 주로 Fragment로 구성함
회원가입 & 로그인 하는 과정 그리고 해당 Flow의 과정을 onboard로 정립했는데 이 부분을 FragmentContainerView를 기반으로 Jetpack의 Navigation을 활용하여 그 UI Flow처리를 간편하게 처리함
위와 같이 구성함에 따라 Onboarding에 해당하는 Welcome Activity에서 위의 Fragment를 하나로 담고 있음
그리고 로그인 & 회원가입 & 소셜 로그인의 경우 서버에서 구현 이를 API를 통해서 처리를 함
그리고 성공적으로 로그인 시 Main 화면으로 진입함
이 때 역시 Fragment 적극 활용함 메인 화면 자체는 BottomNavigation으로 구성되어 있을 뿐 아니라 동일하게 시나리오별 UI Flow가 존재하기 때문에 아래와 같이 구성함
여기서 Fragment Navigation의 장점이 나왔음, 별도로 BottomNavigation에 대해서 Fragment 코드를 처리해줄 필요없이 해당하는 navController를 그대로 연결 Navigation에서 설정한 이름과 실제 만든 Fragment 이름이 같은 경우 BottomNavigation 버튼을 누르면 해당화면이 나오게 처리됨
그리고 Flow는 Navigation으로 간편하게 처리함으로써 UI는 상당히 깔끔하게 처리했음
Model-View-ViewModel을 쓸 때 LiveData & ViewModel & Data Binding을 활용함
이 때, ViewModel에서 주로 데이터를 처리하는데 네트워크를 통해서 받는 것 Fragment Navigation을 통해서 bundle로 넘겨준 값에 대한 처리 등, 그리고 이를 바탕으로 UI 변화나 이벤트 처리가 진행되기 때문에 LiveData를 통해서 받고 처리함
추가로 Data Binding을 활용함으로써 이런 UI처리에 대해서 예를 들면 ViewModel에서도 로그인을 위한 계정 정보를 바로 받고 버튼 클릭 리스너를 굳이 UI에서 받아서 처리하지 않게 하기 위해서 함수에 대해서 Data Binding을 통해 바로 이벤트 처리하도록 진행함
네트워크 처리의 경우도 Repository를 추가해서 ViewModel에서는 해당 Repository의 함수를 호출만 한다면 그 결과값을 바로 받아서 처리할 수 있음
기존의 방식 투박한 방식대로라면 이 네트워크 통신을 위한 인스턴스 생성과 API 결과 처리 등을 직접 Fragment & Activity 등에서 처리했겠지만 이를 구분함
MVVM 패턴 자체가 투박한 방식에서의 View에서 하는 일을 관심사를 분리한 것인데 최대한 View에서는 View의 변경만을 그리고 해당 데이터 처리와 저장 등은 ViewModel에서 처리하게끔 분리를 함
RecyclerView 역시, 기본적인 Adapter가 아닌 DiffUtil과 ListAdapter 방식을 활용함
이는 기존의 RecyclerView에서 데이터 상태 변경과 확인을 직접 처리하는 것을 Adapter 자체의 구현함으로써 그런 부분을 간소화 시켜줌
DiffUtil을 통해서 데이터 셋을 받아서 그 차이를 계산해서 비교하고 데이터 변한 부분만 바꿔줌으로써 직접 매번 갱신하는 것과 다름
그리고 ListAdapter를 통해서 데이터가 어떻게 바뀌든 submitList로 전체 리스트를 넘겨주면 어댑터가 알아서 백그라운드 스레드를 사용해 리스트 차이를 계산하여 화면을 갱신하게 해 줌
여기서 더 나아가서 아래와 같이 이를 Data Binding과 결합해서 쓰기 위해서 submitList의 경우 아래와 같이 Binding Adapter로 구현하여 XML 상에서 바로 연결하게끔 함
@BindingAdapter("newslistData")
fun bindNewsRecyclerView(recyclerView: RecyclerView, newsList: List<ResponseNewsList>?) {
val adapter = recyclerView.adapter as NewsAdapter
adapter.submitList(newsList)
}
private fun getNewsList() {
viewModelScope.launch {
_newsList.value = newsRepository.getNewsList()
}
}
<data>
<variable
name="newsViewModel"
type="com.example.fitin_kotlin.ui.news.NewsViewModel" />
</data>
...
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_news_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:background="#ffffff"
app:layout_constraintStart_toStartOf="@id/gl_start"
app:layout_constraintEnd_toEndOf="@id/gl_end"
app:layout_constraintTop_toBottomOf="@id/sv_search_news"
app:newslistData="@{newsViewModel.newsList}"
tools:listitem="@layout/list_item_news"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
class NewsListViewHolder(private var binding: ListItemNewsBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(responseNewsList: ResponseNewsList) {
binding.newsProperty = responseNewsList
binding.executePendingBindings()
}
}
<data>
<variable
name="newsProperty"
type="com.example.fitin_kotlin.data.model.network.response.ResponseNewsList" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:id="@+id/tv_recruitment_company_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:layout_marginTop="5dp"
android:textSize="12sp"
android:textStyle="bold"
android:text="@{newsProperty.press}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<TextView
android:id="@+id/tv_recruitment_position"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="12sp"
android:text="@{newsProperty.title}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_recruitment_company_name"
android:layout_marginEnd="10dp"
android:maxEms="30"
android:maxLines="1"
android:ellipsize="end"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
의존성 주입을 한 것도 Retrofit의 경우 일반적으로 싱글톤 클래스로 만들어서 네트워크 통신을 위한 객체를 만드는 것은 문제가 없지만 MVVM-Repository를 쓰면서 ViewModel에서 Repository를 써야하는 상황과 그리고 로그인 & 회원가입이나 토큰 값을 저장하고 쓰기 위해서 SharedPreferences를 써야하는 등 매번 객체를 만들어야 하는 상황이 생김
이 때, 매번 객체 생성 코드를 쓰고 해당 화면이 만들어질 때마다 쓰는 것이 상당히 리소스를 차지할 수 있다고 생각이 들었음
그래서 이를 DI 패턴을 활용해서 module을 만들어서 생성자에서 바로 inject를 해서 쓸 수 있게 처리를 한 것임
이렇게 함으로써 해당 객체를 쓸 때 매번 init을 하거나 만들지 않고 모듈에 등록을 하고 inject만 해준다면 생성자 차원에서 만들어지고 해당 객체의 기능을 바로바로 쓸 수 있었음
Retrofit의 경우 서버단이 있기 때문에 서버에서 넘겨주는 API에 맞게 Retrofit을 구성함
이 때 비동기 Coroutine을 베이스로 활용하고 필요한 Dto 모델을 만들어서 처리함(서버의 요구에 맞게)
그리고 SharedPreferences 역시 활용하였지만 이는 필요할 때마다 객체 생성이 문제였는데 DI 패턴 도입으로 해결되어 필요할때마다 처리함
추가로 토큰 처리의 경우 Interceptor와 Authenticator를 활용해서 재발급 처리를 진행함
투박하게 구현하는 것부터 시작했는데 확실히 아키텍처 도입, AAC 도입, DI 패턴 도입등을 통해서 코드 관리가 확실하게 개선되고 선형적으로 처리할 수 있게 되면서 기능 개발에도 자연스럽게 유지보수성이 높아짐
곳곳에서 보일러 플레이트 코드가 보임 Fragment & Activity & ViewModel & Adapter에서 이 경우에 최소한 Data Binding & Retrofit & Hilt로 개선했지만 BaseActivity 등으로 묶어서 처리할 수 있을 것 같음
ViewModel과 Repository 이 부분의 경우 UseCase를 도입해서 Clean Architecture로 좀 더 네트워크 처리를 개선할 수 있어보임
북마크 부분에 대해서 화면 구성만이 남음 & 다음 프로젝트에선 Clean Architecture를 도입해서 좀 더 간결하게 처리하는 것도 좋아보임
기능적으로 그리고 백그라운드 처리 시나리오대로 잘 처리 되는지 UI Flow 체크와 서버 통신 실패시 처리등 이런 부분은 QA가 더 필요해보임