08 November 2014

Let's Fragment - รู้จักกับ FragmentTransaction สำหรับการแสดง Fragment [ตอนที่ 2]

Updated on

        บทความภาคต่อจาก Let's Fragment - รู้จักกับ FragmentTransaction สำหรับการแสดง Fragment [ตอนที่ 1] ที่เนื้อหาเยอะมากเกินซะจนต้องแยกออกมาเป็นตอนที่ 2 เพื่อไม่ให้บทความนั้นยาวเกินเหตุ

        จากเดิมเจ้าของบล็อกได้แนะนำให้รู้จักกับ FragmentTransaction ไปแล้ว รวมไปถึงการ Add Fragment และ BackStack ซึ่งเจ้าของบล็อกก็ขอแยกในส่วนของ Replace Fragment มาที่บทความนี้แทน

        เพิ่มเติม - โปรเจคที่ใช้ในบทความนี้ก็จะเป็นโปรเจคตัวเดิมกับที่ใช้ในบทความก่อนหน้านะครับ


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


Replace Fragment

        ชื่อมันก็บอกอยู่แล้วว่า Replace จึงหมายถึงการนำ Fragment ใหม่ไปแทนที่ Fragment เก่านั่นเอง ซึ่งมีคำสั่งดังนี้

import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;

FragmentManager manager = getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(viewGroupId, fragment);
transaction.commit();
        จะเห็นว่าคำสั่ง replace นั้นไม่ต่างอะไรกับ add เลย

        ดังนั้นให้เปลี่ยนจาก add เป็น replace ให้หมดซะ

MainActivity.java
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends FragmentActivity {
    
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btn_one = (Button)findViewById(R.id.btn_one);
        btn_one.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                OneFragment oneFragment = new OneFragment();
                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
                transaction.replace(R.id.fragment_container, oneFragment);
                transaction.commit();
            }
        });
        
        Button btn_two = (Button)findViewById(R.id.btn_two);
        btn_two.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                TwoFragment twoFragment = new TwoFragment();
                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
                transaction.replace(R.id.fragment_container, twoFragment);
                transaction.commit();
            }
        });

        Button btn_three = (Button)findViewById(R.id.btn_three);
        btn_three.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                ThreeFragment threeFragment = new ThreeFragment();
                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
                transaction.replace(R.id.fragment_container, threeFragment);
                transaction.commit();
            }
        });
    }
}

        แล้วลองรันทดสอบดูอีกครั้งแล้วลองกดเปิด Fragment ทีละตัวดู




        จะเห็นว่าเมื่อกดเปลี่ยนไปแต่ละ Fragment ก็จะแสดงไปตาม Fragment ที่เลือกในทันที (ในขณะที่ Add จะไปซ้อนอยู่ข้างล่างสุดของ Stack) แต่ในการ Replace จะไม่มีการ Stack ของ Fragment เกิดขึ้น


        แล้วมันสามารถเพิ่มเข้าไปใน BackStack ได้มั้ย?

        ได้นะเออ ถึงแม้ว่ามันจะเป็นการ Replace ก็ตาม แต่ถ้าใช้คำสั่ง addToBackStack ก็จะทำให้ FragmentTransaction เก็บ Fragment นั้นๆไว้สำหรับ Back Stack

import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;

FragmentManager manager = getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(viewGroupId, fragment);
transaction.addToBackStack(null);
transaction.commit();

ก็ให้เพิ่มคำสั่ง addToBackStack ไปให้ครบทุก Fragment ซะ

MainActivity.java
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends FragmentActivity {
    
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btn_one = (Button)findViewById(R.id.btn_one);
        btn_one.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                OneFragment oneFragment = new OneFragment();
                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
                transaction.replace(R.id.fragment_container, oneFragment);
                transaction.addToBackStack(null);
                transaction.commit();
            }
        });
        
        Button btn_two = (Button)findViewById(R.id.btn_two);
        btn_two.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                TwoFragment twoFragment = new TwoFragment();
                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
                transaction.replace(R.id.fragment_container, twoFragment);
                transaction.addToBackStack(null);
                transaction.commit();
            }
        });

        Button btn_three = (Button)findViewById(R.id.btn_three);
        btn_three.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                ThreeFragment threeFragment = new ThreeFragment();
                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
                transaction.replace(R.id.fragment_container, threeFragment);
                transaction.addToBackStack(null);
                transaction.commit();
            }
        });
    }
}

        ลองทดสอบดูใหม่อีกครั้ง กดเปิด Fragment ทั้งสามตัวซะ





        แล้วลองกด Back ดูก็จะพบว่า Replace ก็สามารถทำ BackStack ได้






        ทีนี้ให้ลองใหม่อีกครั้งโดยกดเปิด Fragment ให้ครบทั้ง 3 แต่ทว่าลองกดปุ่ม Close ดู


        จะพบว่าต่อให้เปิด Fragment มากหรือน้อยเพียงใด ถ้ากดปุ่ม Close ก็จะหายแว้บไปในทันที ทั้งนี้ก็เพราะว่า Fragment ที่อยู่บน LinearLayout นั้นมีอยู่แค่ตัวเดียวเท่านั้น เพราะว่าใช้ Replace ไม่ได้เก็บ Fragment Stack ไว้แบบคำสั่ง Add และที่กด Back เพื่อย้อนกลับได้ก็เพราะ BackStack คอยเก็บ Fragment ไว้ให้

        Fragment Stack กับ Back Stack คนละส่วนกันนะ อย่าจำสลับสับสนกันล่ะ

        แล้วถ้าอยากจะให้ปุ่ม Close กดแล้วปิดทีละ Fragment ล่ะ?

        สาเหตุที่กดแล้ว Fragment หายไปเลย ก็เพราะว่าใช้คำสั่ง Remove Fragment นั่นเอง ซึ่งคำสั่ง remove จะเหมาะกับ Fragment Stack ที่ใช้คำสั่ง add ซะมากกว่า แต่ Fragment ที่มาจากการ Replace อาจจะต้องเปลี่ยนคำสั่งซะหน่อย

getFragmentManager().popBackStack
        หรือ

getActivity().onBackPressed
        โดยทั้งสองคำสั่งนี้ให้ผลลัพธ์ที่เหมือนกัน ต่างกันเล็กน้อยตรงที่ว่าคำสั่ง popBackStack เป็นการใช้คำสั่งไปที่ BackStack โดยตรง โดยให้ดึง Stack ชั้นบนสุดที่อยู่ใน BackStack ขึ้นมา ส่วน onBackPressed ก็คือคำสั่งเสมือนว่ากดปุ่ม Back ที่ Activity ซึ่งผลก็คือ BackStack จะทำงานเหมือนกัน ถ้าจะให้เหมาะสมก็ควรใช้ popBackStack ไปเลย

        ดังนั้นใน OneFragment, TwoFragment และ ThreeFragment ก็จะเปลี่ยนคำสั่งของปุ่ม Close ให้เหมาะสมกับคำสั่ง Replace ดังนี้

OneFragment.java
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;

public class OneFragment extends Fragment {
    
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_one, container, false);
        
        Button btn_close = (Button)rootView.findViewById(R.id.btn_close);
        btn_close.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                getFragmentManager().popBackStack();
            }
        });
        
        return rootView;
    }
}

TwoFragment.java
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;

public class TwoFragment extends Fragment {
    
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_two, container, false);
        
        Button btn_close = (Button)rootView.findViewById(R.id.btn_close);
        btn_close.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                getFragmentManager().popBackStack();
            }
        });
        
        return rootView;
    }
}

ThreeFragment.java
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;

public class ThreeFragment extends Fragment {
    
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_three, container, false);
        
        Button btn_close = (Button)rootView.findViewById(R.id.btn_close);
        btn_close.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                getFragmentManager().popBackStack();
            }
        });
        
        return rootView;
    }
}

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

        ถ้าไม่ได้ตามที่บอกก็ให้กลับไปเช็คใหม่ตั้งแต่ต้นซะ!!



เวลาใช้งานจริง Add หรือ Replace แบบไหนดีกว่ากัน?

        ต้องบอกว่า Replace เหมาะสมกว่าสำหรับการใช้งาน (แล้วจะสอน Add Fragment ทำไมตั้งยาวเหยียดฟระ!!) เพราะว่างานส่วนใหญ่เน้นการ Replace Fragment ซ้อนขึ้นไปเรื่อยๆ เวลาที่อยากจะย้อนกลับก็ใช้ Back Stack ช่วย



ข้อควรจำเกี่ยวกับ FragmentTransaction

        ถ้าสังเกตดีๆจะเห็นว่า เจ้าของบล็อกเรียกใช้งาน FragmentTransaction ทุกครั้งที่จะ Add หรือ Replace หรือ Remove ทุกครั้ง ยกตัวอย่างเช่น

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

    Button btn_one = (Button)findViewById(R.id.btn_one);
    btn_one.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            ...
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
            transaction.replace(R.id.fragment_container, fragment);
            transaction.commit();
        }
    });
        
    Button btn_two = (Button)findViewById(R.id.btn_two);
    btn_two.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            ...
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
            transaction.replace(R.id.fragment_container, fragment);
            transaction.commit();
        }
    });

    Button btn_three = (Button)findViewById(R.id.btn_three);
    btn_three.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            ...
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
            transaction.replace(R.id.fragment_container, fragment);
            transaction.commit();
        }
    });
}

        ถ้าใช้วิธีประกาศ Transaction ไว้ตั้งแต่แรกแบบนี้ล่ะ จะทำได้มั้ย?

FragmentTransaction transaction;

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

    transaction = getSupportFragmentManager().beginTransaction();

    Button btn_one = (Button)findViewById(R.id.btn_one);
    btn_one.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            ...
            transaction.replace(R.id.fragment_container, fragment);
            transaction.commit();
        }
    });
        
    Button btn_two = (Button)findViewById(R.id.btn_two);
    btn_two.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            ...
            transaction.replace(R.id.fragment_container, fragment);
            transaction.commit();
        }
    });

    Button btn_three = (Button)findViewById(R.id.btn_three);
    btn_three.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            ...
            transaction.replace(R.id.fragment_container, fragment);
            transaction.commit();
        }
    });
}

        คำตอบคือ "ไม่ได้" นะครับ

        เพราะว่า FragmentTransaction มีเงื่อนไขอย่างหนึ่งคือ เมื่อเริ่ม Transaction หนึ่งครั้งจะสามารถ Commit ได้เพียงแค่หนึ่งครั้งเท่านั้น (1 Transaction ต่อ 1 Commit) ดังนั้นจึงไม่สามารถเริ่ม Transaction แล้ว Commit หลายๆครั้งได้ จึงเป็นที่มาว่าเวลาเรียกใช้ Fragment Transation จะต้องสร้างใหม่ทุกๆครั้ง

        และในการเริ่ม Transaction ค่าที่กำหนดไว้ใน Transaction จะยังไม่แสดงผลในทันทีจนกว่าจะ Commit ดังนั้นจึงกำหนดค่าต่างๆได้ตามใจชอบ แล้วค่อย Commit ตู้มเดียวไปเลย

        และที่บอกไว้ในตอนแรกว่า Fragment สามารถยัดลงใน ViewGroup ใดๆก็ได้ ซึ่งในตัวอย่างเจ้าของบล็อกใช้เป็น LinearLayout แต่ในการใช้งานจริงจะนิยมใช้เป็น FrameLayout กันนะครับ



นี่คือส่วนหนึ่งของการใช้งาน Fragment เท่านั้น

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


        รู้สึกพลังงานหมด...



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

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