web analytics

Android Code : ลองแปลงโค้ดให้เป็น MVP pattern

cover-mvp

บทความนี้เป็นบทความสำหรับผู้เริ่มต้น ที่อยากรู้จัก MVP อาจจะเคยได้ยินมาแต่ไม่รู้จะเริ่มยังไง ผมเองก็เป็นผู้เริ่มต้นเช่นกัน เดี๋ยวเรามาลองทำกัน จะเน้นไปที่การแปลงโค้ดแบบปกติให้เป็น MVP โดยบทความนี้เป็นบทความแปลครับ โดยผมไปอ่านเจอใน medium ของคุณ Miquel เลยเอามาลองทำตาม แล้วก็มาเขียนบทความแปล สรุปตามความเข้าใจ ใครสนใจอยากอ่านต้นฉบับดูได้ที่ reference ท้ายบทความเลยนะ

Refactoring to MVP.
A way to learn MVP with real code.

 

อะไรคือ MVP

คงต้องร่ายยาวพอสมควร เกี่ยวกับ MVP แต่มีคนเขียนบทความอธิบายไว้แล้วอย่างละเอียด
ดังนั้นหากยังไม่เคยอ่าน MVP มาก่อน แนะนำให้อ่านบทความของคุณเอก เอ็กโออาซีไอเอสที  ก่อนจะดีมาก

http://www.akexorcist.com/2015/12/android-development-with-mvp-part-1.html

 

เรื่มต้น

มาดูตัวอย่าง code ของ Activity ข้างล่างนี้ ซึ่งเป็นการเรียก service มาแสดงข้อมูลใน listView โดยใช้ retrofit

public class MainActivity extends AppCompatActivity {
    
    private BenznestAPI service;
    private TransactionsAdapter adapter;
    private ListView listView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Configure Retrofit
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://www.benzneststudios.com/benznesttest")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        service = retrofit.create(BenznestAPI.class);
        adapter = new TransactionsAdapter();

        listView = (ListView) findViewById(R.id.listView);
        listView.setAdapter(adapter);

        displayAll();
    }

    private void displayAll() {
        service.getAllTransactions()
                // enqueue runs the request on a separate thread
                .enqueue(new Callback<MessageTransactionsDao>() {

                    @Override
                    public void onResponse(Call<MessageTransactionsDao> call, Response<MessageTransactionsDao> t) {
                        updateUi(t.body().getTransactions());
                    }

                    // In case of error, this method gets called
                    @Override
                    public void onFailure(Call<MessageTransactionsDao> call, Throwable t) {
                        t.printStackTrace();
                    }
                });
    }

    private void updateUi(ArrayList<TransactionsDao> transactions) {
        adapter.setData(transactions);
        adapter.notifyDataSetChanged();
    }
}

 

ปัญหาคือ Activity รู้มากเกินไป

Activity เก็บโค้ดไว้กับตัวเองมากเกินไป มันรู้เกี่ยวกับการดึงข้อมูล service รู้ทั้งการรับข้อมูล การอัพเดท UI ทำให้แก้ไขภายหลังยากลำบาก  เราจึงต้องลดหน้าที่ของมันลง ให้ Activity ทำงานแค่งานของมันพอ คือรับข้อมูลแล้วแสดงผล

 

สร้าง Model

Model ก็คือข้อมูล เป็น POJO คงไม่มีอะไรมากนัก ในที่นี้ของผมคือ TransactionsDao ซึ่งก็ควรจะมีอยู่แล้ว

public class TransactionsDao {
    private int transactionId;
    private String transactionName;

    public int getTransactionId() {
        return transactionId;
    }

    public void setTransactionId(int transactionId) {
        this.transactionId = transactionId;
    }

    public String getTransactionName() {
        return transactionName;
    }

    public void setTransactionName(String transactionName) {
        this.transactionName = transactionName;
    }
}

 

สร้าง Interactor

Interactor ทำหน้าที่ทำงานระหว่างกลาง ระหว่าง Model และ Presenter คอยดึงข้อมูลและส่งต่อข้อมูลไปยัง presenter ก่อนอื่นสร้าง Java interface class ที่ระบุพวก method ก่อนสำหรับจัดการข้อมูล แล้วค่อยสร้างคลาสของ InteractorImpl ที่ implement อีกที

ปัญหาอย่างแรกที่เห็นคือเจ้า Retrofit มันค่อนข้างเกะกะมาก ดังนั้นเราจะแยกมันออกมาเป็นคลาส Interactor
เริ่มจากสร้าง Interface class

TransactionsInteractor.java (Interface class)

public interface TransactionsInteractor {
    Call<MessageTransactionsDao> getAllTransactions();
}

 

จากนั้นก็สร้าง class implementation ผมใส่ Impl ต่อท้ายให้รู้ว่าเป็น Implement แล้วแต่สไตการเขียน ตัวอย่างนี้ทำตามเค้ามาอีกที
TransactionsInteractorImpl.java

public class TransactionInteractorImpl implements TransactionsInteractor {
    private BenznestAPI service;

    public TransactionInteractor() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://www.benzneststudios.com/benznesttest")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        service = retrofit.create(BenznestAPI.class);
    }

    @Override
    public Call<MessageTransactionsDao> getAllTransactions() {
        return service.getAllTransactions();
    }
}

เวลาจะใช้งาน ก็เพียง new Instance

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        interactor = new TransactionInteractorImpl();
        ..

แล้วก็เรียกผ่าน method

interactor.getAllTransactions()
..

 

สร้าง View

ก่อนจะทำ Presenter จะต้องทำ Model กับ View ให้พร้อมก่อน เพราะ Presenter อยู่ตรงกลาง ใช้งานทั้งสองตัว

สร้าง Interface class ขึ้นมา ข้างในระบุ method ที่ view นั้นใช้ เช่น ซ่อนข้อมความนั้น แสดงปุ่มนี้ อัพเดท textview เป็นต้น
ในกรณีนี้ ก็เอาข้อมูลมาแสดงพอ

public interface TransactionView {
    void updateUi(ArrayList<TransactionsDao> transactions);
}

 

แล้วเอาไป implement กับ Activity หรือ Fragment ที่ต้องการ ดังนั้น Activity ก็จัดว่าเป็น View

public class MainActivity extends AppCompatActivity implements TransactionView {
..

 

แล้วก็ override method ใน Activity ก็จะได้แบบนี้
(ในที่นี้คือ set ข้อมูลไปแสดงใน listView)

    @Override
    public void updateUi(ArrayList<TransactionsDao> transactions) {
            adapter.setData(transactions);
            adapter.notifyDataSetChanged();
    }

 

สร้าง Presenter

Presenter จะต้องทำ 3 อย่าง

  • เชื่อมกับ Model&Interactor
  • เชื่อมกับ View
  • method ที่เกี่ยวข้อง

เริ่มจากสร้างคลาส Presenter ขึ้นมา

public class TransactionsPresenter {
}

 

เชื่อม Presenter กับ Interactor Model

ก็แค่ประกาศตัวแปรของ interactor พร้อมกับ setter หรือ constructor ขึ้นมา

public class TransactionsPresenter {
    TransactionsInteractor mInteractor;

    public TransactionsPresenter(TransactionsInteractor interactor) {
        mInteractor = interactor;
    }
}

 

เชื่อม Presenter กับ View

ต่อมาก็ประกาศตัวแปร View เพิ่มใน Presenter แต่จะไม่ใช้ method setter หรือ constructor ในการกำหนดค่า มักจะใช้ method ชื่อเฉพาะแทน เช่น bind/unbind , attach/detach

public class TransactionsPresenter {
    TransactionsView view;
    TransactionsInteractor mInteractor;

    public TransactionsPresenter(TransactionsInteractor interactor) {
        mInteractor = interactor;
    }

    public void bind(TransactionsView view) {
        this.view = view;
    }

    public void unbind() {
        view = null;
    }
}

 

ดังนั้น ก่อนจะใช้ Presenter ก็ต้องทำการ bind ก่อนโดยกำหนด View หรือ Activity ที่ implement view ให้มัน
ดังนั้นก็มักจะทำ bind ใน onCreate() และ unbind ตอน onDestroy เป็นต้น

 

กำหนด method ที่เกี่ยวข้อง

สุดท้ายก็กำหนดงานให้ presenter ซึ่งก็คือ เอา interactor กับ view มาใช้
ในกรณีของเราก็คือ สั่ง interactor ไปดึงข้อมูลมา และพอได้ข้อมูลก็ส่งกลับให้ view เพื่อแสดงผล ผ่าน method ของ View ดังนั้นเราจะย้าย โค้ดส่วน enqueue callback มาไว้ที่ Presenter

public class TransactionsPresenter {
    TransactionsView view;
    TransactionsInteractor mInteractor;

    public TransactionsPresenter(TransactionsInteractor interactor) {
        mInteractor = interactor;
    }

    public void bind(TransactionsView view) {
        this.view = view;
    }

    public void unbind() {
        view = null;
    }

    private void displayAll() {
        mInteractor.getAllTransactions()
                .enqueue(new Callback<MessageTransactionsDao>() {
                    @Override
                    public void onResponse(Call<MessageTransactionsDao> call, Response<MessageTransactionsDao> data) {
                        view.updateUi(data.body().getTransactions());
                    }

                    @Override
                    public void onFailure(Call<MessageTransactionsDao> call, Throwable t) {
                        t.printStackTrace();
                    }
                });
    }
}

 

ทำการ bind View ให้กับ Presenter

กลับมาที่ MainActivity ก็ประกาศตัวแปร Presenter กำหนด interactor ให้มัน แล้วก็ bind View

public class MainActivity extends AppCompatActivity implements TransactionsView {

    ..

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        interactor = new TransactionsInteractorImpl();
        presenter  = new TransactionsPresenter(interactor);
        presenter.bind(this);
        ..

 

อย่าลืม unbind ด้วย

    @Override
    protected void onDestroy() {
        presenter.unbind();
        super.onDestroy();
    }

 

สรุป MainActivity

public class MainActivity extends AppCompatActivity implements TransactionsView {

    private TransactionsAdapter adapter;
    private ListView listView;
    private TransactionsInteractorImpl interactor;
    private TransactionsPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        interactor = new TransactionsInteractorImpl();
        presenter = new TransactionsPresenter(interactor);
        presenter.bind(this);
        
        adapter = new TransactionsAdapter();
        listView = (ListView) findViewById(R.id.listView);
        listView.setAdapter(adapter);

        presenter.displayAll();
    }


    @Override
    public void updateUi(ArrayList<TransactionsDao> transactions) {
        adapter.setData(transactions);
        adapter.notifyDataSetChanged();
    }

    @Override
    protected void onDestroy() {
        presenter.unbind();
        super.onDestroy();
    }
}

 

สรุป

สรุป MVP เป็นการจัดระเบียบการนำเสนอของโค้ดรูปแบบหนึ่ง จะเห็นว่า MainActivity ไม่รู้อะไรเกี่ยวกับ Model เลย ไม่รู้แม้กระทั่งเกี่ยวกับ Retrofit ไม่รู้เลยว่าการ call service จะสำเร็จหรือไม่ มันมีหน้าที่แค่แสดงผลตามที่ได้รับมาเท่านั้น การทำงานหลักเกี่ยวกับ service อยู่ที่ Presenter ทั้งหมด ทำให้โค้ดใน Activity / Fragment จะกระชับขึ้น

 

หากมีข้อผิดพลาด หรือคำแนะนำ คอมเม้นไว้ได้เลยครับ (:

 

References

http://www.akexorcist.com/2015/12/android-development-with-mvp-part-1.html
https://medium.com/@Miqubel/refactoring-to-mvp-b504a3774ffd#.b8q77cdio
https://medium.com/@kenjuwagatsuma/mvp-architecture-in-android-development-3d63cc32707a#.agnr3h5r9