Android Code : ทำ Adapter ของ ListView ให้เป็น MVP pattern
บทความนี้ ผมมาทำต่อจากบทความที่แล้ว ซึ่งยังไม่ได้ทำ Adapter ให้เป็น MVP จริงๆ ขอสารภาพว่ายังไม่ค่อยแม่นเรื่อง MVP และกำลังศึกษาอยู่ด้วยการไปดูโค้ดชาวบ้าน MVP ใน github ดูแนวทางแล้ว นำมาทำตามดู เขียนสรุปตามความเข้าใจ ในบทความนี้จะเป็น Adapter ของ ListView ธรรมดา
บทความก่อนหน้านี้
https://benzneststudios.com/blog/android/refactoring-to-mvp-pattern-android/
ถ้ามีข้อผิดพลาด หรือมีจุดไหนที่ผิด รบกวนช่วยคอมเม้นไว้ด้วยนะครับ
เริ่มต้น
สิ่งที่เกี่ยวกับ Adapter ของ listView ก็จะมีหลักๆ 3 อย่าง ที่ปกติเราจะเขียนกัน
Custom view item layout (.xml)
Custom view item class (.java)
Adapter class (.java)
row_transaction.xml
<?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="wrap_content" android:orientation="horizontal" android:padding="16dp"> <TextView android:id="@+id/tv_transaction_id" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ID" android:layout_gravity="center_vertical" android:textSize="20sp"/> <TextView android:id="@+id/tv_transaction_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="NAME" android:layout_marginLeft="20dp" android:layout_gravity="center_vertical" android:textSize="20sp"/> <View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1"/> <Button android:id="@+id/btn_download" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Edit" /> </LinearLayout>
RowTransactionView.java
public class RowTransactionView extends FrameLayout { TextView tvTransactionId; TextView tvTransactionName; Button btnDownload; public RowTransactionView(Context context) { super(context); init(); } public RowTransactionView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public RowTransactionView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public RowTransactionView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } private void init() { inflate(getContext(), R.layout.row_transaction, this); // init instance. tvTransactionId = (TextView) findViewById(R.id.tv_transaction_id); tvTransactionName = (TextView) findViewById(R.id.tv_transaction_name); btnDownload = (Button) findViewById(R.id.btn_download); btnDownload.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // } }); } public void setTransactionId(int id) { tvTransactionId.setText("" + id); } public void setTransactionName(String name) { tvTransactionName.setText(name); } }
TransactionAdapter.java
public class TransactionsAdapter extends BaseAdapter { ArrayList<TransactionsDao> transactions; public TransactionsAdapter() { transactions = new ArrayList<>(); } public void setData(ArrayList<TransactionsDao> transactions) { this.transactions = transactions; } @Override public int getCount() { return transactions.size(); } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { RowTransactionView row; if (convertView == null) { row = new RowTransactionView(parent.getContext()); }else{ row = (RowTransactionView) convertView; } row.setTransactionId(transactions.get(position).getTransactionId()); row.setTransactionName(transactions.get(position).getTransactionName()); return row; } }
นี่คือการทำงานแบบปกติ เดี๋ยวเราจะมาลองทำ Adapter เป็นแบบ MVP pattern กัน
ปัญหาคือ Adapter รู้มากเกินไป
ตัว Adapter จากด้านบน มันสามารถเสก view ขึ้นมา มันเก็บข้อมูลได้ มัน set ข้อมูลลงไปใน view ได้ด้วย เดี๋ยวเราจะมาแบ่งหน้าที่มันออก เป็น View กับ presenter
การทำงานของ AdapterPresenter จริงๆแล้ว สามารถเขียนได้หลายแบบ เท่าที่ผมดูจากชาวบ้านมา บางคนก็เขียนให้ 1 AdapterPresenter ทำงานกับทุก view แต่ก็มีแบบทำ AdapterPresenter เป็น Array หรือ HashMap แล้วให้ เป็น 1:1 เลยกับ view ก็ทำได้เหมือนกัน อยู่ที่ความซับซ้อนของ item และ type
ผมเห็นว่ากรณีตัวอย่างของผม คือมี type เดียว แบบง่ายๆ Presenter สามารถ bind view เพื่อ init ข้อมูล เสร็จแล้วก็ unbind ได้ ดังนั้นก็ทำตัวเดียวละกัน (หรือว่าเข้าใจผิดก็ไม่รู้)
สร้าง View
view ในที่นี้ก็คือตัว item ที่แสดงใน listView นั่นเอง
สร้าง interface class ของ View
IRowTransactionView.java
public interface IRowTransactionView { void setTransactionId(int id); void setTransactionName(String name); void onDownloadClicked(TransactionsDao t); }
จากนั้นที่ คลาสของ Custom groupview ก็ implement ตัว interface
public class RowTransactionView extends FrameLayout implements IRowTransactionView { TextView tvTransactionId; TextView tvTransactionName; Button btnDownload; ..
override พวก method setter
.. @Override public void setTransactionId(int id) { tvTransactionId.setText("" + id); } @Override public void setTransactionName(String name) { tvTransactionName.setText(name); } @Override public void onDownloadClicked(final TransactionsDao t) { btnDownload.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getContext() , t.getTransactionName() ,Toast.LENGTH_SHORT).show(); } }); } }
สร้าง Adapter Presenter
Adapter Presenter คือ ตัว presenter ที่ทำงานระหว่าง view (แถว layout) กับ model (ข้อมูลที่เตรียมจะแสดง)
ก่อนอื่นก็ต้องสร้าง Presenter ก่อน
สร้างคลาส interface ของ Adapter presenter
เป็นการระบุ method ที่ใช้งานใน adapter เช่น การกำหนดค่าข้อมูล
TransactionsAdapterPresenter.java
public interface TransactionsAdapterPresenter { void init(int position); }
สร้างคลาส Adapter presenter ที่ implement
TransactionsAdapterPresenterImpl.java
public class TransactionsAdapterPresenterImpl implements TransactionsAdapterPresenter { }
Presenter จะต้องทำ 3 สิ่ง
เชื่อมกับ Model
เชื่อมกับ View
implement method
เชื่อมกับ Model
ในที่นี้ก็แล้วแต่ว่าจะเอา Model มาจากไหน ผมส่งผ่านด้วย method setter อาจจะไปดึงในฐานข้อมูลหรือเรียก service มาก่อน
public class TransactionsAdapterPresenterImpl implements TransactionsAdapterPresenter { ArrayList<TransactionsDao> mTransactionsDaos; public TransactionsAdapterPresenterImpl() { mTransactionsDaos = new ArrayList<>(); } public void setData(ArrayList<TransactionsDao> data) { this.mTransactionsDaos = data; } ..
เชื่อมกับ View
ประกาศตัวแปร view (interface) ใน PresenterImpl พร้อมกับ method สำหรับ bind/unbind view
public class TransactionsAdapterPresenterImpl implements TransactionsAdapterPresenter { .. IRowTransactionView view; public void bind(IRowTransactionView v) { view = v; } public void unbind() { view = null; } ..
Method ที่เกี่ยวข้อง
ใน AdapterPresenter หน้าที่หลักของมันคือ กำหนดค่าจาก model ไปใส่ใน view
ดังนั้น method init() ก็จะทำหน้าที่ส่วนนี้
.. @Override public void init(int position) { if (view != null) { view.setTransactionName(mTransactionsDaos.get(position).getTransactionName()); view.setTransactionId(mTransactionsDaos.get(position).getTransactionId()); view.onDownloadClicked(mTransactionsDaos.get(position)); } }
กำหนด AdapterPresenter ใน Adapter
Adapter ของ listView ก็จะไม่ต้องทำงานส่วน set view แล้ว ไม่ได้เก็บ Model ไว้ด้วย โดย Model และการ set ข้อมูลจะโยนไปให้ presenter จัดการ
public class TransactionsAdapter extends BaseAdapter { TransactionsAdapterPresenterImpl presenter; public TransactionsAdapter() { presenter = new TransactionsAdapterPresenterImpl(); } public void setData(ArrayList<TransactionsDao> transactions) { presenter.setData(transactions); } @Override public int getCount() { return presenter.getCount(); } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(final int position, View convertView, ViewGroup parent) { RowTransactionView row; if (convertView == null) { row = new RowTransactionView(parent.getContext()); } else { row = (RowTransactionView) convertView; } presenter.bind(row); presenter.init(position); presenter.unbind(); return row; } }
สรุป
ตัว Adapter จะรู้แค่ว่า สร้าง view แล้วให้ presenter เสร็จแล้วก็ return view กลับไป ให้ listview ซึ่ง Adapter ไม่ได้เก็บ Model ไว้ ทำให้ไม่รู้กระบวนการของการกำหนดค่า view นั่นเอง
Reference
https://medium.com/@jsuch2362/adapter-what-role-is-it-data-view-13c713cdae0b
https://github.com/kaedea/android-mvp-pattern