Fragment最佳實踐

前言

上一篇文章中詳細分析了Fragment相關知識,那麼作爲“小Activity”,Fragment能做什麼呢,如何使用Fragment得到最佳實踐呢。Fragment的設計最初也許是爲了大屏幕平板設備的需求,不過現在Fragment已經廣泛運用到我們普通的手機設備上。下圖是我們幾乎在主流App中都能發現的一個功能。


熟悉Android的朋友一定都會知道,很簡單嘛,使用TabHost就OK了!但是殊不知,TabHost並非是那麼的簡單,它的可擴展性非常的差,不能隨意地定製Tab項顯示的內容,而且運行還要依賴於ActivityGroup。ActivityGroup原本主要是用於爲每一個TabHost的子項管理一個單獨的Activity,但目前已經被廢棄了。爲什麼呢?當然就是因爲Fragment的出現了!


先創建宿主Activity

新建BestFragmentActivity


public class BestFragmentActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_best_fragment);
        
    //下面是LuseenBottomNavigation的使用
    BottomNavigationView bottomNavigationView = (BottomNavigationView) findViewById(R.id.bottomNavigation);

    BottomNavigationItem bottomNavigationItem = new BottomNavigationItem
            ("首頁", ContextCompat.getColor(this, R.color.firstColor), R.mipmap.ic_account_balance_white_48dp);
    BottomNavigationItem bottomNavigationItem1 = new BottomNavigationItem
            ("分類", ContextCompat.getColor(this, R.color.secondColor), R.mipmap.ic_list_white_48dp);

    BottomNavigationItem bottomNavigationItem2 = new BottomNavigationItem
            ("任務", ContextCompat.getColor(this, R.color.firstColor), R.mipmap.ic_add_circle_outline_white_48dp);
    BottomNavigationItem bottomNavigationItem3 = new BottomNavigationItem
            ("購物車", ContextCompat.getColor(this, R.color.thirdColor), R.mipmap.ic_add_shopping_cart_white_48dp);

    BottomNavigationItem bottomNavigationItem4 = new BottomNavigationItem
            ("我的", ContextCompat.getColor(this, R.color.colorAccent), R.mipmap.ic_account_box_white_48dp);

    bottomNavigationView.addTab(bottomNavigationItem);
    bottomNavigationView.addTab(bottomNavigationItem1);
    bottomNavigationView.addTab(bottomNavigationItem2);
    bottomNavigationView.addTab(bottomNavigationItem3);
    bottomNavigationView.addTab(bottomNavigationItem4);
    }

}


對應的佈局文件activity_best_fragment


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/main_content"
    android:fitsSystemWindows="true"
    >

    <!--Fragment之後就動態的放在該佈局文件下-->
    <FrameLayout
        android:id="@+id/frame_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none"
        android:layout_above="@+id/bottomNavigation"
        />

    <!--關於底層佈局我這裏使用了Github上的開源項目-->
    <com.luseen.luseenbottomnavigation.BottomNavigation.BottomNavigationView
        android:id="@+id/bottomNavigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"

        android:layout_alignParentBottom="true"
        app:bnv_colored_background="false"
        app:bnv_with_text="true"
        app:bnv_shadow="false"
        app:bnv_tablet="false"
        app:bnv_viewpager_slide="true"
        app:bnv_active_color="@color/colorPrimary"
        app:bnv_active_text_size="@dimen/bottom_navigation_text_size_active"
        app:bnv_inactive_text_size="@dimen/bottom_navigation_text_size_inactive"/>

</RelativeLayout>



關於底層佈局我這裏使用了Github上的開源項目LuseenBottomNavigation,該項目地址是https://github.com/armcha/LuseenBottomNavigation讀者可自行查看

接着創建Fragment

目前Fragment作爲演示使用,可以看到佈局內容都非常簡單,我這裏只給出其中一個Fragment的創建過程和源碼,項目完整源碼可見文末的源碼地址。

我們就拿第一個GoodsFragment舉例把


public class GoodsFragment extends Fragment {
    private static String TAG= GoodsFragment.class.getSimpleName();
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.d(TAG,"onAttach");
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Log.d(TAG,"onCreateView");
        View view = inflater.inflate(R.layout.fragment_goods, null);
        return view;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Log.d(TAG,"onViewCreated");
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG,"onActivityCreated");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG,"onCreate");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG,"onStart");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG,"onResume");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG,"onPause");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d(TAG,"onStop");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG,"onDestroyView");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG,"onDestroy");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(TAG,"onDetach");
    }

}


源碼非常的簡單,在onCreateView中加載佈局文件,該佈局文件也非常簡單,僅僅定義了一個幀佈局,在幀佈局中包含了一個TextView


<?xml version="1.0" encoding="utf-8"?>

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Goods"
        android:textStyle="bold"
        android:textSize="30sp"
        android:layout_gravity="center"/>

</FrameLayout>


按照上面的流程我們建立了所需的Fragment,接着該更改BestFragmentActivity的代碼,更改後的源碼如下


public class BestFragmentActivity extends AppCompatActivity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_best_fragment);
        //底部導航佈局
        BottomNavigationView bottomNavigationView = (BottomNavigationView) findViewById(R.id.bottomNavigation);

        BottomNavigationItem bottomNavigationItem = new BottomNavigationItem
                ("首頁", ContextCompat.getColor(this, R.color.firstColor), R.mipmap.ic_account_balance_white_48dp);
        BottomNavigationItem bottomNavigationItem1 = new BottomNavigationItem
                ("分類", ContextCompat.getColor(this, R.color.secondColor), R.mipmap.ic_list_white_48dp);

        BottomNavigationItem bottomNavigationItem2 = new BottomNavigationItem
                ("任務", ContextCompat.getColor(this, R.color.firstColor), R.mipmap.ic_add_circle_outline_white_48dp);
        BottomNavigationItem bottomNavigationItem3 = new BottomNavigationItem
                ("購物車", ContextCompat.getColor(this, R.color.thirdColor), R.mipmap.ic_add_shopping_cart_white_48dp);

        BottomNavigationItem bottomNavigationItem4 = new BottomNavigationItem
                ("我的", ContextCompat.getColor(this, R.color.colorAccent), R.mipmap.ic_account_box_white_48dp);

        bottomNavigationView.addTab(bottomNavigationItem);
        bottomNavigationView.addTab(bottomNavigationItem1);
        bottomNavigationView.addTab(bottomNavigationItem2);
        bottomNavigationView.addTab(bottomNavigationItem3);
        bottomNavigationView.addTab(bottomNavigationItem4);

        //爲底部導航佈局設置點擊事件
        bottomNavigationView.setOnBottomNavigationItemClickListener(new OnBottomNavigationItemClickListener() {
            @Override
            public void onNavigationItemClick(int i) {
                switch (i){
                    case 0:
                        switchToHome();
                        break;
                    case 1:
                        switchToCategory();
                        break;
                    case 2:
                        switchToTask();
                        break;
                    case 3:
                        switchToGoodCar();
                        break;
                    case 4:
                        switchToAbout();
                        break;
                }
            }
        });
        
        //初始加載首頁,即GoodsFragment
        switchToHome();
    }
    

    private void switchToAbout() {
        getSupportFragmentManager().beginTransaction().replace(R.id.frame_content,new AboutFragment(),AboutFragment.class.getName()).commit();
    }
    private void switchToCategory() {
        getSupportFragmentManager().beginTransaction().replace(R.id.frame_content,new CategoryFragment(),CategoryFragment.class.getName()).commit();
    }

    private void switchToTask() {
        getSupportFragmentManager().beginTransaction().replace(R.id.frame_content,new TaskFragment(),TaskFragment.class.getName()).commit();
    }

    private void switchToGoodCar() {
        getSupportFragmentManager().beginTransaction().replace(R.id.frame_content,new GoodCarFragment(),GoodCarFragment.class.getName()).commit();
    }

    private void switchToHome() {
        getSupportFragmentManager().beginTransaction().replace(R.id.frame_content,new GoodsFragment(),GoodsFragment.class.getName()).commit();
    }
}


上面的代碼可以根據上一篇文章比較容易的寫出來,而且正常運行,可是在實際開發過程中我們不得不考慮代碼的性能問題。其實上面的代碼存在性能問題,尤其是在底部導航這種場景中,Fragment之間的來回切換,這裏使用的replace方法。關於這個方法帶來的問題以及如何進行優化,將在下一節詳細說明。

Fragment 性能優化問題

FragmentTransaction

談到Fragment的性能優化問題,就不得不對FragmentTransaction進行深入的研究以及探討,上面使用了getSupportFragmentManager().beginTransaction()得到了FragmentTransaction對象,並依次調用其replace方法和commit方法。

  • replace(int containerViewId, Fragment fragment)、replace(int containerViewId, Fragment fragment, String tag)

該方法的作用是,類似於先remove掉視圖容器所有的Fragment,再add方法參數中的fragment,併爲該Fragment設置標籤tag。


getSupportFragmentManager().beginTransaction().add(R.id.frame_content,new AboutFragment(),AboutFragment.class.getName()).commit();
    getSupportFragmentManager().beginTransaction().add(R.id.frame_content,new CategoryFragment(),CategoryFragment.class.getName()).commit();
    getSupportFragmentManager().beginTransaction().add(R.id.frame_content,new TaskFragment(),TaskFragment.class.getName()).commit();
    getSupportFragmentManager().beginTransaction().replace(R.id.frame_content,new GoodsFragment(),GoodsFragment.class.getName()).commit();


如上面所示代碼塊中,我們先進行了3次添加操作,之後的replace操作會移出前面添加的Fragment,再添加方法參數中指定的Frament。

  • add(int containerViewId, Fragment fragment, String tag)、 remove(Fragment fragment)

FragmentTransaction的Add()操作是維持着一個隊列的,在這個隊列中,根據ADD進去的先後順序形成了一個鏈表,我們上面的操作在這個列表中的形式變化如下圖所示:

  • remove(Fragment fragment) : 移除一個已經存在的Fragment.
  • show(Fragment fragment): 顯示一個以前被隱藏過的Fragment
  • hide(Fragment fragment) : 隱藏一個存在的Fragment

    注:①Fragment被hide/show,僅僅是隱藏/顯示Fragment的視圖,不會有任何生命週期方法的調用。

    ②在Fragment中重寫onHiddenChanged方法可以對Fragment的hide和show狀態進行監聽。

還有一些其他的方法這裏就不一一列舉了,有了上面所列出的方法,我們就能對Fragment有個很不錯的優化了。

Fragment性能問題分析與解決

Fragment性能問題分析

我們上面是使用replace來切換頁面,那麼在每次切換的時候,Fragment都會重新實例化,重新加載一邊數據,這樣非常消耗性能和用戶的數據流量。這是因爲replace操作,每次都會把container中的現有的fragment實例清空,然後再把指定的fragment添加進去,就就造成了在切換到以前的fragment時,就會重新實例會fragment。

Fragment性能問題解決

知道了問題的根源所在,那麼解決的辦法也呼之欲出了。我們不能使用replace來進行頁面的切換,那麼可使用的方法貌似只有add了,我們可以在加載的時候判斷Fragment是不是已經被添加到隊列中,如果已添加,我們就顯示(show)該Fragment,隱藏(hide)其他,如果沒有添加過呢,就添加。這樣就能做到多個Fragment切換不重新實例化。具體到代碼中就是這樣的


public class BestFragmentActivity extends AppCompatActivity{
    
    //當前的Fragment
    private Fragment mCurFragment = new Fragment();
    //初始化其他的Fragment
    private GoodsFragment mGoodsFragment = new GoodsFragment();
    private GoodCarFragment mGoodCarFragment = new GoodCarFragment();
    private TaskFragment mTaskFragment = new TaskFragment();
    private AboutFragment mAboutFragment  = new AboutFragment();
    private CategoryFragment mCategoryFragment  = new CategoryFragment();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_best_fragment);
        BottomNavigationView bottomNavigationView = (BottomNavigationView) findViewById(R.id.bottomNavigation);

        BottomNavigationItem bottomNavigationItem = new BottomNavigationItem
                ("首頁", ContextCompat.getColor(this, R.color.firstColor), R.mipmap.ic_account_balance_white_48dp);
        BottomNavigationItem bottomNavigationItem1 = new BottomNavigationItem
                ("分類", ContextCompat.getColor(this, R.color.secondColor), R.mipmap.ic_list_white_48dp);

        BottomNavigationItem bottomNavigationItem2 = new BottomNavigationItem
                ("任務", ContextCompat.getColor(this, R.color.firstColor), R.mipmap.ic_add_circle_outline_white_48dp);
        BottomNavigationItem bottomNavigationItem3 = new BottomNavigationItem
                ("購物車", ContextCompat.getColor(this, R.color.thirdColor), R.mipmap.ic_add_shopping_cart_white_48dp);

        BottomNavigationItem bottomNavigationItem4 = new BottomNavigationItem
                ("我的", ContextCompat.getColor(this, R.color.colorAccent), R.mipmap.ic_account_box_white_48dp);

        bottomNavigationView.addTab(bottomNavigationItem);
        bottomNavigationView.addTab(bottomNavigationItem1);
        bottomNavigationView.addTab(bottomNavigationItem2);
        bottomNavigationView.addTab(bottomNavigationItem3);
        bottomNavigationView.addTab(bottomNavigationItem4);

        bottomNavigationView.setOnBottomNavigationItemClickListener(new OnBottomNavigationItemClickListener() {
            @Override
            public void onNavigationItemClick(int i) {
                switch (i){
                    case 0:
                        switchToHome();
                        break;
                    case 1:
                        switchToCategory();
                        break;
                    case 2:
                        switchToTask();
                        break;
                    case 3:
                        switchToGoodCar();
                        break;
                    case 4:
                        switchToAbout();
                        break;
                }
            }
        });
        switchToHome();
    }

   private void switchFragment(Fragment targetFragment){
        FragmentTransaction transaction = getSupportFragmentManager()
                .beginTransaction();
        if (!targetFragment.isAdded()) {//如果要顯示的targetFragment沒有添加過
            transaction
                    .hide(mCurFragment)//隱藏當前Fragment
                    .add(R.id.frame_content, targetFragment,targetFragment.getClass().getName())//添加targetFragment
                    .commit();
        } else {//如果要顯示的targetFragment已經添加過
            transaction//隱藏當前Fragment
                    .hide(mCurFragment)
                    .show(targetFragment)//顯示targetFragment
                    .commit();
        }
        //更新當前Fragment爲targetFragment
        mCurFragment = targetFragment;

    }


    private void switchToAbout() {
        switchFragment(mAboutFragment);
    }
    private void switchToCategory() {
        switchFragment(mCategoryFragment);
    }
    private void switchToTask() {
        switchFragment(mTaskFragment);
    }
    private void switchToGoodCar() {
        switchFragment(mGoodCarFragment);
    }
    private void switchToHome() {
        switchFragment(mGoodsFragment);
    }

}


這樣就達到了我們的目的,我們在來回切換的操作中,Fragment只實例一次,少了銷燬又重新創建等帶來的性能消耗,另我們想要在Fragment中更新數據時,我們可以在自定義Fragment中重寫其onHiddenChanged方法


@Override
public void onHiddenChanged(boolean hidden) {
    super.onHiddenChanged(hidden);
    if (hidden){
       //Fragment隱藏時調用
    }else {
        //Fragment顯示時調用
    }

}


源碼地址:源碼傳送門

本篇總結

我們在本篇博客中比較詳細的給出了一個Fragment的最佳實踐,我們在許多主流App中都能看到這種頂部、底部導航的效果,並且在此基礎上我們探討了使用Fragment不當的存在性能問題及優化。



Good luck!

Reprinted by Jimmy.li












發佈了74 篇原創文章 · 獲贊 76 · 訪問量 71萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章