I have a file sharing app, that transmits files over network.
In order to make user selects files properly, i list all the files available on the device and to make a better UX, i get thumbnails of videos and images in OnBindViewHolder. Since there are arbitrary number of photos and videos on a device, this results in loading huge number of thumbnails, and given its a recyclerview i expected that it won't use much memory, but apparently it does. I tried calling bitmap.recycle in onViewRecycled since some views might also have a refrence to the thumbnail. Tried using glide but didn't go beyond glide.with(context).load(bitmap).into(holder.imageview).
Also when i finish the activity, some bitmaps might persist (checked in memory profiler)
But i want to begin with reducing memory usage while activity is still in foreground.
I'm missing something?
Edit also i have to say that the Activity is using a tablayout, that have 5 tabs for 5 different MimeTypes
so here is the page adapter
public class SectionsPagerAdapter extends FragmentStateAdapter {
public static final int[] TAB_TITLES = new int[]{R.string.Text_files, R.string.audio_files, R.string.image_files, R.string.video_files/*, R.string.unknown_files*/,R.string.apk_files};
public static final int[] SEARCH_TITLE = new int[]{R.string.search_res};
private final boolean isSearching;
private ArrayList<FileItemsModel> searchResModel;
private ArrayList<FileItemsModel> txtModel;
private ArrayList<FileItemsModel> audioModel;
private ArrayList<FileItemsModel> videoModel;
private ArrayList<FileItemsModel> imageModel;
private ArrayList<FileItemsModel> apkModel;
//private ArrayList<FileItemsModel> unknownModel;
public SectionsPagerAdapter(FragmentActivity fm, ArrayList<FileItemsModel> txtModel, ArrayList<FileItemsModel> audioModel,
ArrayList<FileItemsModel> videoModel, ArrayList<FileItemsModel> imageModel, ArrayList<FileItemsModel> apkModel/*, ArrayList<FileItemsModel> unknownModel*/) {
super(fm);
this.apkModel = apkModel;
this.txtModel = txtModel;
this.audioModel = audioModel;
this.videoModel = videoModel;
this.imageModel = imageModel;
//this.unknownModel = unknownModel;
isSearching = false;
}
public SectionsPagerAdapter(FragmentActivity fm, ArrayList<FileItemsModel> searchResModel) {
super(fm);
isSearching = true;
this.searchResModel = searchResModel;
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
searchResModel = txtModel = audioModel = videoModel = imageModel = apkModel = null;
}
@NonNull
@Override
public Fragment createFragment(int position) {
if (!isSearching) {
switch (position) {
case 0:
return PlaceholderFragment.newInstance(txtModel);
case 1:
return PlaceholderFragment.newInstance(audioModel);
case 2:
return PlaceholderFragment.newInstance(imageModel);
case 3:
return PlaceholderFragment.newInstance(videoModel);
case 4:
return PlaceholderFragment.newInstance(apkModel);
/*case 5:
return PlaceholderFragment.newInstance(unknownModel);*/
default:
throw new RuntimeException("invalid tab index " + position);
}
}
return PlaceholderFragment.newInstance(searchResModel);
}
@Override
public int getItemCount() {
if (!isSearching)
return TAB_TITLES.length;
return SEARCH_TITLE.length;
}
}
Fragment that holds the recyclerview
public class PlaceholderFragment extends Fragment implements FileItemsAdapter.ItemClickListener{
private static final String MODEL_KEY = "MODEL_KEY";
private ArrayList<FileItemsModel> model;
private FragmentFileSelectorBinding binding;
private FileItemsAdapter adapter;
private RecyclerView recyclerGrid;
private FileSelectorActivity fileActivity;
public static PlaceholderFragment newInstance(ArrayList<FileItemsModel> model) {
PlaceholderFragment fragment = new PlaceholderFragment();
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(MODEL_KEY, model);
fragment.setArguments(bundle);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
fileActivity = (FileSelectorActivity) getActivity();
if (getArguments() != null)
model = getArguments().getParcelableArrayList(MODEL_KEY);
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentFileSelectorBinding.inflate(inflater, container, false);
View root = binding.getRoot();
recyclerGrid = binding.FilesGridList;
recyclerGrid.setLayoutManager(new GridLayoutManager(getContext(),3));
adapter = new FileItemsAdapter(model);
adapter.setClickListener(this);
recyclerGrid.setAdapter(adapter);
return root;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
model = null;
fileActivity = null;
recyclerGrid.removeAllViews();
recyclerGrid = null;
adapter.setClickListener(null);
adapter = null;
}
@Override
public void onItemClick(View view, int position, ArrayList<FileItemsModel> list) {
if(fileActivity.ChosenFiles.contains(list.get(position).getPath()))
fileActivity.ChosenFiles.remove(list.get(position).getPath());
else
fileActivity.ChosenFiles.add(list.get(position).getPath());
view.setAlpha(view.getAlpha() == 1 ? 0.5f : 1);
list.get(position).setIsSelected(view.getAlpha() != 1);
}
}
My adapter
public class FileItemsAdapter extends RecyclerView.Adapter<FileSelectorViewHolder> {
private static final String[] queryRes = new String[] { MediaStore.MediaColumns._ID };
private static final int thumbType = MediaStore.Images.Thumbnails.MICRO_KIND;
private static final Uri imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
private static final Uri videoUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
private static final String queryFilter = MediaStore.MediaColumns.DATA + "=?";
private final ArrayList<FileItemsModel> FileItemsModelArrayList;
private ItemClickListener listener;
public FileItemsAdapter(ArrayList<FileItemsModel> FileItemsModelArrayList)
{
super();
this.FileItemsModelArrayList = FileItemsModelArrayList;
}
@Override
public int getItemCount() {
return FileItemsModelArrayList.size();
}
@NonNull
@Override
public FileSelectorViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View item = inflater.inflate(R.layout.file_item,parent,false);
return new FileSelectorViewHolder(item, listener,FileItemsModelArrayList);
}
@Override
public void onBindViewHolder(@NonNull FileSelectorViewHolder holder, int position) {
//TODO: sometimes item is null.
FileItemsModel item = FileItemsModelArrayList.get(position);
if(item != null) {
holder.linearLayout.setAlpha(item.getIsSelected() ? 0.5f : 1);
holder.fileInfo.setText(item.getFileName());
String minSdk = item.getMinSdk();
if (minSdk != null)
holder.minSdk.setText(minSdk);
setFileIcon(item, holder);
}
item = null;
}
@Override
public void onViewRecycled(@NonNull FileSelectorViewHolder holder) {
super.onViewRecycled(holder);
// Glide.with(holder.itemView.getContext()).clear(holder.fileImg);
if(holder.fileBitmap != null)
{
holder.fileBitmap.recycle();
holder.fileBitmap = null;
}
holder.fileImg.setImageBitmap(null);
holder.itemView.setOnClickListener(null);
this.listener = null;
}
@Override
public void onViewDetachedFromWindow(@NonNull FileSelectorViewHolder holder) {
super.onViewDetachedFromWindow(holder);
if(holder.fileBitmap != null)
{
holder.fileBitmap.recycle();
holder.fileBitmap = null;
}
holder.fileImg.setImageBitmap(null);
holder.itemView.setOnClickListener(null);
this.listener = null;
}
public void setClickListener(@Nullable ItemClickListener itemClickListener) {
this.listener = itemClickListener;
}
public interface ItemClickListener {
void onItemClick(View view, int position, ArrayList<FileItemsModel> list);
}
private Bitmap getThumbnail(ContentResolver contentResolver,FileItemsModel item) {
String mimeType = item.getMimeType();
Cursor ca = contentResolver.query(mimeType.startsWith("video") ? videoUri : imageUri,queryRes, queryFilter, new String[] {item.getPath()}, null);
if (ca != null && ca.moveToFirst()) {
int id = ca.getInt(ca.getColumnIndex(MediaStore.MediaColumns._ID));
ca.close();
return mimeType.startsWith("video") ? MediaStore.Video.Thumbnails.getThumbnail(contentResolver, id,thumbType , null )
: MediaStore.Images.Thumbnails.getThumbnail(contentResolver, id,thumbType , null );
}
ca.close();
return null;
}
public void setFileIcon(FileItemsModel item, @NonNull FileSelectorViewHolder holder)
{
ContentResolver contentResolver = item.getContentResolver();
if(contentResolver != null) {
holder.fileBitmap = getThumbnail(contentResolver, item);
if (holder.fileBitmap != null) {
holder.fileImg.setImageBitmap(holder.fileBitmap);
return;
}
}
holder.fileImg.setImageDrawable(FileSelectorActivity.getFileDrawable(item.getMimeType()));
}
}
my ViewHolder
public class FileSelectorViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
TextView fileInfo ;
TextView minSdk;
ImageView fileImg;
LinearLayout linearLayout;
Bitmap fileBitmap;
FileItemsAdapter.ItemClickListener listener;
private final ArrayList<FileItemsModel> FileItemsModelArrayList;
public FileSelectorViewHolder(View item, FileItemsAdapter.ItemClickListener listener, ArrayList<FileItemsModel> FileItemsModelArrayList)
{
super(item);
this.minSdk = item.findViewById(R.id.minSdk);
this.fileInfo = item.findViewById(R.id.FileName);
this.fileImg = item.findViewById(R.id.FileImg);
this.linearLayout = item.findViewById(R.id.FileContainer);
item.setOnClickListener(this);
this.listener = listener;
this.FileItemsModelArrayList = FileItemsModelArrayList;
}
@Override
public void onClick(View view) {
if(listener != null) listener.onItemClick(view,getAdapterPosition(), FileItemsModelArrayList);
}
}