31 August 2015

Let's Fragment - วิธีการรับส่งข้อมูลของ Fragment

Updated on


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

        สำหรับการรับส่งข้อมูลของ Fragment นั้นทำได้หลากหลายวิธี ซึ่งขึ้นอยู่กับความเหมาะสมและรูปแบบในการทำงานของ Fragment โดยจะขอแบ่งประเภทของรูปแบบหน่อยนะ

การส่งข้อมูลด้วย Construction Arguments 

Activity >> Fragment

        ถึงแม้ชื่อจะดูไม่คุ้นหูคุ้นตา แต่ใช้กันค่อนข้างบ่อย นั่นก็คือ Activity สร้าง Fragment ขึ้นมาโดยส่งข้อมูลเป็น Bundle ไปให้ Fragment นั่นเอง

Fragment
public class MyFragment extends Fragment {
    public static final String KEY_MESSAGE = "message";
    public static final String KEY_NUMBER = "number";

    private String message;
    private int number;

    public static MyFragment newInstance(String message, int number) {
        MyFragment fragment = new MyFragment();
        Bundle bundle = new Bundle();
        bundle.putString(KEY_MESSAGE, message);
        bundle.putInt(KEY_NUMBER, number);
        fragment.setArguments(bundle);
        return fragment;
    }

    public MyFragment() { }
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Bundle bundle = getArguments();
        if(bundle != null) {
            message = bundle.getString(KEY_MESSAGE);
            number = bundle.getInt(KEY_NUMBER);
        }
    }

    ...
    
}

        ส่วน Activity ก็จะแนบข้อมูลแบบนี้

Activity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        MyFragment myFragment = MyFragment.newInstance("My Password", 1234);
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction transaction = manager.beginTransaction();
        transaction.replace(R.id.layout_fragment_container, myFragment);
        transaction.commit();
        ...
    }
    ...
}

        และเมื่อ Fragment เริ่มทำงานแล้ว ก็จะดึงข้อมูลที่ส่งมาจาก Activity ไปใช้งานตามที่เขียนโค๊ดไว้

        วิธีนี้มีไว้สำหรับ Activity ที่ต้องการส่งข้อมูลไปให้ Fragment ตอนที่ถูกสร้างขึ้นเท่านั้น ไม่สามารถส่งข้อมูลระหว่างที่ Fragment ทำงานหรือแสดงได้ และไม่ควร Overload Method เพื่อส่งข้อมูลหลายๆรูปแบบ เพราะตอนดึงข้อมูลมาใช้งาน (คำสั่งใน onCreate) จะทำให้ยุ่งยากมากขึ้น



การส่งข้อมูลผ่าน Event Listener

Fragment <> Activity

        เป็นการสร้าง Listener บน Fragment เพื่อให้ Activity ไป Implements ทิ้งไว้ โดย Listener ที่ว่านั้นสามารถสร้างได้ตามใจชอบ ยกตัวอย่างเช่น

public interface MyFragmentListener {
    public void onButtonOkClick();
    public void onButtonCloseClick();
    public void onLoginSuccess(UserData data);
}

        โดย Listener ตัวนี้จะประกาศไว้ใน Fragment

Fragment
public class MyFragment extends Fragment {

    ...

    public static MyFragment newInstance() {
        return new MyFragment();
    }

    ...

    public interface MyFragmentListener {
        public void onButtonOkClick();
        public void onButtonCloseClick();
        public void onLoginSuccess(UserData data);
    }
}

        เมื่อเตรียม Listener พร้อมแล้วก็สร้าง Instance ของ Listener โดยอิงจาก Activity ที่ Fragment นั้นๆสิงสถิตอยู่ ซึ่งแนะนำให้ใช้คำสั่งใน onAttach เพราะเป็นตอนที่ Fragment ถูกแปะเข้ากับ Activity

Fragment
public class MyFragment extends Fragment implements View.OnClickListener {

    private MyFragmentListener listener;

    ...

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        try {
            listener = (MyFragmentListener) getActivity();
        } catch (ClassCastException e) {
            throw new ClassCastException("Must implement MyFragmentListener");
        }
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if(id == R.id.btn_ok) {
            listener.onButtonOkClick();
        } else if(id == R.id.btn_close) {
            listener.onButtonCloseClick();
        }
    }

    ...

    public interface MyFragmentListener {
        public void onButtonOkClick();
        public void onButtonCloseClick();
        public void onLoginSuccess(UserData data);
    }
}
        อันนี้เจ้าของบล็อกยกตัวอย่างขึ้นมานะครับ สมมติว่า Fragment ตัวนี้อยากจะส่ง Event ไปบอก Activity ว่ามีการกดปุ่ม Ok หรือ Close โดยการทำงานจะเริ่มต้นที่ onAttach ซึ่งจะเอา Activity มาทำ Class Cast ให้กลายเป็น MyFragmentListener แล้วเก็บไว้ที่ Global ซะ นั่นหมายความว่า Activity นั้นๆจะต้องประกาศ Implement Listener ตัวนี้ไว้ด้วย ไม่งั้นจะเกิด ClassCastException

        และเมื่ออยากจะส่ง Event ใดๆก็ตามกลับไปให้ Activity ก็เรียกผ่าน MyFragmentListener แล้วตามด้วย Event ที่สร้างเตรียมไว้ตั้งแต่แรก

        ส่วนฝั่ง Activity ก็ให้ Implement Listener ตัวนี้ไว้ให้เรียบร้อยซะ ไม่งั้นจะเกิด ClassCastException

Activity
public class MainActivity extends AppCompatActivity implements MyFragment.MyFragmentListener {

    ...

    @Override
    public void onButtonOkClick() {
        // Do something when button OK on fragment was clicked
    }

    @Override
    public void onButtonCloseClick() {
        // Do something when button Close on fragment was clicked
    }

    @Override
    public void onLoginSuccess(MyFragment.UserData data) {
        // Do something when user succeed to login from fragment 
    }
}

        เมื่อใดที่ Fragment ส่งข้อมูลมาทาง Listener ก็จะเข้ามาในฟังก์ชันที่เตรียมไว้นั่นเอง

        ซึ่งวิธีนี้จะทำให้สามารถโยน Object ใดๆก็ได้ไปให้ Activity ได้ทุกเวลา และ Fragment ไม่ผูกกับ Activity จึงทำให้ Fragment มีความยืดหยุ่นในการใช้งานกับ Activity ตัวอื่นๆด้วย



        ในทางกลับกัน ถ้าอยากจะใช้วิธีนี้เพื่อส่งข้อมูลจาก Activity ไปยัง Fragment ก็สามารถทำได้เช่นกัน ก็ให้ฝั่ง Activity สร้าง Listener ขึ้นมาแล้วให้ Fragment เป็นฝ่าย Implement แทน

        โดยมีเงื่อนไขว่า Listener ที่สร้างขึ้นควรจะสร้าง Class แยกออกมาเลยเพื่อไม่ให้ Fragment ผูกกับ Activity จากการ Implement Listener

Listener
public interface MyEventListener {
    public void onEvent();
}

        และไป Implement ไว้บน Fragment ให้เรียบร้อยซะ

Fragment
public class MyFragment extends Fragment implements MyEventListener {
    ...

    @Override
    public void onEvent() {
        // Do something when event from activity was happened
    }

    ...
}

        สำหรับบน Activity เวลาสร้าง Fragment ก็จะให้สร้าง Listener เก็บไว้ด้วย และเรียกใช้เพื่อส่งข้อมูลไปยัง Fragment (ในตัวอย่างนี้ไม่ได้ส่งข้อมูลอะไรไป)

Activity
public class MainActivity extends AppCompatActivity {
    MyEventListener fragmentEventListener;

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
       
        MyFragment myFragment = MyFragment.newInstance();

        try {
            fragmentEventListener = (MyEventListener) myFragment;
        } catch(ClassCastException e) {
            throw new ClassCastException("Must implement MyEventListener");
        }

        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction transaction = manager.beginTransaction();
        transaction.replace(R.id.layout_fragment_container, myFragment);
        transaction.commit();

        ...
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if(id == R.id.btn_confirm) {
            fragmentEventListener.onEvent();
        }
    }
}

        เพียงเท่านี้ก็ส่งข้อมูลไปให้ Fragment ได้ตามใจชอบแล้ว แต่ทว่าวิธีแบบนี้จะเหมาะกับ Fragment ที่แปะอยู่บน Activity โดยตรงซะมากกว่า จะไม่เหมาะกับ Dynamic Fragment ที่มีการแปะ Fragment ตัวใหม่ๆมาแทนที่ตลอดเวลาซักเท่าไรนัก เพราะว่า Listener จะผูกกับ Fragment เพียงหนึ่งตัวเท่านั้น



การส่งข้อมูลโดยตรงด้วยการ Call Activity/Fragment Method Directly

Activity <> Fragment

        วิธีนี้ค่อนข้างง่ายเพราะถ้าอยากจะส่งข้อมูลจาก Fragment ไปให้ Activity ก็จะสร้าง Method เตรียมไว้ใน Activity แล้วให้ Fragment ดึง Activity ขึ้นมาแล้วเรียกไปที่ Method นั้นๆโดยตรง และในการส่งข้อมูลจาก Activity ไปยัง Fragment ก็จะทำแบบเดียวกัน

         เริ่มจากการส่งข้อมูลจาก Fragment ไปยัง Activity ก่อนนะ ดังนั้นบน Activity ก็ต้องสร้าง Method ขึ้นมาเตรียมไว้ให้ Fragment เรียก

Activity
public class MainActivity extends AppCompatActivity {
    ...

    public void doSomethingByFragment(AnyObject data) {
        // Do something when any event on fragment was happened
    }

    ...
}

        และเวลาที่ Fragment ต้องการจะส่งข้อมูลก็จะใช้วิธี getActivity แล้วทำการ Class Cast ให้กลายเป็น Activity ที่ต้องการซะ จากนั้นก็เรียก Method เพื่อส่งข้อมูล

Fragment
public class MyFragment extends Fragment {
    private AnyData data;
    private MainActivity activity;

    ...

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        activity = (MainActivity) getActivity();
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if(id == R.id.btn_confirm_data) {
            activity.doSomethingByFragment(data);
        }
    }

    ...
}


       สำหรับการส่งข้อมูลจาก Activity ไปยัง Fragment ก็เหมือนกันเป๊ะๆ ทำยังไงก็ได้ให้ได้ Instance ของ Fragment ตัวนั้นมา แล้วเรียก Method ตรงๆเลย

        บน Fragment ก็เตรียม Method ไว้ให้ Activity เรียกให้เรียบร้อยซะ

Fragment
public class MyFragment extends Fragment {
    ...

    public void doSomethingByActivity(AnyObject data) {
        // Do something when any event on activity was happened
    }
}

    ฝั่ง Activity อยากจะส่งข้อมูลให้ Fragment เมื่อไรก็เรียกผ่าน Method ที่เตรียมไว้ได้เลย

Activity
public class MainActivity extends AppCompatActivity {
    private MyFragment myFragment;
    private AnyObject data;

    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...

        myFragment = MyFragment.newInstance();
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction transaction = manager.beginTransaction();
        transaction.replace(R.id.layout_fragment_container, myFragment);
        transaction.commit();

        ...
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if(id == R.id.btn_submit_new_data) {
            myFragment.doSomethingByActivity(data);
        }
    }

    ...
}


        ซึ่งวิธีนี้ค่อนข้างเข้าใจง่าย และเขียนไม่ต้องเยอะมาก แต่ทว่าวิธีนี้ก็มีข้อเสียตรงที่ Fragment กับ Activity ผูกกันค่อนข้างแน่นหนามาก ตัว Fragment ก็เอาไปใช้กับ Activity ตัวอื่นไม่ได้เลย (เพราะใน Fragment มีการ Cast Activity ให้เป็น MainActivity)


การส่งข้อมูลด้วย Event Bus

Activity <> Fragment

        วิธีนี้จะเป็นการใช้ Event Bus เข้ามาช่วย โดยเจ้าของบล็อกจะใช้ไลบรารีของ Otto (สามารถใช้ตัวอื่นแทนได้) ซึ่งจะไม่ขออธิบายวิธีการ Setup โค๊ดสำหรับ Otto นะครับ ให้ไปอ่านวิธีการใช้งานของ Otto เอาเอง

         เมื่อเจ้าของบล็อกอยากจะส่งข้อมูลจาก Fragment ไปยัง Activity ก็จะต้องใช้คำสั่งให้ Activity ทำการ Subscribe ใน Event Bus เสียก่อน

Activity
public class MainActivity extends AppCompatActivity {
    ...


    @Override
    public void onStart() {
        ...

        Bus.getInstance().register(this);
    }

    @Override
    public void onStop() {
        ...

        Bus.getInstance().unregister(this);
    }

    @Subscribe
    public void onFragmentEvent(AnyEventObject data) {
        // Do something when any event on fragment was happened
    }

    ...
}

        ส่วน Fragment ก็จะส่งข้อมูลโดยยิงเป็น Event ไป

Fragment
public class MyFragment extends Fragment {
    private AnyEventObject data;
    
    ...

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if(id == R.id.btn_confirm_data) {
            Bus.getInstance().post(data);
        }
    }
}

        เพียงเท่านี้ข้อมูลก็จะถูกส่งจาก Fragment ไปยัง Activity โดยผ่าน Event Bus


        และถ้าต้องการส่งจาก Activity ไปยัง Fragment ก็ทำกลับกันเท่านั้นเอง

Fragment
public class MyFragment extends Fragment {
    ...


    @Override
    public void onStart() {
        ...

        Bus.getInstance().register(this);
    }

    @Override
    public void onStop() {
        ...

        Bus.getInstance().unregister(this);
    }

    @Subscribe
    public void onActivityEvent(AnyEventObject data) {
        // Do something when any event on activity was happened
    }

    ...
}

        ส่วนฝั่ง Activity ก็สามารถส่งข้อมูลผ่าน Bus Event ได้เลย

Activity
public class MainActivity extends AppCompatActivity {
    private AnyEventObject data;
    
    ...

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if(id == R.id.btn_add_new_data) {
            Bus.getInstance().post(data);
        }
    }
}


        ซึ่งการใช้ Event Bus จะมีข้อดีคือ Activity และ Fragment จะไม่ผูกกันโดยสิ้นเชิง เพราะข้อมูลที่ส่งไปมากันจะวิ่งผ่าน Event Bus ทั้งหมด จึงทำให้ Fragment มีความยืดหยุ่นสูง สามารถ Reusable ได้ค่อนข้างง่าย


การส่งข้อมูลระหว่าง Fragment กับ Fragment

Fragment <> Fragment

        พูดถึงการส่งข้อมูลระหว่าง Activity กับ Fragment ไปแล้ว คราวนี้มาพูดถึงการส่งข้อมูลระหว่าง Fragment กับ Fragment กันบ้าง เพราะกรณีแบบนี้เกิดขึ้นได้บ่อยเหมือนกัน ไม่ว่าจะเป็นการส่งข้อมูลระหว่าง Fragment อีกตัวไปยังอีกตัวที่อยู่บน Activity ตัวเดียวกัน (Activity ที่มี Navigation Drawer ก็คือหนึ่งในนั้น) หรือต้องการให้ Fragment ที่อยู่บน View Pager ส่งข้อมูลไปมาระหว่างกัน

        ในกรณีที่ Activity มี Fragment สองตัวที่อยากจะส่งข้อมูลระหว่างกัน ก็ให้ใช้ Activity เป็นตัวกลางในการส่งข้อมูลแทนจะใช้วิธี Implement Event Listener ที่กล่าวไปในข้างต้นก็ได้ เริ่มจาก Fragment 1 ส่งข้อมูลมาที่ Activity ก่อน แล้วค่อยให้ Activity ส่งข้อมูลไปที่ Fragment 2 อีกที (สามารถใส่ Logic Code ตอนที่ส่งข้อมูลมายัง Activity ได้ด้วย)


        หรือถ้าใช้ Event Bus ก็จะสามารถส่งข้อมูลจาก Fragment 1 ไปยัง Fragment 2 ได้โดยตรงเลยนะ



        ส่วนกรณีที่ต้องการส่งข้อมูลระหว่าง Fragment ที่อยู่ใน ViewPager ก็จะมีวิธีที่ต่างจากเดิมไม่มากนัก เพียงแค่ว่ามี PagerAdapter เป็นตัวจัดการกับ Fragment แต่ละตัว แทนที่จะเป็น Activity เหมือนปกติ

        ซึ่งถ้าใช้วิธีการ Implement Event Listener แบบเดิมก็จะทำให้ Event เกิดขึ้นบน Activity โดยตรง โดยไม่ผ่าน Adapter


        แต่บอกเลยว่าวิธีนี้โคตรอ้อม แทนที่ Event ที่เกิดขึ้นควรจะอยู่ในขอบเขตของ PagerAdapter กลับกลายเป็นว่ามีโค๊ดบางส่วนไปโผล่อยู่ใน Activity ด้วย และการจะส่งให้ Fragment อีกตัวก็จะต้องผ่าน PagerAdater อยู่ดี

        ดังนั้นจึงควรทำให้ Event เกิดขึ้นอยู่ในขอบเขตของ PagerAdapter เป็นหลัก ซึ่งเปลี่ยนวิธี Event Listener ให้กำหนดผ่าน Listener Setter Method แทนดีกว่า

Fragment
public class MyFragment1 extends Fragment {
    private DataObject data;
    private MyFragmentListener myFragmentListener;

    ...

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if(id == R.id.btn_confirm) {
            confirm();
        } 
    }

    public void confirm() {
        if(myFragmentListener != null) 
            myFragmentListener.anyEvent(data);
    } 

    public void setMyFragmentListener(MyFragmentListener listener) {
        myFragmentListener = listener;
    }

    public interface MyFragmentListener {
        public void anyEvent(DataObject data);
    }
}

        ส่วน Fragment ตัวที่สองก็เพียงแค่สร้าง Method เตรียมทิ้งไว้ให้ PagerAdapter เรียกอีกทีหนึ่ง

Fragment
public class MyFragment2 extends Fragment {
    ...

    public void onEventFromMyFragment1(DataObject data) {
        // Do something when received data from MyFragment1
    }

    ...
}

        เมื่อ Fragment ทั้งสองตัวเตรียมพร้อมเรียบร้อยแล้ว ก็ให้ PagerAdapter เป็นตัวกลางในการส่งต่อค่าจาก Fragment ตัวแรกไปยัง Fragment ตัวที่สอง

PagerAdapter
public class MyPagerAdapter extends FragmentPagerAdapter implements MyFragment1.MyFragmentListener {
    private MyFragment1 myFragment1;
    private MyFragment2 myFragment2;

    ...

    public Fragment getItem(int position) {
        if(position == 0) {
            if(myFragment1 != null)
                myFragment1 = MyFragment1.newInstance();
            myFragment1.setMyFragmentListener(this);
            return myFragment1;
        } else if(position == 1) {
            if(myFragment2 != null)
                myFragment2 = MyFragment2.newInstance();
            return myFragment2;
        }

        ...
    }
    
    @Override
    public void anyEvent(DataObject data) {
        if(myFragment2 != nul) 
            myFragment2.onEventFromMyFragment1(data);
    }
}

        ด้วยวิธีนี้จะทำให้การส่งข้อมูลเกิดขึ้นอยู่ภายใน PagerAdapter เท่านั้น

        ถ้าอยากให้มีการส่ง Event ไปที่ Activity ด้วยก็เพียงแค่สร้าง Event Listener ของ PagerAdapter เพื่อให้ Activity ไป Implement แทนก็ได้

        และถ้าใช้ Event Bus ก็จะสะดวกสุดๆ เพราะยิงข้าม Fragment กันได้เลย


เพิ่มเติม

        ล่าสุดเจ้าของบล็อกแก้ปัญหาการส่งข้อมูลของ Fragment ด้วย LiveData และ ViewModel ใน Android Architecture Components เป็นที่เรียบร้อยแล้ว ถ้าสนใจก็สามารถตามไปอ่านกันได้ในบทความ ส่งข้อมูลระหว่าง Activity/Fragment แบบหล่อๆด้วย LiveData และ ViewModel ของ Android Architecture Components

สรุป 

        จะเห็นว่ารูปแบบในการรับส่งข้อมูลระหว่าง Fragment กับ Activity นั้นมีหลายวิธีมาก ขึ้นอยู่กับรูปแบบการใช้งาน อย่างการใช้วิธีส่งข้อมูลผ่าน Construction Arguments ก็จะใช้ได้เฉพาะเวลาที่สร้าง Fragment ขึ้นมาเท่านั้น ไม่สามารถส่งข้อมูลระหว่าง Fragment นั้นทำงานอยูได้

        ส่วนการทำ Event Listener ก็เป็นอีกวิธีที่ควรใช้ เพราะวิธีนี้จะช่วยเลี่ยงการผูก Fragment เข้ากับ Activity ได้พอสมควร (ในขณะที่ Activity สามารถผูกเข้ากับ Fragment ได้อยู่แล้ว) แต่ในการจัดการกับ Fragment ด้วยกันก็อาจจะต้องทำเป็น 2 ขั้น เพื่อส่งจาก Fragement1 > Activity > Fragment2 ซึงควรจัดการให้ดี เพราะอาจจะเกิด NullPointerException ขึ้นมาได้ถ้าเขียนไม่ระวังมากพอ

       สำหรับการใช้ Event Bus น่าจะดูเหมือนตอบโจทย์ได้ค่อนข้างดีที่สุด เพราะตัว Event Bus ไม่ผูกกับอะไรเลย จึงทำให้ระหว่าง Activity และ Fragment ก็ตัดขาดออกจากกันอย่างสิ้นเชิง การจะส่งข้อมูลระหว่าง Fragment ด้วยกันก็ทำได้ง่ายมาก เพราะส่งข้อมูลกันตรงๆได้เลย เวลาจะเขียนเทสก็ทำแยกกันได้เลย แต่อาจจะ Track การทำงานของโค๊ดได้ยาก เพราะ Event Bus ไม่ผูกกับคลาสใดๆจึงทำให้ไม่รู้ว่า Event ที่ส่งออกมานั้นมาจากที่ไหน (ถ้าจัดการกับ Event ไม่ดีพอ) และถ้า Fragment มี Event ที่ต้องส่งข้อมูลระหว่างกันเยอะมาก ก็จะทำให้ต้องสร้าง Event Class เพิ่มมากขึ้นเท่านั้น เพราะงั้นจงอย่าใช้เยอะเกินไป