Galeria MediaStore
MediaStore w Androidzie to specjalna baza danych systemu Android, która przechowuje informacje (metadane) o wszystkich plikach multimedialnych znajdujących się w pamięci urządzenia. Dzięki niemu aplikacje mogą wygodnie i szybko wyszukiwać zdjęcia, filmy, czy pliki audio, bez konieczności samodzielnego przeszukiwania folderów w systemie plików.
Można powiedzieć, że MediaStore działa jak katalog biblioteczny- zamiast otwierać każdą książkę (plik) po kolei, przeglądamy gotowy spis treści (baza z metadanymi).
Jak działa MediaStore?
- Android automatycznie indeksuje multimedia w pamięci telefonu oraz na karcie SD (jeśli jest).
- Informacje o każdym pliku (np. ID, nazwa, rozdzielczość, data dodania, album, lokalizacja) zapisywane są w bazie MediaStore.
- Programista korzysta z ContentResolver i specjalnych URI (np. MediaStore.Images.Media.EXTERNAL_CONTENT_URI), aby wykonać zapytanie i otrzymać listę obiektów.
- Otrzymujemy metadane (np. nazwa pliku, ID, data utworzenia) oraz możemy utworzyć URI do odczytu zawartości (zdjęcie, wideo, muzyka).
Dlaczego używa się MediaStore?
- Bezpieczeństwo i uprawnienia- aplikacja nie musi znać dokładnej ścieżki do pliku, wystarczy URI.
- Spójność- wszystkie aplikacje (Galeria, Muzyka, nasze aplikacje) korzystają z tego samego systemowego katalogu.
- Wydajność- zamiast ręcznie przeszukiwać foldery, korzystamy z gotowej bazy danych.
- Filtrowanie i sortowanie- MediaStore pozwala wyszukać pliki wg nazwy, daty, typu MIME (np. tylko obrazy JPG), albumu.
- Kompatybilność- działa od pierwszych wersji Androida i jest nadal podstawowym sposobem dostępu do multimediów (choć od Androida 13 Google zaleca również Photo Picker).
Podsumowanie
- MediaStore, to systemowa baza danych multimediów.
- Umożliwia wygodne przeglądanie, wyszukiwanie i filtrowanie zdjęć, filmów, audio.
- Zastępuje ręczne skanowanie folderów w pamięci.
- Jest podstawą działania galerii i odtwarzaczy multimedialnych w Androidzie.
- READ_MEDIA_IMAGES- to uprawnienie wprowadzone od Androida 13 (API 33). Pozwala aplikacji czytać pliki graficzne (zdjęcia) zapisane w pamięci urządzenia.
- READ_EXTERNAL_STORAGE- to starsze uprawnienie używane do Androida 12L (API 32). Daje dostęp do wszystkich typów plików na pamięci zewnętrznej (karta SD, pamięć współdzielona).
- system wie, jakie zasoby aplikacja będzie chciała wykorzystać,
- użytkownik może zobaczyć, do czego program żąda dostępu,
- aplikacja działa poprawnie na różnych wersjach Androida (starsze telefony użyją READ_EXTERNAL_STORAGE, nowsze- READ_MEDIA_IMAGES).
- Toolbar: @+id/toolbar
- Pole tekstowe do wyszukiwania: @+id/editSzukaj
- Pole wyboru albumu (AutoCompleteTextView): @+id/editAlbum
- Lista zdjęć (RecyclerView): @+id/RecyclerView
- Przycisk udostępniania: @+id/btUdostepnij
- Funkcja aktualizujUdostepnione() zmienia tekst na przycisku, pokazując, ile plików zostało zaznaczonych, oraz blokuje przycisk, gdy nie ma nic do wysłania.
- Funkcja udostepnijZaznaczone() tworzy listę adresów URI wszystkich zaznaczonych zdjęć i przygotowuje intencję systemową ACTION_SEND_MULTIPLE. Dzięki temu Android sam wyświetli listę aplikacji, które obsługują udostępnianie obrazów.
- pobiera aktualne kryteria: tekst z wyszukiwarki (filtrNazwa) i wybrany album (filtrAlbum);
- filtruje listę wszystkie po nazwie pliku i albumie;
- sortuje wynik po dacie dodania (rosnąco/malejąco);
- przekazuje wynik do adaptera i odświeża licznik udostępnionych.
- buduje listę unikalnych nazw albumów z wczytanych obrazów;
- podłącza ją do pola editAlbum (AutoComplete) i reaguje na wybór.
- TextWatcher- każde wpisanie znaku natychmiast aktualizuje filtr.
- toolbar.setOnMenuItemClickListener- przełącza tryb sortowania (najnowsze/najstarsze).
MediaStore jest odpowiednikiem spisu treści multimediów w systemie. Dzięki niemu aplikacja może szybko odnaleźć i otworzyć pliki (np. zdjęcia w galerii), zamiast samodzielnie szukać ich w folderach.
Uprawnienia w manifeście aplikacji
Aplikacja mobilna nie ma automatycznie dostępu do plików użytkownika. System Android chroni dane (np. zdjęcia, muzykę, dokumenty), dlatego programista musi jawnie zadeklarować, że aplikacja potrzebuje takich danych.Dodajemy uprawnienia w manifeście aplikacji
Wskazówka:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- Do Android 12L włącznie -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
Zmiany w module gradle
W pliku build.gradle.kts (Module: app) definiujemy, z jakich zewnętrznych bibliotek będzie korzystał nasz projekt. Dzięki temu nie musimy samodzielnie pisać całej logiki (np. do ładowania grafik czy obsługi list)- używamy gotowych, sprawdzonych rozwiązań.
implementation("io.coil-kt:coil:2.6.0")To biblioteka Coil, która umożliwia szybkie i wygodne ładowanie obrazów z różnych źródeł (np. Uri z MediaStore) bez ręcznego dekodowania bitmap. Coil automatycznie skaluje i pamięta wczytane obrazki (cache), dzięki czemu aplikacja działa płynnie.
implementation("androidx.recyclerview:recyclerview:1.3.2")To biblioteka odpowiedzialna za RecyclerView- nowoczesną kontrolkę do wyświetlania dużych list lub siatek elementów (np. galeria zdjęć). RecyclerView pozwala recyklingować widoki, czyli ponownie używać już utworzonych elementów, zamiast tworzyć nowe- oszczędza to pamięć i przyspiesza działanie aplikacji.
Dodanie tych linii w Gradle umożliwia korzystanie w aplikacji z Coil (ładowanie obrazów) i RecyclerView (wydajne listy/siatki). Dzięki nim możemy zbudować galerię zdjęć w Androidzie szybko i profesjonalnie.
W module Gradle w pliku build.gradle.kts (Module :app) dodaj poniższe implementacje bibliotek
Wskazówka:
dependencies {
// Coil- ładowanie obrazów po Uri (ContentResolver/MediaStore)
implementation ("io.coil-kt:coil:2.6.0")
implementation("androidx.recyclerview:recyclerview:1.3.2")
}
Menu główne aplikacji
Dodaj zasób XML dla menu

Utworzymy proste menu górnego paska narzędziowego
Wskazówka:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/sortujNajnowsze"
android:title="Najnowsze"/>
<item
android:id="@+id/sortujNajstarsze"
android:title="Najstarsze"/>
</menu>
Do zasobów graficznych folderu res/ drawable dodaj ikonę udostępniania lub dowolną inną, która by reprezentowała udostępnianie.

Układ widoku aplikacji
Układ widoku aplikacji oparty jest na układzie LinearLayout (vertical). W tym układzie przewidujemy górny pasek narzędzi, pasek filtrowania, obszar podglądu galerii zdjęć ora przycisk udostępniania.

Przyjęte identyfikatory kontrolek, których będziemy się trzymać w dalszym projekcie:
Zawartość pliku XML activity_main.xml
Wskazówka:
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:layout_editor_absoluteX="-16dp"
tools:layout_editor_absoluteY="0dp">
<!-- Pasek narzędzi -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
app:menu="@menu/menu_glowne"
app:title="Galeria (MediaStore)" />
<!-- Pasek filtrowania -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="205dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Szukaj po nazwie">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editSzukaj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/editAlbum"
android:layout_width="154dp"
android:layout_height="61dp"
android:hint="Album"
android:paddingStart="8dp" />
</LinearLayout>
<!-- RecyclerView na zdjęcia -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/RecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:padding="2dp" />
<!-- Przycisk udostępniania -->
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/btUdostepnij"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_margin="16dp"
android:text="Udostępnij (0)"
app:icon="@drawable/ic_share_24" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Po kompilacji widzimy aktywne menu i puste miejsce na galerię obrazów

Dodajemy klasę danych dla kolejnych elementów graficznych- obrazów ładowanych w podglądzie (New/ Kotlin Class/ Data class)

Pola danych w tej klasie mają za zadanie przechowywać podstawowe informacje o pliku graficznym
Wskazówka:
import android.net.Uri
data class ElementObraz(
val id: Long, // identyfikator rekordu w MediaStore
val adres: Uri, // URI do obrazka
val nazwaPliku: String, // nazwa pliku (np. IMG_1234.jpg)
val dataDodania: Long, // data dodania (w sekundach od epoki)
val album: String // nazwa albumu/katalogu (bucket)
)
Tworzymy plik XML dla widoku dodawanego elementu do podglądu galerii obrazów. W hierarchii projektu dodaj nowy plik zasobów XML, w którym podaj Root element: FrameLayout

Wypełniamy poniższą zawartością
Wskazówka:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="140dp"
android:layout_margin="2dp">
<ImageView
android:id="@+id/obrazMiniatura"
android:contentDescription="Miniatura"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!-- Nakładka zaznaczenia -->
<View
android:id="@+id/nakladkaZaznaczenia"
android:visibility="gone"
android:background="#80FFFFFF"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/znacznikWyboru"
android:src="@drawable/ic_check_circle_24"
android:layout_gravity="top|end"
android:layout_margin="6dp"
android:visibility="gone"
android:layout_width="24dp"
android:layout_height="24dp"/>
<!-- Nazwa pliku -->
<TextView
android:id="@+id/tekstNazwaPliku"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:maxLines="1"
android:ellipsize="end"
android:textSize="12sp"
android:textColor="@android:color/white"
android:background="#66000000"
android:text=""/>
</FrameLayout>
Nakładka spełnia rolę graficznej informacji dla użytkownika, że dany zasób graficzny został wybrany (ikona ptaszka wyboru @drawable/ic_check_circle_24)

Na tę chwilę aplikacja jeszcze nie reaguje na wybór, to należy oprogramować.
Adapter ładowania obrazów
Dodajemy nową klasę o nazwie AdapterObrazow. Tworzony adapter będzie bardziej zaawansowany, ponieważ będzie obsługiwać zdarzenie kliknięcia, dłuższego kliknięcia (zaznaczenia) i wyboru. Ten adapter zostanie przypisany do kontrolki RecyclerView
Wskazówka:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
class AdapterObrazow(
private val onClick: (ElementObraz) -> Unit,
private val onLongClick: (ElementObraz) -> Unit,
private val isSelected: (Long) -> Boolean
) : ListAdapter<ElementObraz, AdapterObrazow.VH>(DIFF) {
companion object {
val DIFF = object : DiffUtil.ItemCallback<ElementObraz>() {
override fun areItemsTheSame(oldItem: ElementObraz, newItem: ElementObraz) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: ElementObraz, newItem: ElementObraz) =
oldItem == newItem
}
}
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val img = itemView.findViewById<ImageView>(R.id.obrazMiniatura)
private val overlay = itemView.findViewById<View>(R.id.nakladkaZaznaczenia)
private val check = itemView.findViewById<ImageView>(R.id.znacznikWyboru)
private val nazwaPliku = itemView.findViewById<TextView>(R.id.tekstNazwaPliku)
fun bind(item: ElementObraz) {
// Ładowanie obrazu miniatury
img.load(item.adres) { crossfade(true) }
// Ustawianie nazwy pliku
nazwaPliku.text = item.nazwaPliku
// Obsługa zaznaczenia
val sel = isSelected(item.id)
overlay.visibility = if (sel) View.VISIBLE else View.GONE
check.visibility = if (sel) View.VISIBLE else View.GONE
// Obsługa kliknięć
itemView.setOnClickListener { onClick(item) }
itemView.setOnLongClickListener { onLongClick(item); true }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.widok_element_obraz, parent, false)
return VH(v)
}
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(getItem(position))
}
Funkcje i metody odpowiedzialne za ładowanie grafiki
W projekcie dodajemy poniższe zmienne i pytanie o zgodę użytkownika o dostęp do zasobów telefonu.
Wskazówka:
class MainActivity : AppCompatActivity() {
// --- Widoki (Twoje ID z activity_main.xml) ---
private lateinit var toolbar: MaterialToolbar
private lateinit var editSzukaj: TextInputEditText
private lateinit var editAlbum: MaterialAutoCompleteTextView
private lateinit var lista: RecyclerView
private lateinit var btUdostepnij: ExtendedFloatingActionButton
// --- Adapter i dane ---
private lateinit var adapter: AdapterObrazow
private val wszystkie = mutableListOf<ElementObraz>()
private val przefiltrowane = mutableListOf<ElementObraz>()
//typ kolekcji zaznaczanych elementów
private val zaznaczone = linkedSetOf<Long>()
private var sortNajnowsze = true
private var filtrAlbum = "Wszystkie"
private var filtrNazwa = ""
// Uprawnienie zależne od wersji
// tu jest wysłana prośba do uzytkowanika o zgodę
private val permission: String =
if (Build.VERSION.SDK_INT >= 33) Manifest.permission.READ_MEDIA_IMAGES
else Manifest.permission.READ_EXTERNAL_STORAGE
private val reqPerm =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) wczytajZdjecia()
else Toast.makeText(this, "Brak uprawnienia do odczytu zdjęć", Toast.LENGTH_LONG).show()
}
override fun onCreate(savedInstanceState: Bundle?) {
Sprawdzamy uprawnienia aplikacji- bez tego nie da się wczytać obrazów.
Wskazówka:
// --- Uprawnienia ---
private fun sprawdzUprawnienieIStart() {
val ok = ContextCompat.checkSelfPermission(this, permission)== PackageManager.PERMISSION_GRANTED
if (ok) wczytajZdjecia() else reqPerm.launch(permission)
}
Pobranie listy obrazów z MediaStore. Tu faktycznie ładowane są dane o plikach graficznych (ID, nazwa, ścieżka Uri, album, data).
Wskazówka:
// --- Wczytanie MediaStore ---
private fun wczytajZdjecia() {
lifecycleScope.launch {
val listaObrazow = withContext(Dispatchers.IO) { zapytajMediaStore() }
wszystkie.clear()
wszystkie.addAll(listaObrazow)
zbudujAlbumy()
zastosujFiltryISort()
}
}
private fun zapytajMediaStore(): List<ElementObraz> {
val wynik = mutableListOf<ElementObraz>()
val kolekcja: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME
)
val selection = "${MediaStore.Images.Media.MIME_TYPE} LIKE ?"
val selectionArgs = arrayOf("image/%")
val sortOrder = if (sortNajnowsze)
"${MediaStore.Images.Media.DATE_ADDED} DESC"
else
"${MediaStore.Images.Media.DATE_ADDED} ASC"
contentResolver.query(kolekcja, projection, selection, selectionArgs, sortOrder)?.use { c ->
val idCol = c.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val nameCol = c.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
val dateCol = c.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)
val bucketCol = c.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
while (c.moveToNext()) {
val id = c.getLong(idCol)
val name = c.getString(nameCol) ?: ""
val date = c.getLong(dateCol)
val bucket = c.getString(bucketCol) ?: "Nieznany"
val uri = ContentUris.withAppendedId(kolekcja, id)
wynik.add(
ElementObraz(
id = id,
adres = uri,
nazwaPliku = name,
dataDodania = date,
album = bucket
)
)
}
}
return wynik
}
Podgląd pojedynczego zdjęcia
Ta metoda określa, co się stanie po kliknięciu miniatury zdjęcia w galerii.
1. Sprawdzenie zaznaczenia
Jeśli lista zaznaczone nie jest pusta (użytkownik wcześniej zaznaczył jakieś zdjęcia), to kliknięcie służy tylko do przełączania zaznaczenia (przelaczZaznaczenie(item)).
2. Podgląd zdjęcia
Jeśli nie ma aktywnego zaznaczenia, aplikacja tworzy intencję systemową (Intent.ACTION_VIEW), która otwiera wybrane zdjęcie w domyślnej przeglądarce obrazów telefonu.
3. setDataAndType(item.adres, "image/*") ? przekazujemy adres pliku (Uri) i informację, że jest to obraz.
4. addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) ? dajemy innym aplikacjom tymczasowy dostęp do tego pliku.
5. Uruchomienie podglądu
Wywołanie startActivity(intent) otwiera aplikację systemową (np. ?Zdjęcia Google?), aby użytkownik mógł zobaczyć zdjęcie w pełnym rozmiarze.
Kod funkcji:
Wskazówka:
private fun klikPojedynczy(item: ElementObraz) {
if (zaznaczone.isNotEmpty()) {
przelaczZaznaczenie(item)
} else {
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(item.adres, "image/*")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
runCatching { startActivity(intent) }
}
}
Wywołanie powyższych funkcji osadzone jest w ciele metody onCreate():
Wskazówka:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
// findViewById według ID
toolbar = findViewById(R.id.toolbar)
editSzukaj = findViewById(R.id.editSzukaj)
editAlbum = findViewById(R.id.editAlbum)
lista = findViewById(R.id.RecyclerView)
btUdostepnij = findViewById(R.id.btUdostepnij)
// Siatka 3 kolumny
lista.layoutManager = GridLayoutManager(this, 3)
adapter = AdapterObrazow(
onClick = { item -> klikPojedynczy(item) },
onLongClick = { item -> przelaczZaznaczenie(item) },
isSelected = { id -> zaznaczone.contains(id) }
)
lista.adapter = adapter
// Start
sprawdzUprawnienieIStart()
}
Po kompilacji i wyrażeniu zgody na dostęp aplikacja załaduje znaleziona grafikę z zasobach urządzenia lub emulatora. Jeżeli w emulatorze nie ma żadnej grafiki to ją możesz dodać na zasadach opisanych w tym temacie: wczytywanie grafiki

Grafika się wczytuje, a po dłuższym przytrzymaniu klawisza myszki obraz się zaznacza.
Funkcja udostępniania
Funkcjonalność aplikacji rozszerzymy do udostępniania i licznika udostępnionych plików graficznych. Dzięki temu użytkownik może zaznaczyć kilka zdjęć i przesłać je dalej, np. przez komunikator, e-mail lub media społecznościowe.
Wskazówka:
//aktualizacja licznika udostepnionych w kontrolce przycisku
private fun aktualizujUdostepnione() {
btUdostepnij.text = "Udostępnij (${zaznaczone.size})"
btUdostepnij.isEnabled = zaznaczone.isNotEmpty()
}
private fun udostepnijZaznaczone() {
if (zaznaczone.isEmpty()) return
val uris = przefiltrowane.filter { it.id in zaznaczone }.map { it.adres }
val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
type = "image/*"
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(intent, "Udostępnij zdjęcia"))
}
Wywołanie powyższych funkcji osadzone jest w ciele metody onCreate()
Wskazówka:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lista.adapter = adapter
// Start
sprawdzUprawnienieIStart()
btUdostepnij.setOnClickListener { udostepnijZaznaczone() }
aktualizujUdostepnione()
}
Po skompilowaniu sprawdź działanie aplikacji.

Filtrowanie i sortowanie
Kolejnym krokiem tego projektu jest filtrowanie i sortowanie zasobów. Do projektu dodajemy poniższe funkcje w klasie MainActivity.kt
Wskazówka:
// --- Filtrowanie/sortowanie + albumy ---
private fun zastosujFiltryISort() {
val q = filtrNazwa.trim().lowercase()
val album = filtrAlbum
przefiltrowane.clear()
przefiltrowane.addAll(
wszystkie.filter { item ->
val okNazwa = if (q.isEmpty()) true else item.nazwaPliku.lowercase().contains(q)
val okAlbum = (album == "Wszystkie") || item.album.equals(album, ignoreCase = true)
okNazwa && okAlbum
}
)
if (sortNajnowsze) przefiltrowane.sortByDescending { it.dataDodania }
else przefiltrowane.sortBy { it.dataDodania }
adapter.submitList(przefiltrowane.toList())
aktualizujUdostepnione()
}
private fun zbudujAlbumy() {
val lista = mutableListOf("Wszystkie")
lista.addAll(wszystkie.map { it.album }.distinct().sorted())
val spinnerAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, lista)
editAlbum.setAdapter(spinnerAdapter)
editAlbum.setOnItemClickListener { _, _, pos, _ ->
filtrAlbum = lista[pos]
zastosujFiltryISort()
}
}
Wywołani funkcji realizowane jest w metodzie onCreate(). Dopisz poniższe linijki kodu do tej metody
Wskazówka:
// Filtrowanie po nazwie
editSzukaj.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
filtrNazwa = s?.toString().orEmpty()
zastosujFiltryISort()
}
})
// Menu sortowania (w menu_glowne.xml)
toolbar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sortujNajnowsze -> { sortNajnowsze = true; zastosujFiltryISort(); true }
R.id.sortujNajstarsze -> { sortNajnowsze = false; zastosujFiltryISort(); true }
else -> false
}
}
Filtrowanie i sortowanie- co robi kod?
zastosujFiltryISort():Po kompilacji można sprawdzić działanie dopisanych funkcji. Poniżej sortowanie

Pełny kod pliku MainActivity.kt
Wskazówka:
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import android.Manifest
import android.content.ContentUris
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.text.Editable
import android.text.TextWatcher
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.textfield.TextInputEditText
import kotlinx.coroutines.*
import androidx.lifecycle.lifecycleScope
class MainActivity : AppCompatActivity() {
// --- Widoki (Twoje ID z activity_main.xml) ---
private lateinit var toolbar: MaterialToolbar
private lateinit var editSzukaj: TextInputEditText
private lateinit var editAlbum: MaterialAutoCompleteTextView
private lateinit var lista: RecyclerView
private lateinit var btUdostepnij: ExtendedFloatingActionButton
// --- Adapter i dane ---
private lateinit var adapter: AdapterObrazow
private val wszystkie = mutableListOf<ElementObraz>()
private val przefiltrowane = mutableListOf<ElementObraz>()
//typ kolekcji zaznaczanych elementów
private val zaznaczone = linkedSetOf<Long>()
private var sortNajnowsze = true
private var filtrAlbum = "Wszystkie"
private var filtrNazwa = ""
// Uprawnienie zależne od wersji
// tu jest wysłana prośba do uzytkowanika o zgodę
private val permission: String =
if (Build.VERSION.SDK_INT >= 33) Manifest.permission.READ_MEDIA_IMAGES
else Manifest.permission.READ_EXTERNAL_STORAGE
private val reqPerm =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) wczytajZdjecia()
else Toast.makeText(this, "Brak uprawnienia do odczytu zdjęć", Toast.LENGTH_LONG).show()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
// findViewById według ID
toolbar = findViewById(R.id.toolbar)
editSzukaj = findViewById(R.id.editSzukaj)
editAlbum = findViewById(R.id.editAlbum)
lista = findViewById(R.id.RecyclerView)
btUdostepnij = findViewById(R.id.btUdostepnij)
// Siatka 3 kolumny
lista.layoutManager = GridLayoutManager(this, 3)
adapter = AdapterObrazow(
onClick = { item -> klikPojedynczy(item) },
onLongClick = { item -> przelaczZaznaczenie(item) },
isSelected = { id -> zaznaczone.contains(id) }
)
lista.adapter = adapter
// Start
sprawdzUprawnienieIStart()
btUdostepnij.setOnClickListener { udostepnijZaznaczone() }
aktualizujUdostepnione()
// Filtrowanie po nazwie
editSzukaj.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
filtrNazwa = s?.toString().orEmpty()
zastosujFiltryISort()
}
})
// Menu sortowania (w menu_glowne.xml)
toolbar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sortujNajnowsze -> { sortNajnowsze = true; zastosujFiltryISort(); true }
R.id.sortujNajstarsze -> { sortNajnowsze = false; zastosujFiltryISort(); true }
else -> false
}
}
}
// --- Uprawnienia ---
private fun sprawdzUprawnienieIStart() {
val ok = ContextCompat.checkSelfPermission(this, permission)== PackageManager.PERMISSION_GRANTED
if (ok) wczytajZdjecia() else reqPerm.launch(permission)
}
// --- Wczytanie MediaStore ---
private fun wczytajZdjecia() {
lifecycleScope.launch {
val listaObrazow = withContext(Dispatchers.IO) { zapytajMediaStore() }
wszystkie.clear()
wszystkie.addAll(listaObrazow)
zbudujAlbumy()
zastosujFiltryISort()
}
}
private fun zapytajMediaStore(): List<ElementObraz> {
val wynik = mutableListOf<ElementObraz>()
val kolekcja: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME
)
val selection = "${MediaStore.Images.Media.MIME_TYPE} LIKE ?"
val selectionArgs = arrayOf("image/%")
val sortOrder = if (sortNajnowsze)
"${MediaStore.Images.Media.DATE_ADDED} DESC"
else
"${MediaStore.Images.Media.DATE_ADDED} ASC"
contentResolver.query(kolekcja, projection, selection, selectionArgs, sortOrder)?.use { c ->
val idCol = c.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val nameCol = c.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
val dateCol = c.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)
val bucketCol = c.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
while (c.moveToNext()) {
val id = c.getLong(idCol)
val name = c.getString(nameCol) ?: ""
val date = c.getLong(dateCol)
val bucket = c.getString(bucketCol) ?: "Nieznany"
val uri = ContentUris.withAppendedId(kolekcja, id)
wynik.add(
ElementObraz(
id = id,
adres = uri,
nazwaPliku = name,
dataDodania = date,
album = bucket
)
)
}
}
return wynik
}
// --- Filtrowanie/sortowanie + albumy ---
private fun zastosujFiltryISort() {
val q = filtrNazwa.trim().lowercase()
val album = filtrAlbum
przefiltrowane.clear()
przefiltrowane.addAll(
wszystkie.filter { item ->
val okNazwa = if (q.isEmpty()) true else item.nazwaPliku.lowercase().contains(q)
val okAlbum = (album == "Wszystkie") || item.album.equals(album, ignoreCase = true)
okNazwa && okAlbum
}
)
if (sortNajnowsze) przefiltrowane.sortByDescending { it.dataDodania }
else przefiltrowane.sortBy { it.dataDodania }
adapter.submitList(przefiltrowane.toList())
aktualizujUdostepnione()
}
private fun zbudujAlbumy() {
val lista = mutableListOf("Wszystkie")
lista.addAll(wszystkie.map { it.album }.distinct().sorted())
val spinnerAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, lista)
editAlbum.setAdapter(spinnerAdapter)
editAlbum.setOnItemClickListener { _, _, pos, _ ->
filtrAlbum = lista[pos]
zastosujFiltryISort()
}
}
// --- Zaznaczanie/kliknięcia/udostępnianie ---
private fun przelaczZaznaczenie(item: ElementObraz) {
if (zaznaczone.contains(item.id)) zaznaczone.remove(item.id) else zaznaczone.add(item.id)
aktualizujUdostepnione()
val idx = przefiltrowane.indexOfFirst { it.id == item.id }
if (idx >= 0) adapter.notifyItemChanged(idx)
}
private fun klikPojedynczy(item: ElementObraz) {
if (zaznaczone.isNotEmpty()) {
przelaczZaznaczenie(item)
} else {
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(item.adres, "image/*")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
runCatching { startActivity(intent) }
}
}
//aktualizacja licznika udostepnionych w kontrolce przycisku
private fun aktualizujUdostepnione() {
btUdostepnij.text = "Udostępnij (${zaznaczone.size})"
btUdostepnij.isEnabled = zaznaczone.isNotEmpty()
}
private fun udostepnijZaznaczone() {
if (zaznaczone.isEmpty()) return
val uris = przefiltrowane.filter { it.id in zaznaczone }.map { it.adres }
val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
type = "image/*"
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(intent, "Udostępnij zdjęcia"))
}
}
Wskazówka:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
class AdapterObrazow(
private val onClick: (ElementObraz) -> Unit,
private val onLongClick: (ElementObraz) -> Unit,
private val isSelected: (Long) -> Boolean
) : ListAdapter<ElementObraz, AdapterObrazow.VH>(DIFF) {
companion object {
val DIFF = object : DiffUtil.ItemCallback<ElementObraz>() {
override fun areItemsTheSame(oldItem: ElementObraz, newItem: ElementObraz) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: ElementObraz, newItem: ElementObraz) =
oldItem == newItem
}
}
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val img = itemView.findViewById<ImageView>(R.id.obrazMiniatura)
private val overlay = itemView.findViewById<View>(R.id.nakladkaZaznaczenia)
private val check = itemView.findViewById<ImageView>(R.id.znacznikWyboru)
private val nazwaPliku = itemView.findViewById<TextView>(R.id.tekstNazwaPliku)
fun bind(item: ElementObraz) {
// Ładowanie obrazu miniatury
img.load(item.adres) { crossfade(true) }
// Ustawianie nazwy pliku
nazwaPliku.text = item.nazwaPliku
// Obsługa zaznaczenia
val sel = isSelected(item.id)
overlay.visibility = if (sel) View.VISIBLE else View.GONE
check.visibility = if (sel) View.VISIBLE else View.GONE
// Obsługa kliknięć
itemView.setOnClickListener { onClick(item) }
itemView.setOnLongClickListener { onLongClick(item); true }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.widok_element_obraz, parent, false)
return VH(v)
}
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(getItem(position))
}
Wskazówka:
import android.net.Uri
data class ElementObraz(
val id: Long, // identyfikator rekordu w MediaStore
val adres: Uri, // URI do obrazka
val nazwaPliku: String, // nazwa pliku (np. IMG_1234.jpg)
val dataDodania: Long, // data dodania (w sekundach od epoki)
val album: String // nazwa albumu/katalogu (bucket)
)
Wskazówka:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- Do Android 12L włącznie -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GaleriaMediaStore">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>