From c6eb7acc3aa5be74e5b3af358c3647faee06ce32 Mon Sep 17 00:00:00 2001 From: Sergey Buksha Date: Sun, 29 Jan 2023 14:32:09 +0300 Subject: [PATCH 1/6] RemoteMediator added --- app/build.gradle | 1 + .../java/ru/netology/nmedia/api/ApiModule.kt | 2 +- .../java/ru/netology/nmedia/dao/PostDao.kt | 8 ++ .../netology/nmedia/dao/PostRemoteKeyDao.kt | 27 ++++++ .../main/java/ru/netology/nmedia/db/AppDb.kt | 9 +- .../java/ru/netology/nmedia/db/DbModule.kt | 4 + .../ru/netology/nmedia/entity/PostEntity.kt | 9 +- .../nmedia/entity/PostRemoteKeyEntity.kt | 16 ++++ .../nmedia/repository/PostPagingSource.kt | 40 --------- .../nmedia/repository/PostRemoteMediator.kt | 88 +++++++++++++++++++ .../nmedia/repository/PostRepository.kt | 1 - .../repository/PostRepositoryServerImpl.kt | 37 ++++---- .../ru/netology/nmedia/view/FeedFragment.kt | 5 +- .../nmedia/viewmodel/PostViewModel.kt | 17 +--- 14 files changed, 183 insertions(+), 81 deletions(-) create mode 100644 app/src/main/java/ru/netology/nmedia/dao/PostRemoteKeyDao.kt create mode 100644 app/src/main/java/ru/netology/nmedia/entity/PostRemoteKeyEntity.kt delete mode 100644 app/src/main/java/ru/netology/nmedia/repository/PostPagingSource.kt create mode 100644 app/src/main/java/ru/netology/nmedia/repository/PostRemoteMediator.kt diff --git a/app/build.gradle b/app/build.gradle index 6d896a3..b014866 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,6 +82,7 @@ dependencies { implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-compiler:$hilt_version" implementation "androidx.paging:paging-runtime-ktx:$paging_version" + implementation "androidx.room:room-paging:$room_version" testImplementation 'junit:junit:4.13.2' implementation "androidx.arch.core:core-testing:$arch_version" diff --git a/app/src/main/java/ru/netology/nmedia/api/ApiModule.kt b/app/src/main/java/ru/netology/nmedia/api/ApiModule.kt index 6f79cb4..9e033c6 100644 --- a/app/src/main/java/ru/netology/nmedia/api/ApiModule.kt +++ b/app/src/main/java/ru/netology/nmedia/api/ApiModule.kt @@ -19,7 +19,7 @@ import javax.inject.Singleton class ApiModule { companion object { - private const val BASE_URL = "${BuildConfig.BASE_URL}/api/" + private const val BASE_URL = "${BuildConfig.BASE_URL}/api/slow/" } @Provides diff --git a/app/src/main/java/ru/netology/nmedia/dao/PostDao.kt b/app/src/main/java/ru/netology/nmedia/dao/PostDao.kt index 38f65dd..ccc30d0 100644 --- a/app/src/main/java/ru/netology/nmedia/dao/PostDao.kt +++ b/app/src/main/java/ru/netology/nmedia/dao/PostDao.kt @@ -1,5 +1,6 @@ package ru.netology.nmedia.dao +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy @@ -13,6 +14,9 @@ interface PostDao { @Query("SELECT * FROM PostEntity WHERE shown = 1 ORDER BY id DESC") fun getAll(): Flow> + @Query("SELECT * FROM PostEntity ORDER BY id DESC") + fun getPagingSource(): PagingSource + @Query("SELECT COUNT(*) FROM PostEntity WHERE shown = 0") fun getInvisibleAmount(): Int @@ -45,6 +49,10 @@ interface PostDao { @Query("DELETE FROM PostEntity WHERE id = :id") suspend fun removeById(id: Long) + @Query("DELETE FROM PostEntity") + suspend fun clear() + + @Query("UPDATE PostEntity SET shown = 1 WHERE shown = 0") suspend fun showAll() } \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/dao/PostRemoteKeyDao.kt b/app/src/main/java/ru/netology/nmedia/dao/PostRemoteKeyDao.kt new file mode 100644 index 0000000..7ef0485 --- /dev/null +++ b/app/src/main/java/ru/netology/nmedia/dao/PostRemoteKeyDao.kt @@ -0,0 +1,27 @@ +package ru.netology.nmedia.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import ru.netology.nmedia.entity.PostRemoteKeyEntity + +@Dao +interface PostRemoteKeyDao { + + @Query("SELECT max('key') FROM PostRemoteKeyEntity") + suspend fun max(): Long? + + @Query("SELECT min('key') FROM PostRemoteKeyEntity") + suspend fun min(): Long? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(postRemoteKeyEntity: PostRemoteKeyEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(postRemoteKeyEntity: List) + + @Query("DELETE FROM PostRemoteKeyEntity") + suspend fun clear() + +} \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/db/AppDb.kt b/app/src/main/java/ru/netology/nmedia/db/AppDb.kt index 3c62130..006e662 100644 --- a/app/src/main/java/ru/netology/nmedia/db/AppDb.kt +++ b/app/src/main/java/ru/netology/nmedia/db/AppDb.kt @@ -4,12 +4,17 @@ import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import ru.netology.nmedia.dao.PostDao +import ru.netology.nmedia.dao.PostRemoteKeyDao import ru.netology.nmedia.entity.Converters import ru.netology.nmedia.entity.PostEntity +import ru.netology.nmedia.entity.PostRemoteKeyEntity -@Database(entities = [PostEntity::class], version = 1, exportSchema = false) +@Database(entities = [PostEntity::class, PostRemoteKeyEntity::class], + version = 1, + exportSchema = false +) @TypeConverters(Converters::class) abstract class AppDb : RoomDatabase() { abstract fun postDao(): PostDao - + abstract fun postRemoteKeyDao(): PostRemoteKeyDao } \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/db/DbModule.kt b/app/src/main/java/ru/netology/nmedia/db/DbModule.kt index e992047..c73322f 100644 --- a/app/src/main/java/ru/netology/nmedia/db/DbModule.kt +++ b/app/src/main/java/ru/netology/nmedia/db/DbModule.kt @@ -8,6 +8,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import ru.netology.nmedia.dao.PostDao +import ru.netology.nmedia.dao.PostRemoteKeyDao import javax.inject.Singleton @InstallIn(SingletonComponent::class) @@ -27,4 +28,7 @@ class DbModule { fun providePostDao( appDb: AppDb ): PostDao = appDb.postDao() + + @Provides + fun providePostRemoteKeyDao(db: AppDb): PostRemoteKeyDao = db.postRemoteKeyDao() } \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/entity/PostEntity.kt b/app/src/main/java/ru/netology/nmedia/entity/PostEntity.kt index 5e0b341..446f076 100644 --- a/app/src/main/java/ru/netology/nmedia/entity/PostEntity.kt +++ b/app/src/main/java/ru/netology/nmedia/entity/PostEntity.kt @@ -21,7 +21,8 @@ data class PostEntity( val likedByMe: Boolean = false, val shown: Boolean = true, @Embedded - var attachment: AttachmentEmbeddable? = null + var attachment: AttachmentEmbeddable? = null, + val ownedByMe: Boolean ) { fun toDto() = Post( id, @@ -34,7 +35,8 @@ data class PostEntity( 0, 0, likedByMe, - attachment = attachment?.toDto() + attachment = attachment?.toDto(), + ownedByMe = ownedByMe ) fun hide() = this.copy(shown = false) @@ -48,7 +50,8 @@ data class PostEntity( dto.published, dto.likes, dto.likedByMe, - attachment = AttachmentEmbeddable.fromDto(dto.attachment) + attachment = AttachmentEmbeddable.fromDto(dto.attachment), + ownedByMe = dto.ownedByMe ) } diff --git a/app/src/main/java/ru/netology/nmedia/entity/PostRemoteKeyEntity.kt b/app/src/main/java/ru/netology/nmedia/entity/PostRemoteKeyEntity.kt new file mode 100644 index 0000000..4554cee --- /dev/null +++ b/app/src/main/java/ru/netology/nmedia/entity/PostRemoteKeyEntity.kt @@ -0,0 +1,16 @@ +package ru.netology.nmedia.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class PostRemoteKeyEntity( + @PrimaryKey + val type: KeyType, + val key: Long?, +) { + enum class KeyType { + AFTER, + BEFORE + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/repository/PostPagingSource.kt b/app/src/main/java/ru/netology/nmedia/repository/PostPagingSource.kt deleted file mode 100644 index 3e34535..0000000 --- a/app/src/main/java/ru/netology/nmedia/repository/PostPagingSource.kt +++ /dev/null @@ -1,40 +0,0 @@ -package ru.netology.nmedia.repository - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import retrofit2.HttpException -import ru.netology.nmedia.api.ApiService -import ru.netology.nmedia.model.Post -import java.io.IOException - -class PostPagingSource( - private val apiService: ApiService -) : PagingSource() { - - override fun getRefreshKey(state: PagingState): Long? = null - - override suspend fun load(params: LoadParams): LoadResult { - try { - val result = when (params) { - is LoadParams.Refresh -> { - apiService.getLatest(params.loadSize) - } - is LoadParams.Append -> { - apiService.getBefore(id = params.key, count = params.loadSize) - } - is LoadParams.Prepend -> return LoadResult.Page( - data = emptyList(), nextKey = null, prevKey = params.key, - ) - } - - if (!result.isSuccessful) { - throw HttpException(result) - } - - val data = result.body().orEmpty() - return LoadResult.Page(data, prevKey = params.key, nextKey = data.lastOrNull()?.id) - } catch (e: IOException) { - return LoadResult.Error(e) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/repository/PostRemoteMediator.kt b/app/src/main/java/ru/netology/nmedia/repository/PostRemoteMediator.kt new file mode 100644 index 0000000..2daa7a3 --- /dev/null +++ b/app/src/main/java/ru/netology/nmedia/repository/PostRemoteMediator.kt @@ -0,0 +1,88 @@ +package ru.netology.nmedia.repository + +import androidx.paging.* +import androidx.room.withTransaction +import ru.netology.nmedia.api.ApiService +import ru.netology.nmedia.dao.PostDao +import ru.netology.nmedia.dao.PostRemoteKeyDao +import ru.netology.nmedia.db.AppDb +import ru.netology.nmedia.entity.PostEntity +import ru.netology.nmedia.entity.PostRemoteKeyEntity +import ru.netology.nmedia.error.ApiError +import java.io.IOException + +@OptIn(ExperimentalPagingApi::class) +class PostRemoteMediator( + private val apiService: ApiService, + private val postDao: PostDao, + private val postRemoteKeyDao: PostRemoteKeyDao, + private val appDb: AppDb +) : RemoteMediator() { + + override suspend fun load(loadType: LoadType, state: PagingState): MediatorResult { + try { + val result = when (loadType) { + LoadType.REFRESH -> apiService.getLatest(state.config.initialLoadSize) + LoadType.PREPEND -> { + /*val id = postRemoteKeyDao.max() ?: */return MediatorResult.Success(true) + //apiService.getAfter(id, state.config.pageSize) + } + LoadType.APPEND -> { + val id = postRemoteKeyDao.min() ?: return MediatorResult.Success(false) + apiService.getBefore(id, state.config.pageSize) + } + } + + if (!result.isSuccessful) { + throw ApiError(result.code(), result.message()) + } + + val body = result.body() ?: throw ApiError(result.code(), result.message()) + + appDb.withTransaction { + when (loadType) { + LoadType.REFRESH -> { + postRemoteKeyDao.clear() + postRemoteKeyDao.insert( + listOf( + PostRemoteKeyEntity( + PostRemoteKeyEntity.KeyType.AFTER, + body.firstOrNull()?.id, + ), + PostRemoteKeyEntity( + PostRemoteKeyEntity.KeyType.BEFORE, + body.lastOrNull()?.id, + ) + ) + ) +// postDao.clear() + } +// LoadType.PREPEND -> { +// postRemoteKeyDao.insert( +// PostRemoteKeyEntity( +// PostRemoteKeyEntity.KeyType.AFTER, +// body.firstOrNull()?.id, +// ) +// ) +// } + LoadType.APPEND -> { + postRemoteKeyDao.insert( + PostRemoteKeyEntity( + PostRemoteKeyEntity.KeyType.BEFORE, + body.lastOrNull()?.id, + ) + ) + } + else -> {} + } + + + postDao.insert(body.map(PostEntity::fromDto)) + } + + return MediatorResult.Success(body.isEmpty()) + } catch (e: IOException) { + return MediatorResult.Error(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/repository/PostRepository.kt b/app/src/main/java/ru/netology/nmedia/repository/PostRepository.kt index c6da38f..ef48bd9 100644 --- a/app/src/main/java/ru/netology/nmedia/repository/PostRepository.kt +++ b/app/src/main/java/ru/netology/nmedia/repository/PostRepository.kt @@ -17,6 +17,5 @@ interface PostRepository { fun getInvisibleAmount(): Int suspend fun saveWithAttachment(post: Post, uploadItem: MediaUpload) suspend fun upload(uploadItem: MediaUpload): Media - fun refreshData() } \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/repository/PostRepositoryServerImpl.kt b/app/src/main/java/ru/netology/nmedia/repository/PostRepositoryServerImpl.kt index 5358d20..7b264db 100644 --- a/app/src/main/java/ru/netology/nmedia/repository/PostRepositoryServerImpl.kt +++ b/app/src/main/java/ru/netology/nmedia/repository/PostRepositoryServerImpl.kt @@ -1,18 +1,15 @@ package ru.netology.nmedia.repository -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData +import androidx.paging.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.* import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody import ru.netology.nmedia.api.ApiService import ru.netology.nmedia.dao.PostDao +import ru.netology.nmedia.dao.PostRemoteKeyDao +import ru.netology.nmedia.db.AppDb import ru.netology.nmedia.entity.PostEntity import ru.netology.nmedia.entity.toEntity import ru.netology.nmedia.enumeration.AttachmentType @@ -31,20 +28,24 @@ import javax.inject.Singleton @Singleton class PostRepositoryServerImpl @Inject constructor( private val postDao: PostDao, - private val apiService: ApiService - ) : PostRepository { + private val apiService: ApiService, + postRemoteKeyDao: PostRemoteKeyDao, + appDb: AppDb +) : PostRepository { - override var data = getNewPager() - - override fun refreshData() { - data = getNewPager() - } - private fun getNewPager() : Flow> = Pager( + @OptIn(ExperimentalPagingApi::class) + override var data = Pager( config = PagingConfig(pageSize = 10, enablePlaceholders = false), - pagingSourceFactory = { - PostPagingSource(apiService) - } + pagingSourceFactory = { postDao.getPagingSource() }, + remoteMediator = PostRemoteMediator( + apiService = apiService, + postDao = postDao, + postRemoteKeyDao = postRemoteKeyDao, + appDb = appDb + ) ).flow + .map { it.map(PostEntity::toDto) } + override suspend fun getAll(): List { try { diff --git a/app/src/main/java/ru/netology/nmedia/view/FeedFragment.kt b/app/src/main/java/ru/netology/nmedia/view/FeedFragment.kt index f2489cc..64947b4 100644 --- a/app/src/main/java/ru/netology/nmedia/view/FeedFragment.kt +++ b/app/src/main/java/ru/netology/nmedia/view/FeedFragment.kt @@ -92,11 +92,12 @@ class FeedFragment : Fragment() { adapter.submitData(it) } } - +/* +проверка на работоспособность без обновления при log in/out viewModel.authChanged.observe(viewLifecycleOwner) { viewModel.refreshData() } - +*/ // FIXME: Сломалось при переходе на paging // viewModel.data.observe(viewLifecycleOwner) { state -> // adapter.submitList(state.posts) { diff --git a/app/src/main/java/ru/netology/nmedia/viewmodel/PostViewModel.kt b/app/src/main/java/ru/netology/nmedia/viewmodel/PostViewModel.kt index 98b1457..4bc5069 100644 --- a/app/src/main/java/ru/netology/nmedia/viewmodel/PostViewModel.kt +++ b/app/src/main/java/ru/netology/nmedia/viewmodel/PostViewModel.kt @@ -3,13 +3,11 @@ package ru.netology.nmedia.viewmodel import android.net.Uri import androidx.lifecycle.* import androidx.paging.PagingData -import androidx.paging.map import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import ru.netology.nmedia.auth.AppAuth import ru.netology.nmedia.model.FeedModelState import ru.netology.nmedia.model.MediaUpload @@ -50,15 +48,9 @@ class PostViewModel @Inject constructor( val data: Flow> = appAuth .authStateFlow - .flatMapLatest { (myId, _) -> - repository - .data - .map { posts -> - posts.map { it.copy(ownedByMe = it.authorId == myId) } - } - }.flowOn(Dispatchers.Default) + .flatMapLatest { repository.data }.flowOn(Dispatchers.Default) - val authChanged = appAuth.authStateFlow.asLiveData() + val authChanged = appAuth.authStateFlow.asLiveData() private val _state = SingleLiveEvent() val state: LiveData @@ -78,7 +70,7 @@ class PostViewModel @Inject constructor( private val scope = MainScope() - var onScroll = false + private var onScroll = false private lateinit var lastFailArgs: Pair @@ -195,7 +187,4 @@ class PostViewModel @Inject constructor( _photo.value = PhotoModel(uri, file) } - fun refreshData() { - repository.refreshData() - } } \ No newline at end of file From 09ddf518d26e1c75eed74a9e5c74e1a868a39a34 Mon Sep 17 00:00:00 2001 From: Sergey Buksha Date: Sat, 4 Feb 2023 11:24:05 +0300 Subject: [PATCH 2/6] Load function fixed --- .../nmedia/repository/PostRemoteMediator.kt | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/ru/netology/nmedia/repository/PostRemoteMediator.kt b/app/src/main/java/ru/netology/nmedia/repository/PostRemoteMediator.kt index 2daa7a3..2c54b36 100644 --- a/app/src/main/java/ru/netology/nmedia/repository/PostRemoteMediator.kt +++ b/app/src/main/java/ru/netology/nmedia/repository/PostRemoteMediator.kt @@ -21,14 +21,21 @@ class PostRemoteMediator( override suspend fun load(loadType: LoadType, state: PagingState): MediatorResult { try { + val id: Long? val result = when (loadType) { - LoadType.REFRESH -> apiService.getLatest(state.config.initialLoadSize) + LoadType.REFRESH -> { + id = postRemoteKeyDao.max() + if (id == null) { + apiService.getLatest(state.config.initialLoadSize) + } else { + apiService.getAfter(id, state.config.pageSize) + } + } LoadType.PREPEND -> { - /*val id = postRemoteKeyDao.max() ?: */return MediatorResult.Success(true) - //apiService.getAfter(id, state.config.pageSize) + return MediatorResult.Success(true) } LoadType.APPEND -> { - val id = postRemoteKeyDao.min() ?: return MediatorResult.Success(false) + id = postRemoteKeyDao.min() ?: return MediatorResult.Success(false) apiService.getBefore(id, state.config.pageSize) } } @@ -42,29 +49,20 @@ class PostRemoteMediator( appDb.withTransaction { when (loadType) { LoadType.REFRESH -> { - postRemoteKeyDao.clear() postRemoteKeyDao.insert( - listOf( - PostRemoteKeyEntity( - PostRemoteKeyEntity.KeyType.AFTER, - body.firstOrNull()?.id, - ), + PostRemoteKeyEntity( + PostRemoteKeyEntity.KeyType.AFTER, + body.firstOrNull()?.id + ) + ) + if (id == null) + postRemoteKeyDao.insert( PostRemoteKeyEntity( PostRemoteKeyEntity.KeyType.BEFORE, - body.lastOrNull()?.id, + body.lastOrNull()?.id ) ) - ) -// postDao.clear() } -// LoadType.PREPEND -> { -// postRemoteKeyDao.insert( -// PostRemoteKeyEntity( -// PostRemoteKeyEntity.KeyType.AFTER, -// body.firstOrNull()?.id, -// ) -// ) -// } LoadType.APPEND -> { postRemoteKeyDao.insert( PostRemoteKeyEntity( From 041ea7bd33f9398afede52414185f2340e515681 Mon Sep 17 00:00:00 2001 From: Sergey Buksha Date: Sat, 4 Feb 2023 09:39:41 +0300 Subject: [PATCH 3/6] in process before HW --- .../netology/nmedia/adapter/AdViewHolder.kt | 16 +++ .../nmedia/adapter/PagingLoadStateAdapter.kt | 47 ++++++++ .../nmedia/adapter/PostDiffCallback.kt | 22 ++++ .../{view => adapter}/PostViewHolder.kt | 21 +--- .../netology/nmedia/adapter/PostsAdapter.kt | 61 ++++++++++ .../java/ru/netology/nmedia/model/Post.kt | 13 ++- .../repository/PostRepositoryServerImpl.kt | 8 +- .../ru/netology/nmedia/view/FeedFragment.kt | 33 +++++- .../netology/nmedia/view/OpenPhotoFragment.kt | 3 +- .../netology/nmedia/view/OpenPostFragment.kt | 18 +-- .../netology/nmedia/view/PostDiffCallback.kt | 18 --- .../ru/netology/nmedia/view/PostsAdapter.kt | 37 ------ .../ru/netology/nmedia/view/ViewExtensions.kt | 16 +++ .../nmedia/viewmodel/PostViewModel.kt | 33 ++++-- app/src/main/res/layout/card_ad.xml | 11 ++ app/src/main/res/layout/fragment_feed.xml | 110 +++++++++--------- app/src/main/res/layout/load_state.xml | 22 ++++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 19 files changed, 332 insertions(+), 159 deletions(-) create mode 100644 app/src/main/java/ru/netology/nmedia/adapter/AdViewHolder.kt create mode 100644 app/src/main/java/ru/netology/nmedia/adapter/PagingLoadStateAdapter.kt create mode 100644 app/src/main/java/ru/netology/nmedia/adapter/PostDiffCallback.kt rename app/src/main/java/ru/netology/nmedia/{view => adapter}/PostViewHolder.kt (81%) create mode 100644 app/src/main/java/ru/netology/nmedia/adapter/PostsAdapter.kt delete mode 100644 app/src/main/java/ru/netology/nmedia/view/PostDiffCallback.kt delete mode 100644 app/src/main/java/ru/netology/nmedia/view/PostsAdapter.kt create mode 100644 app/src/main/java/ru/netology/nmedia/view/ViewExtensions.kt create mode 100644 app/src/main/res/layout/card_ad.xml create mode 100644 app/src/main/res/layout/load_state.xml diff --git a/app/src/main/java/ru/netology/nmedia/adapter/AdViewHolder.kt b/app/src/main/java/ru/netology/nmedia/adapter/AdViewHolder.kt new file mode 100644 index 0000000..4f0cede --- /dev/null +++ b/app/src/main/java/ru/netology/nmedia/adapter/AdViewHolder.kt @@ -0,0 +1,16 @@ +package ru.netology.nmedia.adapter + +import androidx.recyclerview.widget.RecyclerView +import ru.netology.nmedia.BuildConfig +import ru.netology.nmedia.databinding.CardAdBinding +import ru.netology.nmedia.model.Ad +import ru.netology.nmedia.view.load + +class AdViewHolder( + private val binding: CardAdBinding +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(ad: Ad) { + binding.image.load("${BuildConfig.BASE_URL}/media/${ad.image}") + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/adapter/PagingLoadStateAdapter.kt b/app/src/main/java/ru/netology/nmedia/adapter/PagingLoadStateAdapter.kt new file mode 100644 index 0000000..c4bde5e --- /dev/null +++ b/app/src/main/java/ru/netology/nmedia/adapter/PagingLoadStateAdapter.kt @@ -0,0 +1,47 @@ +package ru.netology.nmedia.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.paging.LoadState +import androidx.paging.LoadStateAdapter +import androidx.recyclerview.widget.RecyclerView +import ru.netology.nmedia.databinding.LoadStateBinding + +class PagingLoadStateAdapter( + private val onInteractionListener: OnInteractionListener +) : LoadStateAdapter() { + + interface OnInteractionListener { + fun onRetry() {} + } + + override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + return LoadStateViewHolder( + LoadStateBinding.inflate(layoutInflater, parent, false), + onInteractionListener + ) + } + + override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) { + holder.bind(loadState) + } + + class LoadStateViewHolder( + private val binding: LoadStateBinding, + private val onInteractionListener: OnInteractionListener + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(loadState: LoadState) { + binding.apply { + progress.isVisible = loadState is LoadState.Loading + retry.isVisible = loadState is LoadState.Error + + retry.setOnClickListener { + onInteractionListener.onRetry() + } + } + } + } + +} diff --git a/app/src/main/java/ru/netology/nmedia/adapter/PostDiffCallback.kt b/app/src/main/java/ru/netology/nmedia/adapter/PostDiffCallback.kt new file mode 100644 index 0000000..7622b3f --- /dev/null +++ b/app/src/main/java/ru/netology/nmedia/adapter/PostDiffCallback.kt @@ -0,0 +1,22 @@ +package ru.netology.nmedia.adapter + +import androidx.recyclerview.widget.DiffUtil +import ru.netology.nmedia.model.FeedItem +import ru.netology.nmedia.model.Post + +class PostDiffCallback : DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: FeedItem, newItem: FeedItem): Boolean { + if (oldItem::class != newItem::class) { + return false + } + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: FeedItem, newItem: FeedItem): Boolean { + return oldItem == newItem + } + + override fun getChangePayload(oldItem: FeedItem, newItem: FeedItem): Any? { + return if ((oldItem as Post).likedByMe != (newItem as Post).likedByMe) true else null + } +} diff --git a/app/src/main/java/ru/netology/nmedia/view/PostViewHolder.kt b/app/src/main/java/ru/netology/nmedia/adapter/PostViewHolder.kt similarity index 81% rename from app/src/main/java/ru/netology/nmedia/view/PostViewHolder.kt rename to app/src/main/java/ru/netology/nmedia/adapter/PostViewHolder.kt index 2427f2d..ce22669 100644 --- a/app/src/main/java/ru/netology/nmedia/view/PostViewHolder.kt +++ b/app/src/main/java/ru/netology/nmedia/adapter/PostViewHolder.kt @@ -1,13 +1,15 @@ -package ru.netology.nmedia.view +package ru.netology.nmedia.adapter import android.view.View import android.widget.PopupMenu import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide import ru.netology.nmedia.R import ru.netology.nmedia.databinding.CardPostBinding import ru.netology.nmedia.model.Post import ru.netology.nmedia.util.ViewUtils +import ru.netology.nmedia.view.OnInteractionListener +import ru.netology.nmedia.view.load +import ru.netology.nmedia.view.loadCircleCrop class PostViewHolder( private val binding: CardPostBinding, @@ -54,22 +56,11 @@ class PostViewHolder( } val url = "$G_BASE_URL/avatars/${post.authorAvatar}" - Glide.with(ivAvatar) - .load(url) - .timeout(10_000) - .placeholder(R.drawable.ic_baseline_image_24) - .error(R.drawable.ic_baseline_cancel_24) - .circleCrop() - .into(ivAvatar) + ivAvatar.loadCircleCrop(url) if (post.attachment != null) { ivContent.visibility = View.VISIBLE - Glide.with(ivContent) - .load("$G_BASE_URL/media/${post.attachment.url}") - .timeout(10_000) - .placeholder(R.drawable.ic_baseline_image_24) - .error(R.drawable.ic_baseline_cancel_24) - .into(ivContent) + ivContent.load("$G_BASE_URL/media/${post.attachment.url}") } else { ivContent.visibility = View.GONE } diff --git a/app/src/main/java/ru/netology/nmedia/adapter/PostsAdapter.kt b/app/src/main/java/ru/netology/nmedia/adapter/PostsAdapter.kt new file mode 100644 index 0000000..e2a4fe0 --- /dev/null +++ b/app/src/main/java/ru/netology/nmedia/adapter/PostsAdapter.kt @@ -0,0 +1,61 @@ +package ru.netology.nmedia.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.RecyclerView +import ru.netology.nmedia.R +import ru.netology.nmedia.databinding.CardAdBinding +import ru.netology.nmedia.databinding.CardPostBinding +import ru.netology.nmedia.model.Ad +import ru.netology.nmedia.model.FeedItem +import ru.netology.nmedia.model.Post +import ru.netology.nmedia.view.OnInteractionListener + +class PostsAdapter( + private val onInteractionListener: OnInteractionListener + ) : PagingDataAdapter(PostDiffCallback()) { + + override fun getItemViewType(position: Int): Int = + when (getItem(position)) { + is Ad -> R.layout.card_ad + is Post -> R.layout.card_post + null -> error("unknown item type") + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + when (viewType) { + R.layout.card_post -> { + val binding = CardPostBinding.inflate(LayoutInflater.from(parent.context), parent, false) + PostViewHolder(binding, onInteractionListener) + } + R.layout.card_ad -> { + val binding = CardAdBinding.inflate(LayoutInflater.from(parent.context), parent, false) + AdViewHolder(binding) + } + else -> error("unknown view type: $viewType") + } + + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (val item = getItem(position)) { + is Ad -> (holder as? AdViewHolder)?.bind(item) + is Post -> (holder as? PostViewHolder)?.bind(item) + null -> error("unknown item type") + } + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + } else { + if (payloads[0] == true) { + (holder as PostViewHolder).bindOnLikeChanged(getItem(position)!! as Post) + } + } + } +} diff --git a/app/src/main/java/ru/netology/nmedia/model/Post.kt b/app/src/main/java/ru/netology/nmedia/model/Post.kt index 9b2b7d4..d51bf43 100644 --- a/app/src/main/java/ru/netology/nmedia/model/Post.kt +++ b/app/src/main/java/ru/netology/nmedia/model/Post.kt @@ -2,8 +2,12 @@ package ru.netology.nmedia.model import ru.netology.nmedia.enumeration.AttachmentType +sealed interface FeedItem { + val id: Long +} + data class Post( - val id: Long, + override val id: Long, val authorId: Long, val author: String, val authorAvatar: String, @@ -15,7 +19,12 @@ data class Post( val likedByMe: Boolean = false, val attachment: Attachment? = null, val ownedByMe: Boolean = false -) +) : FeedItem + +data class Ad( + override val id: Long, + val image: String, +) : FeedItem data class Attachment( val url: String, diff --git a/app/src/main/java/ru/netology/nmedia/repository/PostRepositoryServerImpl.kt b/app/src/main/java/ru/netology/nmedia/repository/PostRepositoryServerImpl.kt index 7b264db..665e324 100644 --- a/app/src/main/java/ru/netology/nmedia/repository/PostRepositoryServerImpl.kt +++ b/app/src/main/java/ru/netology/nmedia/repository/PostRepositoryServerImpl.kt @@ -17,10 +17,7 @@ import ru.netology.nmedia.error.ApiError import ru.netology.nmedia.error.AppError import ru.netology.nmedia.error.NetworkError import ru.netology.nmedia.error.UnknownError -import ru.netology.nmedia.model.Attachment -import ru.netology.nmedia.model.Media -import ru.netology.nmedia.model.MediaUpload -import ru.netology.nmedia.model.Post +import ru.netology.nmedia.model.* import java.io.IOException import javax.inject.Inject import javax.inject.Singleton @@ -43,8 +40,7 @@ class PostRepositoryServerImpl @Inject constructor( postRemoteKeyDao = postRemoteKeyDao, appDb = appDb ) - ).flow - .map { it.map(PostEntity::toDto) } + ).flow.map { it.map(PostEntity::toDto) } override suspend fun getAll(): List { diff --git a/app/src/main/java/ru/netology/nmedia/view/FeedFragment.kt b/app/src/main/java/ru/netology/nmedia/view/FeedFragment.kt index 64947b4..77550db 100644 --- a/app/src/main/java/ru/netology/nmedia/view/FeedFragment.kt +++ b/app/src/main/java/ru/netology/nmedia/view/FeedFragment.kt @@ -13,11 +13,15 @@ import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.paging.LoadState +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collectLatest import ru.netology.nmedia.R +import ru.netology.nmedia.adapter.PagingLoadStateAdapter +import ru.netology.nmedia.adapter.PostsAdapter import ru.netology.nmedia.databinding.FragmentFeedBinding import ru.netology.nmedia.model.FeedModelState import ru.netology.nmedia.model.Post @@ -85,7 +89,34 @@ class FeedFragment : Fragment() { } ) - binding.rvList.adapter = adapter + binding.rvList.adapter = adapter.withLoadStateHeaderAndFooter( + header = PagingLoadStateAdapter(object : PagingLoadStateAdapter.OnInteractionListener { + override fun onRetry() { + adapter.retry() + } + }), + footer = PagingLoadStateAdapter(object : PagingLoadStateAdapter.OnInteractionListener { + override fun onRetry() { + adapter.retry() + } + }) + ) + + ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( + 0, ItemTouchHelper.START or ItemTouchHelper.END + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + TODO("Not yet implemented") + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + println("DO SOMETHING") + } + }).attachToRecyclerView(binding.rvList) lifecycleScope.launchWhenCreated { viewModel.data.collectLatest { diff --git a/app/src/main/java/ru/netology/nmedia/view/OpenPhotoFragment.kt b/app/src/main/java/ru/netology/nmedia/view/OpenPhotoFragment.kt index 0e771a6..d76e552 100644 --- a/app/src/main/java/ru/netology/nmedia/view/OpenPhotoFragment.kt +++ b/app/src/main/java/ru/netology/nmedia/view/OpenPhotoFragment.kt @@ -1,11 +1,10 @@ package ru.netology.nmedia.view import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.net.toUri +import androidx.fragment.app.Fragment import com.bumptech.glide.Glide import ru.netology.nmedia.R import ru.netology.nmedia.databinding.OpenPhotoBinding diff --git a/app/src/main/java/ru/netology/nmedia/view/OpenPostFragment.kt b/app/src/main/java/ru/netology/nmedia/view/OpenPostFragment.kt index 3f984d7..70e3be3 100644 --- a/app/src/main/java/ru/netology/nmedia/view/OpenPostFragment.kt +++ b/app/src/main/java/ru/netology/nmedia/view/OpenPostFragment.kt @@ -10,14 +10,13 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController -import com.bumptech.glide.Glide import kotlinx.coroutines.ExperimentalCoroutinesApi import ru.netology.nmedia.R import ru.netology.nmedia.databinding.FragmentOpenPostBinding import ru.netology.nmedia.model.Post import ru.netology.nmedia.util.StringArg import ru.netology.nmedia.util.ViewUtils -import ru.netology.nmedia.view.PostViewHolder.Companion.G_BASE_URL +import ru.netology.nmedia.adapter.PostViewHolder.Companion.G_BASE_URL import ru.netology.nmedia.viewmodel.PostViewModel @OptIn(ExperimentalCoroutinesApi::class) @@ -86,22 +85,11 @@ class OpenPostFragment : Fragment() { } val url = "${G_BASE_URL}/avatars/${post.authorAvatar}" - Glide.with(ivAvatar) - .load(url) - .timeout(10_000) - .placeholder(R.drawable.ic_baseline_image_24) - .error(R.drawable.ic_baseline_cancel_24) - .circleCrop() - .into(ivAvatar) + ivAvatar.loadCircleCrop(url) if (post.attachment != null) { ivContent.visibility = View.VISIBLE - Glide.with(ivContent) - .load("$G_BASE_URL/media/${post.attachment.url}") - .timeout(10_000) - .placeholder(R.drawable.ic_baseline_image_24) - .error(R.drawable.ic_baseline_cancel_24) - .into(ivContent) + ivContent.load("$G_BASE_URL/media/${post.attachment.url}") } else { ivContent.visibility = View.GONE } diff --git a/app/src/main/java/ru/netology/nmedia/view/PostDiffCallback.kt b/app/src/main/java/ru/netology/nmedia/view/PostDiffCallback.kt deleted file mode 100644 index 926906c..0000000 --- a/app/src/main/java/ru/netology/nmedia/view/PostDiffCallback.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ru.netology.nmedia.view - -import androidx.recyclerview.widget.DiffUtil -import ru.netology.nmedia.model.Post - -class PostDiffCallback : DiffUtil.ItemCallback(){ - override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean { - return oldItem == newItem - } - - override fun getChangePayload(oldItem: Post, newItem: Post): Any? { - return if (oldItem.likedByMe != newItem.likedByMe) true else null - } -} diff --git a/app/src/main/java/ru/netology/nmedia/view/PostsAdapter.kt b/app/src/main/java/ru/netology/nmedia/view/PostsAdapter.kt deleted file mode 100644 index 3225715..0000000 --- a/app/src/main/java/ru/netology/nmedia/view/PostsAdapter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package ru.netology.nmedia.view - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.paging.PagingDataAdapter -import ru.netology.nmedia.databinding.CardPostBinding -import ru.netology.nmedia.model.Post - -class PostsAdapter( - private val onInteractionListener: OnInteractionListener - ) : PagingDataAdapter(PostDiffCallback()) { - - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder { - val binding = CardPostBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return PostViewHolder(binding, onInteractionListener) - } - - override fun onBindViewHolder(holder: PostViewHolder, position: Int) { - val post = getItem(position) ?: return - holder.bind(post) - } - - override fun onBindViewHolder( - holder: PostViewHolder, - position: Int, - payloads: MutableList - ) { - if (payloads.isEmpty()) { - super.onBindViewHolder(holder, position, payloads) - } else { - if (payloads[0] == true) { - holder.bindOnLikeChanged(getItem(position)!!) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/view/ViewExtensions.kt b/app/src/main/java/ru/netology/nmedia/view/ViewExtensions.kt new file mode 100644 index 0000000..b1ee817 --- /dev/null +++ b/app/src/main/java/ru/netology/nmedia/view/ViewExtensions.kt @@ -0,0 +1,16 @@ +package ru.netology.nmedia.view + +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import com.bumptech.glide.load.resource.bitmap.CircleCrop + +fun ImageView.load(url: String, vararg transforms: BitmapTransformation = emptyArray()) = + Glide.with(this) + .load(url) + .timeout(10_000) + .transform(*transforms) + .into(this) + +fun ImageView.loadCircleCrop(url: String,vararg transforms: BitmapTransformation = emptyArray()) = + load(url, CircleCrop(), *transforms) \ No newline at end of file diff --git a/app/src/main/java/ru/netology/nmedia/viewmodel/PostViewModel.kt b/app/src/main/java/ru/netology/nmedia/viewmodel/PostViewModel.kt index 4bc5069..26f50de 100644 --- a/app/src/main/java/ru/netology/nmedia/viewmodel/PostViewModel.kt +++ b/app/src/main/java/ru/netology/nmedia/viewmodel/PostViewModel.kt @@ -3,20 +3,23 @@ package ru.netology.nmedia.viewmodel import android.net.Uri import androidx.lifecycle.* import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.insertSeparators import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import ru.netology.nmedia.auth.AppAuth -import ru.netology.nmedia.model.FeedModelState -import ru.netology.nmedia.model.MediaUpload -import ru.netology.nmedia.model.PhotoModel -import ru.netology.nmedia.model.Post +import ru.netology.nmedia.model.* import ru.netology.nmedia.repository.PostRepository import ru.netology.nmedia.util.SingleLiveEvent import java.io.File import javax.inject.Inject +import kotlin.random.Random private val empty = Post( id = 0, @@ -46,9 +49,23 @@ class PostViewModel @Inject constructor( var postToOpen: Post? = null - val data: Flow> = appAuth + private val cached: Flow> = repository + .data + .map { pagingData -> + pagingData.insertSeparators( + generator = { before, _ -> + if (before?.id?.rem(5) != 0L) null else + Ad( + Random.nextLong(), + "figma.jpg" + ) + } + ) + }.cachedIn(viewModelScope) + + val data: Flow> = appAuth .authStateFlow - .flatMapLatest { repository.data }.flowOn(Dispatchers.Default) + .flatMapLatest { cached } val authChanged = appAuth.authStateFlow.asLiveData() diff --git a/app/src/main/res/layout/card_ad.xml b/app/src/main/res/layout/card_ad.xml new file mode 100644 index 0000000..b28aa80 --- /dev/null +++ b/app/src/main/res/layout/card_ad.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml index d33bfd8..8581edc 100644 --- a/app/src/main/res/layout/fragment_feed.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -5,71 +5,71 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> - - - + android:id="@+id/swiper"> - - - + tools:context=".view.FeedFragment"> - + + - + - - + - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/load_state.xml b/app/src/main/res/layout/load_state.xml new file mode 100644 index 0000000..256705e --- /dev/null +++ b/app/src/main/res/layout/load_state.xml @@ -0,0 +1,22 @@ + + + + + +