Uvod
U osnovnoj verziji adaptera se koristi samo jedan String podatak koji se smešta u neki od predefinisanih android layouta sa jednim TextView-om.
Custom adapter se razlikuje od “običnog” po tome što je kod njega jedan element liste predstavljen sa više podataka te je potrebno definisati “složeniji layout” koji bi prihvatio i prikazao te podatke. Takav customLayout row liste može da sadrži više subView-ova i widgeta: sliku, tekstualne podatke (rasporedjene na svojim specifičnim pozicijama)…
Postupak kreiranja Custom ArrayAdapter-a
a) Custom model (prihvata više podataka za jedan član liste)
U ovome primeru jedan član liste sadrži dva podatka, stoga je potrebno napraviti klasu koja će da bude “modla” za kreiranje objekata koji čuvaju podatke jednog člana nekog AdapterView-a:
User.java
1 2 3 4 5 6 7 8 9 |
public class User { public String name; public String hometown; public User(String name, String hometown) { this.name = name; this.hometown = hometown; } } |
Sada u okviru Aktivnosti (Fragmenta) možemo na sledeći način da definišemo inicijalni niz podataka kreirajući nove objekte:
Aktivnost
1 2 3 4 5 6 7 8 9 10 11 |
User[] spisakLjudi = new User[]{ new User("Pera", "Beograd"), new User("Mika", "Pančevo"), new User("Zika", "Lozinica"), new User("Steva", "Smederevo"), new User("Rade", "Čačak"), new User("Jovan", "Beočin"), new User("Sima", "Barajevo"), new User("Dejan", "Prokuplje"), new User("Milan", "Niš") }; |
ili da na osnovu njega generišemo ArrayList User objekata:
1 2 3 4 |
final ArrayList listaLjudi = new ArrayList(); for (int i = 0; i < nizLjudi.length; ++i) { listaLjudi.add(nizLjudi[i]); } |
User.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class User { // Konstruktor koji konvertuje JSON objekat u Java class instancu public User(JSONObject object){ try { this.name = object.getString("name"); this.hometown = object.getString("hometown"); } catch (JSONException e) { e.printStackTrace(); } } // Statična factory metoda koja konvertuje niz JSON objekata u listu objekata, pristupa joj se sa User.fromJson(jsonArray) public static ArrayList<User> fromJson(JSONArray jsonObjects) { ArrayList<User> users = new ArrayList<User>(); for (int i = 0; i < jsonObjects.length(); i++) { try { users.add(new User(jsonObjects.getJSONObject(i))); } catch (JSONException e) { e.printStackTrace(); } } return users; } } |
Za generisanje podataka iz JSON-a koristimo sledeći kod koji konvertuje JSON u ArrayList User objekata:
Aktivnost
1 2 3 |
JSONArray jsonArray = ...; ArrayList<User> newUsers = User.fromJson(jsonArray) adapter.addAll(newUsers); |
b) Kreiranje AdapterView-a
Potrebno je u sklopu layout-a Aktivnosti (fragmenta) definisati mesto gde će biti smeštena lista podataka. Ovaj deo je isti kao kod običnog adaptera, kreira se jedan ListView koji će da prihvati po jedan custom row svakog člana.
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_orange_dark"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android:id="@+id/mojaLista" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> </android.support.constraint.ConstraintLayout> |
c) Kreiranje custom layout-a za jedan elementa liste
Sada je potrebno napraviti custom layout, koji će se koristiti da prikaže više podataka jednog elementa liste. Ovaj deo nije bio potreban kod “običnog” adaptera ali je sada potreban zbog viška podataka, jer ne možemo da koristimo već predefinisane androidove layout-e koji prihvataju samo jedan podatak.
item_user.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/tvName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Name" /> <TextView android:id="@+id/tvHome" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="HomeTown" /> </LinearLayout> |
d) Kreiranje Custom Adapter klase
U nastavku aplikacije je potrebno da definišemo Adapter klasu na osnovu koje će biti kreirana nova instanca adaptera u sklopu Aktivnosti (Fragmenta). U ovoj klasi treba da bude smešten ceo proces konvertovanja Java objekta u View popunjen podacima.
Pravimo custom adapter tako što kreiramo novu klasu koja ekstenduje ArrayAdapter klasu. Ekstendovanjm klase dobijamo metodu getView() koju možemo da “pregazimo” (override) tako da za svaki element liste uzme podatke iz modela i da sa njima popuni prethodno definisani Custom layout elementa.
NEekonomični ArrayAdapter
Ovo je primer ArrayAdaptera koji troši veliku količinu resursa pri skrolovanju ekrana, što se naručito oseti kod velikih lista podataka.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class UsersAdapter extends ArrayAdapter<User> { public UsersAdapter(Context context, ArrayList<User> users) { super(context, 0, users); } @Override public View getView(int position, View convertView, ViewGroup parent) { View rowView = LayoutInflater.from(getContext()).inflate(R.layout.item_user, parent, false); // Popunjavanje custom layout-a sa podacima: TextView tvName = (TextView) rowView.findViewById(R.id.tvName); TextView tvHome = (TextView) rowView.findViewById(R.id.tvHome); tvName.setText(user.name); tvHome.setText(user.hometown); return rowView; } } |
Sve mane ovog postupka kao i sam poboljšani postupak koji rešava te manjkovasti je prikazan u narednom primeru.
Poboljšani ArrayAdapter (sa primenjenim ViewHolder pattern-om)
Prva stvar na koju trebamo da obratimo pažnju je ta da se u NEekonomičnom primeru pri svakom pozivanju metode getView() kreira novi “rowView”.
1 |
View rowView = LayoutInflater.from(getContext()).inflate(R.layout.item_user, parent, false); |
Ukoliko naša lista ima veći broj elemenata koji ne mogu da stanu na jedan ekran, pri svakom novom skrolovanju se generišu novi View-i (svaki View je 1-2kB), što stalno povećava opterećenje memorije.
convertView
Upravo zbog ovog problema Android prosledjuje metodi getVIew() parametar “convertView”. Ovaj parametar se koristi da u njega “skladištimo” View (npr. naš jedan rowView), koji ćemo kasnije koristiti iznova i iznova. Da bi smo sprečili “inflating uvek istog XML-a” (koja je skupa operacija) za svaki novi element liste, jednom napravljen View ćemo sačuvati u okviru convertView promenjive.
1 2 3 |
if (convertView == null) { convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_user, parent, false); } |
U prethodnom kodu proveravamo da li već postoji neki sačuvan View u promenjivoj, a ukoliko ga nema, mi ćemo ga kreirati (samo prvi put) kao naš rowView, jer će nam tako definisan convertView biti uvek dostupan kao parametar getView() metode.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override public View getView(int position, View convertView, ViewGroup parent) { // Iskorištavanje prednosti koju nam daje android sa convertView-om: if (convertView == null) { convertView = LayoutInflater.from(getContext()).inflate(R.layout.user_item_row, parent, false); } TextView tvName = (TextView) convertView.findViewById(R.id.tvName); TextView tvHome = (TextView) convertView.findViewById(R.id.tvHome); // Popunjavanje custom layout-a sa podacima: tvName.setText(user.name); tvHome.setText(user.hometown); return convertView; } |
ViewHolder pattern
Sledeći problem koji se javlja je učestalo pozivanje metode findViewById() za svaku podstavku (subView) convertView-a. Često pozivanje metoda findViewById() opterećuje sistem i smanjuje performance aplikacije (naručito ako ima mnogo podataka).
Za rešavanje ovoga problema nam u pomoć nam priskače tzv. “ViewHolder pattern” koji se oslanja na to da već koristimo convertView, i da možemo jednostavno da targetiramo sve njegove subView-ove (samo jednom) i tako izbegnemo stalno pozivanje metode findViewById(). Stoga kreiramo jednu unutrašnju statičnu klasu koja se često naziva ViewHolder.
1 2 3 4 |
private static class ViewHolder { TextView tvName; TextView tvHome; } |
Članove klase možemo da povežemo sa odgovrajućim View-om tako što ćemo pozivati findViewById(), ali uz napomenu da ćemo to uraditi samo jednom i to u trenutku kada prvi put definišemo convertView-a:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ViewHolder rowViewHolder; if (convertView == null) { convertView = LayoutInflater.from(getContext()).inflate(R.layout.user_item_row, parent, false); rowViewHolder = new ViewHolder(); rowViewHolder.tvName = convertView.findViewById(R.id.tvName); rowViewHolder.tvHome = convertView.findViewById(R.id.tvHome); rowViewHolder.ivSlika = convertView.findViewById(R.id.ivSlika); convertView.setTag(rowViewHolder); } else { rowViewHolder = (ViewHolder) convertView.getTag(); } |
U prethodnom primeru smo koristili i metodu setTag() da bi smo sačuvali ViewHolder objekat ako smo ga već jednom napravili i onda smo ga kasnije pozvali metodu getTag() i tako dobili nazad sačuvani objekat.
Korišćenje ViewHolder pattern-a ubrzava populaciju ListView-a, te sa njim dobijamo glatko i brzo učitavanje stavki. Njegova implementacija omogućava da se izbegne korišćenje “skupog” metoda findViewById() u okviru adapter-a. Ceo primer custom adapter-a:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class UsersAdapter extends ArrayAdapter<User> { Context mContext; public UsersAdapter(Context context, ArrayList<User> users) { super(context, 0, users); } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder rowViewHolder; if (convertView == null) { convertView = LayoutInflater.from(getContext()).inflate(R.layout.user_item_row, parent, false); rowViewHolder = new ViewHolder(); rowViewHolder.tvName = convertView.findViewById(R.id.tvName); rowViewHolder.tvHome = convertView.findViewById(R.id.tvHome); convertView.setTag(rowViewHolder); } else { rowViewHolder = (ViewHolder) convertView.getTag(); } // Popunjavanje custom layout-a sa podacima: User user = getItem(position); rowViewHolder.tvName.setText(user.name); rowViewHolder.tvHome.setText(user.hometown); return convertView; } private static class ViewHolder { TextView tvName; TextView tvHome; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:attr/selectableItemBackground"> <ImageView android:id="@+id/imageView" android:layout_width="60dp" android:layout_height="60dp" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@mipmap/ic_launcher" /> <TextView android:id="@+id/textView" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="16dp" android:layout_marginEnd="8dp" android:layout_toEndOf="@+id/imageView" android:text="Line 1" android:textColor="@android:color/black" android:textSize="20sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toEndOf="@+id/imageView" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/textView2" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="12dp" android:layout_marginEnd="8dp" android:layout_toEndOf="@+id/imageView" android:text="Line 2" android:textSize="15sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.003" app:layout_constraintStart_toEndOf="@+id/imageView" app:layout_constraintTop_toBottomOf="@+id/textView" /> </android.support.constraint.ConstraintLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public class ExampleAdapter extends RecyclerView.Adapter <ExampleAdapter.ExampleViewHolder>{ ArrayList<ExampleItem> mExampleList; public ExampleAdapter(ArrayList<ExampleItem> exampleList) { mExampleList = exampleList; } // ViewHolder public static class ExampleViewHolder extends RecyclerView.ViewHolder { public ImageView mImageView; public TextView mTextView1; public TextView mTextView2; // konstruktor ViewHolder klase public ExampleViewHolder(@NonNull View itemView) { super(itemView); mImageView = itemView.findViewById(R.id.imageView); mTextView1 = itemView.findViewById(R.id.textView); mTextView2 = itemView.findViewById(R.id.textView2); } } @NonNull @Override public ExampleViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.example_item, viewGroup, false); ExampleViewHolder evh = new ExampleViewHolder(v); return evh; } @Override public void onBindViewHolder(@NonNull ExampleViewHolder exampleViewHolder, int i) { ExampleItem currentItem = mExampleList.get(i); exampleViewHolder.mImageView.setImageResource(currentItem.getImageResource()); exampleViewHolder.mTextView1.setText(currentItem.getText1()); exampleViewHolder.mTextView2.setText(currentItem.getText2()); } @Override public int getItemCount() { return mExampleList.size(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class MainActivity extends AppCompatActivity { private RecyclerView mRecyclerView; private RecyclerView.LayoutManager mLayoutManager; private ExampleAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ArrayList<ExampleItem> exampleList = new ArrayList<>(); exampleList.add(new ExampleItem(R.drawable.nasmejan, "Pera", "Pancevo")); exampleList.add(new ExampleItem(R.drawable.ravnodusan, "Mika", "Beograd")); exampleList.add(new ExampleItem(R.drawable.tuzan, "Steva", "Nis")); mRecyclerView = findViewById(R.id.recyclerView); mRecyclerView.setHasFixedSize(true); mLayoutManager = new LinearLayoutManager(this); mRecyclerView.setLayoutManager(mLayoutManager); mAdapter = new ExampleAdapter(exampleList); mRecyclerView.setAdapter(mAdapter); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public class MainActivity extends AppCompatActivity { private RecyclerView mRecyclerView; private RecyclerView.LayoutManager mLayoutManager; private ExampleAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ArrayList<ExampleItem> exampleList = new ArrayList<>(); exampleList.add(new ExampleItem(R.drawable.nasmejan, "Pera", "Pancevo")); exampleList.add(new ExampleItem(R.drawable.ravnodusan, "Mika", "Beograd")); exampleList.add(new ExampleItem(R.drawable.tuzan, "Steva", "Nis")); mRecyclerView = findViewById(R.id.recyclerView); mRecyclerView.setHasFixedSize(true); mLayoutManager = new LinearLayoutManager(this); mRecyclerView.setLayoutManager(mLayoutManager); mAdapter = new ExampleAdapter(exampleList); mRecyclerView.setAdapter(mAdapter); mAdapter.setOnItemClickListener(new ExampleAdapter.OnItemClickListener() { @Override public void onItemClick(int position) { Toast.makeText(MainActivity.this, "Kliknut element sa pozicijom " + position, Toast.LENGTH_SHORT).show(); } }); } } |
Method | Description |
---|---|
notifyItemChanged(int pos) | Notify that item at position has changed. |
notifyItemInserted(int pos) | Notify that item reflected at position has been newly inserted. |
notifyItemRemoved(int pos) | Notify that items previously located at position has been removed from the data set. |
notifyDataSetChanged() | Notify that the dataset has changed. Use only as last resort. |