In Android Studio choose New Project → Empty Views Activity, with Language Java and Minimum SDK API 24.
2. Create the table
DBHelper creates an images table with an id, a name and an image column of type BLOB: a column that holds raw bytes.
3. Pick a photo
Register ActivityResultContracts.PickVisualMedia and launch it for images only. This is Android's Photo Picker: no storage permission needed, and the user shares only the photo they choose.
4. Shrink it first
A phone photo can be 5–10 MB, but SQLite can read only about 2 MB from one row. Saving full photos makes the app crash when it reads them back. ImageUtils loads a smaller copy (inSampleSize), scales it to at most 1024 pixels, and compresses it to JPEG, usually 100–300 KB.
5. Save the bytes
insertImage puts the name and the JPEG bytes into the table with ContentValues.
6. Read them back
getImages reads every row with cursor.getBlob. BitmapFactory.decodeByteArray turns the bytes back into a picture for the grid.
7. Keep the screen smooth
Decoding images and database work can be slow, so they run on a background ExecutorService. Results come back to the screen with a main-thread Handler.
8. Run it
Choose a photo, give it a name, tap Save, and watch it appear in the grid. Delete one with the bin icon.
Should you store images in a database?
For a few small images, yes, as here. For many or large photos, save the file in app storage and keep only its path in the database. That's faster and keeps the database small.
What's improved over the video
It uses the Photo Picker (no permission), shrinks images so the app never hits SQLite's 2 MB limit, works off the main thread, and shows a grid with delete.
Files affected
Check what to add and what to replace before copying code.
Picks a photo, saves it, and loads the grid, all off the main thread.
MainActivity.java
packagecom.example.storeimagessqlite;importandroid.net.Uri;importandroid.os.Bundle;importandroid.os.Handler;importandroid.os.Looper;importandroid.view.View;importandroid.widget.Button;importandroid.widget.ImageView;importandroid.widget.TextView;importandroid.widget.Toast;importandroidx.activity.result.ActivityResultLauncher;importandroidx.activity.result.PickVisualMediaRequest;importandroidx.activity.result.contract.ActivityResultContracts;importandroidx.appcompat.app.AppCompatActivity;importandroidx.recyclerview.widget.GridLayoutManager;importandroidx.recyclerview.widget.RecyclerView;importcom.google.android.material.textfield.TextInputEditText;importjava.util.List;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;/**
* Pick a photo, shrink it, save it into SQLite as a BLOB, and show every saved photo in a grid.
* Image work and database work run on a background thread so the screen never freezes.
*/publicclassMainActivityextendsAppCompatActivity{privatefinalExecutorService background =Executors.newSingleThreadExecutor();privatefinalHandler main =newHandler(Looper.getMainLooper());privateDBHelper db;privateImageAdapter adapter;privatebyte[] picked;ImageView preview;TextInputEditText name;Button save;TextView empty;// The Photo Picker needs no storage permission; the user chooses exactly which photo to share.privatefinalActivityResultLauncher<PickVisualMediaRequest> pickImage =registerForActivityResult(newActivityResultContracts.PickVisualMedia(),this::onPicked);@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);Screens.setUp(this,R.layout.activity_main);
db =newDBHelper(this);
preview =findViewById(R.id.preview);
name =findViewById(R.id.name);
save =findViewById(R.id.btnSave);
empty =findViewById(R.id.textEmpty);Button choose =findViewById(R.id.btnChoose);
choose.setOnClickListener(v -> pickImage.launch(newPickVisualMediaRequest.Builder().setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE).build()));
save.setOnClickListener(v ->saveImage());RecyclerView grid =findViewById(R.id.grid);
grid.setLayoutManager(newGridLayoutManager(this,2));
adapter =newImageAdapter(image -> background.execute(()->{
db.deleteImage(image.id);loadImages();}));
grid.setAdapter(adapter);loadImages();}privatevoidonPicked(Uri uri){if(uri ==null)return;// the user closed the picker
background.execute(()->{try{byte[] bytes =ImageUtils.shrinkToJpeg(getContentResolver(), uri);
main.post(()->{
picked = bytes;
preview.setImageBitmap(android.graphics.BitmapFactory.decodeByteArray(bytes,0, bytes.length));
save.setEnabled(true);});}catch(Exception e){
main.post(()->Toast.makeText(this,R.string.error_read,Toast.LENGTH_SHORT).show());}});}privatevoidsaveImage(){if(picked ==null)return;String title =String.valueOf(name.getText()).trim();String finalTitle = title.isEmpty()?getString(R.string.untitled): title;byte[] bytes = picked;
background.execute(()->{boolean ok = db.insertImage(finalTitle, bytes);
main.post(()->{Toast.makeText(this, ok ?R.string.saved :R.string.error_save,Toast.LENGTH_SHORT).show();if(ok){
picked =null;
preview.setImageResource(R.drawable.ic_image);
name.setText("");
save.setEnabled(false);}});loadImages();});}/** Reads every image on the background thread, then updates the grid on the main thread. */privatevoidloadImages(){
background.execute(()->{List<SavedImage> images = db.getImages();
main.post(()->{
adapter.setImages(images);
empty.setVisibility(images.isEmpty()?View.VISIBLE:View.GONE);});});}@OverrideprotectedvoidonDestroy(){
background.shutdown();super.onDestroy();}}