先說結論:
1. 當需要給Fragment傳值時,使用newInstance()方式來實例化一個Fragment,能夠更好的將該Fragment使用的參數捆綁起來,不必每次實例化時都寫下面的代碼:
Bundle args = new Bundle();
2. 由於是在工廠方法內部封裝了傳參的方法,所以傳入的參數將會得到保留,即使Fragment旋轉重建也能夠重新獲取到這些傳入參數。
Android日常研發中不可避免的肯定要用到Fragment,你如何使用的呢?Compare the two methods of use,是否覺得第二種更加簡潔。
這時很多人肯定提出疑問:這兩種使用方式有何區別,我的代碼中到底使用哪種方式更好一些,以及為什么要使用這種方式 and so on,各位看官稍安勿躁,且聽老衲娓娓道來。
Usage 1:
@Override public void initView(Bundle savedInstanceState) { BlankFragment mFragment = new BlankFragment(); Bundle bundle = new Bundle(); bundle.putString("arg1", "a"); bundle.putString("arg2", "b"); bundle.putString("arg3", "c"); mFragment.setArguments(bundle); getFragmentManager().beginTransaction().replace(R.id.frame, mFragment).commit(); }
Usage 2:
@Override public void initView(Bundle savedInstanceState) { getFragmentManager().beginTransaction().replace(R.id.frame, BlankFragment.newInstance("a", "b")).commit(); }
首先我們新建一個fragment,我們一起來看一下android建議的fragment如何編寫(請嚴格按照截圖的來步步創建哦)
package com.itbird.utils; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.itbird.base.R; /** * A simple {@link Fragment} subclass. * Activities that contain this fragment must implement the * {@link BlankFragment.OnFragmentInteractionListener} interface * to handle interaction events. * Use the {@link BlankFragment#newInstance} factory method to * create an instance of this fragment. */ public class BlankFragment extends Fragment { // TODO: Rename parameter arguments, choose names that match // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER private static final String ARG_PARAM1 = "param1"; private static final String ARG_PARAM2 = "param2"; // TODO: Rename and change types of parameters private String mParam1; private String mParam2; private OnFragmentInteractionListener mListener; public BlankFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param param1 Parameter 1. * @param param2 Parameter 2. * @return A new instance of fragment BlankFragment. */ // TODO: Rename and change types and number of parameters public static BlankFragment newInstance(String param1, String param2) { BlankFragment fragment = new BlankFragment(); Bundle args = new Bundle(); args.putString(ARG_PARAM1, param1); args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mParam1 = getArguments().getString(ARG_PARAM1); mParam2 = getArguments().getString(ARG_PARAM2); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_blank, container, false); } // TODO: Rename method, update argument and hook method into UI event public void onButtonPressed(Uri uri) { if (mListener != null) { mListener.onFragmentInteraction(uri); } } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnFragmentInteractionListener) { mListener = (OnFragmentInteractionListener) context; } else { throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated * to the activity and potentially other fragments contained in that * activity. * <p> * See the Android Training lesson <a href= * "http://developer.android.com/training/basics/fragments/communicating.html" * >Communicating with Other Fragments</a> for more information. */ public interface OnFragmentInteractionListener { // TODO: Update argument type and name void onFragmentInteraction(Uri uri); } }
上述代碼其實就是在一個Fragment的newInstance方法中傳遞兩個參數,並且通過fragment.setArgument保存在它自己身上,而后通過onCreate()調用的時候將這些參數取出來。這樣寫沒什么特殊的啊,不就是用靜態工廠方法傳個參數么,用構造器傳參數不一樣處理么?No,No,No,如果僅僅是個靜態工廠而已,又怎么能成為谷歌推薦呢。
實踐是檢驗真理的唯一標准,我們一起通過一個樣例來實際操作一番
fragment_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <framelayout android:id="@+id/layout_top" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <framelayout android:id="@+id/layout_bottom" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> </LinearLayout>
由圖和代碼可知,我們在xml中定義兩個FrameLayout,平分整個屏幕高度
MainActivity.java
package com.itbird.myapplication; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; public class MainActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fragment_main); if (savedInstanceState == null) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.add(R.id.layout_top, new BlankFragment("頂部的Fragment", "test")); transaction.add(R.id.layout_bottom, BlankFragment.newInstance("底部的Fragment", "test")); transaction.commit(); } } }
BlankFragment.java
package com.itbird.myapplication; import android.annotation.SuppressLint; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; public class BlankFragment extends Fragment { // TODO: Rename parameter arguments, choose names that match // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER private static final String ARG_PARAM1 = "param1"; private static final String ARG_PARAM2 = "param2"; // TODO: Rename and change types of parameters private String mParam1; private String mParam2; public BlankFragment() { // Required empty public constructor } @SuppressLint("ValidFragment") public BlankFragment(String mParam1, String mParam2) { this.mParam1 = mParam1; this.mParam2 = mParam2; } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param param1 Parameter 1. * @param param2 Parameter 2. * @return A new instance of fragment BlankFragment. */ // TODO: Rename and change types and number of parameters public static BlankFragment newInstance(String param1, String param2) { BlankFragment fragment = new BlankFragment(); Bundle args = new Bundle(); args.putString(ARG_PARAM1, param1); args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mParam1 = getArguments().getString(ARG_PARAM1); mParam2 = getArguments().getString(ARG_PARAM2); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_blank, container, false); TextView textView = view.findViewById(R.id.text); textView.setText(mParam1 + mParam2); return view; } }
fragment_blank.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" /> </FrameLayout>
通過閱讀代碼可知,我們通過兩種不同的方式創建fragment,同樣在其中心textview中展示相應拼接字段。
嗯,效果如預期的一樣完美,此時,我們把屏幕橫過來,看看會出現怎樣的狀況

My god,頂部的fragment 文本內容咋都變成null了。。。
protected void onCreate(@Nullable Bundle savedInstanceState) { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState); if (mLastNonConfigurationInstances != null) { mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders); } if (mActivityInfo.parentActivityName != null) { if (mActionBar == null) { mEnableDefaultActionBarUp = true; } else { mActionBar.setDefaultDisplayHomeAsUpEnabled(true); } } if (savedInstanceState != null) { Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); mFragments.restoreAllState(p, mLastNonConfigurationInstances != null ? mLastNonConfigurationInstances.fragments : null); } mFragments.dispatchCreate(); getApplication().dispatchActivityCreated(this, savedInstanceState); if (mVoiceInteractor != null) { mVoiceInteractor.attachActivity(this); } mCalled = true; }
顯而易見,fragment的重建是在restoreAllState方法中,跟進
FragmentController.java
/** * Restores the saved state for all Fragments. The given FragmentManagerNonConfig are Fragment * instances retained across configuration changes, including nested fragments * * @see #retainNestedNonConfig() */ public void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) { mHost.mFragmentManager.restoreAllState(state, nonConfig); }
繼續跟進
FragmentManager.java
void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) { // If there is no saved state at all, then there can not be // any nonConfig fragments either, so that is that. if (state == null) return; FragmentManagerState fms = (FragmentManagerState)state; if (fms.mActive == null) return; List<FragmentManagerNonConfig> childNonConfigs = null; // First re-attach any non-config instances we are retaining back // to their saved state, so we don't try to instantiate them again. ... // Build the full list of active fragments, instantiating them from // their saved state. mActive = new ArrayList<>(fms.mActive.length); if (mAvailIndices != null) { mAvailIndices.clear(); } for (int i=0; i<fms.mActive.length; i++) { FragmentState fs = fms.mActive[i]; if (fs != null) { FragmentManagerNonConfig childNonConfig = null; if (childNonConfigs != null && i < childNonConfigs.size()) { childNonConfig = childNonConfigs.get(i); } Fragment f = fs.instantiate(mHost, mParent, childNonConfig); if (DEBUG) Log.v(TAG, "restoreAllState: active #" + i + ": " + f); mActive.add(f); // Now that the fragment is instantiated (or came from being // retained above), clear mInstance in case we end up re-restoring // from this FragmentState again. fs.mInstance = null; } else { mActive.add(null); if (mAvailIndices == null) { mAvailIndices = new ArrayList<>(); } if (DEBUG) Log.v(TAG, "restoreAllState: avail #" + i); mAvailIndices.add(i); } } // Update the target of all retained fragments. ... // Build the list of currently added fragments. ... // Build the back stack. ... }
通過閱讀, 找到關鍵代碼
Fragment f = fs.instantiate(mHost, mParent, childNonConfig);
然后鍥而不舍跟進
FragmentManager.java
public Fragment instantiate(FragmentHostCallback host, Fragment parent, FragmentManagerNonConfig childNonConfig) { if (mInstance == null) { final Context context = host.getContext(); if (mArguments != null) { mArguments.setClassLoader(context.getClassLoader()); } mInstance = Fragment.instantiate(context, mClassName, mArguments); if (mSavedFragmentState != null) { mSavedFragmentState.setClassLoader(context.getClassLoader()); mInstance.mSavedFragmentState = mSavedFragmentState; } mInstance.setIndex(mIndex, parent); mInstance.mFromLayout = mFromLayout; mInstance.mRestored = true; mInstance.mFragmentId = mFragmentId; mInstance.mContainerId = mContainerId; mInstance.mTag = mTag; mInstance.mRetainInstance = mRetainInstance; mInstance.mDetached = mDetached; mInstance.mHidden = mHidden; mInstance.mFragmentManager = host.mFragmentManager; if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG, "Instantiated fragment " + mInstance); } mInstance.mChildNonConfig = childNonConfig; return mInstance; }
跟進到這里,終於有點頭緒了,至少看到fragment實例化的地方了,迫不及待的再次點擊去view一下下
Fragment.java
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) { try { Class<?> clazz = sClassMap.get(fname); if (clazz == null) { // Class not found in the cache, see if it's real, and try to add it clazz = context.getClassLoader().loadClass(fname); if (!Fragment.class.isAssignableFrom(clazz)) { throw new InstantiationException("Trying to instantiate a class " + fname + " that is not a Fragment", new ClassCastException()); } sClassMap.put(fname, clazz); } Fragment f = (Fragment)clazz.newInstance(); if (args != null) { args.setClassLoader(f.getClass().getClassLoader()); f.mArguments = args; } return f; } catch (ClassNotFoundException e) { throw new InstantiationException("Unable to instantiate fragment " + fname + ": make sure class name exists, is public, and has an" + " empty constructor that is public", e); } catch (java.lang.InstantiationException e) { throw new InstantiationException("Unable to instantiate fragment " + fname + ": make sure class name exists, is public, and has an" + " empty constructor that is public", e); } catch (IllegalAccessException e) { throw new InstantiationException("Unable to instantiate fragment " + fname + ": make sure class name exists, is public, and has an" + " empty constructor that is public", e); } }
山重水復疑無路,柳暗花明又一村
原來Fragment對象被反射創建之后,會調用這么一句代碼
f.mArguments = args;
哦,なるほど(原來如此),Fragment在重新創建的時候只會調用無參的構造方法,並且如果之前通過fragment.setArguments(bundle)這種方式設置過參數的話,Fragment重建時會得到這些參數,所以,在onCreate中我們可以通過getArguments()的方式拿到我們之前設置的參數。同時由於Fragment在重建時並不會調用我們自定義的帶參數的構造方法,所以我們傳遞的參數它也就獲取不到了。
也許有網友依然會繼續追問,重新set時,mArguments確定不會為空嗎?Fragment銷毀時,這個變量不會置空嗎?我們通過源碼看一下:
Fragment.java
/** * Called when the view previously created by {@link #onCreateView} has * been detached from the fragment. The next time the fragment needs * to be displayed, a new view will be created. This is called * after {@link #onStop()} and before {@link #onDestroy()}. It is called * <em>regardless</em> of whether {@link #onCreateView} returned a * non-null view. Internally it is called after the view's state has * been saved but before it has been removed from its parent. */ @CallSuper public void onDestroyView() { mCalled = true; } /** * Called when the fragment is no longer in use. This is called * after {@link #onStop()} and before {@link #onDetach()}. */ @CallSuper public void onDestroy() { mCalled = true; //Log.v("foo", "onDestroy: mCheckedForLoaderManager=" + mCheckedForLoaderManager // + " mLoaderManager=" + mLoaderManager); if (!mCheckedForLoaderManager) { mCheckedForLoaderManager = true; mLoaderManager = mHost.getLoaderManager(mWho, mLoadersStarted, false); } if (mLoaderManager != null) { mLoaderManager.doDestroy(); } }
看到此處,相信各位看官已經有“了然大明白”的感覺了,我就不再多說了。
總結
1.通過對比兩種使用方式,我們知道兩種方式別無其他,只是事關風格而已(代碼”整”“潔”之道)
2.使用Fragment過程中在涉及到傳參時,千萬不要通過構造方法或者setParam方式直接賦值傳入參數,必須使用setArguments來傳參,否則程序在某些應用情景下,會丟參
強烈建議:兩者雖無嚴格的對錯之分,都可以使用,但是newInstance方式無論從代碼整潔之道還是程序規范的穩定性而言,都是每個程序員應該學習使用的方式。