I am using a Androidx Paging library with RecyclerView for pagination in my view.
The issue I am facing is when I scroll down RecyclerView, after the mentioned number of PAGE_SIZE, loadAfter is called and once the new data is received, my RecyclerView scroll up to the top(first item) instead of further scroll down.
I debug my code and saw that my RecyclerView's onBindViewHolder also getting called with position 0. So, I think the whole data is being refreshed. Ideally, this should not happen.
Library version:
implementation 'androidx.paging:paging-runtime:2.1.2'
Here is the code:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.xyz.viewmodel.orders.OrderListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:background="#color/colorWindowsSecondaryBg"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="#dimen/_10sdp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!--List-->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/srlFav"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
bind:isRefreshing="#{viewModel.refreshVisibility}"
bind:onRefreshListener="#{viewModel.swipeRefreshListener}">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvOrders"
android:layout_width="match_parent"
android:layout_height="0dp"
bind:listitem="#layout/item_order" />
<androidx.constraintlayout.widget.Barrier
android:id="#+id/barrierVisibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:barrierAllowsGoneWidgets="true"
app:barrierDirection="top"
app:constraint_referenced_ids="clBottomLoading" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/clBottomLoading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/_10sdp"
android:background="#android:color/transparent"
android:paddingTop="#dimen/_2sdp"
android:visibility="#{viewModel.bottomProgressVisibility && !viewModel.refreshVisibility? View.VISIBLE : View.GONE,default=gone}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/barrierVisibility">
<com.github.ybq.android.spinkit.SpinKitView
android:id="#+id/skv_loading_bottom"
style="#style/SpinKitView.ThreeBounce"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#android:color/transparent"
app:SpinKit_Color="#color/colorPrimaryDark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!--No data layout-->
<include
android:id="#+id/includeError"
layout="#layout/include_layout_error"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="#{!viewModel.progressVisibility && viewModel.errorVisibility ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:message="#{viewModel.errorString}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
OrderListDataSource
class OrderListDataSource(
private val orderRepository: OrderRepository,
private val coroutineScope: CoroutineScope?,
private var showProgress: Boolean = true
) : PageKeyedDataSource<Int, OrdersItem>() {
val ordersLiveData: MutableLiveData<Resource<ResOrders>> = MutableLiveData()
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, OrdersItem>
) {
if (BaseApp.instance?.let { NetworkUtils.isNetworkAvailable(it) } == true) {
if (showProgress) {
ordersLiveData.postValue(Resource.Loading(EnumLoading.LOADING_FIRST_PAGE))
}
coroutineScope?.launch {
val result = orderRepository.getOrders(1)
if (result is ResOrders) {
ordersLiveData.postValue(Resource.Success(result))
result.orders?.let {
callback.onResult(it, null, 2)
}
} else if (result is BaseError) {
ordersLiveData.postValue(Resource.Error(result))
}
}
} else {
ordersLiveData.postValue(Resource.Loading(EnumLoading.LOADING_CLOSE))
}
}
override fun loadAfter(
params: LoadParams<Int>,
callback: LoadCallback<Int, OrdersItem>
) {
if (BaseApp.instance?.let { NetworkUtils.isNetworkAvailable(it) } == true) {
if (showProgress) {
ordersLiveData.postValue(Resource.Loading(EnumLoading.LOADING_MORE))
}
coroutineScope?.launch {
val result = orderRepository.getOrders(params.key)
if (result is ResOrders) {
ordersLiveData.postValue(Resource.Success(result))
result.orders?.let { callback.onResult(it, params.key + 1) }
} else if (result is BaseError) {
ordersLiveData.postValue(Resource.Error(result))
}
}
} else {
ordersLiveData.postValue(Resource.Loading(EnumLoading.LOADING_CLOSE))
}
}
override fun loadBefore(
params: LoadParams<Int>,
callback: LoadCallback<Int, OrdersItem>
) {
}
}
OrderListDataSourceFactory
class OrderListDataSourceFactory(
private val productRepository: OrderRepository, private val coroutineScope: CoroutineScope?
) : DataSource.Factory<Int, OrdersItem>() {
val ordersLiveData = MutableLiveData<OrderListDataSource>()
private var showProgress: Boolean = true
private var orderListDataSource: OrderListDataSource? = null
override fun create(): DataSource<Int, OrdersItem> {
orderListDataSource = OrderListDataSource(
productRepository, coroutineScope,
showProgress
)
ordersLiveData.postValue(orderListDataSource)
return orderListDataSource!!
}
}
OrderListViewModel
private val pageSize = 10
val listHasData = MutableLiveData<Boolean>().apply { value = true }
private val orderListDataSourceFactory: OrderListDataSourceFactory =
OrderListDataSourceFactory(orderRepository, viewModelScope)
init {
val config = PagedList.Config.Builder()
.setPageSize(pageSize)
.setInitialLoadSizeHint(pageSize * 2)
.setEnablePlaceholders(false)
.build()
orderItem = LivePagedListBuilder(orderListDataSourceFactory, config).build()
}
OrdersAdapter
class OrdersAdapter constructor(private val itemTouchListener: ItemTouchListener) :
PagedListAdapter<OrdersItem, OrderViewHolder>(DiffUtilCallBack()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder {
val mItemProductBinding = DataBindingUtil.inflate<ViewDataBinding>(
LayoutInflater.from(parent.context),
R.layout.item_order,
parent,
false
)
return OrderViewHolder(mItemProductBinding)
}
override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
getItem(position)?.let {
holder.bind(it)
val itemClickListener: View.OnClickListener = View.OnClickListener { _ ->
itemTouchListener.onOrderClick(position, it)
}
holder.setItemClickListener(itemClickListener)
}
}
class DiffUtilCallBack : DiffUtil.ItemCallback<OrdersItem?>() {
override fun areItemsTheSame(
oldItem: OrdersItem,
newItem: OrdersItem
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: OrdersItem,
newItem: OrdersItem
): Boolean {
return oldItem.id == newItem.id
&& oldItem.orderDate.equals(newItem.orderDate)
&& oldItem.orderId.equals(newItem.orderId)
&& oldItem.totalPrice == newItem.totalPrice
&& oldItem.deliveryStatus.equals(newItem.deliveryStatus)
}
}
interface ItemTouchListener {
fun onOrderClick(position: Int, ordersItem: OrdersItem)
}
}
class OrderViewHolder constructor(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(ordersItem: OrdersItem) {
binding.setVariable(BR.order, ordersItem)
binding.executePendingBindings()
}
fun setItemClickListener(clickListener: View.OnClickListener) {
binding.root.lOrder.setOnClickListener(clickListener)
}
}
OrderListingFragment
mViewModel.orderItem.observe(this, Observer {
ordersAdapter.submitList(it)
})}
mViewModel.getOrders().observe(this, Observer {
...
}
Can anyone please help me with this?
Thank you in advance!
The issue was because of Skeleton I have used with RecyclerView.
mSkeleton?.hide()
This line was called every time I got the API response.
Ideally, the Skeleton will be displayed initially. So, this line should be called only once. I handled this case and now it's working as expected.
Related
RecyclerView
class RecyclerViewAdapter: RecyclerView.Adapter<RecyclerViewAdapter.RepoViewHolder>(){
inner class RepoViewHolder(val binding: RepoelementBinding): RecyclerView.ViewHolder(binding.root)
private val diffCallBack = object : DiffUtil.ItemCallback<userReposItem>(){
override fun areItemsTheSame(oldItem: userReposItem, newItem: userReposItem): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: userReposItem, newItem: userReposItem): Boolean {
return oldItem == newItem
}
}
private val differ= AsyncListDiffer(this, diffCallBack)
var repos: List<userReposItem>
get()=differ.currentList
set(value) {differ.submitList(value)}
override fun getItemCount() = repos.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepoViewHolder {
return RepoViewHolder(RepoelementBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
))
}
override fun onBindViewHolder(holder: RepoViewHolder, position: Int) {
holder.binding.apply{
val repo = repos[position]
urlOfRepo.text = repo.url
nameOfRepo.text= repo.description
}
}
}
RepoService
private const val BASE_URL = "https://api.github.com/"
//private const val ENDPOINT_URL= "https://api.github.com/users/${username}/repos"
interface GitHubReposAPIService{
//using only GetUserRepos
#GET("/users/{username}/repos")
suspend fun getUserRepos(#Path("username") username: String) : Response<List<userReposItem>>
#GET(BASE_URL+"users/{username}/{repo}/language")
suspend fun getLanguages(#Path("username") username:String,
#Path("repo") repo: String) : String
}
object UserRepoAPI{
private val moshi= Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val retrofitService : GitHubReposAPIService by lazy {
Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
.create(GitHubReposAPIService::class.java)
}
//val retrofitService : GitHubReposAPIService by lazy {
retrofit.create(GitHubReposAPIService::class.java)}
}
Activity with List
const val TAG= "Repos"
class UserProfile : AppCompatActivity() {
private lateinit var userName: String
private lateinit var Repos : RecyclerViewAdapter
private lateinit var binding: ActivityUserProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// val binding: ViewDataBinding? = DataBindingUtil.setContentView(this,
R.layout.activity_user_profile)
binding= ActivityUserProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
userName = intent.getStringExtra("username").toString()
val viewUserName= findViewById<TextView>(R.id.userDisplay)
viewUserName.text=userName
setupRepos()
lifecycleScope.launchWhenCreated {
val response=try{
UserRepoAPI.retrofitService.getUserRepos(userName)
} catch (e: IOException){
Log.e(TAG, "Might not have internet connection")
return#launchWhenCreated
} catch (e: HttpException){
Log.e(TAG, "HttpException, invalid response")
return#launchWhenCreated
}
if(response.isSuccessful && response.body()!=null){
Repos.repos=response.body()!!
}else{
Log.e(TAG, "Response not succesful")
}
}
}
private fun setupRepos() = binding.recyclerView?.apply{
Repos = RecyclerViewAdapter()
adapter = Repos
layoutManager = LinearLayoutManager(this#UserProfile)
}
}
Data class
I only want two parameters ( url of repo and its description). There is many more information, but I only need those 2 for every repo.
#JsonClass(generateAdapter = true)
#JsonIgnoreProperties(ignoreUnknown = true)
data class userReposItem(
#Json(name = "description")
val description: String? = "",
#Json(name = "url")
val url: String? = "",
) : Parcelable{
constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readString()!!
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(description)
parcel.writeString(url)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<userReposItem> {
override fun createFromParcel(parcel: Parcel): userReposItem {
return userReposItem(parcel)
}
override fun newArray(size: Int): Array<userReposItem?> {
return arrayOfNulls(size)
}
}
}
Layouts:
Main layout for printing list of repos. At the top of layout we have name of user and below should be list of repos (but as I mentioned it doesnt work)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com
/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".UserProfile">
<TextView
android:id="#+id/userDisplay"
android:layout_width="374dp"
android:layout_height="38dp"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="21dp"
android:text="#string/username"
app:layout_constraintBottom_toTopOf="#+id/recyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="409dp"
android:layout_height="547dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
List item
I would like to print url of repo and its name ("description" as shown in github API)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/urlOfRepo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="89dp"
android:layout_marginBottom="489dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/nameOfRepo" />
<TextView
android:id="#+id/nameOfRepo"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/urlOfRepo" />
</LinearLayout>
I hope is all well with you.
I have constructed my code and build according the MVP style as well pulling JSON data but when I run the the code the horizontal recycle view and the JSON data is not being displayed. I tried going through every line of code and watching other tutorials but still no results.
Here is below my main activity:
class ProductCategoryActivity : BaseMvpActivity<ProductCategoryActivityView, ProductCategoryActivityPresnter> (),
ProductCategoryActivityView, CategoryAdapter.onItemClickListener{
private lateinit var binding: FragmentProductCategoryBinding
val data :MutableList<CateogryResponse> = ArrayList()
val adapter= CategoryAdapter(data, this)
#Inject
lateinit var presenter: ProductCategoryActivityPresnter
#Inject
lateinit var progressDialog: ProgressDialog
override fun onCreateComponent() {
userComponent.plus(CategoryActivityModule(this)).inject(this)
}
override fun providePresenter(): ProductCategoryActivityPresnter {
return presenter
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = FragmentProductCategoryBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
presenter.fetchcatogries()
showdata(ArrayList())
}
override fun showError(message: String) {
}
override fun showProgress() {
}
override fun hideProgress() {
}
override fun showdata(data: ArrayList<CateogryResponse>) {
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recyclerViewPrimary.layoutManager = layoutManager
recyclerViewPrimary.setHasFixedSize(true)
recyclerViewPrimary.adapter = adapter
for(i in data)
data.add(CateogryResponse("product"))
}
override fun onItemClick(position: Int) {
Toast.makeText(this, "Item $position clicked", Toast.LENGTH_SHORT).show()
val clickedItem = data[position]
adapter.notifyItemChanged(position)
}
}
Here is my presenter:
#ActivityScope
class ProductCategoryActivityPresnter #Inject constructor(
private val stringProvider: StringProvider,
#AndroidScheduler private val observeOnScheduler: Scheduler,
#IOScheduler private val subscribeOnScheduler: Scheduler,
private val getCategoryUseCase: CategoryUseCase
) : BasePresenter<ProductCategoryActivityView>() {
lateinit var catogriesservice: CategoryRepositoryImpl
val catogriesLoadError = MutableLiveData<Boolean>()
val loading = MutableLiveData<Boolean>()
var catogries: ArrayList<CateogryResponse> = arrayListOf()
override fun onCreatePresenter(savedInstanceState: Bundle?) {
}
override fun onSaveInstanceState(outState: Bundle?) {
}
override fun onLoadData(arguments: Bundle?) {
fetchcatogries()
}
fun fetchcatogries() {
getCategoryUseCase.execute()
.observeOn(observeOnScheduler)
.subscribeOn(subscribeOnScheduler)
.subscribe(
SingleRequestSubscriber(
{
it
if (it != null && it.size > 0) {
catogries.removeAll(catogries)
catogries.addAll(it)
}
},
onFailure = { appException ->
view?.showError(
ErrorHandler.getErrorMessage(
appException,
stringProvider
)
)
},
onApiError = { apiException ->
view?.showError(
ErrorHandler.getErrorMessage(
apiException,
stringProvider
)
)
},
onAuthenticationError = { requestxception ->
view?.showError(
ErrorHandler.getErrorMessage(
requestxception,
stringProvider
)
)
},
onShowProgress = {
if (it) {
view?.showProgress()
} else {
view?.hideProgress()
}
},
onSubscribed = {
disposable.add(it)
})
)
}
}
Here is my view:
interface ProductCategoryActivityView {
fun showError(message: String)
fun showProgress()
fun hideProgress()
fun showdata ( data: ArrayList<CateogryResponse>)
}
Here is my adapter:
class CategoryAdapter(
private val data: List<CateogryResponse>,
private val listener: onItemClickListener
) : RecyclerView.Adapter<CategoryAdapter.ViewHolder>() {
private val items: MutableList<CardView>
init {
this.items = ArrayList()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_category_adapter, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.tvTitle.text = data[position].product
items.add(holder.card)
}
override fun getItemCount(): Int {
return data.size
}
inner class ViewHolder(itemView: View
) : RecyclerView.ViewHolder(itemView),
View.OnClickListener {
val tvTitle: TextView = itemView.featured_title
val card: CardView = itemView.CardView
init {
itemView.setOnClickListener(this)
}
override fun onClick(v: View?) {
val position: Int = adapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onItemClick(position)
}
}
}
interface onItemClickListener {
fun onItemClick(position: Int)
}
}
Image:
Fragment Category Adapter:
android:id="#+id/CardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:cardCornerRadius="2dp"
app:cardElevation="8dp">
<!-- We Will Add here the card & ImageViews -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="15dp">
<ImageView
android:id="#+id/featured_image"
android:layout_width="match_parent"
android:layout_height="140dp"
android:scaleType="centerCrop" />
<TextView
android:id="#+id/featured_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineHeight="23dp"
android:text="Chairs"
android:textColor="#color/colorAccent"
android:textSize="20sp" />
<TextView
android:id="#+id/featured_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="asbkd asudhlasn saudnas jasdjasl hisajdl asjdlnas" />
</LinearLayout>
</androidx.cardview.widget.CardView>
Fragment Product Category:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fafcfe"
tools:context="ui.category.ProductCategoryActivity">
<ImageView
android:id="#+id/imageView3"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
<ImageView
android:id="#+id/imageView"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="#+id/imageView"
app:layout_constraintTop_toTopOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerViewPrimary"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginTop="20dp"
android:text="#string/topselling"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/constraintLayout" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintLayout2"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="#+id/imageView3"
app:layout_constraintEnd_toEndOf="#+id/constraintLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerViewSecondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="#+id/imageView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="#+id/imageView3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
/>
<TextView
android:id="#+id/textViewCategories"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="15dp"
android:text="#string/catogries"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="#+id/constraintLayout"
app:layout_constraintStart_toStartOf="parent"
tools:layout_editor_absoluteX="39dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Not sure what went wrong.
If someone can point it out or show me what needs to be done to be able to display the data in horzintal recycle view, I would be really thanlful
You should update your recycler view adapter once your fetch call is executed, not before it ends.
Modify your fetchcatogries declaration so it accepts a callback method, which will be executed after the data are loaded.
This method will accept a list object as a parameter, which you will manage in your ProductCategoryActivity to populate the RecyclerView.
fun fetchcatogries(callback: (MutableList<CateogryResponse>) -> Unit) {
getCategoryUseCase.execute()
.observeOn(observeOnScheduler)
.subscribeOn(subscribeOnScheduler)
.subscribe(
SingleRequestSubscriber(
{ categories ->
callback(categories)
},
...
)
}
You can now edit your onCreate and showdata methods in ProductCategoryActivity like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = FragmentProductCategoryBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
presenter.fetchcatogries { categories ->
showdata(categories as ArrayList<CateogryResponse>)
}
}
override fun showdata(data: ArrayList<CateogryResponse>) {
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recyclerViewPrimary.layoutManager = layoutManager
recyclerViewPrimary.setHasFixedSize(true)
recyclerViewPrimary.adapter = CategoryAdapter(data.toList(), this)
}
I'm trying to make simple app to show list of cocktails using data binding and retrofit. I can see by logging interceptor request is 200 but when i debug i can see the result list is null.debug screenshot
responce screenshot
Fragment class
class CocktailListFragment : Fragment() {
private val viewModel: CocktailListViewModel by lazy {
ViewModelProvider(this).get(CocktailListViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = CocktailListFragmentBinding.inflate(inflater)
binding.lifecycleOwner = this
setHasOptionsMenu(true)
binding.cocktail = viewModel
binding.cocktailList.adapter = CocktailListAdapter()
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.filter_menu, menu)
super.onCreateOptionsMenu(menu, inflater)
}
}
ViewModel
class CocktailListViewModel : ViewModel() {
private val _cocktails = MutableLiveData<CocktailsList>()
val cocktails: LiveData<CocktailsList>
get() = _cocktails
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main)
init {
getCocktails()
}
private fun getCocktails() {
coroutineScope.launch {
val getCocktailsDeferred = CocktailsApi.RETROFIT_SERVICE.getCocktailsAsync()
try {
val result = getCocktailsDeferred.await()
_cocktails.value = result
} catch (e: Exception) {
Log.d("error", "error")
}
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Adapter
class CocktailListAdapter :
ListAdapter<Cocktail, CocktailListAdapter.CocktailListViewHolder>(DiffCallback) {
class CocktailListViewHolder(private var binding: CocktailItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(cocktail: Cocktail) {
binding.cocktail = cocktail
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CocktailListViewHolder {
return CocktailListViewHolder(CocktailItemBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: CocktailListViewHolder, position: Int) {
val cocktail = getItem(position)
holder.bind(cocktail)
}
companion object DiffCallback : DiffUtil.ItemCallback<Cocktail>() {
override fun areItemsTheSame(oldItem: Cocktail, newItem: Cocktail): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Cocktail, newItem: Cocktail): Boolean {
return oldItem.id == newItem.id
}
}
}
BindingAdapters
#BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: CocktailsList?) {
val adapter = recyclerView.adapter as CocktailListAdapter
adapter.submitList(data?.list)
}
#BindingAdapter("strDrinkThumb")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Picasso.get()
.load(imgUri)
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image)
.into(imgView)
}
}
ApiService
private const val BASE_URL =
"https://www.thecocktaildb.com"
private val gson = GsonConverterFactory.create(GsonBuilder().create())
val logging = HttpLoggingInterceptor()
val okhttpClient = OkHttpClient.Builder()
.addInterceptor(logging.setLevel(HttpLoggingInterceptor.Level.BODY))
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okhttpClient)
.addConverterFactory(gson)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
interface CocktailsApiService {
#GET("./api/json/v1/1/filter.php?c=Ordinary_Drink")
fun getCocktailsAsync():
Deferred<CocktailsList>
}
object CocktailsApi {
val RETROFIT_SERVICE: CocktailsApiService by lazy {
retrofit.create(CocktailsApiService::class.java)
}
}
data classes
data class CocktailsList(
val list: List<Cocktail>
)
data class Cocktail (
#SerializedName("idDrink")
val id: String,
#SerializedName("strDrinkThumb")
val drinkImg: String,
#SerializedName("strDrink")
val drinkTitle: String
)
cocktail_list_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="cocktail"
type="com.example.coctaildb.cocktaillist.CocktailListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appBarLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#android:color/white"
app:menu="#menu/filter_menu"
app:title="#string/drinks"
app:titleTextColor="#android:color/black" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/cocktail_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/appBarLayout2"
app:listData="#{cocktail.cocktails}"
tools:listitem="#layout/cocktail_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
cocktail_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="cocktail"
type="com.example.coctaildb.network.Cocktail" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/cocktail_img"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
app:strDrinkThumb="#{cocktail.drinkImg}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="#tools:sample/avatars" />
<TextView
android:id="#+id/cocktail_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="21dp"
android:layout_marginEnd="20dp"
android:fontFamily="#font/roboto"
android:text="#{cocktail.drinkTitle}"
android:textColor="#7E7E7E"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="#+id/cocktail_img"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/cocktail_img"
app:layout_constraintTop_toTopOf="#+id/cocktail_img" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I suppose i messed up with data type in binding, please help.
I think problem is in serialisation. try
data class CocktailsList(
#SerializedName("drinks")
val list: List<Cocktail>
)
I have a list of 5 elements in a recyclerview, set up like a to do list. There is a listener on the checkbox in each row, and for the purposes of this minimal reproducible example whenever you check any boxes it randomly sets the value of the 5 checkboxes. When an item is unchecked, it should appear in black text, and when an item is checked it should appear in gray text and italic.
When I check a box and reset the values, usually the UI updates as expected. However, sometimes one item sticks in the wrong layout so the checkbox shows the correct value but the text style is wrong. Why is this behavior inconsistent and how can I ensure the UI is refreshed every time?
Here's the entire MRE:
MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.dalydays.android.mre_recyclerview_refresh_last_item.databinding.ActivityMainBinding
import kotlin.random.Random
class MainActivity : AppCompatActivity() {
private lateinit var adapter: ToDoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
binding.itemsList.layoutManager = LinearLayoutManager(this)
val onCheckboxClickListener: (ToDoItem) -> Unit = { _ ->
adapter.submitList(getSampleList())
}
adapter = ToDoAdapter(onCheckboxClickListener)
binding.itemsList.adapter = adapter
adapter.submitList(getSampleList())
}
private fun getSampleList(): List<ToDoItem> {
val sampleList = mutableListOf<ToDoItem>()
sampleList.add(ToDoItem(id=1, description = "first item", completed = Random.nextBoolean()))
sampleList.add(ToDoItem(id=2, description = "second item", completed = Random.nextBoolean()))
sampleList.add(ToDoItem(id=3, description = "third item", completed = Random.nextBoolean()))
sampleList.add(ToDoItem(id=4, description = "fourth item", completed = Random.nextBoolean()))
sampleList.add(ToDoItem(id=5, description = "fifth item", completed = Random.nextBoolean()))
return sampleList
}
}
ToDoItem.kt
data class ToDoItem(
var id: Long? = null,
var description: String,
var completed: Boolean = false
)
ToDoAdapter.kt
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.dalydays.android.mre_recyclerview_refresh_last_item.databinding.ChecklistItemCheckedBinding
import com.dalydays.android.mre_recyclerview_refresh_last_item.databinding.ChecklistItemUncheckedBinding
const val ITEM_UNCHECKED = 0
const val ITEM_CHECKED = 1
class ToDoAdapter(private val onCheckboxClick: (ToDoItem) -> Unit): ListAdapter<ToDoItem, RecyclerView.ViewHolder>(ToDoItemDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_CHECKED -> ViewHolderChecked.from(parent)
else -> ViewHolderUnchecked.from(parent)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val toDoItem = getItem(position)
when (holder) {
is ViewHolderChecked -> {
holder.bind(toDoItem, onCheckboxClick)
}
is ViewHolderUnchecked -> {
holder.bind(toDoItem, onCheckboxClick)
}
}
}
override fun getItemViewType(position: Int): Int {
val toDoItem = getItem(position)
return when (toDoItem.completed) {
true -> ITEM_CHECKED
else -> ITEM_UNCHECKED
}
}
class ViewHolderChecked private constructor(private val binding: ChecklistItemCheckedBinding)
: RecyclerView.ViewHolder(binding.root) {
fun bind(toDoItem: ToDoItem, onCheckboxClick: (ToDoItem) -> Unit) {
binding.todoItem = toDoItem
binding.checkboxCompleted.setOnClickListener {
onCheckboxClick(toDoItem)
}
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolderChecked {
val layoutInflater = LayoutInflater.from(parent.context)
return ViewHolderChecked(ChecklistItemCheckedBinding.inflate(layoutInflater, parent, false))
}
}
}
class ViewHolderUnchecked private constructor(private val binding: ChecklistItemUncheckedBinding)
: RecyclerView.ViewHolder(binding.root) {
fun bind(toDoItem: ToDoItem, onCheckboxClick: (ToDoItem) -> Unit) {
binding.todoItem = toDoItem
binding.checkboxCompleted.setOnClickListener {
onCheckboxClick(toDoItem)
}
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolderUnchecked {
val layoutInflater = LayoutInflater.from(parent.context)
return ViewHolderUnchecked(ChecklistItemUncheckedBinding.inflate(layoutInflater, parent, false))
}
}
}
}
class ToDoItemDiffCallback : DiffUtil.ItemCallback<ToDoItem>() {
override fun areItemsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
return oldItem == newItem
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<RelativeLayout
android:id="#+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/items_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:scrollbars="none" />
</RelativeLayout>
</layout>
checklist_item_checked.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="todoItem"
type="com.dalydays.android.mre_recyclerview_refresh_last_item.ToDoItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="#+id/checkbox_completed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:checked="#{todoItem.completed}"
android:textAppearance="?attr/textAppearanceListItem"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/tv_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:text="#{todoItem.description}"
android:textAppearance="?attr/textAppearanceListItem"
android:textColor="#65000000"
android:textStyle="italic"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/checkbox_completed"
app:layout_constraintTop_toTopOf="parent"
tools:text="Mow the lawn" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
checklist_item_unchecked.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="todoItem"
type="com.dalydays.android.mre_recyclerview_refresh_last_item.ToDoItem" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="#+id/checkbox_completed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:checked="#{todoItem.completed}"
android:textAppearance="?attr/textAppearanceListItem"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/tv_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:text="#{todoItem.description}"
android:textAppearance="?attr/textAppearanceListItem"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/checkbox_completed"
app:layout_constraintTop_toTopOf="parent"
tools:text="Mow the lawn" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Modify these methods
override fun areItemsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ToDoItem, newItem: ToDoItem): Boolean {
return ((oldItem.id == newItem.id) && (oldItem.description == newItem.description) && (oldItem.completed == newItem.completed)
}
Also override the methods getNewListSize() & getOldListSize()
Maybe not the "best" solution, but this is what I came up with. I determined that there is probably an issue with the timing between the checkbox animation and the recyclerview refresh animation. Sometimes, depending on how long it takes the recyclerview to diff, the recyclerview can attempt to refresh before or after the checkbox finishes animating. When it finishes before, the checkbox animation blocks the recyclerview animation and leaves the UI in the wrong state. Otherwise it appears to work as intended.
I decided to manually run adapter.notifyItemChanged(position) even though RecyclerView.ListAdapter is supposed to handle this automatically. It still animates somewhat inconsistently depending on when the diff finishes calculating, but it's much better than leaving the UI in a bad state, and it's much better than refreshing the entire list every time with notifyDataSetChanged().
In MainActivity, change the checkboxlistener to this:
val onCheckboxClickListener: (ToDoItem, Int) -> Unit = { _, position ->
adapter.submitList(getSampleList())
adapter.notifyItemChanged(position)
}
In ToDoAdapter, change class header to this:
class ToDoAdapter(private val onCheckboxClick: (ToDoItem, Int) -> Unit): ListAdapter<ToDoItem, RecyclerView.ViewHolder>(ToDoItemDiffCallback()) {
And change both bind() functions to this:
fun bind(toDoItem: ToDoItem, onCheckboxClick: (ToDoItem, Int) -> Unit) {
binding.todoItem = toDoItem
binding.checkboxCompleted.setOnClickListener {
onCheckboxClick(toDoItem, layoutPosition)
}
binding.executePendingBindings()
}
I'm struggling with glide loading custom item layout with groupie. Being particular - on item bind when I use glide to load images (images are ~12kb). Experiencing a "frozen frame". Profiler shows big CPU usage jump on the moment when glide loads images. It lags even if I add 3 items at a time but I need to add more (there is not much difference in cpu usage between 3 and 48 items so it freezes for about the same time). Tested with loading it from Resources/Precached/Direct download, with and without RequestOptions() - everything is the same
Some code related to the issue:
HomeScreen.kt
var subscription = 0
//Transformation
var loading = false
var itemWidth = 120
class HomeScreen : AppCompatActivity() {
val categoriesReference = FirebaseDatabase.getInstance().getReference("/categories")
var photos = ArrayList<PhotoItem>()
val adapter = GroupAdapter<ViewHolder>()
var itemsAllowed = 48
lateinit var manager: GridLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home_screen)
itemWidth = getItemWidth(this#HomeScreen)
setSupportActionBar(toolbar)
window.exitTransition = null
//Action bar options
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.title = "Все обои"
//FIX BLINKING ON TRANSITIONS
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
/*var fade = Fade()
fade.excludeTarget(toolbar, true)
fade.excludeTarget(toolbar2, true)
fade.excludeTarget(android.R.id.statusBarBackground, true);
fade.excludeTarget(android.R.id.navigationBarBackground, true);
getWindow().setEnterTransition(fade)
getWindow().setExitTransition(fade)*/
}
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_FULLSCREEN)
//Working with layout
manager = GridLayoutManager(this, calculateNoOfColumns(this#HomeScreen))
adapter.setHasStableIds(true)
recycleView.isNestedScrollingEnabled = false
recycleView.adapter = adapter
recycleView.layoutManager = manager
loadPageFully()
addItemsToMenu()
//Listener for navigation view
navigView.setNavigationItemSelectedListener {
/*if (it.toString() == "Купить тариф") {
subscription = 1
alert("Тариф успешно установлен") {
navigView.menu.clear()
addItemsToMenu()
loadPageFully()
yesButton { }
supportActionBar?.title = "Все обои"
}.show()
true
} else {*/
when(it.title.toString()){
"Все обои" -> {
loadPageFully()
}
}
drawerLayoutMain.closeDrawer(GravityCompat.START, true)
for (i in 0 until navigView.menu.size()) {
navigView.menu.getItem(i).setChecked(false)
}
it.setChecked(true)
loadPageFully(it.toString())
loadingImageView.visibility = View.GONE
waveImageView.visibility = View.GONE
supportActionBar?.title = it.toString()
true
// }
}
//Shuffle images in layout manager
shuffleButton.onClick {
photos.shuffle()
adapter.clear()
photos.forEach {
if (manager.itemCount < itemsAllowed) {
adapter.add(it)
}
}
}
scrollView3.setOnScrollChangeListener(object : View.OnScrollChangeListener {
override fun onScrollChange(v: View?, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) {
println(loading.toString())
if (!loading && !scrollView3.canScrollVertically(1) && manager.itemCount > 0) {
loading = true
if (itemsAllowed == photos.size) {
} else if (itemsAllowed + 48 >= photos.size) {
for (i in 48.downTo(0)) {
if (itemsAllowed + i == photos.size) {
itemsAllowed += i
waveImageView.visibility = View.VISIBLE
doAsync {
Thread.sleep(300)
scrollView3.fullScroll(View.FOCUS_DOWN)
}
}
}
} else {
waveImageView.visibility = View.VISIBLE
doAsync {
Thread.sleep(300)
scrollView3.fullScroll(View.FOCUS_DOWN)
}
itemsAllowed += 48
}
checkOk()
}
}
})
recycleView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
SCROLL_STATE_IDLE -> Glide.with(this#HomeScreen).resumeRequests()
SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING -> Glide.with(this#HomeScreen).pauseRequests()
}
}
})
}
//On home button clicked
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
drawerLayoutMain.openDrawer(GravityCompat.START)
true
}
else -> super.onOptionsItemSelected(item)
}
}
fun getEntries(categoryName: String = "All") {
//This fun with ChildEventListeners(Firebase SDK) gets urls and pushes them to photos no problem here because by the time lags occur they all are in photos.
}
var okToLoad = false
var loaded = 0
fun loadImages() {
if (okToLoad) {
okToLoad = false
if (photos.size < itemsAllowed) {
itemsAllowed = photos.size
}
loading = true
try {
for (i in manager.findLastVisibleItemPosition() + 1 until itemsAllowed) {
Glide.with(this#HomeScreen).load(photos[i].url).apply(
RequestOptions().diskCacheStrategy(
DiskCacheStrategy.ALL
)/*.skipMemoryCache(true)*/
).apply(RequestOptions().dontTransform()/*.override(220, 330)*/)
.listener(object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: com.bumptech.glide.request.target.Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
loaded += 1
println("1:$loaded")
println("2:$itemsAllowed")
if (loaded == itemsAllowed) {
Timer().schedule(object : TimerTask() {
override fun run() {
runOnUiThread {
waveImageView.visibility = View.GONE
shuffleButton.visibility = View.VISIBLE
okToLoad = true
loading = false
loadingImageView.visibility = View.GONE
for (i in manager.findLastVisibleItemPosition() + 1 until loaded) {
adapter.add(photos[i])
}
}
}
}, 1000)
}
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: com.bumptech.glide.request.target.Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return false
}
}).preload(220, 330)
}
} catch (e: IndexOutOfBoundsException) {
} catch (e: FileNotFoundException) {
Toast.makeText(this#HomeScreen, "Квота превышена", Toast.LENGTH_LONG).show()
}
}
}
fun checkOk() {
doAsync {
if (photos.size == 0) {
okToLoad = false
Thread.sleep(100)
checkOk()
} else {
okToLoad = true
uiThread {
loadImages()
}
}
}
}
fun addItemsToMenu() {
categoriesReference.addChildEventListener(object : ChildEventListener {
override fun onChildAdded(p0: DataSnapshot, p1: String?) {
navigView.menu.add(p0.key.toString()).setIcon(R.drawable.ic_bookmark_black_24dp)
}
override fun onCancelled(p0: DatabaseError) = Unit
override fun onChildChanged(p0: DataSnapshot, p1: String?) = Unit
override fun onChildMoved(p0: DataSnapshot, p1: String?) = Unit
override fun onChildRemoved(p0: DataSnapshot) = Unit
})
}
fun loadPageFully(key: String = "All") {
shuffleButton.visibility = View.GONE
loaded = 0
okToLoad = false
loadingImageView.visibility = View.VISIBLE
doAsync {
Glide.get(this#HomeScreen).clearDiskCache()
}
Glide.get(this#HomeScreen).clearMemory()
manager.removeAndRecycleAllViews(recycleView.Recycler())
adapter.clear()
photos.clear()
getEntries(key)
checkOk()
}
fun calculateNoOfColumns(context: Context): Int {
val displayMetrics = context.getResources().getDisplayMetrics();
val dpWidth = displayMetrics.widthPixels / displayMetrics.density;
val scalingFactor = 110 // You can vary the value held by the scalingFactor
// variable. The smaller it is the more no. of columns you can display, and the
// larger the value the less no. of columns will be calculated. It is the scaling
// factor to tweak to your needs.
var columnCount = (dpWidth / scalingFactor).toInt()
if (columnCount < 3) columnCount = 3
return columnCount // if column no. is less than 2, we still display 2 columns
}
fun getItemWidth(context: Context): Int {
val displayMetrics = context.resources.displayMetrics
val dpWidth = displayMetrics.widthPixels
return (dpWidth / calculateNoOfColumns(context)).toInt()
}
}
val factory = DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build()
//Layout item for adapter
class PhotoItem(
val cnt: Context,
var url: String,
var downloads: Int,
var ref: DatabaseReference,
var locked: Boolean
) : Item<ViewHolder>() {
override fun bind(viewHolder: ViewHolder, position: Int) {
viewHolder.itemView.itemImageView.layoutParams.width = itemWidth
viewHolder.itemView.itemImageView.layoutParams.height = itemWidth
Glide.with(cnt)
.load(url)
.apply(RequestOptions().placeholder(R.drawable.placeholder).format(DecodeFormat.PREFER_RGB_565))/*transition(DrawableTransitionOptions.withCrossFade(factory)).apply(RequestOptions().placeholder(R.drawable.background))*//**//*.skipMemoryCache(true)*//**//*priority(Priority.HIGH))*//**//*.listener(
object : RequestListener<Bitmap> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: com.bumptech.glide.request.target.Target<Bitmap>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Bitmap?,
model: Any?,
target: Target<Bitmap>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
return false
}
})*/.into(viewHolder.itemView.itemImageView)
viewHolder.itemView.itemImageView.transitionName = url
viewHolder.itemView.setOnClickListener {
var intent = Intent(cnt, PhotoScreen::class.java).apply {
putExtra("url", url)
putExtra("ref", ref.toString())
putExtra("downs", downloads.toString())
putExtra("locked", locked.toString())
}
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
cnt as AppCompatActivity,
viewHolder.itemView.itemImageView,
viewHolder.itemView.itemImageView.transitionName
)
cnt.startActivity(intent, options.toBundle())
}
}
override fun getLayout(): Int {
return R.layout.image_item
}
Home
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/drawerLayoutMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".HomeScreen"
android:background="#drawable/background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent" android:id="#+id/relativeLayout"
>
<androidx.core.widget.NestedScrollView
android:layout_width="0dp"
android:layout_height="0dp"
tools:layout_conversion_absoluteHeight="598dp"
tools:layout_conversion_absoluteWidth="384dp" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="#+id/toolbar"
android:id="#+id/scrollView3"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintBottom_toBottomOf="parent"
android:overScrollMode="ifContentScrolls"
android:fillViewport="false">
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:id="#+id/linearLayout"
android:descendantFocusability="blocksDescendants"
android:layout_marginLeft="0dp" android:layout_marginRight="0dp"
android:layout_gravity="fill">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="wrap_content"
android:layout_height="-100dp"
android:layout_marginTop="0dp" android:id="#+id/recycleView"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_marginRight="0dp"
android:focusableInTouchMode="true"
android:scrollbars="vertical"
android:layout_gravity="center_horizontal|fill"
>
</androidx.recyclerview.widget.RecyclerView>
<pl.droidsonroids.gif.GifImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/waveImageView" android:src="#drawable/intro_image"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone" android:background="#drawable/background"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<androidx.appcompat.widget.Toolbar android:id="#+id/toolbar" android:layout_width="0dp"
android:layout_height="45dp"
android:background="#drawable/background"
app:titleTextColor="#android:color/white"
android:textAlignment="gravity"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:isScrollContainer="false"
android:fitsSystemWindows="false"
tools:layout_conversion_absoluteHeight="42dp"
tools:layout_conversion_absoluteWidth="384dp"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="#drawable/ic_list_black_24dp">
</androidx.appcompat.widget.Toolbar>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" app:srcCompat="#drawable/ic_shuffle_black_24dp"
android:id="#+id/shuffleButton"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="#+id/toolbar"
android:padding="10dp"
app:layout_constraintBottom_toTopOf="#+id/scrollView3"
android:focusedByDefault="true"
/>
<pl.droidsonroids.gif.GifImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/loadingImageView" android:visibility="visible"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent" android:src="#drawable/placeholder_gif"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.navigation.NavigationView
android:layout_width="250dp"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="#layout/header_menu"
android:theme="#style/ThemeOverlay.AppCompat.ActionBar" android:id="#+id/navigView"
app:itemBackground="#drawable/menu_item_background"
android:background="#drawable/menu_bitmap"/>
</androidx.drawerlayout.widget.DrawerLayout>
ImageItem layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<ImageView
android:layout_width="110dp"
android:layout_height="110dp"
android:id="#+id/itemImageView"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"
android:scaleType="centerCrop"/>
</FrameLayout>
OnMeasure() and nativePollOnce() methods takes a lot as I see. Still, don't know what to do with it
Solved it
The main problem was that groupie adapter as for now makes diffs on UI thread. And that caused lags. Switching to another library may help you. I've switched to Epoxy by airbnb
P.S. I had nested recyclerview in scrollview. Removing scrollview added more performance