1. 程式人生 > >Android Loader用法總結

Android Loader用法總結

Android提供了幾種非同步載入資料的方式,AsyncTaskLoader就是其中一種,這裡對它的用法做一個總結。

Android在3.0引入了Loader(載入器),支援在Activity或Fragment中非同步載入資料。想要在低版本上使用Loader可以用v4相容包。

3.0之後官方文件強烈推薦使用Loader載入資料。

Loader的特性

  • 可用於每個 Activity 和 Fragment。
  • 支援非同步載入資料。
  • 監控其資料來源並在內容變化時傳遞新結果。
  • 在某一配置改變(如橫豎屏切換)後重建Loader時,會自動重新連線上一個Loader的遊標(cursor)。 因此,它們無需重新查詢資料。

LoaderManager

一個與 Activity 或 Fragment 相關聯的的抽象類,用於管理一個或多個 Loader 例項。 這有助於應用管理與 Activity 或 Fragment 生命週期相關聯的、執行時間較長的操作。

它最常見的用法是與 CursorLoader 一起使用,你也可以實現自己的Loader來載入其他型別的資料。

每個 Activity 或Fragment中只有一個 LoaderManager。但一個 LoaderManager 可以有多個Loader。

LoaderManager.LoaderCallbacks

一個回撥介面,用於客戶端與 LoaderManager 進行互動。例如,你可以使用 onCreateLoader() 回撥方法建立新的Loader。

Loader

一個執行非同步資料載入的抽象類,是Loader的基類。

通常使用 CursorLoader,你也可以實現自己的子類。

Loader處於活動狀態時,它們應該監視它們的資料來源並且在資料改變時傳送新的結果。

Loader本身並不支援非同步載入機制,所以當我們編寫自己的Loader的時候,不應該直接繼承Loader類,應該繼承AsyncTaskLoader,AsyncTaskLoader支援非同步載入機制。

AsyncTaskLoader

一個使用AsyncTask來執行非同步載入工作的抽象類。

CursorLoader

AsyncTaskLoader的子類,查詢ContentResolver然後返回一個Cursor。

該類以標準遊標查詢實現了Loader協議,它的查詢是通過AsyncTaskLoader在後臺執行緒中執行,所以不會阻塞UI執行緒。使用這個Loader是從ContentProvider非同步載入資料的最好方式。

框架結構

這裡寫圖片描述

啟動Loader

通常要在Activity的onCreate()方法中或Fragment的onActivityCreated()方法中初始化Loader,可以按照下面的方式建立:

//準備Loader,重連一個已存在的Loader或者啟動新的Loader
//id:一個唯一的ID用於標識這個Loader,在這個例子中是0;
//args:可選的引數,在Loader初始化時作為引數傳入,本例中是null;
//callbacks:一個LoaderManager.LoaderCallbacks的實現,LoaderManager 將呼叫此實現來報告Loader事件;在這個例子中,當前類實現了這個介面,所以傳的是自身的引用:this。
getLoaderManager().initLoader(0, null, this);

initLoader() 確保了Loader已初始化且處於活動狀態。這可能會出現兩種結果:

  1. 如果 ID 所指的Loader已存在,則將重複使用上次建立的Loader,這時候args會被忽略,因為重用了之前的Loader。
  2. 如果 ID 所指的Loader不存在,則 initLoader() 將觸發 LoaderManager.LoaderCallbacks 中的回撥方法 onCreateLoader()。在此方法中,你可以在這裡例項化並返回新的Loader。

在這兩種情況下,傳入的LoaderManager.LoaderCallbacks的實現都與Loader繫結在一起,並且會在Loader狀態變化時被呼叫;

如果在呼叫這個initLoader()方法時,Loader已經處於啟動狀態(也就是說這個Loader已存在),並且所請求的Loader已產生了資料,那麼系統會馬上呼叫onLoadFinished(),所以你必須為這種情況的發生做好準備。

intiLoader()會返回一個建立的Loader,但是你不用獲取它的引用,因為LoadeManager會自動管理該Loader的生命週期,你只用在它回撥提供的生命週期方法中做自己資料邏輯的處理即可。

重啟Loader

當你使用initLoader()時,如果指定ID的Loader已經存在,則它使用這個Loader;如果不存在,它將建立一個新的。但是有的時候你卻想丟棄舊的Loader然後開始一個新的Loader。

要想丟棄舊的Loader,應該使用restartLoader()。例如,下面這個SearchView.OnQueryTextListener的實現在使用者查詢發生改變時重啟了Loader,Loader需要重啟從而才能使用新的搜尋過濾詞來進行一次新的查詢。

public boolean onQueryTextChanged(String newText) {
    // 當搜尋內容變化時,更新搜尋過濾詞,重啟Loader並用這個搜尋過濾詞重新查詢
    mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
    getLoaderManager().restartLoader(0, null, this);
    return true;
}

使用LoaderManager中的回撥

LoaderManager.LoaderCallbacks是一個回撥介面,它使得客戶端可以與LoaderManager進行互動。

Loader,尤其是CursorLoader,我們希望在它停止後依然保持資料,這讓應用可以在Activity或Fragment的onStop()和onStart()方法保持資料;當用戶返回應用時,他們不需要再等待資料載入,你可以使用LoaderManager.LoaderCallbacks中的方法,在需要時建立新的Loader,並且告訴應用什麼時候要停止使用Loader中的資料。

LoaderManager.LoaderCallbacks包含以下方法:

  • onCreateLoader()
    根據傳入的ID,初始化並返回一個新的Loader。

    當你試圖去操作一個Loader時(比如,呼叫initLoader()方法),它會檢查是否指定ID的Loader已經存在,如果不存在,將會觸發LoaderManager.LoaderCallbacks中的onCreateLoader(),你需要在這裡建立一個新Loader並返回。

  • onLoadFinished():當一個Loader完成了資料載入之後呼叫。

    這個方法確保會在Loader上的資料被釋放之前被呼叫。在此方法中,你必須移除所有對舊資料的使用(因為它們將很快會被刪除),但是不要自己去釋放它們,因為Loader會去做這些事情。

    Loader一旦瞭解到應用不再使用這些資料時,將馬上釋放這些資料。

    例如,如果資料是一個從CursorLoader來的Cursor,你不應該自己呼叫cursor的close()方法;如果cursor被放置在一個CursorAdapter中,你可以使用swapCursor()方法進行新資料交換,這樣,舊的cursor就不會被關閉(CursorLoader的實現中會自動幫你關閉舊的cursor),就不會導致Adapter載入異常。

  • onLoaderReset():當一個Loader被重置,從而使得其資料無效時被呼叫。

    這個回撥告訴你什麼時候資料將被釋放,所以你可以釋放對它的引用。

    下面實現了呼叫引數為null的swapCursor()

public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
}

例子

下面的例子實現了從通訊錄載入使用者暱稱,通過SearchView過濾搜尋結果。
因為要讀取通訊錄,所以要現在Manifest中宣告READ_CONTACTS 許可權

public class CursorLoaderActivity
        extends Activity
        implements SearchView.OnQueryTextListener,
        LoaderManager.LoaderCallbacks<Cursor> {

    private SimpleCursorAdapter mAdapter;
    private String mCurFilter;// 搜尋過濾器
    private ListView mListView;
    private TextView mTxtEmpty;

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

        setContentView(R.layout.activity_loader);
        mListView = (ListView) findViewById(R.id.listview);
        mTxtEmpty = (TextView) findViewById(R.id.txt_empty);

        mListView.setEmptyView(mTxtEmpty);//沒有資料時顯示的view

        mAdapter = new SimpleCursorAdapter(this,
                android.R.layout.simple_list_item_2, null,
                new String[]
                ContactsContract.Contacts.DISPLAY_NAME, 
                ContactsContract.Contacts.CONTACT_STATUS},
                new int[]{android.R.id.text1,  
                android.R.id.text2}, 0);
        mListView.setAdapter(mAdapter);

        // 啟動loader,可以重連一個已經存在的也可以啟動一個新的
        getLoaderManager().initLoader(0, null, this);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        //在ActionBar上顯示SearchView
        MenuItem item = menu.add("Search");
        item.setIcon(android.R.drawable.ic_menu_search);
        item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
        SearchView sv = new SearchView(this);
        sv.setOnQueryTextListener(this);
        item.setActionView(sv);
        return super.onCreateOptionsMenu(menu);
    }


    public boolean onQueryTextChange(String newText) {
        //當搜尋字串變化時呼叫
        //使用新的搜尋過濾器重啟loader
        mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
        getLoaderManager().restartLoader(0, null, this);
        return true;
    }

    @Override
    public boolean onQueryTextSubmit(String query) {
        return true;
    }

    static final String[] CONTACTS_SUMMARY_PROJECTION = new String[]{
            ContactsContract.Contacts._ID,
            ContactsContract.Contacts.DISPLAY_NAME,
            ContactsContract.Contacts.CONTACT_STATUS,
            ContactsContract.Contacts.CONTACT_PRESENCE,
            ContactsContract.Contacts.PHOTO_ID,
            ContactsContract.Contacts.LOOKUP_KEY,
    };

    /**
     * 建立一個CursorLoader並返回,如果initLoader()指定id的loader不存在,會觸發這個回撥方法
     *
     * @param id   給loader指定的id,如果只有一個loader,就不用管id
     * @param args 傳遞的引數
     * @return
     */
    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        //根據是否對結果過濾來確定uri
        Uri baseUri;
        if (mCurFilter != null) {
            baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,
                    Uri.encode(mCurFilter));
        } else {
            baseUri = ContactsContract.Contacts.CONTENT_URI;
        }
        String select = "((" + ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND ("
                + ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND ("
                + ContactsContract.Contacts.DISPLAY_NAME + " != '' ))";
        CursorLoader cursorLoader = new CursorLoader(
                this,
                baseUri,//uri:要獲取內容的uri
                CONTACTS_SUMMARY_PROJECTION, //要返回的列的集合,傳入null將會返回所有列(比較低效)
                select,//一個過濾器,表示哪些行將會被返回,格式化成類似SQL where子句的樣子,傳入null將返回所有行
                null,//selectionArgs:可以在selection中包含一些'?',它將被本引數的值替換掉,這些值出現的順序與'?'在selection中出現的順序一致,值將作為字串
                ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"//sortOrder:排序字串,格式化成類似於SQL ORDER BY子句樣子(除去ORDER BY),傳入null將使用預設順序,預設順序可能是無序的
        );
        return cursorLoader;
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        //釋放cursor(系統會幫我們關閉舊的cursor)         
        mAdapter.swapCursor(data);
    }

    public void onLoaderReset(Loader<Cursor> loader) {
        //當onLoadFinished()中的cursor將要被關閉的時候呼叫,我們需要確保它不再被使用
        mAdapter.swapCursor(null);

    }
}

什麼時候不該用Loader

如果你需要確保後臺任務執行完成,就不要用Loader,因為當Activity或Fragment被銷燬的時候,這個Activity或Fragment裡面的Loader也會被銷燬。如果要執行後臺任務,可以使用Service。
要記得Loader是用來幫助你建立相應的UI,非同步載入資料到這些UI上。這就是Loader和建立它的元件的生命週期繫結到一起的原因,不要濫用Loader!

在Service中使用

LoaderManager明顯的是為Activity或Fragment生命週期而設計的來管理Loader的類,因為Service沒有這些狀態變化,所以沒必要在Service中使用LoaderManager。
但是如果你已經為Activity或Fragment寫好了Loader,現在要在Service中重用這些程式碼,可以不使用LoaderManager,直接操作Loader。
在Service被建立的時候,create, register, start你的Loader

@Override
public void onCreate() {
    mCursorLoader = new CursorLoader(context, contentUri, projection, selection, selectionArgs, orderBy);
    mCursorLoader.registerListener(LOADER_ID_NETWORK, this);
    mCursorLoader.startLoading();
}

使Service implements OnLoadCompleteListener介面,在回撥函式onLoadComplete中繫結資料和UI

@Override
public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
}

在onDestroy中銷燬Loader

@Override
public void onDestroy() {
    // Stop the cursor loader
    if (mCursorLoader != null) {
        mCursorLoader.unregisterListener(this);
        mCursorLoader.cancelLoad();
        mCursorLoader.stopLoading();
    }
}

當然你也可以這樣寫

private MyCursorLoader mMyCursorLoader;

Loader.OnLoadCompleteListener<Cursor> mCursorListener = new    Loader.OnLoadCompleteListener<ConnectionStatus>() {
        @Override
        public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
        }
};

Bundle bundle = new Bundle();
bundle.putString(BUNDLE_KEY_UID, uid);
mMyCursorLoader = new MyCursorLoader(mContext, bundle, null, this);
mMyCursorLoader.registerListener(LOADER_1, mCursorListener);
mMyCursorLoader.startLoading();

AsyncTaskLoader & AsyncTAsk

AsyncTaskLoader
優勢:會自動重新整理資料變化,會自動處理Activity配置變化造成的影響,適合處理純資料載入。
劣勢:不能實時通知UI重新整理,不能在onLoadFinished時主動切換生命週期(比如replace fragment)。
AsyncTask
優勢:可以與UI實時互動及replace操作。
劣勢:不會自動處理Activity配置變化造成的影響。

Loader生命週期

幾個坑

Loader id重複導致資料邏輯異常
多執行緒中restartLoader導致Loader丟擲異常(都在UI執行緒中執行就行了)

參考: