diff --git a/README.md b/README.md index 76092e7..8752e89 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A simple Android MVVM pattern example and template. ## Android Studio Version -- Android Studio Arctic Fox | 2020.3.1 RC 1 +- Android Studio Arctic Fox | 2020.3.1 Patch 2 ## Used Components/Libraries @@ -20,8 +20,8 @@ A simple Android MVVM pattern example and template. - Timber (https://github.com/JakeWharton/timber) - Custom Fonts (https://developer.android.com/guide/topics/ui/look-and-feel/fonts-in-xml) - Shimmer for Android (https://github.com/facebook/shimmer-android) -- Dagger-Hilt (https://github.com/google/dagger) -- Paging (https://developer.android.com/topic/libraries/architecture/paging) +- Hilt (https://dagger.dev/hilt/) +- Paging 3 (https://developer.android.com/topic/libraries/architecture/paging) - Dexter (https://github.com/Karumi/Dexter) **(coming soon...)** ## Others @@ -56,13 +56,13 @@ A simple Android MVVM pattern example and template. - `SharedPref` - `AlarmUtils` - `LocationProviderUtilClient` +- `Event` Class ## Todo - Add New Post - Demo Login - Demo Registration -- Migrate to Paging 3 - OneSignal integration - Splash (Introduced in Android S) - Testing diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostListAdapter.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostListAdapter.kt index e44a03e..4d37d56 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostListAdapter.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostListAdapter.kt @@ -5,24 +5,27 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation import org.imaginativeworld.simplemvvm.databinding.DemoItemPostBinding import org.imaginativeworld.simplemvvm.interfaces.BindableAdapter import org.imaginativeworld.simplemvvm.interfaces.OnObjectListInteractionListener -import org.imaginativeworld.simplemvvm.models.DemoPostResult +import org.imaginativeworld.simplemvvm.models.DemoPost +import org.imaginativeworld.simplemvvm.utils.extensions.dpToPx class DemoPostListAdapter( - private val listener: OnObjectListInteractionListener -) : ListAdapter(DIFF_CALLBACK), - BindableAdapter> { + private val listener: OnObjectListInteractionListener +) : ListAdapter(DIFF_CALLBACK), + BindableAdapter> { companion object { - private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: DemoPostResult, newItem: DemoPostResult): Boolean { + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DemoPost, newItem: DemoPost): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: DemoPostResult, newItem: DemoPostResult): Boolean { + override fun areContentsTheSame(oldItem: DemoPost, newItem: DemoPost): Boolean { return oldItem == newItem } } @@ -38,7 +41,7 @@ class DemoPostListAdapter( holder.bind(item) } - override fun setItems(data: List?) { + override fun setItems(data: List?) { submitList(data) { data?.apply { checkEmptiness() @@ -56,19 +59,26 @@ class DemoPostListAdapter( class ListViewHolder private constructor( private val binding: DemoItemPostBinding, - private val listener: OnObjectListInteractionListener + private val listener: OnObjectListInteractionListener ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: DemoPostResult) { + fun bind(item: DemoPost) { binding.post = item binding.executePendingBindings() + // Image + val imageUrl = "https://picsum.photos/seed/${item.id}/128" + binding.img.load(imageUrl) { + crossfade(true) + transformations(RoundedCornersTransformation(8.dpToPx().toFloat())) + } + binding.root.setOnClickListener { - listener.onClick(adapterPosition, item) + listener.onClick(bindingAdapterPosition, item) } binding.root.setOnLongClickListener { - listener.onLongClick(adapterPosition, item) + listener.onLongClick(bindingAdapterPosition, item) true } } @@ -76,7 +86,7 @@ class DemoPostListAdapter( companion object { fun from( parent: ViewGroup, - listener: OnObjectListInteractionListener + listener: OnObjectListInteractionListener ): ListViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val binding = DemoItemPostBinding.inflate(layoutInflater, parent, false) diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostPagedListAdapter.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostPagedListAdapter.kt index 14992c5..56dfb05 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostPagedListAdapter.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostPagedListAdapter.kt @@ -3,8 +3,7 @@ package org.imaginativeworld.simplemvvm.adapters import android.graphics.Color import android.view.LayoutInflater import android.view.ViewGroup -import androidx.paging.PagedList -import androidx.paging.PagedListAdapter +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import coil.load @@ -13,32 +12,29 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.imaginativeworld.simplemvvm.databinding.DemoItemPostBinding -import org.imaginativeworld.simplemvvm.interfaces.BindableAdapter import org.imaginativeworld.simplemvvm.interfaces.OnObjectListInteractionListener -import org.imaginativeworld.simplemvvm.models.DemoPostResult +import org.imaginativeworld.simplemvvm.models.DemoPost import org.imaginativeworld.simplemvvm.utils.calculatePaletteInImage import org.imaginativeworld.simplemvvm.utils.extensions.dpToPx -import timber.log.Timber class DemoPostPagedListAdapter( - private val listener: OnObjectListInteractionListener -) : PagedListAdapter(DIFF_CALLBACK), - BindableAdapter> { + private val listener: OnObjectListInteractionListener +) : PagingDataAdapter(DIFF_CALLBACK) { companion object { private val DIFF_CALLBACK = object : - DiffUtil.ItemCallback() { + DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: DemoPostResult, - newItem: DemoPostResult + oldItem: DemoPost, + newItem: DemoPost ): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame( - oldItem: DemoPostResult, - newItem: DemoPostResult + oldItem: DemoPost, + newItem: DemoPost ): Boolean { return oldItem == newItem } @@ -56,35 +52,18 @@ class DemoPostPagedListAdapter( holder.bind(item) } - override fun setItems(data: PagedList?) { - submitList(data) { - data?.apply { - checkEmptiness() - } - } - } - - private fun checkEmptiness() { - if (itemCount > 0) { - listener.hideEmptyView() - } else { - listener.showEmptyView() - } - } - - class ListViewHolder private constructor( private val binding: DemoItemPostBinding, - private val listener: OnObjectListInteractionListener + private val listener: OnObjectListInteractionListener ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: DemoPostResult?) { + fun bind(item: DemoPost?) { item?.also { _item -> binding.post = _item binding.executePendingBindings() // Image - val imageUrl = "https://picsum.photos/200?$bindingAdapterPosition" + val imageUrl = "https://picsum.photos/seed/${_item.id}/128" binding.img.load(imageUrl) { crossfade(true) transformations(RoundedCornersTransformation(8.dpToPx().toFloat())) @@ -106,8 +85,6 @@ class DemoPostPagedListAdapter( context = binding.root.context, imageUrl = imageUrl )?.let { swatch -> - Timber.e("position: $position | bindingAdapterPosition: $bindingAdapterPosition") - if (position == bindingAdapterPosition) { binding.root.setBackgroundColor( swatch.rgb @@ -136,11 +113,11 @@ class DemoPostPagedListAdapter( companion object { fun from( parent: ViewGroup, - listener: OnObjectListInteractionListener - ): DemoPostPagedListAdapter.ListViewHolder { + listener: OnObjectListInteractionListener + ): ListViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val binding = DemoItemPostBinding.inflate(layoutInflater, parent, false) - return DemoPostPagedListAdapter.ListViewHolder(binding, listener) + return ListViewHolder(binding, listener) } } diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostPagedLoadStateAdapter.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostPagedLoadStateAdapter.kt new file mode 100644 index 0000000..7f5abe5 --- /dev/null +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoPostPagedLoadStateAdapter.kt @@ -0,0 +1,54 @@ +package org.imaginativeworld.simplemvvm.adapters + +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 org.imaginativeworld.simplemvvm.databinding.DemoLoadStateFooterViewItemBinding + +class DemoPostPagedLoadStateAdapter( + private val retry: () -> Unit +) : LoadStateAdapter() { + + override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ItemViewHolder { + return ItemViewHolder.from(parent, retry) + } + + override fun onBindViewHolder(holder: ItemViewHolder, loadState: LoadState) { + holder.bind(loadState) + } + + class ItemViewHolder private constructor( + private val binding: DemoLoadStateFooterViewItemBinding, + retry: () -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + + init { + binding.retryButton.setOnClickListener { retry.invoke() } + } + + fun bind(loadState: LoadState) { + if (loadState is LoadState.Error) { + binding.errorMsg.text = loadState.error.localizedMessage + } + binding.progressBar.isVisible = loadState is LoadState.Loading + binding.retryButton.isVisible = loadState is LoadState.Error + binding.errorMsg.isVisible = loadState is LoadState.Error + } + + companion object { + fun from( + parent: ViewGroup, + retry: () -> Unit + ): ItemViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = + DemoLoadStateFooterViewItemBinding.inflate(layoutInflater, parent, false) + return ItemViewHolder(binding, retry) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoUserListAdapter.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoUserListAdapter.kt index d4ea74c..653d309 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoUserListAdapter.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/adapters/DemoUserListAdapter.kt @@ -66,10 +66,10 @@ class DemoUserListAdapter( binding.user = item binding.executePendingBindings() binding.root.setOnClickListener { - listener.onClick(adapterPosition, item) + listener.onClick(bindingAdapterPosition, item) } binding.root.setOnLongClickListener { - listener.onLongClick(adapterPosition, item) + listener.onLongClick(bindingAdapterPosition, item) true } diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/datasource/PostPagedDataSource.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/datasource/PostPagedDataSource.kt deleted file mode 100644 index 3d0be0b..0000000 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/datasource/PostPagedDataSource.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.imaginativeworld.simplemvvm.datasource - -import android.content.Context -import androidx.paging.PageKeyedDataSource -import kotlinx.coroutines.runBlocking -import org.imaginativeworld.simplemvvm.interfaces.OnDataSourceErrorListener -import org.imaginativeworld.simplemvvm.models.DemoPostResult -import org.imaginativeworld.simplemvvm.network.ApiException -import org.imaginativeworld.simplemvvm.network.ApiInterface -import org.imaginativeworld.simplemvvm.network.SafeApiRequest - -class PostPagedDataSource( - private val context: Context, - private val format: String, - private val accessToken: String, - private val api: ApiInterface, - private val listener: OnDataSourceErrorListener -) : PageKeyedDataSource() { - override fun loadInitial( - params: LoadInitialParams, - callback: LoadInitialCallback - ) { - - runBlocking { - - try { - - val result = SafeApiRequest.apiRequest(context) { - api.getPostsPaged( - format, - accessToken, - 1 - ) - } - - callback.onResult(result.result, null, 2) - - } catch (e: ApiException) { - e.printStackTrace() - - listener.onDataSourceError(e) - } - - } - - } - - override fun loadAfter(params: LoadParams, callback: LoadCallback) { - - runBlocking { - - try { - - val result = SafeApiRequest.apiRequest(context) { - api.getPostsPaged( - format, - accessToken, - params.key - ) - } - - callback.onResult(result.result, params.key + 1) - - } catch (e: ApiException) { - e.printStackTrace() - - listener.onDataSourceError(e) - } - - } - - } - - override fun loadBefore(params: LoadParams, callback: LoadCallback) { - - } -} \ No newline at end of file diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/datasource/PostPagedDataSourceFactory.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/datasource/PostPagedDataSourceFactory.kt deleted file mode 100644 index 36aa2e1..0000000 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/datasource/PostPagedDataSourceFactory.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.imaginativeworld.simplemvvm.datasource - -import android.content.Context -import androidx.paging.DataSource -import org.imaginativeworld.simplemvvm.interfaces.OnDataSourceErrorListener -import org.imaginativeworld.simplemvvm.models.DemoPostResult -import org.imaginativeworld.simplemvvm.network.ApiInterface - -class PostPagedDataSourceFactory( - private val context: Context, - private val format: String, - private val accessToken: String, - private val api: ApiInterface, - private val listener: OnDataSourceErrorListener -) : DataSource.Factory() { - override fun create(): DataSource { - return PostPagedDataSource( - context, - format, - accessToken, - api, - listener - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/datasource/PostPagingSource.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/datasource/PostPagingSource.kt new file mode 100644 index 0000000..4eb15a6 --- /dev/null +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/datasource/PostPagingSource.kt @@ -0,0 +1,61 @@ +package org.imaginativeworld.simplemvvm.datasource + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import okio.IOException +import org.imaginativeworld.simplemvvm.models.DemoPost +import org.imaginativeworld.simplemvvm.network.ApiException +import org.imaginativeworld.simplemvvm.repositories.AppRepository +import retrofit2.HttpException + + +class PostPagingSource( + private val repository: AppRepository, +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + val pagePosition = params.key ?: 1 + + return try { + + val response = repository.getPostsPaged( + pagePosition + ) + + val result = response.data + + if (result == null) { + LoadResult.Error(ApiException("No data returned!")) + } else { + val nextKey = if (result.isEmpty()) { + null + } else { + pagePosition + 1 + } + + LoadResult.Page( + data = result, + prevKey = if (pagePosition == 1) null else pagePosition - 1, + nextKey = nextKey + ) + } + } catch (exception: IOException) { + return LoadResult.Error(exception) + } catch (exception: HttpException) { + return LoadResult.Error(exception) + } catch (exception: ApiException) { + return LoadResult.Error(exception) + } + } + + // The refresh key is used for subsequent refresh calls to PagingSource.load after the initial load + override fun getRefreshKey(state: PagingState): Int? { + // We need to get the previous key (or next key if previous is null) of the page + // that was closest to the most recently accessed index. + // Anchor position is the most recently accessed index + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } +} diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPostResult.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPost.kt similarity index 93% rename from app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPostResult.kt rename to app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPost.kt index 4e8fb03..e9c6aba 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPostResult.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPost.kt @@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass @Keep @JsonClass(generateAdapter = true) -data class DemoPostResult( +data class DemoPost( @Json(name = "body") val body: String, @Json(name = "id") diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPostResponse.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPostResponse.kt index 8cefbef..2346caa 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPostResponse.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/models/DemoPostResponse.kt @@ -7,8 +7,6 @@ import com.squareup.moshi.JsonClass @Keep @JsonClass(generateAdapter = true) data class DemoPostResponse( - @Json(name = "code") - val code: Int, @Json(name = "data") - val result: List + val data: List? ) \ No newline at end of file diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/network/ApiInterface.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/network/ApiInterface.kt index e686da1..e74706b 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/network/ApiInterface.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/network/ApiInterface.kt @@ -20,16 +20,9 @@ interface ApiInterface { // Posts // ---------------------------------------------------------------- - @GET("posts") - suspend fun getPosts( - @Query("_format") format: String, - @Query("access-token") accessToken: String - ): Response + @GET("v1/posts") + suspend fun getPosts(): Response - @GET("posts") - suspend fun getPostsPaged( - @Query("_format") format: String, - @Query("access-token") accessToken: String, - @Query("page") page: Long - ): Response + @GET("v1/posts") + suspend fun getPostsPaged(@Query("page") page: Int): Response } diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/repositories/AppRepository.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/repositories/AppRepository.kt index 7927dd0..2c4e4db 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/repositories/AppRepository.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/repositories/AppRepository.kt @@ -10,21 +10,13 @@ package org.imaginativeworld.simplemvvm.repositories import android.content.Context -import androidx.lifecycle.LiveData -import androidx.paging.LivePagedListBuilder -import androidx.paging.PagedList import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.imaginativeworld.simplemvvm.datasource.PostPagedDataSourceFactory import org.imaginativeworld.simplemvvm.db.AppDatabase -import org.imaginativeworld.simplemvvm.interfaces.OnDataSourceErrorListener -import org.imaginativeworld.simplemvvm.models.DemoPostResponse -import org.imaginativeworld.simplemvvm.models.DemoPostResult import org.imaginativeworld.simplemvvm.models.DemoUserEntity import org.imaginativeworld.simplemvvm.network.ApiInterface import org.imaginativeworld.simplemvvm.network.SafeApiRequest -import java.util.concurrent.Executors import javax.inject.Inject class AppRepository @Inject constructor( @@ -68,55 +60,24 @@ class AppRepository @Inject constructor( // Post // ---------------------------------------------------------------- - suspend fun getPosts( - format: String, - accessToken: String - ): DemoPostResponse { - return withContext(Dispatchers.IO) { - - SafeApiRequest.apiRequest(context) { - api.getPosts( - format, - accessToken - ) - } + suspend fun getPosts() = withContext(Dispatchers.IO) { + SafeApiRequest.apiRequest(context) { + api.getPosts() } + } - fun getPostsPaged( - format: String, - accessToken: String, - listener: OnDataSourceErrorListener - ): LiveData> { - - val config = PagedList.Config.Builder() - .run { - setEnablePlaceholders(false) - setPrefetchDistance(4) - build() - } - - val executor = Executors.newFixedThreadPool(4) - - return PostPagedDataSourceFactory( - context, - format, - accessToken, - api, - listener - ).let { - - LivePagedListBuilder( - it, - config - ) - .setFetchExecutor(executor) - .build() + suspend fun getPostsPaged( + page: Int, + ) = withContext(Dispatchers.IO) { + SafeApiRequest.apiRequest(context) { + api.getPostsPaged( + page + ) } } - } \ No newline at end of file diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_post/DemoPostFragment.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_post/DemoPostFragment.kt index 4827327..553867e 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_post/DemoPostFragment.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_post/DemoPostFragment.kt @@ -17,15 +17,14 @@ import org.imaginativeworld.simplemvvm.databinding.DemoFragmentPostBinding import org.imaginativeworld.simplemvvm.interfaces.CommonFunctions import org.imaginativeworld.simplemvvm.interfaces.OnFragmentInteractionListener import org.imaginativeworld.simplemvvm.interfaces.OnObjectListInteractionListener -import org.imaginativeworld.simplemvvm.models.DemoPostResult -import org.imaginativeworld.simplemvvm.utils.Constants +import org.imaginativeworld.simplemvvm.models.DemoPost import timber.log.Timber @AndroidEntryPoint class DemoPostFragment : Fragment(), CommonFunctions, - OnObjectListInteractionListener { + OnObjectListInteractionListener { private var listener: OnFragmentInteractionListener? = null @@ -75,10 +74,7 @@ class DemoPostFragment : } private fun load() { - viewModel.getPosts( - Constants.SERVER_FORMAT, - Constants.SERVER_TOKEN - ) + viewModel.getPosts() } override fun onPause() { @@ -151,7 +147,7 @@ class DemoPostFragment : listener = null } - override fun onClick(position: Int, dataObject: DemoPostResult) { + override fun onClick(position: Int, dataObject: DemoPost) { this.context?.apply { AlertDialog.Builder(this) @@ -161,7 +157,7 @@ class DemoPostFragment : } } - override fun onLongClick(position: Int, dataObject: DemoPostResult) { + override fun onLongClick(position: Int, dataObject: DemoPost) { } override fun showEmptyView() { diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_post/DemoPostViewModel.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_post/DemoPostViewModel.kt index 5be0f2e..f4513f2 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_post/DemoPostViewModel.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_post/DemoPostViewModel.kt @@ -6,10 +6,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import org.imaginativeworld.simplemvvm.models.DemoPostResult +import org.imaginativeworld.simplemvvm.models.DemoPost import org.imaginativeworld.simplemvvm.network.ApiException import org.imaginativeworld.simplemvvm.repositories.AppRepository -import java.net.HttpURLConnection import javax.inject.Inject @HiltViewModel @@ -35,38 +34,23 @@ class DemoPostViewModel @Inject constructor( // ---------------------------------------------------------------- - private val _postItems: MutableLiveData> by lazy { - MutableLiveData>() + private val _postItems: MutableLiveData> by lazy { + MutableLiveData>() } - val postItems: LiveData?> + val postItems: LiveData?> get() = _postItems // ---------------------------------------------------------------- - fun getPosts( - format: String, - accessToken: String - ) = viewModelScope.launch { + fun getPosts() = viewModelScope.launch { _eventShowLoading.value = true try { - val postResponse = repository.getPosts( - format, - accessToken - ) - - if (postResponse.code == HttpURLConnection.HTTP_OK) { - - _postItems.value = postResponse.result - - } else { - - throw ApiException("Code: ${postResponse.code}") - - } + val postResponse = repository.getPosts() + _postItems.value = postResponse.data } catch (e: ApiException) { _eventShowMessage.value = e.message } diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_postpaged/DemoPostPagedFragment.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_postpaged/DemoPostPagedFragment.kt index 3d9b440..8fb3ba2 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_postpaged/DemoPostPagedFragment.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_postpaged/DemoPostPagedFragment.kt @@ -5,25 +5,30 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch import org.imaginativeworld.simplemvvm.R import org.imaginativeworld.simplemvvm.adapters.DemoPostPagedListAdapter +import org.imaginativeworld.simplemvvm.adapters.DemoPostPagedLoadStateAdapter import org.imaginativeworld.simplemvvm.databinding.DemoFragmentPostPagedBinding import org.imaginativeworld.simplemvvm.interfaces.CommonFunctions import org.imaginativeworld.simplemvvm.interfaces.OnDataSourceErrorListener import org.imaginativeworld.simplemvvm.interfaces.OnFragmentInteractionListener import org.imaginativeworld.simplemvvm.interfaces.OnObjectListInteractionListener -import org.imaginativeworld.simplemvvm.models.DemoPostResult -import org.imaginativeworld.simplemvvm.utils.Constants +import org.imaginativeworld.simplemvvm.models.DemoPost @AndroidEntryPoint class DemoPostPagedFragment : Fragment(), CommonFunctions, OnDataSourceErrorListener, - OnObjectListInteractionListener { + OnObjectListInteractionListener { private var listener: OnFragmentInteractionListener? = null @@ -36,18 +41,15 @@ class DemoPostPagedFragment : Fragment(), CommonFunctions, OnDataSourceErrorList override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - initObservers() - adapter = DemoPostPagedListAdapter(this) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { binding = DemoFragmentPostPagedBinding.inflate(inflater, container, false) binding.lifecycleOwner = this.viewLifecycleOwner - binding.viewModel = viewModel return binding.root } @@ -61,20 +63,8 @@ class DemoPostPagedFragment : Fragment(), CommonFunctions, OnDataSourceErrorList initViews() initListeners() - } - - override fun onResume() { - super.onResume() - - load() - } - private fun load() { - viewModel.getPostsPaged( - Constants.SERVER_FORMAT, - Constants.SERVER_TOKEN, - this - ) + initAdapterObserver() } override fun onAttach(context: Context) { @@ -104,27 +94,33 @@ class DemoPostPagedFragment : Fragment(), CommonFunctions, OnDataSourceErrorList ) binding.recyclerView.addItemDecoration(dividerItemDecoration) - binding.recyclerView.adapter = adapter - - } - - override fun initObservers() { - - viewModel.eventShowMessage - .observe(this, Observer { - - it?.run { + binding.recyclerView.adapter = adapter.withLoadStateFooter( + DemoPostPagedLoadStateAdapter { adapter.retry() } + ) - listener?.showSnackbar(this, "Retry") { + // Add paging data + val pagingData = viewModel.getPostsPaged().distinctUntilChanged() - load() + lifecycleScope.launch { + pagingData.collect { + adapter.submitData(it) + } + } + } - } + private fun initAdapterObserver() { + lifecycleScope.launch { + adapter.loadStateFlow.collect { loadState -> - } + val isListEmpty = + loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0 + val isLoading = loadState.refresh is LoadState.Loading - }) + binding.emptyView.root.isVisible = isListEmpty + binding.loadingView.isVisible = isLoading + } + } } override fun onDataSourceError(exception: Exception) { @@ -135,11 +131,11 @@ class DemoPostPagedFragment : Fragment(), CommonFunctions, OnDataSourceErrorList } } - override fun onClick(position: Int, dataObject: DemoPostResult) { + override fun onClick(position: Int, dataObject: DemoPost) { } - override fun onLongClick(position: Int, dataObject: DemoPostResult) { + override fun onLongClick(position: Int, dataObject: DemoPost) { } diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_postpaged/DemoPostPagedViewModel.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_postpaged/DemoPostPagedViewModel.kt index e3d6f9a..a0a9c6b 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_postpaged/DemoPostPagedViewModel.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/ui/fragments/demo_postpaged/DemoPostPagedViewModel.kt @@ -1,13 +1,15 @@ package org.imaginativeworld.simplemvvm.ui.fragments.demo_postpaged -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.paging.PagedList +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel -import org.imaginativeworld.simplemvvm.interfaces.OnDataSourceErrorListener -import org.imaginativeworld.simplemvvm.models.DemoPostResult +import kotlinx.coroutines.flow.Flow +import org.imaginativeworld.simplemvvm.datasource.PostPagingSource +import org.imaginativeworld.simplemvvm.models.DemoPost import org.imaginativeworld.simplemvvm.repositories.AppRepository import javax.inject.Inject @@ -16,55 +18,14 @@ class DemoPostPagedViewModel @Inject constructor( private val repository: AppRepository ) : ViewModel() { - private val _eventShowMessage: MutableLiveData by lazy { - MutableLiveData() - } - - val eventShowMessage: LiveData - get() = _eventShowMessage - - // ---------------------------------------------------------------- - - private val _eventShowLoading: MutableLiveData by lazy { - MutableLiveData() - } - - val eventShowLoading: LiveData - get() = _eventShowLoading - - // ---------------------------------------------------------------- - - private val _postItems: MediatorLiveData> by lazy { - MediatorLiveData>() - } - - val postItems: LiveData?> - get() = _postItems - - // ---------------------------------------------------------------- - - fun getPostsPaged( - format: String, - accessToken: String, - listener: OnDataSourceErrorListener - ) { - - _eventShowLoading.value = true - - val result = repository.getPostsPaged( - format, - accessToken, - listener - ) - - _postItems.addSource(result) { - - _postItems.value = it - - _eventShowLoading.value = false - + fun getPostsPaged(): Flow> { + return Pager( + PagingConfig(pageSize = 20) + ) { + PostPagingSource(repository) } - + .flow + .cachedIn(viewModelScope) } } diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/utils/Constants.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/utils/Constants.kt index 089aa97..5c6c7e7 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/utils/Constants.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/utils/Constants.kt @@ -16,9 +16,7 @@ object Constants { * Server endpoint without end slash. */ // const val SERVER_ENDPOINT = "http://jsonplaceholder.typicode.com" - const val SERVER_ENDPOINT = "https://gorest.co.in/public-api" - const val SERVER_TOKEN = "VnGB6avSC9AAjSsmBsZCq1FM_L46SjjObJy1" - const val SERVER_FORMAT = "json" + const val SERVER_ENDPOINT = "https://gorest.co.in/public" /** * For MyNotificationOpenedHandler diff --git a/app/src/main/java/org/imaginativeworld/simplemvvm/utils/DynamicTheming.kt b/app/src/main/java/org/imaginativeworld/simplemvvm/utils/DynamicTheming.kt index 2428da9..7271287 100644 --- a/app/src/main/java/org/imaginativeworld/simplemvvm/utils/DynamicTheming.kt +++ b/app/src/main/java/org/imaginativeworld/simplemvvm/utils/DynamicTheming.kt @@ -35,7 +35,8 @@ suspend fun calculatePaletteInImage( val r = ImageRequest.Builder(context) .data(imageUrl) // We scale the image to cover 128px x 128px (i.e. min dimension == 128px) - .size(128).scale(Scale.FILL) + // Note: For speeding things up we using 32px. + .size(32).scale(Scale.FILL) // Disable hardware bitmaps, since Palette uses Bitmap.getPixels() .allowHardware(false) .build() diff --git a/app/src/main/res/layout/demo_fragment_post_paged.xml b/app/src/main/res/layout/demo_fragment_post_paged.xml index 132a77b..f7a802e 100644 --- a/app/src/main/res/layout/demo_fragment_post_paged.xml +++ b/app/src/main/res/layout/demo_fragment_post_paged.xml @@ -1,16 +1,8 @@ - - - - - @@ -19,7 +11,6 @@ android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" - app:items="@{viewModel.postItems}" tools:listitem="@layout/demo_item_post" tools:visibility="visible" /> @@ -33,7 +24,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" - app:visibility="@{viewModel.eventShowLoading}" tools:viewBindingIgnore="true"> + type="org.imaginativeworld.simplemvvm.models.DemoPost" /> @@ -36,6 +36,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" + android:ellipsize="end" + android:maxLines="2" android:text="@{post.title}" android:textColor="?attr/colorOnSurface" android:textSize="18sp" diff --git a/app/src/main/res/layout/demo_load_state_footer_view_item.xml b/app/src/main/res/layout/demo_load_state_footer_view_item.xml new file mode 100644 index 0000000..8bd0183 --- /dev/null +++ b/app/src/main/res/layout/demo_load_state_footer_view_item.xml @@ -0,0 +1,32 @@ + + + + + + + +