10 May 2017

Let's Fragment - ว่าด้วยเรื่องการสร้าง Fragment จาก Constructor ที่ถูกต้อง

Updated on

        สำหรับซีรีย์ Fragment ในคราวนี้ก็ขอพูดถึงเรื่องการสร้าง Fragment กันเสียหน่อย เพราะเป็นอีกหนึ่งเรื่องที่นักพัฒนาอาจจะทำผิดได้โดยไม่รู้ตัว ดังนั้นการรู้วิธีที่ถูกต้องไว้ก่อนก็ย่อมสำคัญเหมือนกันเนอะ

บทความที่เกี่ยวข้อง

        • Fragment Principle - มารู้จักกับ Fragment กันเถอะ~
        • Let's Fragment - เริ่มต้นง่ายๆกับ Fragment แบบพื้นฐาน
        • Let's Fragment - ว่าด้วยเรื่องการสร้าง Fragment จาก Constructor ที่ถูกต้อง
        • Let's Fragment - รู้จักกับ FragmentTransaction สำหรับการแสดง Fragment [ตอนที่ 1]
        • Let's Fragment - รู้จักกับ FragmentTransaction สำหรับการแสดง Fragment [ตอนที่ 2]
        • Let's Fragment - วงจรชัวิตของ Fragment (Fragment Lifecycle)
        • Let's Fragment - มาทำ View Pager กันเถิดพี่น้อง~ [ตอนที่ 1]
        • Let's Fragment - มาทำ View Pager กันเถิดพี่น้อง~ [ตอนที่ 2]


        สมมติว่าเจ้าของบล็อกสร้างคลาส Fragment ขึ้นมาแบบนี้

HomeFragment.java
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class HomeFragment extends Fragment {

    public HomeFragment() {

    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_home, container, false);
    }
    
    ...
    
}

        เวลาเรียกใช้งานก็สร้าง Instance ของ Fragment ขึ้นมาแล้วใช้ FragmentTransaction เพื่อแปะลงบน ViewGroup แบบนี้

getSupportFragmentManager()
        .beginTransaction()
        .replace(R.id.layout_fragment_container, new HomeFragment())
        .commit();

        แล้ว... ถ้าอยากจะสร้าง Fragment ขึ้นมาพร้อมกับส่งค่าบางอย่างเข้าไปด้วยล่ะ?

        ก็เพิ่ม Argument เข้าไปใน Constructor แบบนี้สิ!!

public class HomeFragment extends Fragment {
    private String key;

    public HomeFragment(String key) {
        this.key = key;
    }

    ...
    
}

        นี่คือวิธีที่ไม่ถูกต้องนะฮะ ซึ่งดีหน่อยที่ Android Studio มีการแจ้งเออเรอร์บอกว่าห้ามใช้วิธีนี้นะ


        เงื่อนไขของ Constructor ของ Fragment ก็คือต้องเป็น Default Constructor หรือเป็น Constructor ที่ไม่มี Argument ใดๆ เท่านั้น

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

String key = "ANY_VALUE";

HomeFragment fragment = new HomeFragment();
Bundle bundle = new Bundle();
bundle.putString("EXTRA_KEY", key);
fragment.setArguments(bundle);

getSupportFragmentManager()
        .beginTransaction()
        .replace(R.id.layout_fragment_container, fragment)
        .commit();

        และใน Fragment ก็จะดึงค่ามาใช้งานได้ผ่าน getArguments()

public class HomeFragment extends Fragment {

    public HomeFragment() {

    }

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

        String key = getArguments().getString("EXTRA_KEY");
    }

    ...
    
}

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

public class HomeFragment extends Fragment {
    private static final String EXTRA_KEY = "extra_key";

    public static HomeFragment newInstance(String key) {
        HomeFragment fragment = new HomeFragment();
        Bundle bundle = new Bundle();
        bundle.putString(EXTRA_KEY, key);
        fragment.setArguments(bundle);
        return fragment;
    }

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

        String key = getArguments().getString(EXTRA_KEY);

    }

    ...
    
}

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

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

        ดังนั้นคำสั่งเวลาเรียกใช้งาน Fragment ในรูปแบบนี้ก็จะดูสบายตาขึ้นเยอะเลย

String key = "ANY_VALUE";

getSupportFragmentManager()
        .beginTransaction()
        .replace(R.id.layout_fragment_container, HomeFragment.newInstance(key))
        .commit();

ทำไมต้องสร้าง Fragment ด้วย Default Constructor เท่านั้น?

        เนื่องจาก Fragment นั้นมีโอกาสที่จะถูกทำลายได้เสมอเมื่อ Memory ไม่เพียงพอใช้งานตาม Life Cycle ที่ทางทีมแอนดรอยด์ได้กำหนดไว้ ซึ่งช่วงที่ Fragment ตัวนั้นถูกสร้างขึ้นมาใหม่ (Recreate) เพื่อให้ทำงานต่อจากเดิมได้ สิ่งที่เกิดขึ้นคือระบบจะสร้าง Fragment ขึ้นมาจาก Default Constructor เท่านั้น

        ดังนั้นต่อให้ผู้ที่หลงเข้ามาอ่านสร้าง Constructor แบบไหนไว้ก็ตาม ก็จะไม่ถูกเรียกเมื่อมีการ Recreate ซึ่งนั่นอาจจะทำให้เกิดปัญหาว่าข้อมูลที่ส่งมาอาจจะมีการสูญหายไปได้ แต่ถ้าเก็บข้อมูลไว้ใน Bundle ข้อมูลเหล่านั้นก็จะคงอยู่ได้ตาม Life Cycle ของ Fragment

        และประโยชน์อีกอย่างหนึ่งที่เจ้าของบล็อกมองเห็นก็คือ การเอาข้อมูลเก็บไว้ใน Bundle นั้นจะต้องเป็นข้อมูลที่เป็น Model เท่านั้น จึงช่วยป้องกันไม่ให้นักพัฒนาใช้วิธีผิดๆอย่างการโยน View หรือ Class ต่างๆที่ไม่ใช่ Model เข้ามาให้ Fragment

สรุป 

        การสร้าง Fragment ควรจะสร้างจาก Default Constructor ที่ทางแอนดรอยด์ได้กำหนดไว้ และการกำหนดข้อมูลตอนที่สร้าง Instance ของ Fragment ก็ให้ยัดไว้ใน Bundle แทน

        ส่วนคำสั่งที่ใช้ในการสร้าง Fragment ก็แนะนำว่าให้สร้างเป็น Static Method ไว้ในคลาส Fragment นั้นๆไปเลย ถึงแม้ว่า Fragment ตัวนั้นไม่มีค่าอะไรที่ต้องกำหนดก็ตาม

public class HomeFragment extends Fragment {

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

    ...
}

public class DetailFragment extends Fragment {
    private static final String EXTRA_INDEX = "extra_index";

    public static DetailFragment newInstance(int index) {
        DetailFragment fragment = new DetailFragment();
        Bundle bundle = new Bundle();
        bundle.putInt(EXTRA_INDEX, index);
        fragment.setArguments(bundle);
        return fragment;
    }

    ... 

}

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