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 POJO. Jest 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ć.