From dab15f6e9655c7a56a2fc825dc2c46cebf482d7d Mon Sep 17 00:00:00 2001 From: dolchvita Date: Thu, 23 Jan 2025 16:24:13 +0900 Subject: [PATCH] =?UTF-8?q?details=20=EA=B0=9C=EC=84=A0=EC=82=AC=ED=95=AD?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 4 +- .../flab/deepsleep/data/api/UnplashService.kt | 4 +- .../data/entity/{photos => unplash}/Exif.kt | 8 +- .../entity/{photos => unplash}/Results.kt | 2 +- .../{photos => unplash}/SearchPhotos.kt | 2 +- .../entity/{photos => unplash}/SearchUser.kt | 2 +- .../entity/{photos => unplash}/SinglePhoto.kt | 22 +---- .../data/entity/{photos => unplash}/Urls.kt | 7 +- .../data/entity/{photos => unplash}/User.kt | 7 +- .../repository/db/OffLinePhotoRepository.kt | 9 +- .../repository/photo/UnplashRepository.kt | 4 +- .../repository/photo/UnplashRepositoryImpl.kt | 4 +- .../data/source/PhotoPagingSource.kt | 2 +- .../deepsleep/ui/details/DetailsActivity.kt | 33 ++++---- .../deepsleep/ui/details/DetailsViewModel.kt | 40 +++++++++ .../{MainApplication.kt => ui/main/App.kt} | 13 +-- .../deepsleep/{ => ui/main}/MainActivity.kt | 32 +++---- .../MainViewModel.kt} | 84 +++++++------------ .../ui/photo/OnButtonClickListener.kt | 4 +- .../ui/photo/OnPhotoItemClickListener.kt | 4 +- .../flab/deepsleep/ui/photo/PhotoAdapter.kt | 47 +++++------ .../com/flab/deepsleep/ui/photo/UiItem.kt | 27 ++++++ .../com/flab/deepsleep/utils/Constants.kt | 6 -- .../java/com/flab/deepsleep/utils/Debounce.kt | 23 +++++ .../deepsleep/utils/EditTextExtensions.kt | 15 ---- .../flab/deepsleep/utils/TimberDebugTree.kt | 10 --- app/src/main/res/layout/activity_main.xml | 2 +- .../{item_photo.xml => holder_item_photo.xml} | 0 28 files changed, 207 insertions(+), 210 deletions(-) rename app/src/main/java/com/flab/deepsleep/data/entity/{photos => unplash}/Exif.kt (74%) rename app/src/main/java/com/flab/deepsleep/data/entity/{photos => unplash}/Results.kt (97%) rename app/src/main/java/com/flab/deepsleep/data/entity/{photos => unplash}/SearchPhotos.kt (84%) rename app/src/main/java/com/flab/deepsleep/data/entity/{photos => unplash}/SearchUser.kt (93%) rename app/src/main/java/com/flab/deepsleep/data/entity/{photos => unplash}/SinglePhoto.kt (64%) rename app/src/main/java/com/flab/deepsleep/data/entity/{photos => unplash}/Urls.kt (70%) rename app/src/main/java/com/flab/deepsleep/data/entity/{photos => unplash}/User.kt (82%) create mode 100644 app/src/main/java/com/flab/deepsleep/ui/details/DetailsViewModel.kt rename app/src/main/java/com/flab/deepsleep/{MainApplication.kt => ui/main/App.kt} (79%) rename app/src/main/java/com/flab/deepsleep/{ => ui/main}/MainActivity.kt (69%) rename app/src/main/java/com/flab/deepsleep/ui/{photo/PhotoViewModel.kt => main/MainViewModel.kt} (58%) create mode 100644 app/src/main/java/com/flab/deepsleep/ui/photo/UiItem.kt delete mode 100644 app/src/main/java/com/flab/deepsleep/utils/Constants.kt create mode 100644 app/src/main/java/com/flab/deepsleep/utils/Debounce.kt delete mode 100644 app/src/main/java/com/flab/deepsleep/utils/EditTextExtensions.kt delete mode 100644 app/src/main/java/com/flab/deepsleep/utils/TimberDebugTree.kt rename app/src/main/res/layout/{item_photo.xml => holder_item_photo.xml} (100%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 537c969..412517b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/java/com/flab/deepsleep/data/api/UnplashService.kt b/app/src/main/java/com/flab/deepsleep/data/api/UnplashService.kt index 92833d4..54ddd96 100644 --- a/app/src/main/java/com/flab/deepsleep/data/api/UnplashService.kt +++ b/app/src/main/java/com/flab/deepsleep/data/api/UnplashService.kt @@ -1,8 +1,8 @@ package com.flab.deepsleep.data.api -import com.flab.deepsleep.data.entity.photos.SinglePhoto +import com.flab.deepsleep.data.entity.unplash.SinglePhoto -import com.flab.deepsleep.data.entity.photos.SearchPhotos +import com.flab.deepsleep.data.entity.unplash.SearchPhotos import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/photos/Exif.kt b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/Exif.kt similarity index 74% rename from app/src/main/java/com/flab/deepsleep/data/entity/photos/Exif.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/unplash/Exif.kt index d8cdaad..b75b114 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/photos/Exif.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/Exif.kt @@ -1,11 +1,7 @@ -package com.flab.deepsleep.data.entity.photos +package com.flab.deepsleep.data.entity.unplash -import android.os.Parcel -import android.os.Parcelable import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize -@Parcelize data class Exif( @SerializedName("aperture") val aperture: String?, @@ -21,4 +17,4 @@ data class Exif( val model: String?, @SerializedName("name") val name: String? -) : Parcelable \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/photos/Results.kt b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/Results.kt similarity index 97% rename from app/src/main/java/com/flab/deepsleep/data/entity/photos/Results.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/unplash/Results.kt index 83663b4..9d35985 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/photos/Results.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/Results.kt @@ -1,4 +1,4 @@ -package com.flab.deepsleep.data.entity.photos +package com.flab.deepsleep.data.entity.unplash import android.os.Parcel import android.os.Parcelable import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchPhotos.kt b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/SearchPhotos.kt similarity index 84% rename from app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchPhotos.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/unplash/SearchPhotos.kt index 4b4025a..7b95d4e 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchPhotos.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/SearchPhotos.kt @@ -1,4 +1,4 @@ -package com.flab.deepsleep.data.entity.photos +package com.flab.deepsleep.data.entity.unplash import com.google.gson.annotations.SerializedName data class SearchPhotos( diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchUser.kt b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/SearchUser.kt similarity index 93% rename from app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchUser.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/unplash/SearchUser.kt index 68f5209..9a6212b 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/photos/SearchUser.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/SearchUser.kt @@ -1,4 +1,4 @@ -package com.flab.deepsleep.data.entity.photos +package com.flab.deepsleep.data.entity.unplash import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/photos/SinglePhoto.kt b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/SinglePhoto.kt similarity index 64% rename from app/src/main/java/com/flab/deepsleep/data/entity/photos/SinglePhoto.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/unplash/SinglePhoto.kt index 8e4d4e7..7526cef 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/photos/SinglePhoto.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/SinglePhoto.kt @@ -1,11 +1,7 @@ -package com.flab.deepsleep.data.entity.photos +package com.flab.deepsleep.data.entity.unplash -import android.os.Parcelable -import com.flab.deepsleep.data.entity.room.Photo import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize -@Parcelize data class SinglePhoto( @SerializedName("blur_hash") val blurHash: String?, @@ -36,17 +32,5 @@ data class SinglePhoto( @SerializedName("urls") val urls: Urls?, @SerializedName("user") - val user: User?, - var isLike: Boolean = false -) : Parcelable - -fun SinglePhoto.toPhoto(): Photo { - return Photo( - id = this.id, - likes = this.likes, - urls = this.urls?.raw, - createdAt = this.createdAt, - username = this.user?.username, - isLike = this.isLike - ) -} + val user: User? +) \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/photos/Urls.kt b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/Urls.kt similarity index 70% rename from app/src/main/java/com/flab/deepsleep/data/entity/photos/Urls.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/unplash/Urls.kt index 7cf3fcb..23789cf 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/photos/Urls.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/Urls.kt @@ -1,10 +1,7 @@ -package com.flab.deepsleep.data.entity.photos +package com.flab.deepsleep.data.entity.unplash -import android.os.Parcelable import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize -@Parcelize data class Urls( @SerializedName("full") val full: String?, @@ -16,4 +13,4 @@ data class Urls( val small: String?, @SerializedName("thumb") val thumb: String? -) : Parcelable +) \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/entity/photos/User.kt b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/User.kt similarity index 82% rename from app/src/main/java/com/flab/deepsleep/data/entity/photos/User.kt rename to app/src/main/java/com/flab/deepsleep/data/entity/unplash/User.kt index f284082..9f1a587 100644 --- a/app/src/main/java/com/flab/deepsleep/data/entity/photos/User.kt +++ b/app/src/main/java/com/flab/deepsleep/data/entity/unplash/User.kt @@ -1,10 +1,7 @@ -package com.flab.deepsleep.data.entity.photos +package com.flab.deepsleep.data.entity.unplash -import android.os.Parcelable import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize -@Parcelize data class User( @SerializedName("bio") val bio: String?, @@ -26,4 +23,4 @@ data class User( val updatedAt: String?, @SerializedName("username") val username: String? -) : Parcelable \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/repository/db/OffLinePhotoRepository.kt b/app/src/main/java/com/flab/deepsleep/data/repository/db/OffLinePhotoRepository.kt index 8fa8d25..6442fe3 100644 --- a/app/src/main/java/com/flab/deepsleep/data/repository/db/OffLinePhotoRepository.kt +++ b/app/src/main/java/com/flab/deepsleep/data/repository/db/OffLinePhotoRepository.kt @@ -2,11 +2,16 @@ package com.flab.deepsleep.data.repository.db import com.flab.deepsleep.data.entity.room.Photo import com.flab.deepsleep.data.entity.room.PhotoDao +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext class OffLinePhotoRepository(private val photoDao: PhotoDao) : PhotoRepository { override fun getAllPhotos(): Flow> = photoDao.getAll() override fun getSinglePhoto(id: String): Flow = photoDao.getPhoto(id) - override suspend fun insertPhoto(photo: Photo) = photoDao.insert(photo) - override suspend fun deletePhoto(photo: Photo) = photoDao.delete(photo) + override suspend fun insertPhoto(photo: Photo) = + withContext(Dispatchers.IO) { photoDao.insert(photo) } + + override suspend fun deletePhoto(photo: Photo) = + withContext(Dispatchers.IO) { photoDao.delete(photo) } } \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepository.kt b/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepository.kt index bd46239..042a4f4 100644 --- a/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepository.kt +++ b/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepository.kt @@ -1,7 +1,7 @@ package com.flab.deepsleep.data.repository.photo -import com.flab.deepsleep.data.entity.photos.SinglePhoto -import com.flab.deepsleep.data.entity.photos.SearchPhotos +import com.flab.deepsleep.data.entity.unplash.SinglePhoto +import com.flab.deepsleep.data.entity.unplash.SearchPhotos interface UnplashRepository { suspend fun getRandomPhotos(count: Int): List diff --git a/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepositoryImpl.kt b/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepositoryImpl.kt index a845167..df7d6c3 100644 --- a/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepositoryImpl.kt +++ b/app/src/main/java/com/flab/deepsleep/data/repository/photo/UnplashRepositoryImpl.kt @@ -2,8 +2,8 @@ package com.flab.deepsleep.data.repository.photo import com.flab.deepsleep.BuildConfig import com.flab.deepsleep.data.api.UnplashService -import com.flab.deepsleep.data.entity.photos.SinglePhoto -import com.flab.deepsleep.data.entity.photos.SearchPhotos +import com.flab.deepsleep.data.entity.unplash.SinglePhoto +import com.flab.deepsleep.data.entity.unplash.SearchPhotos import javax.inject.Inject class UnplashRepositoryImpl @Inject constructor(private val unplashService: UnplashService) : diff --git a/app/src/main/java/com/flab/deepsleep/data/source/PhotoPagingSource.kt b/app/src/main/java/com/flab/deepsleep/data/source/PhotoPagingSource.kt index e5e6f35..74566a5 100644 --- a/app/src/main/java/com/flab/deepsleep/data/source/PhotoPagingSource.kt +++ b/app/src/main/java/com/flab/deepsleep/data/source/PhotoPagingSource.kt @@ -2,7 +2,7 @@ package com.flab.deepsleep.data.source import androidx.paging.PagingSource import androidx.paging.PagingState -import com.flab.deepsleep.data.entity.photos.SinglePhoto +import com.flab.deepsleep.data.entity.unplash.SinglePhoto class PhotoPagingSource( private val photos: List diff --git a/app/src/main/java/com/flab/deepsleep/ui/details/DetailsActivity.kt b/app/src/main/java/com/flab/deepsleep/ui/details/DetailsActivity.kt index e6aa7e8..2f99a36 100644 --- a/app/src/main/java/com/flab/deepsleep/ui/details/DetailsActivity.kt +++ b/app/src/main/java/com/flab/deepsleep/ui/details/DetailsActivity.kt @@ -7,9 +7,8 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.bumptech.glide.Glide import com.flab.deepsleep.R -import com.flab.deepsleep.data.entity.photos.SinglePhoto import com.flab.deepsleep.databinding.ActivityDetailsBinding -import com.flab.deepsleep.ui.photo.PhotoViewModel +import com.flab.deepsleep.ui.photo.UiItem import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber import java.text.NumberFormat @@ -20,26 +19,26 @@ class DetailsActivity : AppCompatActivity() { private val detailsBinding: ActivityDetailsBinding by lazy { ActivityDetailsBinding.inflate(layoutInflater) } - private val photoViewModel: PhotoViewModel by viewModels() - private val singlePhoto: SinglePhoto? by lazy { - @Suppress("DEPRECATION") intent.getParcelableExtra("singlePhoto") + private val detailsViewModel: DetailsViewModel by viewModels() + private val uiItem: UiItem? by lazy { + @Suppress("DEPRECATION") intent.getParcelableExtra("uiItem") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(detailsBinding.root) - singlePhoto?.let { - loadImage(it.urls?.raw) + uiItem?.let { + loadImage(it.urls) bindPhotoDetails(it) /* 좋아요 표시 */ - singlePhoto?.id?.let { photoViewModel.loadPhotoLikeStatus(it) } + uiItem?.id?.let { detailsViewModel.loadPhotoLikeStatus(it) } } ?: run { loadImage(null) Timber.d("singlePhoto is null") } - photoViewModel.isLiked.observe(this) { isLiked -> + detailsViewModel.isLiked.observe(this) { isLiked -> detailsBinding.detailBtHeart.isSelected = isLiked } } @@ -52,25 +51,25 @@ class DetailsActivity : AppCompatActivity() { .into(detailsBinding.detailsImageView) } - private fun bindPhotoDetails(singlePhoto: SinglePhoto) { + private fun bindPhotoDetails(uiItem: UiItem) { val numberFormat = NumberFormat.getNumberInstance(Locale.KOREA) - val result = numberFormat.format(singlePhoto.likes) + val result = numberFormat.format(uiItem.likes) detailsBinding.apply { - detailDescription.text = singlePhoto.description ?: "No description available" - detailCreateAt.text = singlePhoto.createdAt?.take(10) ?: "Unknown date" + detailDescription.text = uiItem.description ?: "No description available" + detailCreateAt.text = uiItem.createdAt?.take(10) ?: "Unknown date" detailLikes.text = result - detailUsername.text = singlePhoto.user?.username + detailUsername.text = uiItem.username } detailsBinding.detailBtHeart.setOnClickListener { - photoViewModel.insertPhoto(singlePhoto) + detailsViewModel.insertPhoto(uiItem) } } companion object { - fun startActivity(context: Context, singlePhoto: SinglePhoto) { + fun startActivity(context: Context, uiItem: UiItem) { val intent = Intent(context, DetailsActivity::class.java).apply { - putExtra("singlePhoto", singlePhoto) + putExtra("uiItem", uiItem) } context.startActivity(intent) } diff --git a/app/src/main/java/com/flab/deepsleep/ui/details/DetailsViewModel.kt b/app/src/main/java/com/flab/deepsleep/ui/details/DetailsViewModel.kt new file mode 100644 index 0000000..15764d9 --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/ui/details/DetailsViewModel.kt @@ -0,0 +1,40 @@ +package com.flab.deepsleep.ui.details + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.flab.deepsleep.data.repository.db.PhotoRepository +import com.flab.deepsleep.ui.photo.UiItem +import com.flab.deepsleep.ui.photo.toPhoto +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DetailsViewModel @Inject constructor( + private val photoRepository: PhotoRepository +) : ViewModel() { + + /* Bookmark */ + private val _isLiked = MutableLiveData() + val isLiked: LiveData get() = _isLiked + + /* 즐겨찾기 추가 */ + fun insertPhoto(uiItem: UiItem) { + viewModelScope.launch { + photoRepository.insertPhoto(uiItem.toPhoto()) + } + } + + fun loadPhotoLikeStatus(photoId: String) { + viewModelScope.launch { + photoRepository.getSinglePhoto(photoId).collectLatest { photo -> + photo?.let { + _isLiked.value = photo.isLike + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/MainApplication.kt b/app/src/main/java/com/flab/deepsleep/ui/main/App.kt similarity index 79% rename from app/src/main/java/com/flab/deepsleep/MainApplication.kt rename to app/src/main/java/com/flab/deepsleep/ui/main/App.kt index ef1426f..2b54e03 100644 --- a/app/src/main/java/com/flab/deepsleep/MainApplication.kt +++ b/app/src/main/java/com/flab/deepsleep/ui/main/App.kt @@ -1,19 +1,16 @@ -package com.flab.deepsleep +package com.flab.deepsleep.ui.main import android.app.Application -import com.flab.deepsleep.utils.TimberDebugTree +import com.flab.deepsleep.BuildConfig import dagger.hilt.android.HiltAndroidApp import timber.log.Timber - @HiltAndroidApp -class MainApplication: Application(){ - +class App : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { - // Timber Initialize Timber.uprootAll() Timber.plant(object : Timber.DebugTree() { override fun createStackElementTag(element: StackTraceElement): String { @@ -21,8 +18,6 @@ class MainApplication: Application(){ return "<$threadName> (${element.fileName}:${element.lineNumber})#${element.methodName} " } }) - } } -} - +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/MainActivity.kt b/app/src/main/java/com/flab/deepsleep/ui/main/MainActivity.kt similarity index 69% rename from app/src/main/java/com/flab/deepsleep/MainActivity.kt rename to app/src/main/java/com/flab/deepsleep/ui/main/MainActivity.kt index 4373726..52996a6 100644 --- a/app/src/main/java/com/flab/deepsleep/MainActivity.kt +++ b/app/src/main/java/com/flab/deepsleep/ui/main/MainActivity.kt @@ -1,11 +1,8 @@ -package com.flab.deepsleep +package com.flab.deepsleep.ui.main -import PhotoAdapter -import android.app.Activity +import com.flab.deepsleep.ui.photo.PhotoAdapter import android.os.Bundle import androidx.activity.enableEdgeToEdge -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity @@ -13,32 +10,28 @@ import androidx.core.widget.doOnTextChanged import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.paging.PagingData -import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.flab.deepsleep.data.entity.photos.SinglePhoto import com.flab.deepsleep.databinding.ActivityMainBinding import com.flab.deepsleep.ui.details.DetailsActivity -import com.flab.deepsleep.ui.photo.PhotoViewModel import com.flab.deepsleep.ui.photo.OnButtonClickListener import com.flab.deepsleep.ui.photo.OnPhotoItemClickListener import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import timber.log.Timber @AndroidEntryPoint class MainActivity : AppCompatActivity() { - private val photoViewModel: PhotoViewModel by viewModels() + private val mainViewModel: MainViewModel by viewModels() private val binding: ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) } private val photoRecyclerView: RecyclerView by lazy { binding.photosRecyclerView } - private val buttonClick = OnButtonClickListener { singlePhoto -> - photoViewModel.insertPhoto(singlePhoto) + private val buttonClick = OnButtonClickListener { uiItem -> + mainViewModel.insertPhoto(uiItem) } private val onPhotoItemClickListener = - OnPhotoItemClickListener { singlePhoto -> - DetailsActivity.startActivity(this, singlePhoto) + OnPhotoItemClickListener { uiItem -> + DetailsActivity.startActivity(this, uiItem) } private val photoAdapter: PhotoAdapter by lazy { @@ -46,7 +39,7 @@ class MainActivity : AppCompatActivity() { } private fun setRecyclerView() { - photoRecyclerView.layoutManager = GridLayoutManager(this, 2) + photoRecyclerView.layoutManager = LinearLayoutManager(this) photoRecyclerView.adapter = photoAdapter } @@ -58,7 +51,7 @@ class MainActivity : AppCompatActivity() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - photoViewModel.items.collectLatest { + mainViewModel.items.collectLatest { photoAdapter.submitData(it) } } @@ -66,11 +59,11 @@ class MainActivity : AppCompatActivity() { /* 검색어 입력시 자동 호출 */ binding.editText.doOnTextChanged { text, start, before, count -> - photoViewModel.searchPhotos(text.toString()) + mainViewModel.searchPhotos(text.toString()) } /* 에러 관찰 */ - photoViewModel.errorMessage.observe(this) { it -> + mainViewModel.errorMessage.observe(this) { it -> it?.let { showErrorDialog(it) } @@ -86,5 +79,4 @@ class MainActivity : AppCompatActivity() { } .show() } - } \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoViewModel.kt b/app/src/main/java/com/flab/deepsleep/ui/main/MainViewModel.kt similarity index 58% rename from app/src/main/java/com/flab/deepsleep/ui/photo/PhotoViewModel.kt rename to app/src/main/java/com/flab/deepsleep/ui/main/MainViewModel.kt index 25ea929..521250e 100644 --- a/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoViewModel.kt +++ b/app/src/main/java/com/flab/deepsleep/ui/main/MainViewModel.kt @@ -1,4 +1,4 @@ -package com.flab.deepsleep.ui.photo +package com.flab.deepsleep.ui.main import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -10,22 +10,20 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map -import com.flab.deepsleep.data.entity.photos.SinglePhoto -import com.flab.deepsleep.data.entity.photos.toPhoto +import com.flab.deepsleep.data.entity.room.Photo +import com.flab.deepsleep.data.entity.unplash.SinglePhoto import com.flab.deepsleep.data.repository.db.PhotoRepository import com.flab.deepsleep.data.repository.photo.UnplashRepository import com.flab.deepsleep.data.source.PhotoPagingSource +import com.flab.deepsleep.ui.photo.UiItem +import com.flab.deepsleep.ui.photo.toPhoto +import com.flab.deepsleep.utils.Debounce import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -33,7 +31,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class PhotoViewModel @Inject constructor( +class MainViewModel @Inject constructor( private val unplashRepository: UnplashRepository, private val photoRepository: PhotoRepository ) : ViewModel() { @@ -43,49 +41,32 @@ class PhotoViewModel @Inject constructor( val errorMessage: LiveData get() = _errorMessage /* Paging Flow */ - private val _photoState = MutableLiveData?>() - private val savedPhotos = photoRepository.getAllPhotos() // Room Flow API 사용 + private val _apiPhotoState = MutableLiveData?>() + + /* Room Flow */ + private val savedPhotos = photoRepository.getAllPhotos() @OptIn(ExperimentalCoroutinesApi::class) - val items: Flow> = _photoState.asFlow() - .map { state -> state ?: emptyList() } + val items: Flow> = _apiPhotoState.asFlow() + .map { state -> state ?: emptyList() } // null 처리 .flatMapLatest { photos -> Pager( config = PagingConfig(pageSize = 20, enablePlaceholders = false), pagingSourceFactory = { PhotoPagingSource(photos) } ).flow } - .cachedIn(viewModelScope) // 여기 + .cachedIn(viewModelScope) .combine(savedPhotos) { pagingData, savedPhotos -> pagingData.map { photo -> val savedPhoto = savedPhotos.find { it.id == photo.id } - if (savedPhoto != null) { - photo.copy(isLike = savedPhoto.isLike) // isLike 복사 - } else { - photo // 그대로 반환 - } + mapToUiItem(photo, savedPhoto) } } - /* Bookmark */ - private val _isLiked = MutableLiveData() - val isLiked: LiveData get() = _isLiked - /* 즐겨찾기 추가 */ - fun insertPhoto(singlePhoto: SinglePhoto) { - viewModelScope.launch(Dispatchers.IO) { - singlePhoto.isLike = true - photoRepository.insertPhoto(singlePhoto.toPhoto()) - } - } - - fun loadPhotoLikeStatus(photoId: String) { + fun insertPhoto(uiItem: UiItem) { viewModelScope.launch { - photoRepository.getSinglePhoto(photoId).collectLatest { photo -> - photo?.let { - _isLiked.postValue(photo.isLike) - } - } + photoRepository.insertPhoto(uiItem.toPhoto()) } } @@ -115,32 +96,29 @@ class PhotoViewModel @Inject constructor( } } - private fun debounce( - timeMillis: Long = 300L, - coroutineScope: CoroutineScope, - block: suspend (T) -> Unit - ): (T) -> Unit { - var debounceJob: Job? = null - return { param: T -> - debounceJob?.cancel() // 이전 작업 취소 - debounceJob = coroutineScope.launch { - delay(timeMillis) - block(param) - } - } + private fun mapToUiItem(photo: SinglePhoto, savedPhoto: Photo?): UiItem { + return UiItem( + id = photo.id, + createdAt = photo.createdAt, + description = photo.description, + likes = photo.likes, + urls = photo.urls?.raw, + username = photo.user?.username, + isLike = savedPhoto?.isLike ?: false + ) } - private val searchDebouncer = debounce( + private val searchDebouncer = Debounce.debounce( timeMillis = 300L, coroutineScope = viewModelScope ) { query -> - _photoState.value = null + _apiPhotoState.value = null try { val photos = getSearchPhotos(query) - _photoState.value = photos + _apiPhotoState.value = photos } catch (e: Exception) { e.printStackTrace() - _photoState.value = emptyList() + _apiPhotoState.value = emptyList() } } } \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/ui/photo/OnButtonClickListener.kt b/app/src/main/java/com/flab/deepsleep/ui/photo/OnButtonClickListener.kt index 6dc36cc..d32f1ac 100644 --- a/app/src/main/java/com/flab/deepsleep/ui/photo/OnButtonClickListener.kt +++ b/app/src/main/java/com/flab/deepsleep/ui/photo/OnButtonClickListener.kt @@ -1,7 +1,5 @@ package com.flab.deepsleep.ui.photo -import com.flab.deepsleep.data.entity.photos.SinglePhoto - fun interface OnButtonClickListener { - fun onButtonClick(singlePhoto: SinglePhoto) + fun onButtonClick(uiItem: UiItem) } \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/ui/photo/OnPhotoItemClickListener.kt b/app/src/main/java/com/flab/deepsleep/ui/photo/OnPhotoItemClickListener.kt index 6147d3b..469f6b1 100644 --- a/app/src/main/java/com/flab/deepsleep/ui/photo/OnPhotoItemClickListener.kt +++ b/app/src/main/java/com/flab/deepsleep/ui/photo/OnPhotoItemClickListener.kt @@ -1,7 +1,5 @@ package com.flab.deepsleep.ui.photo -import com.flab.deepsleep.data.entity.photos.SinglePhoto - fun interface OnPhotoItemClickListener { - fun onPhotoItemClick(singlePhoto : SinglePhoto) + fun onPhotoItemClick(uiItem: UiItem) } \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoAdapter.kt b/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoAdapter.kt index 1f76df4..0ee47b3 100644 --- a/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoAdapter.kt +++ b/app/src/main/java/com/flab/deepsleep/ui/photo/PhotoAdapter.kt @@ -1,44 +1,42 @@ +package com.flab.deepsleep.ui.photo + import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView -import androidx.paging.PagingData import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.flab.deepsleep.R -import com.flab.deepsleep.data.entity.photos.SinglePhoto -import com.flab.deepsleep.data.entity.photos.toPhoto -import com.flab.deepsleep.databinding.ItemPhotoBinding -import com.flab.deepsleep.ui.photo.OnButtonClickListener -import com.flab.deepsleep.ui.photo.OnPhotoItemClickListener -import timber.log.Timber +import com.flab.deepsleep.databinding.HolderItemPhotoBinding class PhotoAdapter( private val buttonClick: OnButtonClickListener, private val onPhotoItemClickListener: OnPhotoItemClickListener ) : - PagingDataAdapter(ARTICLE_DIFF_CALLBACK) { + PagingDataAdapter(ARTICLE_DIFF_CALLBACK) { - inner class ImageViewHolder( - private val binding: ItemPhotoBinding, + class ImageViewHolder( + private val binding: HolderItemPhotoBinding, + private val buttonClick: OnButtonClickListener, + private val onPhotoItemClickListener: OnPhotoItemClickListener ) : RecyclerView.ViewHolder(binding.root) { private val imageView: ImageView = binding.photoImageView - fun bind(photo: SinglePhoto) { - itemView.tag = photo - binding.btHeart.isSelected = photo.isLike + fun bind(uiItem: UiItem) { + itemView.tag = uiItem + binding.btHeart.isSelected = uiItem.isLike /* Like Button */ binding.btHeart.setOnClickListener { - buttonClick.onButtonClick(photo) + buttonClick.onButtonClick(uiItem) } imageView.setOnClickListener { - onPhotoItemClickListener.onPhotoItemClick(photo) + onPhotoItemClickListener.onPhotoItemClick(uiItem) } - val imageUrl = photo.urls?.raw + val imageUrl = uiItem.urls if (imageUrl != null) { Glide.with(imageView.context) .load(imageUrl) @@ -51,25 +49,26 @@ class PhotoAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { - val binding = ItemPhotoBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return ImageViewHolder(binding) + val binding = + HolderItemPhotoBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ImageViewHolder(binding, buttonClick, onPhotoItemClickListener) } override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { - val photo = getItem(position) - photo?.let { - holder.bind(photo) + val uiItem = getItem(position) + uiItem?.let { + holder.bind(uiItem) } } /* Paging */ companion object { - private val ARTICLE_DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: SinglePhoto, newItem: SinglePhoto): Boolean { + private val ARTICLE_DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: UiItem, newItem: UiItem): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: SinglePhoto, newItem: SinglePhoto): Boolean { + override fun areContentsTheSame(oldItem: UiItem, newItem: UiItem): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/com/flab/deepsleep/ui/photo/UiItem.kt b/app/src/main/java/com/flab/deepsleep/ui/photo/UiItem.kt new file mode 100644 index 0000000..20406b6 --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/ui/photo/UiItem.kt @@ -0,0 +1,27 @@ +package com.flab.deepsleep.ui.photo + +import android.os.Parcelable +import com.flab.deepsleep.data.entity.room.Photo +import kotlinx.parcelize.Parcelize + +@Parcelize +data class UiItem( + val id: String?, + val createdAt: String?, + val description: String?, + val likes: Int, + val urls: String?, + val username: String?, + val isLike: Boolean = false +) : Parcelable + +fun UiItem.toPhoto(): Photo { + return Photo( + id = this.id, + likes = this.likes, + urls = this.urls, + createdAt = this.createdAt, + username = this.username, + isLike = true + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/utils/Constants.kt b/app/src/main/java/com/flab/deepsleep/utils/Constants.kt deleted file mode 100644 index 20b6415..0000000 --- a/app/src/main/java/com/flab/deepsleep/utils/Constants.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.flab.deepsleep.utils - -object Constants { - const val NETWORK_PAGE_SIZE = 20 - const val DEFAULT_QUERY = "kotlin" -} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/utils/Debounce.kt b/app/src/main/java/com/flab/deepsleep/utils/Debounce.kt new file mode 100644 index 0000000..6c0fea2 --- /dev/null +++ b/app/src/main/java/com/flab/deepsleep/utils/Debounce.kt @@ -0,0 +1,23 @@ +package com.flab.deepsleep.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +object Debounce { + fun debounce( + timeMillis: Long = 300L, + coroutineScope: CoroutineScope, + block: suspend (T) -> Unit + ): (T) -> Unit { + var debounceJob: Job? = null + return { param: T -> + debounceJob?.cancel() + debounceJob = coroutineScope.launch { + delay(timeMillis) + block(param) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/utils/EditTextExtensions.kt b/app/src/main/java/com/flab/deepsleep/utils/EditTextExtensions.kt deleted file mode 100644 index 3750a1a..0000000 --- a/app/src/main/java/com/flab/deepsleep/utils/EditTextExtensions.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.flab.deepsleep.utils - -import android.text.Editable -import android.text.TextWatcher -import android.widget.EditText - -fun EditText.setOnTextChangedListener(onTextChanged: (String) -> Unit) { - this.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - onTextChanged(s.toString()) - } - override fun afterTextChanged(s: Editable?) {} - }) -} \ No newline at end of file diff --git a/app/src/main/java/com/flab/deepsleep/utils/TimberDebugTree.kt b/app/src/main/java/com/flab/deepsleep/utils/TimberDebugTree.kt deleted file mode 100644 index fa6ceb0..0000000 --- a/app/src/main/java/com/flab/deepsleep/utils/TimberDebugTree.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.flab.deepsleep.utils - -import timber.log.Timber - -class TimberDebugTree: Timber.DebugTree() { - override fun createStackElementTag(element: StackTraceElement): String? { - - return "${element.fileName}:${element.lineNumber}#${element.methodName}" - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index dd05970..a964a5c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,7 +5,7 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + tools:context=".ui.main.MainActivity">