안드로이드 연구소

[안드로이드 클린아키텍처] 안드로이드 아키텍처 가이드라인 만드는 법3 (도메인 레이어, 프레젠테이션 레이어) 본문

안드로이드 연구소/클린아키텍처

[안드로이드 클린아키텍처] 안드로이드 아키텍처 가이드라인 만드는 법3 (도메인 레이어, 프레젠테이션 레이어)

안드로이드 연구원 2023. 8. 16. 23:19

안녕하세요 안드로이드 연구원입니다.

지난 포스터에서 안드로이드 앱 아키텍처 가이드라인의 도메인 부분을 세팅하는 작업을 했었는데요.

Retrofit과 Room으로 불러온 데이터 소스를 Repository에서 상호작용할 수 있도록 작업했습니다. 

이제 마저 뒷 부분을 작업 해보도록 하겠습니다.

 

6. Domain 레이어 만들기(UseCase)

Domain 레이어의 핵심은 UseCase를 만드는 것인데요.

복잡한 Repository를 UI 단에서 바로 호출하는 것이 아니라

UseCase에서 캡슐화 과정을 걸친 다음 ViewModel에 제공됩니다.

 

도메인 디렉토리는 Model과 Usecase로 간단하게 구성이 되어있습니다.

 

Model은 만들어서 사용하는 이유는

도메인 레이어인 Usecase에서 데이터 레이어의 Dto나 Entity를 사용하게되면

서로에 대한 경계가 깨지기 때문에 Dto와 똑같지만 도메인 레이어의 data class 개념으로 생성해줍니다.

data class GalleryModel (
    var galContentId: String,
    var galContentTypeId: String,
    var galTitle: String,
    var galWebImageUrl: String,
    var galCreatedtime: String,
    var galModifiedtime: String,
    var galPhotographyMonth: String,
    var galPhotographyLocation: String,
    var galPhotographer: String,
    var galSearchKeyword: String
)

 

그리고 Usecase에서 Repository의 인스턴스를 의존성 주입하여 받을 수 있게 하기위해서 Repository모듈을 생성합니다.

이번에는 싱글톤이 아니라 ViewModelComponent::class로 세팅합니다.

(ViewModel에서 Usecase를 사용할꺼라서 그런건지.. 아시는 분들은 댓글 부탁드립니다.^^)

@Module
@InstallIn(ViewModelComponent::class)
object RepositoryModule {

    @ViewModelScoped // ViewModelComponent와 세트
    @Provides
    fun providesGalleryRepository(galleryServiceApi: GalleryServiceApi, galleryDao: GalleryDao) : GalleryRepository {
        return GalleryRepository(galleryServiceApi, galleryDao)
    }

}

 

그리고 Repositroy를 의존성 주입을 받아서

레파지토리의 메서드를 사용할 수 있도록 작업해줍니다.

class GalleryUseCase @Inject constructor(private var galleryRepository: GalleryRepository) {
    suspend fun getList() = galleryRepository.getGalleryList()

    suspend fun getSavedList() = galleryRepository.getGallerySavedList()

    suspend fun save(item: GalleryModel) = galleryRepository.insert(item)
}

 

7. Presentation레이어(ViewModel, Activity)

이제 Usecase를 통해서 받은 데이터를 UI에 잘 출력하고

UI 이벤트에 따라 상호작용할 수 있도록 작업을 하는 Presentation 레이어만이 남았습니다.

 

UI와 ViewModel로 나누었고

ViewModel을 제외한 Activity, Fragment, ViewHolder, Adapter 등에 관한 클래스는

UI 디렉토리에 위치하게 됩니다.

 

7-1. ViewModel

저희는 우선 갤러리 탭과 저장 탭, 두개의 프레그먼트를 가지고 있기 때문에 viewmodel또한 두개가 필요한데요.

먼저 갤러리 탭에서 사용할 GalleryViewModel을 보겠습니다.

 

@HiltViewModel 어노테이션을 통해서 손쉽게 의존성 주입 컴포넌트를 세팅한 후

@Inject constructor(private var galleryUseCase: GalleryUseCase)로 의존성 주입 인스턴스를 호출하였습니다.

@HiltViewModel
class GalleryViewModel @Inject constructor(private var galleryUseCase: GalleryUseCase): ViewModel()  {

}

 

 

 

그 후 galleryUseCase의 getList()saveData(item)을 호출할 수 있게 합니다.

이때 UI작업을 하게되는 영역이므로 Dispatchers.IO로 작업을 해야합나디.

또 .getList처럼 반환값을 받을 경우에는 이 처럼 UI state에 저장하여 관리합니다.

@HiltViewModel
class GalleryViewModel @Inject constructor(private var galleryUseCase: GalleryUseCase): ViewModel()  {

    // ui state 만들기(ui 속성을 나타내는 데이터)
    private val _galleryListState = MutableLiveData<List<GalleryModel>>()
    val galleryListState: LiveData<List<GalleryModel>> = _galleryListState

    init {
        getList()
    }

    // .getList() 호출(서버)
    fun getList(){
        viewModelScope.launch(Dispatchers.IO) {
            try {
                _galleryListState.postValue(galleryUseCase.getList())
            } catch (e: Exception){
                Log.d("getGalleryList fail", "에러메시지: ${e.message.toString()}")
            }
        }
    }

    // .save(item) 호출(데이터베이스)
    fun saveData(item: GalleryModel){
        viewModelScope.launch(Dispatchers.IO){
            galleryUseCase.save(item)
        }
    }
}

 

마찬가지로 SaveViewModel도 데이터베이스에 저장한 리스트를 읽어올 수 있도록

.getSavedList()를 호출할 수 있도록 연결합니다.

@HiltViewModel
class SaveViewModel @Inject constructor(private var galleryUseCase: GalleryUseCase): ViewModel() {

    // ui state 만들기(ui 속성을 나타내는 데이터)
    private val _savedListState = MutableLiveData<Flow<List<GalleryModel>>>()
    val savedListState: LiveData<Flow<List<GalleryModel>>> = _savedListState

    init {
        getSavedList()
    }

    // .getSavedList() 호출
    fun getSavedList(){
        viewModelScope.launch(Dispatchers.IO){
            try {
                _savedListState.postValue(galleryUseCase.getSavedList())
                Log.d("getSavedList success", "${galleryUseCase.getSavedList()}")
            } catch (e: Exception){
                Log.d("getSavedList fail", "에러메시지: ${e.message.toString()}")
            }
        }
    }
}

 

 

7-2. UI

이제 viewModel에서 만들어놓은 state를 리스트로 잘 출력해주기만 하면 됩니다.

 

xml에서 dataBinding을 연결시켜준 뒤 

데이터가 없을 때 보일 Textview,사진 리스트가 담길 Recyclerview,

그리고 데이터가 로딩중인걸을 표시할 Progressbar를 만들어줍니다.

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <variable
            name="view"
            type="com.example.photogallery.presentation.ui.GalleryFragment" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/emptyTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="데이터가 없습니다."
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <com.google.android.material.progressindicator.CircularProgressIndicator
            android:id="@+id/progressbar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:indeterminate="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

그리고 viewModel에서 만들어둔 galleryListState 데이터를

관찰하여 데이터의 변화를 감지합니다.

데이터가 있을 시 프로그래스바가 보이지 않고 Recyclerview가 출력되게끔합니다.

private fun observeViewModel() {
    lifecycleScope.launch {
        viewModel.galleryListState.observe(viewLifecycleOwner){
            binding.apply {
                progressbar.isVisible = false
                emptyTextView.isVisible = it.isEmpty()
                recyclerview.isVisible = it.isNotEmpty()
            }
            adapter.submitList(it)
        }
    }
}

 

리스트 중 아이템 클릭시에 viewModel의 saveData(item) 작동하는 Handler 클래스도 생성합니다.

inner class Handler {
    fun onClickItem(item: GalleryModel){
        viewModel.saveData(item)
        Log.d("클릭", "${item.galContentId}/${item.galTitle}")
    }
}

 

GalleryFragment의 전체 코드는 아래와 같습니다.

class GalleryFragment : Fragment() {
    private lateinit var binding : FragmentGalleryBinding
    private lateinit var viewModel : GalleryViewModel

    private val adapter by lazy { GalleryListAdapter(Handler()) }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_gallery, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel = ViewModelProvider(requireActivity()).get(GalleryViewModel::class.java)
        binding.recyclerview.adapter = adapter
        binding.recyclerview.addItemDecoration(DividerItemDecoration(activity, LinearLayout.VERTICAL))

        observeViewModel()
    }


    private fun observeViewModel() {
        lifecycleScope.launch {
            viewModel.galleryListState.observe(viewLifecycleOwner){
                binding.apply {
                    progressbar.isVisible = false
                    emptyTextView.isVisible = it.isEmpty()
                    recyclerview.isVisible = it.isNotEmpty()
                }
                adapter.submitList(it)
            }
        }
    }

    inner class Handler {
        fun onClickItem(item: GalleryModel){
            viewModel.saveData(item)
            Log.d("클릭", "${item.galContentId}/${item.galTitle}")
        }
    }
}

 

7-3. List 출력

그렇다면 이제 viewModel의 galleryListState 데이터를 리스트로 

UI 출력할 수 있게만 하면 마무리됩니다.

데이터바인딩 사용한 리싸이클러뷰를 만들어보도록 하겠습니다.

 

item_gallery.xml

그리고 도메인 레이어의 model과 GalleryFragment의 Handler를 데이터 바인딩으로 연결합니다.

그리고 이미지 뷰 1개와 사진명, 촬영날짜, 장소 데이터가 나오는 텍스트뷰 3개를 만들어줍니다.

그리고 각자의 정보에 맞는 

<?xml version="1.0" encoding="utf-8"?>

<layout 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">

    <data>
        <variable
            name="item"
            type="com.example.photogallery.domain.model.GalleryModel" />

        <variable
            name="handler"
            type="com.example.photogallery.presentation.ui.GalleryFragment.Handler" />
    </data>

</layout>

 

그리고 이미지뷰 1개와 텍스트뷰 3개를 만들어준 뒤에

GalleryModel의 galTitle, galPhotographyMonth, galPhotographyLocation를

텍스트뷰를 android:text="@{ }"로 데이터를 연결해줍니다.

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/photoImageView"
        android:layout_width="match_parent"
        android:layout_height="240dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:adjustViewBounds="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:src="@color/gray"
        android:contentDescription="TODO" />

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@{item.galTitle}"
        tools:text="남산타워"
        android:textSize="12sp"
        android:textStyle="bold"
        android:textColor="@color/black"
        app:layout_constraintTop_toBottomOf="@id/photoImageView"
        app:layout_constraintStart_toStartOf="@id/photoImageView"
        app:layout_constraintEnd_toEndOf="@id/photoImageView"
        android:layout_marginTop="4dp"/>

    <TextView
        android:id="@+id/dateTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@{item.galPhotographyMonth}"
        tools:text="202207"
        android:textSize="8sp"
        android:textStyle="bold"
        android:textColor="@color/black"
        app:layout_constraintTop_toBottomOf="@id/titleTextView"
        app:layout_constraintStart_toStartOf="@id/photoImageView"
        app:layout_constraintEnd_toEndOf="@id/photoImageView"
        android:layout_marginTop="4dp"/>

    <TextView
        android:id="@+id/locationTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@{item.galPhotographyLocation}"
        tools:text="서울특별시 용산구"
        android:textSize="8sp"
        android:textStyle="bold"
        android:textColor="@color/black"
        app:layout_constraintTop_toBottomOf="@id/dateTextView"
        app:layout_constraintStart_toStartOf="@id/photoImageView"
        app:layout_constraintEnd_toEndOf="@id/photoImageView"
        android:layout_marginTop="4dp"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="4dp"
        app:layout_constraintTop_toBottomOf="@id/locationTextView"
        app:layout_constraintStart_toStartOf="@id/photoImageView"
        app:layout_constraintEnd_toEndOf="@id/photoImageView"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

그리고 이미지를 출력하기위해서 Glide를 다운받은 뒤

//Glide
implementation("com.github.bumptech.glide:glide:4.13.0")
implementation("io.coil-kt:coil:2.2.2")

 

xml에서 이미지뷰에 Glide로 url을 넣을 수 있게끔 

Util디렉토리 > BindingAdapter.kt를 생성해서 아래와 같이 함수를 만들어줍니다.

@BindingAdapter("imageFromUrl")
fun ImageView.bindImageUrl(imageUrl: String?) {
    if (!imageUrl.isNullOrEmpty()) {
        Glide.with(this.context)
            .load(imageUrl)
            .centerCrop()
            .transition(DrawableTransitionOptions.withCrossFade())
            .into(this)
    }
}

 

그리하여 BidingAdapter의 imageFromUrl속성을 사용해서 url을 이미지에 연결합니다.

<ImageView
    android:id="@+id/photoImageView"
    imageFromUrl="@{item.galWebImageUrl}"
    android:layout_width="match_parent"
    android:layout_height="240dp"

 

마지막으로 전체 ConstaraintLayout을 클릭하면

데이터바인딩으로 가져온 Handler 함수 실행할 수 있도록합니다.

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{()-> handler.onClickItem(item)}">

 

item_gallery.xml 전체 코드

<?xml version="1.0" encoding="utf-8"?>

<layout 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">

    <data>
        <variable
            name="item"
            type="com.example.photogallery.domain.model.GalleryModel" />

        <variable
            name="handler"
            type="com.example.photogallery.presentation.ui.GalleryFragment.Handler" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="@{()-> handler.onClickItem(item)}">

        <ImageView
            android:id="@+id/photoImageView"
            imageFromUrl="@{item.galWebImageUrl}"
            android:layout_width="match_parent"
            android:layout_height="240dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:adjustViewBounds="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:src="@color/gray"
            android:contentDescription="TODO" />

        <TextView
            android:id="@+id/titleTextView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@{item.galTitle}"
            tools:text="남산타워"
            android:textSize="12sp"
            android:textStyle="bold"
            android:textColor="@color/black"
            app:layout_constraintTop_toBottomOf="@id/photoImageView"
            app:layout_constraintStart_toStartOf="@id/photoImageView"
            app:layout_constraintEnd_toEndOf="@id/photoImageView"
            android:layout_marginTop="4dp"/>

        <TextView
            android:id="@+id/dateTextView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@{item.galPhotographyMonth}"
            tools:text="202207"
            android:textSize="8sp"
            android:textStyle="bold"
            android:textColor="@color/black"
            app:layout_constraintTop_toBottomOf="@id/titleTextView"
            app:layout_constraintStart_toStartOf="@id/photoImageView"
            app:layout_constraintEnd_toEndOf="@id/photoImageView"
            android:layout_marginTop="4dp"/>

        <TextView
            android:id="@+id/locationTextView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@{item.galPhotographyLocation}"
            tools:text="서울특별시 용산구"
            android:textSize="8sp"
            android:textStyle="bold"
            android:textColor="@color/black"
            app:layout_constraintTop_toBottomOf="@id/dateTextView"
            app:layout_constraintStart_toStartOf="@id/photoImageView"
            app:layout_constraintEnd_toEndOf="@id/photoImageView"
            android:layout_marginTop="4dp"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="4dp"
            app:layout_constraintTop_toBottomOf="@id/locationTextView"
            app:layout_constraintStart_toStartOf="@id/photoImageView"
            app:layout_constraintEnd_toEndOf="@id/photoImageView"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

ViewHolder클래스를 만들어준 뒤

class GalleryViewHolder(private val binding: ItemGalleryBinding, private val handler: GalleryFragment.Handler) : RecyclerView.ViewHolder(binding.root){

    fun bind(item: GalleryModel){
        binding.item = item
        binding.handler = handler
    }
}

 

diffUtil을 사용한 ListAdapter인 GalleryListAdapter를 만들어줍니다.

만약 그냥 RecyclerviewAdapter를 사용한다면 기호에 맞게 사용하면 됩니다.

class GalleryListAdapter(private val handler: GalleryFragment.Handler) : ListAdapter<GalleryModel, GalleryViewHolder>(diffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryViewHolder {
        return GalleryViewHolder(ItemGalleryBinding.inflate(LayoutInflater.from(parent.context), parent, false), handler)
    }

    override fun onBindViewHolder(holder: GalleryViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<GalleryModel>() {
            override fun areItemsTheSame(oldItem: GalleryModel, newItem: GalleryModel): Boolean {
                return oldItem.galContentId == newItem.galContentId
            }

            override fun areContentsTheSame(oldItem: GalleryModel, newItem: GalleryModel): Boolean {
                return oldItem == newItem
            }
        }
    }
}

 

이 와 똑같이 save도 똑같이 만들어주면 됩니다.(save는 Handler없음)

 

fragment_save.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <variable
            name="view"
            type="com.example.photogallery.presentation.ui.SaveFragment" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/emptyTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="데이터가 없습니다."
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

SaveFragment.kt

class SaveFragment : Fragment() {
    private lateinit var binding: FragmentSaveBinding
    private lateinit var viewModel: SaveViewModel

    private val adapter by lazy { SavedListAdapter() }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_save, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel = ViewModelProvider(requireActivity()).get(SaveViewModel::class.java)
        binding.recyclerview.adapter = adapter

        observeViewModel()
    }

    private fun observeViewModel(){
        viewModel.savedListState.observe(viewLifecycleOwner){
            lifecycleScope.launch {
                it.collect {
                    binding.emptyTextView.isVisible = it.isEmpty()
                    binding.recyclerview.isVisible = it.isNotEmpty()
                    adapter.submitList(it)
                }
            }
        }
    }
}

 

item_save.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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"
    tools:ignore="MissingDefaultResource">

    <data>
        <variable
            name="item"
            type="com.example.photogallery.domain.model.GalleryModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">


        <ImageView
            android:id="@+id/photoImageView"
            imageFromUrl="@{item.galWebImageUrl}"
            android:layout_width="match_parent"
            android:layout_height="240dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:adjustViewBounds="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:src="@color/gray"
            android:contentDescription="TODO" />

        <TextView
            android:id="@+id/titleTextView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@{item.galTitle}"
            tools:text="남산타워"
            android:textSize="12sp"
            android:textStyle="bold"
            android:textColor="@color/black"
            app:layout_constraintTop_toBottomOf="@id/photoImageView"
            app:layout_constraintStart_toStartOf="@id/photoImageView"
            app:layout_constraintEnd_toEndOf="@id/photoImageView"
            android:layout_marginTop="4dp"/>

        <TextView
            android:id="@+id/dateTextView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@{item.galPhotographyMonth}"
            tools:text="202207"
            android:textSize="8sp"
            android:textStyle="bold"
            android:textColor="@color/black"
            app:layout_constraintTop_toBottomOf="@id/titleTextView"
            app:layout_constraintStart_toStartOf="@id/photoImageView"
            app:layout_constraintEnd_toEndOf="@id/photoImageView"
            android:layout_marginTop="4dp"/>

        <TextView
            android:id="@+id/locationTextView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="@{item.galPhotographyLocation}"
            tools:text="서울특별시 용산구"
            android:textSize="8sp"
            android:textStyle="bold"
            android:textColor="@color/black"
            app:layout_constraintTop_toBottomOf="@id/dateTextView"
            app:layout_constraintStart_toStartOf="@id/photoImageView"
            app:layout_constraintEnd_toEndOf="@id/photoImageView"
            android:layout_marginTop="4dp"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="4dp"
            app:layout_constraintTop_toBottomOf="@id/locationTextView"
            app:layout_constraintStart_toStartOf="@id/photoImageView"
            app:layout_constraintEnd_toEndOf="@id/photoImageView"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

SaveViewHolder.kt

class SaveViewHolder(private val binding: ItemSaveBinding) : RecyclerView.ViewHolder(binding.root){

    fun bind(item: GalleryModel){
        binding.item = item
    }
}

 

SaveListAdapter.kt

class SavedListAdapter() : ListAdapter<GalleryModel, SaveViewHolder>(diffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SaveViewHolder {
        return SaveViewHolder(ItemSaveBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: SaveViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<GalleryModel>() {
            override fun areItemsTheSame(oldItem: GalleryModel, newItem: GalleryModel): Boolean {
                return oldItem.galContentId == newItem.galContentId
            }

            override fun areContentsTheSame(oldItem: GalleryModel, newItem: GalleryModel): Boolean {
                return oldItem == newItem
            }
        }
    }
}

 

이렇게 하면 정말 단순한 안드로이드 앱 아키텍처 가이드라인에 따라 애플리케이션을 만들 수 있습니다.

정말 기능도 없고 디자인도 하나 없지만 앞으로 안드로이드 개발자의 필수 덕목인 클린 아키텍처를

해봤다는 만족감이 너무 좋습니다.

 

앞으로 시간이 될 때 아래 기능들을 만들면서 추가해보도록 해보겠습니다.

1. 갤러리에서 Paging3로 페이지처리되는 기능

2. 갤러리에서 클릭시 상세로 이동되는 기능

3. 저장된 사진을 데이터 베이스에서 지워서 저장 취소하는 기능

 

클린 아키텍처 프로젝트를 만드시는 분게 쪼금이라도 도움이 되셨으면 좋겠습니다.

그럼 이때까지 봐주셔서 감사합니다.~~

Comments