From bc96328ec85024279be0bef5375088f889f56282 Mon Sep 17 00:00:00 2001 From: jaemin-Yoo Date: Wed, 7 Dec 2022 20:41:49 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=AA=A8=EB=A8=BC=ED=8A=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wakeup/data/database/dao/MomentDao.kt | 17 ++++++++ .../wakeup/data/database/dao/PictureDao.kt | 4 ++ .../com/wakeup/data/database/dao/XRefDao.kt | 9 ++++ .../database/entity/MomentWithPictures.kt | 16 +++++++ .../data/repository/MomentRepositoryImpl.kt | 4 ++ .../local/moment/MomentLocalDataSource.kt | 2 + .../local/moment/MomentLocalDataSourceImpl.kt | 18 ++++++++ .../com/wakeup/data/util/InternalFileUtil.kt | 42 +++++++++++++------ .../domain/repository/MomentRepository.kt | 2 + .../domain/usecase/DeleteMomentUseCase.kt | 13 ++++++ .../presentation/ui/home/HomeViewModel.kt | 1 + 11 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 data/src/main/java/com/wakeup/data/database/entity/MomentWithPictures.kt create mode 100644 domain/src/main/java/com/wakeup/domain/usecase/DeleteMomentUseCase.kt diff --git a/data/src/main/java/com/wakeup/data/database/dao/MomentDao.kt b/data/src/main/java/com/wakeup/data/database/dao/MomentDao.kt index aecf244..ec527d3 100644 --- a/data/src/main/java/com/wakeup/data/database/dao/MomentDao.kt +++ b/data/src/main/java/com/wakeup/data/database/dao/MomentDao.kt @@ -2,12 +2,15 @@ package com.wakeup.data.database.dao import androidx.paging.PagingSource import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import com.wakeup.data.database.entity.MomentEntity import com.wakeup.data.database.entity.MomentWithGlobesAndPictures +import com.wakeup.data.database.entity.MomentWithPictures +import com.wakeup.data.database.entity.PictureEntity import kotlinx.coroutines.flow.Flow @Dao @@ -59,4 +62,18 @@ interface MomentDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveMoment(moment: MomentEntity): Long + + @Query( + """ + SELECT picture.picture_id, picture.path + FROM moment_picture + INNER JOIN picture + ON moment_picture.picture_id = picture.picture_id + WHERE moment_picture.moment_id = :momentId + """ + ) + suspend fun getMomentPictures(momentId: Long): List + + @Query("DELETE FROM moment WHERE moment_id = :momentId") + suspend fun deleteMoment(momentId: Long) } \ No newline at end of file diff --git a/data/src/main/java/com/wakeup/data/database/dao/PictureDao.kt b/data/src/main/java/com/wakeup/data/database/dao/PictureDao.kt index c3e8439..7ac6bcd 100644 --- a/data/src/main/java/com/wakeup/data/database/dao/PictureDao.kt +++ b/data/src/main/java/com/wakeup/data/database/dao/PictureDao.kt @@ -1,6 +1,7 @@ package com.wakeup.data.database.dao import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @@ -13,4 +14,7 @@ interface PictureDao { @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun savePictures(pictures: List): List + + @Query("DELETE FROM picture WHERE picture_id = :pictureId") + suspend fun deletePicture(pictureId: Long) } \ No newline at end of file diff --git a/data/src/main/java/com/wakeup/data/database/dao/XRefDao.kt b/data/src/main/java/com/wakeup/data/database/dao/XRefDao.kt index f7e6dab..a4d610c 100644 --- a/data/src/main/java/com/wakeup/data/database/dao/XRefDao.kt +++ b/data/src/main/java/com/wakeup/data/database/dao/XRefDao.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy +import androidx.room.Query import com.wakeup.data.database.entity.MomentGlobeXRef import com.wakeup.data.database.entity.MomentPictureXRef @@ -18,4 +19,12 @@ interface XRefDao { @Delete suspend fun deleteMomentGlobeXRef(momentGlobe: MomentGlobeXRef) + + @Query( + """ + SELECT (CASE WHEN count(*) > 1 THEN 0 ELSE 1 END) FROM moment_picture + WHERE picture_id = :pictureId + """ + ) + suspend fun isOnlyOnePicture(pictureId: Long): Boolean } \ No newline at end of file diff --git a/data/src/main/java/com/wakeup/data/database/entity/MomentWithPictures.kt b/data/src/main/java/com/wakeup/data/database/entity/MomentWithPictures.kt new file mode 100644 index 0000000..00d540e --- /dev/null +++ b/data/src/main/java/com/wakeup/data/database/entity/MomentWithPictures.kt @@ -0,0 +1,16 @@ +package com.wakeup.data.database.entity + +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation + +data class MomentWithPictures( + @Embedded val moment: MomentEntity, + @Relation( + parentColumn = "moment_id", + entity = PictureEntity::class, + entityColumn = "picture_id", + associateBy = Junction(MomentPictureXRef::class) + ) + val pictures: List, +) diff --git a/data/src/main/java/com/wakeup/data/repository/MomentRepositoryImpl.kt b/data/src/main/java/com/wakeup/data/repository/MomentRepositoryImpl.kt index d6c89ee..397d869 100644 --- a/data/src/main/java/com/wakeup/data/repository/MomentRepositoryImpl.kt +++ b/data/src/main/java/com/wakeup/data/repository/MomentRepositoryImpl.kt @@ -38,4 +38,8 @@ class MomentRepositoryImpl @Inject constructor( override suspend fun saveMoment(moment: Moment): Long { return momentLocalDataSource.saveMoment(moment.toEntity()) } + + override suspend fun deleteMoment(momentId: Long) { + momentLocalDataSource.deleteMoment(momentId) + } } \ No newline at end of file diff --git a/data/src/main/java/com/wakeup/data/source/local/moment/MomentLocalDataSource.kt b/data/src/main/java/com/wakeup/data/source/local/moment/MomentLocalDataSource.kt index 707c090..103641d 100644 --- a/data/src/main/java/com/wakeup/data/source/local/moment/MomentLocalDataSource.kt +++ b/data/src/main/java/com/wakeup/data/source/local/moment/MomentLocalDataSource.kt @@ -18,4 +18,6 @@ interface MomentLocalDataSource { fun getAllMoments(query: String): Flow> suspend fun saveMoment(moment: MomentEntity): Long + + suspend fun deleteMoment(momentId: Long) } \ No newline at end of file diff --git a/data/src/main/java/com/wakeup/data/source/local/moment/MomentLocalDataSourceImpl.kt b/data/src/main/java/com/wakeup/data/source/local/moment/MomentLocalDataSourceImpl.kt index d7f73a3..b9cfa68 100644 --- a/data/src/main/java/com/wakeup/data/source/local/moment/MomentLocalDataSourceImpl.kt +++ b/data/src/main/java/com/wakeup/data/source/local/moment/MomentLocalDataSourceImpl.kt @@ -4,16 +4,22 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import com.wakeup.data.database.dao.MomentDao +import com.wakeup.data.database.dao.PictureDao +import com.wakeup.data.database.dao.XRefDao import com.wakeup.data.database.entity.LocationEntity import com.wakeup.data.database.entity.MomentEntity import com.wakeup.data.database.entity.MomentWithGlobesAndPictures import com.wakeup.data.database.entity.PictureEntity +import com.wakeup.data.util.InternalFileUtil import com.wakeup.domain.model.SortType import kotlinx.coroutines.flow.Flow import javax.inject.Inject class MomentLocalDataSourceImpl @Inject constructor( private val momentDao: MomentDao, + private val pictureDao: PictureDao, + private val xRefDao: XRefDao, + private val util: InternalFileUtil ) : MomentLocalDataSource { override fun getMoments( sortType: SortType, @@ -46,6 +52,18 @@ class MomentLocalDataSourceImpl @Inject constructor( return momentDao.saveMoment(moment) } + override suspend fun deleteMoment(momentId: Long) { + val pictures = momentDao.getMomentPictures(momentId) + pictures.forEach { picture -> + val isDelete = xRefDao.isOnlyOnePicture(picture.id) + if (isDelete) { + util.deletePictureInInternalStorage(picture.path) + pictureDao.deletePicture(picture.id) + } + } + momentDao.deleteMoment(momentId) + } + companion object { const val PREFETCH_PAGE = 2 const val ITEMS_PER_PAGE = 10 diff --git a/data/src/main/java/com/wakeup/data/util/InternalFileUtil.kt b/data/src/main/java/com/wakeup/data/util/InternalFileUtil.kt index 97b780e..815a55f 100644 --- a/data/src/main/java/com/wakeup/data/util/InternalFileUtil.kt +++ b/data/src/main/java/com/wakeup/data/util/InternalFileUtil.kt @@ -27,15 +27,27 @@ class InternalFileUtil @Inject constructor( Glide.with(context) .asBitmap() .load(picture.path.toUri()) - .listener(PictureSaveRequestListener(picture, context)) + .listener(PictureSaveRequestListener(picture, context, ::catchException)) .override(1000, 1000) .fitCenter() .submit() } + fun deletePictureInInternalStorage(fileName: String) { + runCatching { + val filePath = "${context.filesDir.path}/$DIR_NAME/$fileName" + val file = File(filePath) + if (file.exists()) { + Timber.d("사진 삭제: $fileName") + file.delete() + } + }.onFailure { catchException(it) } + } + class PictureSaveRequestListener( private val picture: Picture, private val context: Context, + private val catchException: (Throwable) -> Unit ) : RequestListener { override fun onLoadFailed( e: GlideException?, @@ -56,25 +68,31 @@ class InternalFileUtil @Inject constructor( ): Boolean { CoroutineScope(Dispatchers.IO).launch { runCatching { - val dirPath = File(context.filesDir, "images").apply { mkdirs() } + val dirPath = File(context.filesDir, DIR_NAME).apply { mkdirs() } val filePath = File("${dirPath}/${picture.path.substringAfterLast("/")}") Timber.d("${dirPath}/${picture.path.substringAfterLast("/")}") FileOutputStream(filePath).use { out -> resource?.compress(Bitmap.CompressFormat.JPEG, 100, out) } - }.onFailure { exception -> - when (exception) { - is FileNotFoundException -> - Timber.e("FileNotFoundException : " + exception.message) - is IOException -> - Timber.e("IOException : " + exception.message) - else -> - Timber.e("AnotherException : " + exception.message) - } - } + }.onFailure { catchException(it) } } return false } } + + private fun catchException(exception: Throwable) { + when (exception) { + is FileNotFoundException -> + Timber.e("FileNotFoundException : " + exception.message) + is IOException -> + Timber.e("IOException : " + exception.message) + else -> + Timber.e("AnotherException : " + exception.message) + } + } + + companion object { + private const val DIR_NAME = "images" + } } diff --git a/domain/src/main/java/com/wakeup/domain/repository/MomentRepository.kt b/domain/src/main/java/com/wakeup/domain/repository/MomentRepository.kt index 79c71f1..6da432d 100644 --- a/domain/src/main/java/com/wakeup/domain/repository/MomentRepository.kt +++ b/domain/src/main/java/com/wakeup/domain/repository/MomentRepository.kt @@ -16,4 +16,6 @@ interface MomentRepository { fun getAllMoments(query: String): Flow> suspend fun saveMoment(moment: Moment): Long + + suspend fun deleteMoment(momentId: Long) } \ No newline at end of file diff --git a/domain/src/main/java/com/wakeup/domain/usecase/DeleteMomentUseCase.kt b/domain/src/main/java/com/wakeup/domain/usecase/DeleteMomentUseCase.kt new file mode 100644 index 0000000..dc3fc7d --- /dev/null +++ b/domain/src/main/java/com/wakeup/domain/usecase/DeleteMomentUseCase.kt @@ -0,0 +1,13 @@ +package com.wakeup.domain.usecase + +import com.wakeup.domain.repository.MomentRepository +import javax.inject.Inject + +class DeleteMomentUseCase @Inject constructor( + private val momentRepository: MomentRepository +) { + + suspend operator fun invoke(momentId: Long) { + momentRepository.deleteMoment(momentId) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt index 64ce4dd..305aa3b 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt @@ -7,6 +7,7 @@ import androidx.paging.cachedIn import androidx.paging.map import com.wakeup.domain.model.SortType import com.wakeup.domain.model.WeatherType +import com.wakeup.domain.usecase.DeleteMomentUseCase import com.wakeup.domain.usecase.GetAllMomentsUseCase import com.wakeup.domain.usecase.GetMomentListUseCase import com.wakeup.domain.usecase.weather.GetWeatherDataUseCase From 3e3751b2aa6c01d9f97996833b615193ef2e2b42 Mon Sep 17 00:00:00 2001 From: jaemin-Yoo Date: Thu, 8 Dec 2022 18:01:04 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20room=20+=20flow=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=ED=99=9C=EC=9A=A9=20(paging=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/addmoment/AddMomentFragment.kt | 2 - .../presentation/ui/home/HomeFragment.kt | 26 ++++------- .../presentation/ui/home/HomeViewModel.kt | 46 +++++++++---------- .../presentation/ui/home/map/MapFragment.kt | 33 ++++++------- .../ui/home/sheet/BottomSheetFragment.kt | 5 +- 5 files changed, 50 insertions(+), 62 deletions(-) diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/addmoment/AddMomentFragment.kt b/presentation/src/main/java/com/wakeup/presentation/ui/addmoment/AddMomentFragment.kt index a44c625..3aafa94 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/addmoment/AddMomentFragment.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/addmoment/AddMomentFragment.kt @@ -134,8 +134,6 @@ class AddMomentFragment : Fragment() { if (state is UiState.Success) { Toast.makeText(context, "모먼트를 기록하였습니다.", Toast.LENGTH_LONG).show() - val intent = Intent(UPDATE_MOMENTS_KEY) - requireContext().sendBroadcast(intent) findNavController().popBackStack() } } diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt index e213077..29accdc 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt @@ -38,13 +38,15 @@ class HomeFragment : Fragment() { lateinit var broadcastReceiver: BroadcastReceiver private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + private lateinit var mapFragment: MapFragment + private lateinit var bottomSheetFragment: BottomSheetFragment + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initMap() initBottomSheet() initLocation() - initializeBroadcastReceiver() } override fun onCreateView( @@ -66,14 +68,14 @@ class HomeFragment : Fragment() { private fun initMap() { if (childFragmentManager.findFragmentById(R.id.map) == null) { - val mapFragment = MapFragment() + mapFragment = MapFragment() childFragmentManager.beginTransaction().add(R.id.map, mapFragment).commit() } } private fun initBottomSheet() { if (childFragmentManager.findFragmentById(R.id.bottom_sheet) == null) { - val bottomSheetFragment = BottomSheetFragment() + bottomSheetFragment = BottomSheetFragment() childFragmentManager.beginTransaction().add(R.id.bottom_sheet, bottomSheetFragment) .commit() } @@ -107,27 +109,15 @@ class HomeFragment : Fragment() { viewModel.fetchMoments() viewModel.fetchAllMoments() + mapFragment.collectMoments() + bottomSheetFragment.collectMoments() + hideKeyboard() } false // true: 계속 search 가능 } } - private fun initializeBroadcastReceiver() { - broadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - viewModel.setScrollToTop(true) - viewModel.sortType.value = SortType.MOST_RECENT - viewModel.fetchMoments() - } - } - - requireActivity().registerReceiver( - broadcastReceiver, - IntentFilter(UPDATE_MOMENTS_KEY) - ) - } - // 따로 함수로 빼니까, 권한 확인을 했는지 IDE가 인식을 못합니다. @SuppressLint("MissingPermission") private fun fetchWeather() { diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt index 305aa3b..91e5eab 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt @@ -19,9 +19,13 @@ import com.wakeup.presentation.model.WeatherModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -36,11 +40,8 @@ class HomeViewModel @Inject constructor( private val searchQuery = MutableStateFlow("") - private val _allMoments = MutableStateFlow>(emptyList()) - val allMoments: Flow> = _allMoments - - private val _moments = MutableStateFlow>(PagingData.empty()) - val moments: Flow> = _moments + lateinit var allMoments: StateFlow> + lateinit var moments: Flow> private val _scrollToTop = MutableStateFlow(false) val scrollToTop = _scrollToTop.asStateFlow() @@ -61,33 +62,30 @@ class HomeViewModel @Inject constructor( } fun fetchMoments() { - viewModelScope.launch { - _moments.value = getMomentListUseCase( - sortType = sortType.value, - query = searchQuery.value, - myLocation = location.value?.toDomain() - ).map { pagingMoments -> - pagingMoments.map { moment -> - moment.toPresentation() - } + moments = getMomentListUseCase( + sortType = sortType.value, + query = searchQuery.value, + myLocation = location.value?.toDomain() + ).map { pagingMoments -> + pagingMoments.map { moment -> + moment.toPresentation() } - .cachedIn(viewModelScope) - .first() - } + }.cachedIn(viewModelScope) fetchLocationState.value = false location.value = null } fun fetchAllMoments() { - viewModelScope.launch { - _allMoments.value = getAllMomentListUseCase(searchQuery.value).map { moments -> - moments.map { moment -> - moment.toPresentation() - } + allMoments = getAllMomentListUseCase(searchQuery.value).map { moments -> + moments.map { moment -> + moment.toPresentation() } - .first() - } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) } fun fetchWeather(location: LocationModel) { diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/home/map/MapFragment.kt b/presentation/src/main/java/com/wakeup/presentation/ui/home/map/MapFragment.kt index a6b1755..d1279c9 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/home/map/MapFragment.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/home/map/MapFragment.kt @@ -25,7 +25,6 @@ import com.wakeup.presentation.ui.home.HomeFragmentDirections import com.wakeup.presentation.ui.home.HomeViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import timber.log.Timber class MapFragment : Fragment(), OnMapReadyCallback { @@ -52,7 +51,7 @@ class MapFragment : Fragment(), OnMapReadyCallback { super.onViewCreated(view, savedInstanceState) initMomentPreviewClickListener() initMap() - collectData() + collectLocation() } // 프리뷰 터치시, 상세 페이지 이동 @@ -76,7 +75,7 @@ class MapFragment : Fragment(), OnMapReadyCallback { } } - private fun collectData() { + private fun collectLocation() { lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.fetchLocationState.collectLatest { state -> @@ -91,6 +90,20 @@ class MapFragment : Fragment(), OnMapReadyCallback { } } + fun collectMoments() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.allMoments.collectLatest { moments -> + mapHelper.resetMarker(markers) + markers.clear() + moments.forEach { momentModel -> + setMarkerClickListener(momentModel) + } + } + } + } + } + override fun onMapReady(naverMap: NaverMap) { this.naverMap = naverMap @@ -107,19 +120,7 @@ class MapFragment : Fragment(), OnMapReadyCallback { setLogoView(naverMap, binding.lvLogo) } - // 전체 모먼트 불러오기 - // TODO 속도가 엄청 느려진다. - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.allMoments.collectLatest { moments -> - mapHelper.resetMarker(markers) - markers.clear() - moments.forEach { momentModel -> - setMarkerClickListener(momentModel) - } - } - } - } + collectMoments() // Paging 있이, Moment 가져오기 /*viewLifecycleOwner.lifecycleScope.launch { diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/home/sheet/BottomSheetFragment.kt b/presentation/src/main/java/com/wakeup/presentation/ui/home/sheet/BottomSheetFragment.kt index 93d96e0..b0f0b3c 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/home/sheet/BottomSheetFragment.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/home/sheet/BottomSheetFragment.kt @@ -48,7 +48,7 @@ class BottomSheetFragment : BottomSheetDialogFragment() { setAdapterListener() setCallback() - collectData() + collectMoments() } private fun setMenus() { @@ -76,6 +76,7 @@ class BottomSheetFragment : BottomSheetDialogFragment() { } } viewModel.fetchMoments() + collectMoments() } } @@ -112,7 +113,7 @@ class BottomSheetFragment : BottomSheetDialogFragment() { } } - private fun collectData() { + fun collectMoments() { lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.moments.collectLatest { From 22e0004719c237b419e85f47d9f4274baf627f48 Mon Sep 17 00:00:00 2001 From: jaemin-Yoo Date: Thu, 8 Dec 2022 18:33:15 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20weather=20ui=20state=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wakeup/presentation/ui/MainViewModel.kt | 1 + .../presentation/ui/home/HomeFragment.kt | 20 ++++++++++++++++++- .../presentation/ui/home/HomeViewModel.kt | 18 +++++++++++------ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/MainViewModel.kt b/presentation/src/main/java/com/wakeup/presentation/ui/MainViewModel.kt index a427e88..f4444c9 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/MainViewModel.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/MainViewModel.kt @@ -27,6 +27,7 @@ class MainViewModel @Inject constructor( private val _isReady = MutableStateFlow(false) val isReady = _isReady.asStateFlow() + // TODO: HomeViewModel에 활용 val allMoments: Flow?> = getAllMomentListUseCase("").map { moments -> moments.map { moment -> moment.toPresentation() diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt index 29accdc..134cdc9 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt @@ -16,6 +16,9 @@ import android.view.inputmethod.EditorInfo import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.wakeup.domain.model.SortType @@ -24,10 +27,13 @@ import com.wakeup.presentation.databinding.FragmentHomeBinding import com.wakeup.presentation.extension.hideKeyboard import com.wakeup.presentation.model.LocationModel import com.wakeup.presentation.ui.MainActivity +import com.wakeup.presentation.ui.UiState import com.wakeup.presentation.ui.home.map.MapFragment import com.wakeup.presentation.ui.home.sheet.BottomSheetFragment import com.wakeup.presentation.util.UPDATE_MOMENTS_KEY import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch @AndroidEntryPoint class HomeFragment : Fragment() { @@ -35,7 +41,6 @@ class HomeFragment : Fragment() { private lateinit var binding: FragmentHomeBinding private val viewModel: HomeViewModel by viewModels() - lateinit var broadcastReceiver: BroadcastReceiver private lateinit var fusedLocationProviderClient: FusedLocationProviderClient private lateinit var mapFragment: MapFragment @@ -64,6 +69,7 @@ class HomeFragment : Fragment() { setSearchBarListener() fetchWeather() + collectWeather() } private fun initMap() { @@ -130,6 +136,18 @@ class HomeFragment : Fragment() { } } + private fun collectWeather() { + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.state.collect { state -> + if (state is UiState.Success) { + viewModel.setWeather(state.item) + } + } + } + } + } + override fun onDestroyView() { binding.unbind() super.onDestroyView() diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt index 91e5eab..3d39d94 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt @@ -16,6 +16,7 @@ import com.wakeup.presentation.mapper.toPresentation import com.wakeup.presentation.model.LocationModel import com.wakeup.presentation.model.MomentModel import com.wakeup.presentation.model.WeatherModel +import com.wakeup.presentation.ui.UiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -53,6 +54,9 @@ class HomeViewModel @Inject constructor( private val location = MutableStateFlow(null) + private val _state = MutableStateFlow>(UiState.Empty) + val state = _state.asStateFlow() + private val _weather = MutableStateFlow(WeatherModel(0, WeatherType.NONE, "", 0.0)) val weather = _weather.asStateFlow() @@ -89,14 +93,12 @@ class HomeViewModel @Inject constructor( } fun fetchWeather(location: LocationModel) { + _state.value = UiState.Loading viewModelScope.launch { getWeatherDataUseCase(location.toDomain()) - .onSuccess { - _weather.value = it.toPresentation() - }.onFailure { - // TODO UI State 실패 처리하면 좋겠다. - _weather.value = WeatherModel(0, WeatherType.NONE, "", 0.0) - } + .mapCatching { it.toPresentation() } + .onSuccess { weather -> _state.value = UiState.Success(weather) } + .onFailure { _state.value = UiState.Failure } } } @@ -111,4 +113,8 @@ class HomeViewModel @Inject constructor( fun setLocation(location: LocationModel) { this.location.value = location } + + fun setWeather(weather: WeatherModel) { + _weather.value = weather + } } \ No newline at end of file From 8ed49b5ce409ff3bf86f04d6626cacc4674a5a88 Mon Sep 17 00:00:00 2001 From: jaemin-Yoo Date: Thu, 8 Dec 2022 21:07:39 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20splash=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20(=EB=AA=A8=EB=93=A0=20=EB=AA=A8=EB=A8=BC=ED=8A=B8?= =?UTF-8?q?=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wakeup/presentation/ui/MainViewModel.kt | 15 ++++++++++++--- .../wakeup/presentation/ui/home/HomeFragment.kt | 11 +++++++++++ .../wakeup/presentation/ui/home/HomeViewModel.kt | 5 ++++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/MainViewModel.kt b/presentation/src/main/java/com/wakeup/presentation/ui/MainViewModel.kt index f4444c9..d276a64 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/MainViewModel.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/MainViewModel.kt @@ -2,6 +2,8 @@ package com.wakeup.presentation.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import androidx.paging.map import com.wakeup.domain.usecase.GetAllMomentsUseCase import com.wakeup.domain.usecase.weather.GetWeatherDataUseCase import com.wakeup.presentation.mapper.toDomain @@ -12,8 +14,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -27,12 +32,16 @@ class MainViewModel @Inject constructor( private val _isReady = MutableStateFlow(false) val isReady = _isReady.asStateFlow() - // TODO: HomeViewModel에 활용 - val allMoments: Flow?> = getAllMomentListUseCase("").map { moments -> + var allMoments: StateFlow>? = getAllMomentListUseCase("").map { moments -> moments.map { moment -> moment.toPresentation() } - }.apply { _isReady.value = true } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + .apply { _isReady.value = true } fun testGetWeather() { viewModelScope.launch { diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt index 134cdc9..a44d16f 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeFragment.kt @@ -15,6 +15,7 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -27,6 +28,7 @@ import com.wakeup.presentation.databinding.FragmentHomeBinding import com.wakeup.presentation.extension.hideKeyboard import com.wakeup.presentation.model.LocationModel import com.wakeup.presentation.ui.MainActivity +import com.wakeup.presentation.ui.MainViewModel import com.wakeup.presentation.ui.UiState import com.wakeup.presentation.ui.home.map.MapFragment import com.wakeup.presentation.ui.home.sheet.BottomSheetFragment @@ -40,6 +42,7 @@ class HomeFragment : Fragment() { private lateinit var binding: FragmentHomeBinding private val viewModel: HomeViewModel by viewModels() + private val activityViewModel: MainViewModel by activityViewModels() private lateinit var fusedLocationProviderClient: FusedLocationProviderClient @@ -52,6 +55,7 @@ class HomeFragment : Fragment() { initMap() initBottomSheet() initLocation() + initMoments() } override fun onCreateView( @@ -72,6 +76,13 @@ class HomeFragment : Fragment() { collectWeather() } + private fun initMoments() { + activityViewModel.allMoments?.let { moments -> + viewModel.initMoments(moments) + activityViewModel.allMoments = null + } + } + private fun initMap() { if (childFragmentManager.findFragmentById(R.id.map) == null) { mapFragment = MapFragment() diff --git a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt index 3d39d94..70dcc08 100644 --- a/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/wakeup/presentation/ui/home/HomeViewModel.kt @@ -62,7 +62,10 @@ class HomeViewModel @Inject constructor( init { fetchMoments() - fetchAllMoments() + } + + fun initMoments(data: StateFlow>) { + allMoments = data } fun fetchMoments() {