web analytics

Android Code : ทำ Adapter ของ ListView ให้เป็น MVP pattern

 

cover-2

บทความนี้ ผมมาทำต่อจากบทความที่แล้ว ซึ่งยังไม่ได้ทำ 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)

1

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