Sunday, 17 November 2019

How to go about creating a RecyclerView that uses images from Firebase

I have more than 1.5K images in a folder named Icons on my Firebase Storage and i want to load those images into a RecyclerView that uses a GridLayoutManager. Obviously i only want to request the loading of these images only if they are visible in the Recyclerview otherwise it's a waste of reads to the storage right?

So, i started by creating the R.layout.card_icons_rcv_item xml layout -which included a single ImageView inside of a ConstraintLayout- to represent each cell in my RecyclerView.

I then created the Adapter class that you can see below and inside the onBindViewHolder i'm trying to load the image that's associated with each cell in the respective RecyclerView cell but it doesn't work well.

First of all, i only get around 2 images or none (most of the time) loaded and nothing else, second of all, i can see that the Log.d("Adapter","Error loading card icon from firestore for card #" + position); statement gets called for the last 3 cells and lastly i'm seeing in the logcat that all these requests to firebase are made at the same time even though the images aren't present in the recyclerView.

UPDATE

The reycler view seems to load the icons successfully but very very slowly. So i did some digging around in the docs and i stumbled upon Bitmaps. The docs said that since my image view is smaller than the images on Firebase storage, i should probably downsample them. As you can see in my updated onBindViewHolder method i do exactly that. The images load significantly faster but still not fast enough. I also came across a weird exception that i can't seem to fix. I'm updating my questions below :

[UPDATED] So here are my questions :

  1. How do i only request to load the images from firebase for just the visible cells because from what i'm seeing right now in the Logcat, the requests made are equal to the number returned by getItemCount().
  2. In the getItemCount() method i return a random number 50 because i don't know how i'm supposed to return the number of images in my database. There are at least 1.5K images on there and the database will slowly grow even larger. Could this be causing any issues and if so how am i supposed to overcome this?
  3. How can i make the downloading of these images even faster?
  4. After i download the images in the temporary files that you see in the onBindViewHolder method, should i delete them or are they needed for populating the recycler view's image views?
  5. After a few runs, without making any changes to my code, i started getting the following Error in the Logcat :

E/AuthUI: A sign-in error occurred. com.firebase.ui.auth.FirebaseUiException: Error when saving credential. at com.firebase.ui.auth.viewmodel.smartlock.SmartLockHandler$1.onComplete(SmartLockHandler.java:99) at com.google.android.gms.tasks.zzj.run(Unknown Source:4) at android.os.Handler.handleCallback(Handler.java:883) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loop(Looper.java:221) at android.app.ActivityThread.main(ActivityThread.java:7520) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950) Caused by: com.google.android.gms.common.api.ApiException: 16: The save prompt is disabled for the current app. To restore, remove this app from the "Never save" list in the Smart Lock for Passwords settings for all accounts on this device. at com.google.android.gms.common.internal.ApiExceptionUtil.fromStatus(com.google.android.gms:play-services-base@@17.1.0:4) at com.google.android.gms.common.internal.zai.zaf(com.google.android.gms:play-services-base@@17.1.0:2) at com.google.android.gms.common.internal.zak.onComplete(com.google.android.gms:play-services-base@@17.1.0:6) at com.google.android.gms.common.api.internal.BasePendingResult.zaa(com.google.android.gms:play-services-base@@17.1.0:176) at com.google.android.gms.common.api.internal.BasePendingResult.setResult(com.google.android.gms:play-services-base@@17.1.0:135) at com.google.android.gms.common.api.internal.BaseImplementation$ApiMethodImpl.setResult(com.google.android.gms:play-services-base@@17.1.0:36) at com.google.android.gms.internal.auth-api.zzo.zzc(Unknown Source:4) at com.google.android.gms.internal.auth-api.zzv.dispatchTransaction(Unknown Source:9) at com.google.android.gms.internal.auth-api.zzd.onTransact(Unknown Source:12) at android.os.Binder.execTransactInternal(Binder.java:1021) at android.os.Binder.execTransact(Binder.java:994)

The user that i'm trying to sign in as (the owner) is successfully authenticated based on my Log.d statements but for some weird reason it won't allow me to read data from Firebase. The error mentions that i should change a setting in the Google SmartLock but i swear to god i've looked everywhere for that setting on my phone and i could not find it. I also searched about it on Google but without luck. Any ideas on where i can find that setting?

CardIconRCVAdapter.java:

public class CardIconsRCVAdapter extends RecyclerView.Adapter<CardIconsRCVAdapter.ViewHolder> {

    private LayoutInflater inflater;
    private Context mContext;
    private StorageReference storageReference;

    public CardIconsRCVAdapter(Context context,StorageReference storageRef) {
        mContext = context;
        inflater = LayoutInflater.from(context);
        storageReference = storageRef;
    }

    // stores and recycles views as they are scrolled off screen
    class ViewHolder extends RecyclerView.ViewHolder {
        ImageView iconImgView;

        ViewHolder(@NonNull View itemView) {
            super(itemView);
            iconImgView = itemView.findViewById(R.id.cardIcon);
        }
    }

    // Inflates the cell layout from xml when needed
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.card_icons_rcv_item,parent,false);
        return new ViewHolder(view);
    }

    // Binds the data to the views in each cell
    @Override
    public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
        //Log.d("Adapter","OnBindViewHolder called for #" + position);
        StorageReference iconRefJpeg = storageReference.child("Icons/icon" + position + ".jpeg");
        final StorageReference iconRefPng = storageReference.child("Icons/icon" + position + ".png");
        try {
            final File localFile = File.createTempFile("icon" + position, ".jpeg");
            iconRefJpeg.getFile(localFile).addOnSuccessListener(new OnSuccessListener<FileDownloadTask.TaskSnapshot>() {
                @Override
                public void onSuccess(FileDownloadTask.TaskSnapshot taskSnapshot) {
                    holder.iconImgView.setImageBitmap(decodeSampledBitmapFromResource(localFile.getAbsolutePath(), 150, 150));
                }
            }).addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    Log.d("AdapterError", "Failed fetching .jpeg image. Trying to fetch .png");
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static Bitmap decodeSampledBitmapFromResource(String filePath, int reqWidth, int reqHeight) {
        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(filePath,options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(filePath,options);
    }

    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        Log.d("CalcSampleSize","height = " + height + " width = " + width + " Image type : " + options.outMimeType);
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }

    @Override
    public int getItemCount() {
        return 50;
    }
}

MainActivity.kt:

class MainActivity : AppCompatActivity() {

private val TAG: String = MainActivity::class.java.simpleName // Tag used for debugging

// View declarations
private lateinit var iconsRCV : RecyclerView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val storageRef = FirebaseStorage.getInstance().reference // Create a storage reference

    // Setting Toolbar default settings
    val toolbar : Toolbar = findViewById(R.id.mainToolbar)
    setSupportActionBar(toolbar) // set the custom toolbar as the support action bar
    supportActionBar?.setDisplayShowTitleEnabled(false) // remove the default action bar title

    // RecyclerView initializations
    iconsRCV = findViewById(R.id.cardIconsRCV)
    val numOfColumns = 5
    iconsRCV.layoutManager = GridLayoutManager(this,numOfColumns)
    val iconsAdapter = CardIconsRCVAdapter(this,storageRef)
    iconsRCV.adapter = iconsAdapter
}

}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/mainToolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:elevation="4dp"
        android:background="@color/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/toolbar_title"
            android:text="@string/app_name"
            android:textSize="18sp"
            android:textColor="@android:color/white"
            android:layout_gravity="center"/>

    </androidx.appcompat.widget.Toolbar>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/cardIconsRCV"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginStart="8dp"
        android:layout_marginTop="64dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/mainToolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

card_icons_rcv_item.xml:

<?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:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/cardIcon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="cardIcon"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>


from How to go about creating a RecyclerView that uses images from Firebase

No comments:

Post a Comment