อยู่ในระหว่างการปรับปรุง

เนื่องจากบทความตอนที่ 1 ไม่อาจจะลากยาวไปมากกว่านี้ได้แล้ว จึงขอแบ่งครึ่งออกมาต่อเป็นตอนที่ 2 ที่บทความนี้แทนนะครับ

บทความในซีรีย์เดียวกัน

จากตอนที่ 1 เจ้าของบล็อกได้จบท้ายบทไว้ว่าการทำ View Pager จากตอนแรกจะมีปัญหาอยู่สองอย่างคือ เมื่อเรียกคำสั่ง getItem ทุกๆครั้ง Fragment ที่ได้จะเป็นคนละตัวกับของเดิมโดยสิ้นเชิง และอีกปัญหาคือการใช้ Constructor ในการสร้าง Fragment ใน Adapter นั้นไม่ใช่วิธีที่ถูกต้อง

มาดูกันว่าทำไม เพราะอะไร แล้วจะแก้ปัญหาเหล่านี้ยังไงดี

ทำไมการใช้ Constructor ในการสร้าง Fragment สำหรับ View Pager ถึงไม่ถูกต้อง?

จากที่บอกในตอนแรกไปว่า View Pager นั้นทำงานคล้ายๆกับ List View นั้นก็คือ Fragment ที่ไม่แสดงผลจะมีการ Destroy ทิ้ง แล้วจึงสร้างขึ้นมาใหม่อีกครั้งเมื่อต้องการใช้งาน แต่ทว่าตอนที่ Fragment ถูกเรียกขึ้นมาแสดงใหม่อีกครั้งนั้น เมธอด getItem กลับไม่ได้ถูกเรียกขึ้นมาใหม่เหมือนกับ getView ของ List View

public class MyPagerAdapter extends FragmentPagerAdapter { ... public Fragment getItem(int position) { if(position == 0) { Log.i("Check", "Get Item 0"); return new OneFragment("Android,Development"); } ... } }


public class OneFragment extends Fragment { String str; public OneFragment(String str) { this.str = str; } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.i("Check", "OnCreateView"); String[] text = str.split(","); Log.i("Check", text[0]); ... } public void onDestroyView() { Log.i("Check", "OnDestroyView"); } }


* ใน Constructor ของ OneFragment (Fragment ตัวแรกสุดใน View Pager) มีการสร้าง Parameter เป็น String เพื่อเอาไว้ใช้งานใน onCreateView (สมมติว่าเอาไป Split String เพื่อนำไปใช้งานอีกทีหนึ่ง)
* ที่ Adapter ให้ใช้คำสั่ง Log เพื่อแสดงข้อความอะไรก็ได้ใน getView ของ Position 0 (Fragment ตัวแรก) แล้วทำการสร้าง Fragment ด้วย Constructor โดยมีการกำหนด String เข้าไปด้วย
* ที่คลาส Fragment ของ Position 0 ให้ใช้คำสั่ง Log เพื่อแสดงข้อความอะไรก็ได้ใน onCreateView และ onDestroyView
* รันแอพเพื่อทดสอบ เมื่อแอพเปิดขึ้นมาจะพบว่า getItem ทำงานทันทีเพื่อแสดง Fragment ใน View Pager
* ทดสอบด้วยการเลื่อนไปจนถึง Fragment อันสุดท้าย (Position 2) จะพบว่า Fragment แรกสุดถูก Destroy ทิ้ง (สังเกตจาก Log ที่ให้ใส่ไว้)
* เลื่อนกลับมาที่ Fragment อันแรกสุด (Position 0) จะพบว่า Fragment แรกสุดถูกสร้างขึ้นมาใหม่ (สังเกตจาก Log ที่ให้ใส่ไว้)
* ถ้าสังเกตดีๆจะเห็นว่าตอนที่ Fragment ตัวแรกถูกแสดงขึ้นมาใหม่ getItem นั้นกลับไม่ทำงาน

แต่คำว่า “Android” ก็ยังแสดงใน Log ได้ปกติไม่ใช่หรือ?

ทั้งนี้ก็เพราะว่า Memory ยังคงเก็บค่าของ OneFragment ไว้อยู่นั่นเอง และตอนที่เลื่อน View Pager เป็นหน้าอื่นๆก็ไม่ได้ทำอะไรจึงทำให้ไม่มีการใช้ Memory ซักเท่าไรนัก

แต่ในการเขียนแอพจริงๆ Fragment บางหน้าอาจจะมีการใช้ Memory เยอะ

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

ผลก็คือ เมื่อกลับมาที่ OneFragment ในหน้าแรกอีกครั้ง จะทำให้ str มีค่าเป็น Null เพราะ Memory ถูกเคลียร์ทิ้งไปแล้ว และ getItem ก็ไม่ถูกเรียกเพื่อกำหนดค่าใหม่

นี่จึงเป็นปัญหาที่ทำให้การใช้ Constructor มาสร้าง Fragment นั้นไม่เหมาะสม

แล้วควรแก้ปัญหานี้อย่างไร?

ควรใช้ Static เข้ามาช่วยเพื่อสร้าง Fragment ด้วย Constructor อีกที

public class OneFragment extends Fragment { public static OneFragment newInstance() { OneFragment fragment = new OneFragment(); return fragment; } public OneFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... } ... }


และเมื่อต้องการส่ง String เข้ามาด้วย ก็จะใช้ Argument เข้ามาช่วยแทน โดยยัดข้อมูลเก็บไว้ใน Bundle ไม่แนะนำให้โยนขึ้น Global เพื่อส่งข้ามจาก Static ไปยัง Non-Static นะ

public class OneFragment extends Fragment { private static final String KEY_STRING = "key_string"; public static OneFragment newInstance(String str) { OneFragment fragment = new OneFragment(); Bundle bundle = new Bundle(); bundle.putString(KEY_STRING, str); fragment.setArgument(bundle); return fragment; } public OneFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { String str = getArguments().getString(KEY_STRING); String[] text = str.split(","); Log.i("Check", text[0]); ... } ... }


สำหรับ Argument ก็จะคล้ายๆกับ Intent ที่คุ้นเคยกันดีนั่นเอง

จากตัวอย่างข้างต้นนี้ การใช้ Static จะช่วยให้ค่าต่างๆที่ Activity ส่งมายัง Fragment นั้นไม่กลายเป็น Null เมื่อถูกเคลียร์ Memory ทิ้ง

กลับมาดูที่ Adapter กันต่อ เมื่อเปลี่ยนจากการเรียก Constructor โดยตรงมาเป็น newInstance แทนก็จะต้องเปลี่ยนคำสั่งใน getItem ดังนี้

public class MyPagerAdapter extends FragmentPagerAdapter { ... public Fragment getItem(int position) { if(position == 0) { return OneFragment.newInstance("Android,Development"); } ... } }


ดังนั้น Fragment ทุกตัวที่จะเรียกใช้ใน View Pager จะต้องใช้วิธี newInstance ทั้งหมด

ขอสรุปโค๊ดทั้งหมดในตอนนี้ต่อจากบทความตอนที่แล้วอีกรอบหนึ่ง

* MainActivity.java < > activity_main.xml
* MyPagerAdapter.java
* OneFragment.java < > fragment_one.xml
* TwoFragment.java < > fragment.two.xml
* ThreeFragment.java < > fragment.three.xml

import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.view.ViewPager; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; public class MainActivity extends FragmentActivity { MyPageAdapter adapter; ViewPager pager; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); adapter = new MyPageAdapter(getSupportFragmentManager()); pager = (ViewPager) findViewById(R.id.pager); pager.setAdapter(adapter); Button btn_next = (Button)findViewById(R.id.btn_next); btn_next.setOnClickListener(new OnClickListener() { public void onClick(View v) { pager.setCurrentItem(pager.getCurrentItem() + 1); } }); Button btn_prev = (Button)findViewById(R.id.btn_prev); btn_prev.setOnClickListener(new OnClickListener() { public void onClick(View v) { pager.setCurrentItem(pager.getCurrentItem() - 1); } }); } }


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#f2f2f2" tools:context="${relativePackage}.${activityClass}" > <android.support.v4.view.ViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_above="@+id/layout_menu" android:layout_margin="30dp" android:background="#ffffff" /> <LinearLayout android:id="@+id/layout_menu" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_centerHorizontal="true" android:gravity="center_horizontal" > <Button android:id="@+id/btn_prev" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Prev" /> <Button android:id="@+id/btn_next" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Next" /> </LinearLayout> </RelativeLayout>


import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; public class MyPageAdapter extends FragmentPagerAdapter { private final int NUM_ITEMS = 3; public MyPageAdapter(FragmentManager fm) { super(fm); } public int getCount() { return NUM_ITEMS; } public Fragment getItem(int position) { if(position == 0) return OneFragment.newInstance(); else if(position == 1) return TwoFragment.newInstance(); else if(position == 2) return ThreeFragment.newInstance(); return null; } }


import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class OneFragment extends Fragment { public static OneFragment newInstance() { OneFragment fragment = new OneFragment(); return fragment; } public OneFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_one, container, false); return rootView; } }


<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#e6e6e6" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="Fragment One" /> </RelativeLayout>


import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class TwoFragment extends Fragment { public static TwoFragment newInstance() { TwoFragment fragment = new TwoFragment(); return fragment; } public TwoFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_two, container, false); return rootView; } }


<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#e6e6e6" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="Fragment Two" /> </RelativeLayout>


import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class ThreeFragment extends Fragment { public static ThreeFragment newInstance() { ThreeFragment fragment = new ThreeFragment(); return fragment; } public ThreeFragment() { } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_three, container, false); return rootView; } }


<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#e6e6e6" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="Fragment Three" /> </RelativeLayout>


เอาล่ะ! แก้ข้อบกพร่องของการใช้ Constructor โดยตรงได้แล้ว ปัญหาต่อมาก็คือ

Activity จะเรียกใช้งาน Fragment ได้อย่างไร?

ต้องบอกเลยว่าให้ลืม getItem ไปซะ เพราะว่าเจ้าของบล็อกจะไม่เรียกผ่านเมธอดนั้นเด็ดขาด เนื่องจากมันเป็นการสร้าง Instance ตัวใหม่ขึ้นมา ดังนั้นเจ้าของบล็อกจะใช้ฟังก์ชันนี้ในการเรียก Fragment ที่ต้องการ

public Fragment getActiveFragment(ViewPager container, int position) { String name = "android:switcher:" + container.getId() + ":" + position; return getSupportFragmentManager().findFragmentByTag(name); }


ฟังก์ชันนี้มีไว้สำหรับค้นหา Fragment ที่อยู่ใน View Pager โดยอิงจากชื่อ Tag

ก่อนอื่นเจ้าของบล็อกขอเตรียมการเล็กน้อยก่อน ขอเพิ่มคำสั่งสำหรับ Text View ที่อยู่ใน OneFragment ดังนี้

public class OneFragment extends Fragment { TextView textView1; ... public View onCreateView(LayoutInflater inflate, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_one, container, false); textView1 = (TextView)rootView.findViewById(R.id.textView1); return rootView; } public String getMyText() { return textView1.getText().toString(); } }


เจ้าของบล็อกได้สร้างเมธอดให้กับ OneFragment ที่มีชื่อว่า getMyText โดยจะให้ Return ข้อความที่แสดงใน @+id/textView1 ของ fragment_one.xml (ข้อความว่า “Fragment One” นั่นเอง)

จากนั้นกลับมาที่ activity_main.xml ต่อ ที่ตรงนี้ขอเพิ่ม Button ธรรมดาเข้าไป 1 ตัวโดยตั้งชื่อ ID ว่า @+id/btn_request

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#f2f2f2" tools:context="${relativePackage}.${activityClass}" > <android.support.v4.view.ViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_above="@+id/layout_menu" android:layout_margin="30dp" android:background="#ffffff" /> <LinearLayout android:id="@+id/layout_menu" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_centerHorizontal="true" android:gravity="center_horizontal" > <Button android:id="@+id/btn_prev" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Prev" /> <Button android:id="@+id/btn_next" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Next" /> </LinearLayout> <Button android:id="@+id/btn_request" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:text="Request" /> </RelativeLayout>


และประกาศ Button และ OnClickListener เพิ่มเข้าไปใน MainActivity.java

public class MainActivity extends FragmentActivity { ... ViewPager pager; protected void onCreate(Bundle savedInstanceState) { ... pager = (ViewPager) findViewById(R.id.pager); ... Button btn_request = (Button)findViewById(R.id.btn_request); btn_request.setOnClickListener(new OnClickListener() { public void onClick(View v) { } }); } public Fragment getActiveFragment(ViewPager container, int position) { String name = "android:switcher:" + container.getId() + ":" + position; return getSupportFragmentManager().findFragmentByTag(name); } }


อ่ะ เตรียมเสร็จละ! ต่อจากเดิมที่พูดค้างไว้นะครับ เจ้าของบล็อกต้องการดึง OneFragment ที่อยู่ใน Position 0 ของ View Pager ก็จะต้องใช้คำสั่งดังนี้

Button btn_request = (Button)findViewById(R.id.btn_request); btn_request.setOnClickListener(new OnClickListener() { public void onClick(View v) { Fragment fragment = getActiveFragment(pager, 0); OneFragment oneFragment = (OneFragment)fragment; String message = oneFragment.getMyText(); Log.i("Check", message); } });


คงเข้าใจกันไม่ยากเนอะ? ก็คือ View Pager เจ้าของบล็อกตั้งชื่อไว้ว่า pager และ OneFragment ก็อยู่หน้าแรกสุดของ View Pager (Position 0) และเมื่อดึง Fragment ที่ต้องการออกมาได้แล้วก็แปลงเป็น OneFragment แล้วก็เรียกเมธอด getMyText เพื่อดึง String ของ @+id/textView1 ที่อยู่ใน fragment_one.xml มาแสดงใน LogCat

ให้ลองรันทดสอบแล้วกดปุ่ม Request ดู จะพบว่ามีข้อความที่แสดงอยู่ใน OneFragment ไปโผล่ใน LogCat แล้ว นั่นก็หมายความว่าผู้ที่หลงเข้ามาอ่านสามารถเรียก Fragment ผ่าน Activity ได้แล้วนั่นเอง

ทีนี้ให้ลองเพิ่มคำสั่งที่ ThreeFragment.java ให้เหมือนกับใน OneFragment.java (เพิ่ม Text View และ getMyText)

public class ThreeFragment extends Fragment { TextView textView1; ... public View onCreateView(LayoutInflater inflate, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_three, container, false); textView1 = (TextView)rootView.findViewById(R.id.textView1); return rootView; } public String getMyText() { return textView1.getText().toString(); } }


ส่วน MainActivity.java ให้แก้ไขจาก OneFragment เป็น ThreeFragment แทน โดยต้องเปลี่ยนจาก 0 เป็น 2 และแปลง Fragment เป็น ThreeFragment

Button btn_request = (Button)findViewById(R.id.btn_request); btn_request.setOnClickListener(new OnClickListener() { public void onClick(View v) { Fragment fragment = getActiveFragment(pager, 2); ThreeFragment threeFragment = (ThreeFragment)fragment; String message = threeFragment.getMyText(); Log.i("Check", message); } });


ให้รันทดสอบอีกครั้ง เมื่อแอพเปิดขึ้นมาก็ให้ลองกดปุ่ม Request ทันที

ถูกต้องแล้วครับ NullPointerException ครับ เพราะว่าหน้า ThreeFragment ยังไม่ถูกสร้างขึ้นนั่นเอง จะมีแค่ OneFragment กับ TwoFragment แค่สองอันที่ถูกสร้างขึ้น ณ ตอนนี้ ซึ่งต้องเลื่อนไปหน้า TwoFragment ก่อน View Pager จึงจะสร้าง ThreeFragment ขึ้นมา

ดังนั้นการจะดึง ThreeFragment มาใช้จะมีเงื่อนไขว่า Fragment นั้นๆต้องถูกสร้างขึ้นมาก่อน ไม่เช่นนั้นจะเป็น Null ไปในทันที ดังนั้นทางที่ดีเมื่อใช้คำสั่ง getActiveFragment ก็ควรจะเช็ค Null ด้วยทุกครั้ง

Button btn_request = (Button)findViewById(R.id.btn_request); btn_request.setOnClickListener(new OnClickListener() { public void onClick(View v) { Fragment fragment = getActiveFragment(pager, 2); ThreeFragment threeFragment = (ThreeFragment)fragment; if(threeFragment != null) { String message = threeFragment.getMyText(); Log.i("Check", message); } } });


เพียงเท่านี้ก็สามารถเรียก Fragment ผ่าน Activity ได้แล้ว~♪

และสำหรับการเรียก Activity ผ่าน Fragment ก็ทำเหมือนเดิมได้เลย

MainActivity mainActivity = (MainActivity)getActivity();


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

เหมือนเดิมนะครับ บทความนี้ไม่มีแจกตัวอย่าง เพราะอยากจะให้ผู้ที่หลงเข้ามาอ่านได้ลองทำตามเพื่อทำความเข้าใจด้วยตนเองมากกว่าการดาวน์โหลดโค๊ดไปดู

View Pager เบื้องต้นสำหรับ Fragment เสร็จแล้ว~