简体   繁体   English

RecyclerView 在 notifyDatasetChanged() 之后闪烁

[英]RecyclerView blinking after notifyDatasetChanged()

I have a RecyclerView which loads some data from API, includes an image url and some data, and I use.networkImageView to lazy load image.我有一个 RecyclerView,它从 API 加载一些数据,包括图像 url 和一些数据,我使用 .networkImageView 来延迟加载图像。

@Override
public void onResponse(List<Item> response) {
   mItems.clear();
   for (Item item : response) {
      mItems.add(item);
   }
   mAdapter.notifyDataSetChanged();
   mSwipeRefreshLayout.setRefreshing(false);
}

Here is implementation for Adapter:这是适配器的实现:

public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int position) {
        if (isHeader(position)) {
            return;
        }
        // - get element from your dataset at this position
        // - replace the contents of the view with that element
        MyViewHolder holder = (MyViewHolder) viewHolder;
        final Item item = mItems.get(position - 1); // Subtract 1 for header
        holder.title.setText(item.getTitle());
        holder.image.setImageUrl(item.getImg_url(), VolleyClient.getInstance(mCtx).getImageLoader());
        holder.image.setErrorImageResId(android.R.drawable.ic_dialog_alert);
        holder.origin.setText(item.getOrigin());
    }

Problem is when we have refresh in the recyclerView, it is blincking for a very short while in the beginning which looks strange.问题是当我们在 recyclerView 中刷新时,它在开始时闪烁了很短的一段时间,这看起来很奇怪。

I just used GridView/ListView instead and it worked as I expected.我只是改用 GridView/ListView,它按预期工作。 There were no blincking.没有眨眼。

configuration for RecycleView in onViewCreated of my Fragment : onViewCreated of my Fragment配置:

mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
        // use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        mRecyclerView.setHasFixedSize(true);

        mGridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
        mGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return mAdapter.isHeader(position) ? mGridLayoutManager.getSpanCount() : 1;
            }
        });

        mRecyclerView.setAdapter(mAdapter);

Anyone faced with such a problem?有人遇到过这样的问题吗? what could be the reason?可能是什么原因?

Try using stable IDs in your RecyclerView.Adapter尝试在 RecyclerView.Adapter 中使用稳定的 ID

setHasStableIds(true) and override getItemId(int position) . setHasStableIds(true)并覆盖getItemId(int position)

Without stable IDs, after notifyDataSetChanged() , ViewHolders usually assigned to not to same positions.没有稳定的 ID,在notifyDataSetChanged() ,ViewHolders 通常分配到不同的位置。 That was the reason of blinking in my case.这就是我眨眼的原因。

You can find a good explanation here.你可以在这里找到一个很好的解释。

According to this issue page ....it is the default recycleview item change animation... You can turn it off.. try this根据这个问题页面....这是默认的recycleview项目更改动画...您可以将其关闭..试试这个

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Change in latest version最新版本的变化

Quoted from Android developer blog :引自Android 开发者博客

Note that this new API is not backward compatible.请注意,此新 API 不向后兼容。 If you previously implemented an ItemAnimator, you can instead extend SimpleItemAnimator, which provides the old API by wrapping the new API.如果您之前实现了 ItemAnimator,则可以改为扩展 SimpleItemAnimator,它通过包装新 API 来提供旧 API。 You'll also notice that some methods have been entirely removed from ItemAnimator.您还会注意到某些方法已从 ItemAnimator 中完全删除。 For example, if you were calling recyclerView.getItemAnimator().setSupportsChangeAnimations(false), this code won't compile anymore.例如,如果您调用 recyclerView.getItemAnimator().setSupportsChangeAnimations(false),此代码将不再编译。 You can replace it with:您可以将其替换为:

 ItemAnimator animator = recyclerView.getItemAnimator(); if (animator instanceof SimpleItemAnimator) { ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); }

这很简单:

recyclerView.getItemAnimator().setChangeDuration(0);

I have the same issue loading image from some urls and then imageView blinks.我在从某些 url 加载图像然后 imageView 闪烁时遇到了同样的问题。 Solved by using通过使用解决

notifyItemRangeInserted()    

instead of代替

notifyDataSetChanged()

which avoids to reload those unchanged old datas.这避免了重新加载那些未更改的旧数据。

try this to disable the default animation试试这个来禁用默认动画

ItemAnimator animator = recyclerView.getItemAnimator();

if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}

this the new way to disable the animation since android support 23这是禁用动画的新方法,因为 android 支持 23

this old way will work for older version of the support library这种旧方法适用于旧版本的支持库

recyclerView.getItemAnimator().setSupportsChangeAnimations(false)

Recyclerview uses DefaultItemAnimator as it's default animator. Recyclerview 使用 DefaultItemAnimator 作为默认动画制作器。 As you can see from the code below, they change the alpha of the view holder upon item change:正如您从下面的代码中看到的,它们会在项目更改时更改视图持有者的 alpha:

@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
    ...
    final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
    ...
    ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
    if (newHolder != null) {
        ....
        ViewCompat.setAlpha(newHolder.itemView, 0);
    }
    ...
    return true;
}

I wanted to retain the rest of the animations but remove the "flicker" so I cloned DefaultItemAnimator and removed the 3 alpha lines above.我想保留其余的动画但删除“闪烁”,所以我克隆了 DefaultItemAnimator 并删除了上面的 3 条 alpha 线。

To use the new animator just call setItemAnimator() on your RecyclerView:要使用新的动画师,只需在 RecyclerView 上调用 setItemAnimator():

mRecyclerView.setItemAnimator(new MyItemAnimator());

Assuming mItems is the collection that backs your Adapter , why are you removing everything and re-adding?假设mItems是支持您的Adapter的集合,为什么要删除所有内容并重新添加? You are basically telling it that everything has changed, so RecyclerView rebinds all views than I assume the Image library does not handle it properly where it still resets the View even though it is the same image url.您基本上是在告诉它一切都发生了变化,因此 RecyclerView 重新绑定了所有视图,而不是我假设图像库无法正确处理它,即使它是相同的图像 url,它仍然会重置视图。 Maybe they had some baked in solution for AdapterView so that it works fine in GridView.也许他们有一些针对 AdapterView 的解决方案,以便它在 GridView 中正常工作。

Instead of calling notifyDataSetChanged which will cause re-binding all views, call granular notify events (notify added/removed/moved/updated) so that RecyclerView will rebind only necessary views and nothing will flicker.而不是调用将导致重新绑定所有视图的notifyDataSetChanged ,而是调用粒度通知事件(通知添加/删除/移动/更新),以便 RecyclerView 将仅重新绑定必要的视图,并且没有任何闪烁。

In Kotlin you can use 'class extension' for RecyclerView:在 Kotlin 中,您可以为 RecyclerView 使用“类扩展”:

fun RecyclerView.disableItemAnimator() {
    (itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
}

// sample of using in Activity:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    // ...
    myRecyclerView.disableItemAnimator()
    // ...
}

科特林解决方案:

(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false

In my case, neither any of above nor the answers from other stackoverflow questions having same problems worked.就我而言,上述任何一个问题的答案以及其他具有相同问题的 stackoverflow 问题的答案都不起作用。

Well, I was using custom animation each time the item gets clicked, for which I was calling notifyItemChanged(int position, Object Payload) to pass payload to my CustomAnimator class.好吧,每次单击项目时我都使用自定义动画,为此我调用 notifyItemChanged(int position, Object Payload) 将有效负载传递给我的 CustomAnimator 类。

Notice, there are 2 onBindViewHolder(...) methods available in RecyclerView Adapter.注意,在 RecyclerView Adapter 中有 2 个 onBindViewHolder(...) 方法可用。 onBindViewHolder(...) method having 3 parameters will always be called before onBindViewHolder(...) method having 2 parameters.具有 3 个参数的 onBindViewHolder(...) 方法将始终在具有 2 个参数的 onBindViewHolder(...) 方法之前调用。

Generally, we always override the onBindViewHolder(...) method having 2 parameters and the main root of problem was I was doing the same, as each time notifyItemChanged(...) gets called, our onBindViewHolder(...) method will be called, in which I was loading my image in ImageView using Picasso, and this was the reason it was loading again regardless of its from memory or from internet.通常,我们总是覆盖具有 2 个参数的 onBindViewHolder(...) 方法,问题的主要根源是我在做同样的事情,因为每次调用 notifyItemChanged(...) 时,我们的 onBindViewHolder(...) 方法都会被调用,其中我使用 Picasso 在 ImageView 中加载我的图像,这就是它再次加载的原因,无论是从内存还是从互联网。 Until loaded, it was showing me the placeholder image, which was the reason of blinking for 1 sec whenever I click on the itemview.在加载之前,它向我展示了占位符图像,这是每当我单击 itemview 时闪烁 1 秒的原因。

Later, I also override another onBindViewHolder(...) method having 3 parameters.后来,我还覆盖了另一个具有 3 个参数的 onBindViewHolder(...) 方法。 Here I check if the list of payloads is empty, then I return the super class implementation of this method, else if there are payloads, I am just setting the alpha value of the itemView of holder to 1.这里我检查payloads列表是否为空,然后返回这个方法的超类实现,否则如果有payloads,我只是将holder的itemView的alpha值设置为1。

And yay I got the solution to my problem after wasting a one full day sadly!是的,我在遗憾地浪费了一整天之后找到了解决我的问题的方法!

Here's my code for onBindViewHolder(...) methods:这是我的 onBindViewHolder(...) 方法代码:

onBindViewHolder(...) with 2 params: onBindViewHolder(...) 带有 2 个参数:

@Override
public void onBindViewHolder(@NonNull RecyclerAdapter.ViewHolder viewHolder, int position) {
            Movie movie = movies.get(position);

            Picasso.with(context)
                    .load(movie.getImageLink())
                    .into(viewHolder.itemView.posterImageView);
    }

onBindViewHolder(...) with 3 params: onBindViewHolder(...) 带有 3 个参数:

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads);
        } else {
            holder.itemView.setAlpha(1);
        }
    }

Here's the code of method I was calling in onClickListener of viewHolder's itemView in onCreateViewHolder(...):这是我在 onCreateViewHolder(...) 的 viewHolder 的 itemView 的 onClickListener 中调用的方法代码:

private void onMovieClick(int position, Movie movie) {
        Bundle data = new Bundle();
        data.putParcelable("movie", movie);

        // This data(bundle) will be passed as payload for ItemHolderInfo in our animator class
        notifyItemChanged(position, data);
    }

Note: You can get this position by calling getAdapterPosition() method of your viewHolder from onCreateViewHolder(...).注意:您可以通过从 onCreateViewHolder(...) 调用 viewHolder 的 getAdapterPosition() 方法来获取此位置。

I have also overridden getItemId(int position) method as follows:我还重写了 getItemId(int position) 方法,如下所示:

@Override
public long getItemId(int position) {
    Movie movie = movies.get(position);
    return movie.getId();
}

and called setHasStableIds(true);并调用setHasStableIds(true); on my adapter object in activity.在活动中的我的适配器对象上。

Hope this helps if none of the answers above work!如果以上答案都不起作用,希望这会有所帮助!

In my case there was a much simpler problem, but it can look/feel very much like the problem above.在我的情况下,有一个更简单的问题,但它看起来/感觉非常像上面的问题。 I had converted an ExpandableListView to a RecylerView with Groupie (using Groupie's ExpandableGroup feature).我使用 Groupie 将 ExpandableListView 转换为 RecylerView(使用 Groupie 的 ExpandableGroup 功能)。 My initial layout had a section like this:我最初的布局有一个这样的部分:

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@android:color/white" />

With layout_height set to "wrap_content" the animation from expanded group to collapsed group felt like it would flash, but it was really just animating from the "wrong" position (even after trying most of the recommendations in this thread).将 layout_height 设置为“wrap_content”,从展开组到折叠组的动画感觉就像会闪烁,但它实际上只是从“错误”位置开始动画(即使在尝试了此线程中的大部分建议之后)。

Anyway, simply changing layout_height to match_parent like this fixed the problem.无论如何,只需像这样将 layout_height 更改为 match_parent 即可解决问题。

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@android:color/white" />

Try this in Kotlin在 Kotlin 中试试这个

binding.recyclerView.apply {
    (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}

Hey @Ali it might be late replay.嘿@Ali 重播可能晚了。 I also faced this issue and solved with below solution, it may help you please check.我也遇到了这个问题并通过以下解决方案解决了,它可以帮助您检查。

LruBitmapCache.java class is created to get image cache size创建LruBitmapCache.java类以获取图像缓存大小

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import com.android.volley.toolbox.ImageLoader.ImageCache;

public class LruBitmapCache extends LruCache<String, Bitmap> implements
        ImageCache {
    public static int getDefaultLruCacheSize() {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final int cacheSize = maxMemory / 8;

        return cacheSize;
    }

    public LruBitmapCache() {
        this(getDefaultLruCacheSize());
    }

    public LruBitmapCache(int sizeInKiloBytes) {
        super(sizeInKiloBytes);
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }

    @Override
    public Bitmap getBitmap(String url) {
        return get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        put(url, bitmap);
    }
}

VolleyClient.java singleton class [extends Application] added below code VolleyClient.java单例类 [扩展应用程序] 添加到代码下面

in VolleyClient singleton class constructor add below snippet to initialize the ImageLoader在 VolleyClient 单例类构造函数中添加以下代码段以初始化 ImageLoader

private VolleyClient(Context context)
    {
     mCtx = context;
     mRequestQueue = getRequestQueue();
     mImageLoader = new ImageLoader(mRequestQueue,getLruBitmapCache());
}

I created getLruBitmapCache() method to return LruBitmapCache我创建了 getLruBitmapCache() 方法来返回 LruBitmapCache

public LruBitmapCache getLruBitmapCache() {
        if (mLruBitmapCache == null)
            mLruBitmapCache = new LruBitmapCache();
        return this.mLruBitmapCache;
}

Hope its going to help you.希望它会帮助你。

for me recyclerView.setHasFixedSize(true);对我来说recyclerView.setHasFixedSize(true); worked工作过

I had similar issue and this worked for me You can call this method to set size for image cache我有类似的问题,这对我有用你可以调用这个方法来设置图像缓存的大小

private int getCacheSize(Context context) {

    final DisplayMetrics displayMetrics = context.getResources().
            getDisplayMetrics();
    final int screenWidth = displayMetrics.widthPixels;
    final int screenHeight = displayMetrics.heightPixels;
    // 4 bytes per pixel
    final int screenBytes = screenWidth * screenHeight * 4;

    return screenBytes * 3;
}

for my application, I had some data changing but I didn't want the entire view to blink.对于我的应用程序,我更改了一些数据,但我不希望整个视图闪烁。

I solved it by only fading the oldview down 0.5 alpha and starting the newview alpha at 0.5.我通过仅将旧视图降低 0.5 alpha 并从 0.5 开始新视图 alpha 来解决它。 This created a softer fading transition without making the view disappear completely.这创建了更柔和的淡入淡出过渡,而不会使视图完全消失。

Unfortunately because of private implementations, I couldn't subclass the DefaultItemAnimator in order to make this change so I had to clone the code and make the following changes不幸的是,由于私有实现,我无法子类化 DefaultItemAnimator 以进行此更改,因此我不得不克隆代码并进行以下更改

in animateChange:在 animateChange 中:

ViewCompat.setAlpha(newHolder.itemView, 0);  //change 0 to 0.5f

in animateChangeImpl:在 animateChangeImpl 中:

oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { //change 0 to 0.5f

Using appropriate recyclerview methods to update views will solve this issue使用适当的 recyclerview 方法更新视图将解决此问题

First, make changes in the list首先,在列表中进行更改

mList.add(item);
or mList.addAll(itemList);
or mList.remove(index);

Then notify using然后通知使用

notifyItemInserted(addedItemIndex);
or
notifyItemRemoved(removedItemIndex);
or
notifyItemRangeChanged(fromIndex, newUpdatedItemCount);

Hope this will help!!希望这会有所帮助!!

Try to use the stableId in recycler view.尝试在回收者视图中使用 stableId。 The following article briefly explains it下面这篇文章简单解释一下

https://medium.com/@hanru.yeh/recyclerviews-views-are-blinking-when-notifydatasetchanged-c7b76d5149a2 https://medium.com/@hanru.yeh/recyclerviews-views-are-blinking-when-notifydatasetchanged-c7b76d5149a2

In my case I used SwipeRefresh and RecycleView with viewmodel binding and faced with blinking.就我而言,我将 SwipeRefresh 和 RecycleView 与视图模型绑定一起使用,并面临闪烁。 Solved with -> Use submitList() to keep the list updated because DiffUtils have done the job, otherwise the list reloading entirely refer to CodeLab https://developer.android.com/codelabs/kotlin-android-training-diffutil-databinding#4解决了 -> 使用submitList()保持列表更新,因为 DiffUtils 已经完成了这项工作,否则列表重新加载完全参考 CodeLab https://developer.android.com/codelabs/kotlin-android-training-diffutil-databinding# 4

when using livedata I solved it with diffUtil This is how I combined diffUtill and databinding with my adapter使用 livedata 时我用 diffUtil 解决了这就是我将 diffUtill 和数据绑定与我的适配器结合的方式

class ItemAdapter(
    private val clickListener: ItemListener
) :
    ListAdapter<Item, ItemAdapter.ViewHolder>(ItemDiffCallback()) {

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(clickListener, getItem(position)!!)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.from(parent)
    }

    class ViewHolder private constructor(val binding: ListItemViewBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(
            clickListener: ItemListener,
            item: Item) {

            binding.item = item
            binding.clickListener = clickListener
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): ViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val view = ListItemViewBinding
                    .inflate(layoutInflater, parent, false)

                return ViewHolder(view)
            }
        }
    }

    class ItemDiffCallback :
        DiffUtil.ItemCallback<Item>() {
        override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem.itemId == newItem.itemId
        }

        override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
            return newItem
        }

        override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem == newItem
        }
    }
}

class ItemListener(val clickListener: (item: Item) -> Unit) {
    fun onClick(item: Item) = clickListener(item)
}

在我的情况下(一个图像到更高分辨率图像之间的共享元素转换),我向项目装饰器添加了一些延迟以使其不那么明显:

(yourRv.itemAnimator as? DefaultItemAnimator)?.changeDuration = 2000

My own issue was a very specific problem which I'm going to share some insight for here, though I don't yet fully understand it.我自己的问题是一个非常具体的问题,我将在这里分享一些见解,尽管我还没有完全理解它。

Basically I had a re-usable fragment with RecyclerView, so that I could have a menu with nested menus.基本上我有一个带有 RecyclerView 的可重复使用的片段,这样我就可以有一个带有嵌套菜单的菜单。 Pick an item in the RecyclerView, and it opens another fragment with more options.在 RecyclerView 中选择一个项目,它会打开另一个具有更多选项的片段。 All facilitated using the JetPack navigation component and data binding using LiveData in the layout xml.在布局 xml 中使用 JetPack 导航组件和使用 LiveData 进行数据绑定,所有这些都得到了促进。

Anyway, here's how I fixed my issue of items flickering when the RecyclerView changed (although it's worth bearing in mind, this was only the appearance as it was a 'new' RecyclerView each time).无论如何,这就是我如何解决 RecyclerView 更改时项目闪烁的问题(尽管值得牢记,这只是外观,因为它每次都是“新”RecyclerView)。 To update the LiveData list of items (some viewmodels representing objects for the menu items, in my case) I was using LiveData.value = new items .为了更新项目的 LiveData 列表(在我的例子中,一些视图模型代表菜单项的对象)我使用LiveData.value = new items Changing it to postValue(new items) fixed the issue, though I'm not yet sure why.将其更改为postValue(new items)解决了这个问题,但我还不确定为什么。

I've read up the difference between value (setValue in Java) and PostValue, and I understand they're to do with using the main thread or background threading, and the latter only gets applied one time on the main thread when it's ready.我已经阅读了值(Java 中的 setValue)和 PostValue 之间的区别,我知道它们与使用主线程或后台线程有关,而后者仅在主线程准备就绪时应用一次。 But other than that, I'm not sure why this fixed flickering in my RecyclerView.但除此之外,我不确定为什么会在我的 RecyclerView 中固定闪烁。 Maybe someone has some insight?也许有人有一些见识? In any case, hopefully this will help someone facing a similar problem to me.无论如何,希望这能帮助遇到与我类似问题的人。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 RecyclerView 在 notifyDataSetChanged() 上闪烁; - RecyclerView blinking on notifyDataSetChanged(); Notifydatasetchanged() 导致子回收器视图中闪烁 - Notifydatasetchanged() is causing blinking within child recyclerview notifydatasetchanged后recyclerview不更新 - recyclerview not updating after notifydatasetchanged notifyDataSetChanged 后如何避免闪烁效果 - How to avoid blinking effect after notifyDataSetChanged recyclerview在notifyDataSetChanged()之后滚动到顶部 - recyclerview scroll to top after notifyDataSetChanged() 在 RecyclerView 中的 notifyDataSetChanged 之后是否需要 setAdapter()? - Is setAdapter() necessary after notifyDataSetChanged in RecyclerView? 对RecyclerView进行排序后notifyDatasetChanged()不起作用 - notifyDatasetChanged() not working after sorting RecyclerView RecyclerView Adapter notifyDataSetChanged()之后未调用onBindViewHolder - onBindViewHolder not called after RecyclerView Adapter notifyDataSetChanged() 在notifydatasetChanged()之后,recyclerview的findViewByPosition()显示为空 - findViewByPosition() of recyclerview shows null after notifydatasetChanged() 在调用notifydatasetchanged后,Recyclerview滚动到顶部 - Recyclerview scrolling to top after calling notifydatasetchanged
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM