[Odpowiadam] Jak pobrać ID wiersza listy, który ma swój odpowiednik w bazie danych

Na blogu staram się poruszać zagadnienia dla osób początkujących i zaawansowanych. Dziś będzie dla tych pierwszych, ale mam nadzieje, że osoby bardziej obyte w tworzeniu aplikacji na system Android znajdą też coś dla siebie. Na forum dla programistów padło pytanie, w jaki sposób pobrać ID wiersza listy, który ma swój odpowiednik w bazie danych. Dla osób bardziej zaawansowanych w programowaniu nie stanowi to większego problemu. Dla osób początkujących może wydawać się to dość trudne. Jednak nie jest to aż takie trudne 🙂

Założenia projektu

Co potrzebujemy?

  • Stworzyć listę,
  • Pobrać id klikanego elementu.
  • Wyciągnąć odpowiedni rekord z bazy, który odpowiada klikniętego elementu.

Proste? Na pewno. Nasz przykład oprzemy na wyświetleniu listy z imionami, a po kliknięciu będziemy potrzebowali wyświetlić id użytkownika oraz z jakiego miasta pochodzi.

Uzupełnij wiedzę o: Zmiana języka w Androidzie

Kod projektu

W zakładkach przedstawiony jest kod poszczególnych plików:

public class MainActivity extends AppCompatActivity {
    private String DEBUG_TAG = "MainActivity";
    private DBHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new DBHelper(this);
        dbHelper.open();
        showListView();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        dbHelper.close();
    }
    private void showListView() {
        ListView listView = findViewById(R.id.listView1);
        SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
                android.R.layout.simple_list_item_activated_1,
                dbHelper.getAllUsers(),
                new String[]{"name"},
                new int[]{android.R.id.text1},
                0);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    Cursor cursor = null;
                    try {
                        cursor = dbHelper.getUsers(position + 1);
                        cursor.moveToFirst();
                        int idUser = cursor.getInt(cursor.getColumnIndex("_id"));
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String city = cursor.getString(cursor.getColumnIndex("city"));
                        Log.d(DEBUG_TAG, "ID user:" + idUser + ", Name: " + name + ", City: " + city);
                    } catch (Exception e) {
                        Log.d(DEBUG_TAG, e.getMessage());
                    } finally {
                        if (cursor != null) {
                            cursor.close();
                        }
                    }
                }
            });
    }
}
public class DBHelper extends SQLiteOpenHelper {
    private static final String DEBUG_TAG = "DBHelper";
    private static final int DB_VERSION = 1;
    private static final String DB_NAME = "database.db";
    private static final String DB_USER_TABLE = "user";
    private static final String KEY_ID = "_id";
    private static final String ID_OPTIONS = "INTEGER PRIMARY KEY AUTOINCREMENT";
    private static final String KEY_NAME = "name";
    private static final String NAME_OPTIONS = "TEXT NOT NULL";
    private static final String KEY_CITY = "city";
    private static final String CITY_OPTIONS = "TEXT NOT NULL";
    private static final String CREATE_USER_TABLE =
                    "CREATE TABLE IF NOT EXISTS " + DB_USER_TABLE + "( " +
                    KEY_ID + " " + ID_OPTIONS + ", " +
                    KEY_NAME + " " + NAME_OPTIONS + ", " +
                    KEY_CITY + " " + CITY_OPTIONS +
                    ");";
    private static final String DROP_USER_TABLE = "DROP TABLE IF EXISTS " + DB_USER_TABLE;
    private SQLiteDatabase db;
    public DBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
        context.deleteDatabase(DB_NAME);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_USER_TABLE);
        Log.d(DEBUG_TAG, "Table " + CREATE_USER_TABLE + " ver." + DB_VERSION + " created");
        insertUser(db,"Maciej","Krakow");
        insertUser(db,"Maciej","Warszawa");
        insertUser(db,"Tomek","Warszawa");
        insertUser(db,"Kasia","Wroclaw");
        insertUser(db,"Paulina","Gdansk");
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL(DROP_USER_TABLE);
        Log.d(DEBUG_TAG, "Table " + DB_USER_TABLE + " updated from ver." + oldVersion + " to ver." + newVersion);
        onCreate(db);
    }
    public DBHelper open(){
        try {
            db = this.getWritableDatabase();
        } catch (SQLException e) {
            db = this.getReadableDatabase();
        }
        return this;
    }
    public void close() {
        this.close();
    }
    private void insertUser(SQLiteDatabase db, String name, String city) {
        ContentValues Values = new ContentValues();
        Values.put(KEY_NAME, name);
        Values.put(KEY_CITY, city);
        db.insert(DB_USER_TABLE, null, Values);
    }
    public Cursor getAllUsers() {
        String[] columns = {KEY_ID, KEY_NAME, KEY_CITY};
        return db.query(DB_USER_TABLE, columns, null, null, null, null, null);
    }
    public Cursor getUsers(int id) {
        String[] columns = {KEY_ID, KEY_NAME, KEY_CITY};
        String WHERE = KEY_ID+" like "+id;
        return db.query(DB_USER_TABLE, columns, WHERE, null,null, null, null);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>

Kod wydaje się prosty i taki ma być. Moglibyśmy zastosować tutaj jakiś wzorzec projektowy, ale na potrzeby przykładu tego nie robię. W klasie MainActivity tworzymy na początku połączenie z bazą. Dalej metoda showListView() tworzy listę na podstawię imion. Imiona zostały pobrane z bazy danych za pomocą metody getAllUsers(). Nic nie szkodzi na przeszkodzie, aby tylko wybrać kolumnę „name”. Celowo taką metodę zrobiłem, ponieważ będzie nam potrzebna później. Zwróć uwagę też, że w metodzie onItemClick() pobieramy pozycję klikanego elementu i dodajemy 1. Jest to tym spowodowane, że wiersze ListView liczone są od 0. A my nie mamy rekordu w bazie rekordu o _id= 0.

Uzupełnij wiedzę o: Przezroczysty pasek powiadomień

Klasę DBHelper rozszerzamy o SQLiteOpenHelper. Znajdziemy tutaj typowe metody do obsługi bazy. Począwszy od stworzenia bazy skończywszy na zamknięciu połączenia z bazą. Na potrzeby przykładu w konstruktorze dałem kasowanie bazy. Za każdym razem, gdy aplikacja jest otwierana baza jest kasowana i tworzona od nowa. Tego nie stosuj w aplikacji produkcyjnej! Myślę, że reszta kodu jest jasna. Jeśli czegoś nie rozumiesz daj znać w komentarzu.
I tyle? Nie do końca. Zauważ, że mamy dwa połączenia do bazy. Pierwszy raz, gdy pobieramy imiona, a drugi raz, gdy pobieramy dane konkretnego rekordu. Musimy to jakoś zoptymalizować.

Ulepszamy kod

Ogólnie rzecz biorąc mamy już prawie wszystko gotowe musimy tylko przerobić listę. Zamiast ListView skorzystajmy z RecyclerView. Jest on bardziej elastyczny i łatwiej go rozbudować w przyszłości. Moglibyśmy oczywiście wykorzystać ListView i stworzyć własny adapter. Patrząc z punktu wydajności nie jest to dobry pomysł. Nam zależy na wydajności, zwłaszcza gdy będziemy mieć sporo danych. Spójrz na kolejny kod,

public class MainActivity extends AppCompatActivity {
    private String DEBUG_TAG = "MainActivity";
    private DBHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new DBHelper(this);
        dbHelper.open();
        showRecyclerView();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        dbHelper.close();
    }
    private void showRecyclerView(){
        RecyclerView recyclerView = findViewById(R.id.recyclerView);
        // w celach optymalizacji
        recyclerView.setHasFixedSize(true);
        // ustawiamy LayoutManagera
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        // ustawiamy animatora, który odpowiada za animację dodania/usunięcia elementów listy
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        // tworzymy źródło danych - tablicę z artykułami
        ArrayList<User> users = new ArrayList<>();
        Cursor cursor = null;
        try {
            cursor = dbHelper.getAllUsers();
            cursor.moveToFirst();
            while (!cursor.isAfterLast()) {
                User u = new User();
                u.id = cursor.getInt(cursor.getColumnIndex("_id"));
                u.name = cursor.getString(cursor.getColumnIndex("name"));
                u.city = cursor.getString(cursor.getColumnIndex("city"));
                users.add(u);
                cursor.moveToNext();
            }
        }catch (Exception e) {
            Log.d(DEBUG_TAG, e.getMessage());
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        // tworzymy adapter oraz łączymy go z RecyclerView
        recyclerView.setAdapter(new RecyclerViewAdapter(users, recyclerView));
    }
}
class RecyclerViewAdapter extends RecyclerView.Adapter {
    private static final String DEBUG_TAG = "recyclerViewAdapter";
    private ArrayList<User> mUsers;
    private RecyclerView mRecyclerView;
    static class MyViewHolder extends RecyclerView.ViewHolder {
        private TextView mName;
        private MyViewHolder(View pItem) {
            super(pItem);
            mName = pItem.findViewById(R.id.name);
        }
    }
    // konstruktor adaptera
    public RecyclerViewAdapter (ArrayList<User> pUsers, RecyclerView pRecyclerView){
        mUsers = pUsers;
        mRecyclerView = pRecyclerView;
    }
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, final int i) {
        // tworzymy layout oraz obiekt ViewHoldera
        View view = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.list, viewGroup, false);
        // zwracamy obiekt ViewHolder
        return new MyViewHolder(view);
    }
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int i) {
        User users = mUsers.get(i);
        ((MyViewHolder) viewHolder).mName.setText(users.name);
        // reakcja na klikniecie elementu z listy
        viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // odnajdujemy indeks klikniętego elementu
                int position = mRecyclerView.getChildAdapterPosition(v);
                int idUser = mUsers.get(position).id;
                String name = mUsers.get(position).name;
                String city = mUsers.get(position).city;
                Log.d(DEBUG_TAG, "ID user:"+idUser+", Name: "+name+", City: "+city );
                // Zrob cos wiecej...
            }
        });
    }
    @Override
    public int getItemCount() {
        return mUsers.size();
    }
}
public class User {
    public int id;
    public String name;
    public String city;
    //getters and setters...
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/name"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="15sp" >
</TextView>

W metodzie showRecyclerView() tworzymy źródło danych — tablicę z artykułami, a następnie adapter oraz łączymy go z RecyclerView. W klasie onCreate() wywołujemy showRecyclerView() i gotowe.
Klasa Users stanowi przykład prostego obiektu POJOJest też odpowiednikiem kolumn w naszej bazie. Tutaj przedstawione są pola jako publiczne, ale nie powinno tego się robić. Powinieneś ustawić je jako prywatne oraz dodać gettery i settery.
Klasa RecyclerViewAdapter zawiera trzy metody:

  • onCreateViewHolder() – tworzymy obiekt layoutu elementu listy oraz na jego podstawie tworzymy obiekt ViewHolder
  • onBindViewHolder() – uzupełniamy element listy odpowiednimi danymi
  • getItemCount() – zwracamy ilość wszystkich elementów

Oprócz tego mamy wewnętrzną klasę MyViewHolder, która implementuje wzorzec ViewHolder. Każdy obiekt tej klasy przechowuje odniesienie do elementu layoutu listy dzięki temu wywołujemy findViewById() tylko raz dla każdego elementu.

Uzupełnij wiedzę o: Analiza struktury layoutu za pomocą Hierarchy Viewer

Podsumowanie

To nie koniec to dopiero początek. Pamiętaj, że bazę danych musisz mądrze zaprojektować, aby nie było problemów z wydajnością. Jeżeli będziesz miał bardzo dużo rekordów nie ładuj wszystkiego do Cursora. Może skończyć Ci się pamięć operacyjna albo zawiesi się aplikacja. Co możesz w tej sytuacji zrobić? Pierwsza opcja to pobranie 1000 pierwszych rekordów i dodanie przycisku typu „pokaż więcej”, który pobierze kolejne 1000 itd. Druga opcja to skorzystanie z Lazy data loading — ładowanie danych „na bieżąco”. Warto przy ładowaniu danych z bazy pokazywać jakąś animację, aby użytkownik miał wrażenie, że aplikacja działa.
Odpowiadając na pytanie, w jaki sposób pobrać ID wiersza listy, który ma swój odpowiednik w bazie danych można stwierdzić, że da się i to nie tylko metodami, które zostały zaprezentowane. Oczywiście powyższy przykład można dalej rozbudowywać.

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂