一、各種概念:
1、Loaders:
適用於Android3.0以及更高的版本,它提供了一套在UI的主線程中異步加載數據的框架。使用Loaders可以非常簡單的在Activity或者Fragment中異步加載數據,一般適用於大量的數據查詢,或者需要經常修改並及時展示的數據顯示到UI上,這樣可以避免查詢數據的時候,造成UI主線程的卡頓。
即使是查詢SQLite數據庫,用Loaders來操作會更加的簡便。
Loaders有以下特點:
- 可以適用於Activity和Fragment。
- 可以提供異步的方式加載數據。
- 監聽數據源,當數據改變的時候,將新的數據發布到UI上。
- Loaders使用Cursor加載數據,在更改Cursor的時候,會自動重新連接到最后配置的Cursor中讀取數據,因此不需要重新查詢數據。
在Android中使用Loaders機制,需要多個類和接口的配合,以下是它們大致的關系圖,之后的內容會對這幾個類或接口進行詳細講解:
2、LoaderManager
用於在Activity或者Fragment中管理一個或多個Loader實例。在Activity或者Fragment中,可以通過getLoaderManager()方法獲取LoaderManager對象,它是一個單例模式。
- 介紹幾個LoaderManager提供的方法,用於管理Loader:
- Loader<D> initLoader(int id,Bundle bundle,LoaderCallbacks<D> callback):初始化一個Loader,並注冊回調事件。
- Loader<D> restartLoader(int id,Bundle bundle,LoaderCallbacks<D> callback):重新啟動或創建一個Loader,並注冊回調事件。
- Loader<D> getLoader(int id):返回給定Id的Loader,如果沒有找到則返回Null。
- void destroyLoader(int id):根據指定Id,停止和刪除Loader。
通過上面幾個方法的參數可以看到,都有一個id參數,這個Id是Loader的標識,因為LoaderManager可以管理一個或多個Loader,所以必須通過這個Id參數來唯一確定一個Loader。而InitLoader()、restartLoader()中的bundle參數,傳遞一個Bundle對象給LoaderCallbacks中的onCreateLoader()去獲取,下面介紹LoaderCallbacks。
3、LoaderManager.LoaderCallbacks
LoaderCallbacks是LoaderManager和Loader之間的回調接口。它是一個回調接口,所以我們需要實現其定義的三個方法:
- Loader<D> onCreateLoader(int id,Bundle bundle):根據指定Id,初始化一個新的Loader,並返回。
- void onLoadFinished(Loader<D> loader,D data):當Loader被加載完畢后被調用,在其中處理Loader獲取的Cursor數據。
- void onLoaderReset(Loader<D> loader):當Loader被銷毀的時候被調用,在其中可以使Loader的數據不可用。
從LoaderCallbacks的聲明的幾個方法中可以看到,它是一個泛型的接口,需要指定Loader數據的類型。如果是數據源是從一個ContentProvider中獲取的,一般直接使用它的子類CursorLoader,下面介紹CursorLoader。
4、CursorLoader
我們知道,Loader一個抽象的類,用於執行異步加載數據,這個Loader對象可以監視數據源的改變和在內容改變后,以新數據的內容改變UI的展示。它是一個抽象接口,所有需要實現的Loader功能的類都需要實現這個接口,但是如果需要自己開發一個裝載機的話,一般並不推薦繼承Loader接口,而是繼承它的子類AsyncTaskLoader,這是一個以AsyncTask框架執行的異步加載。
Android中還提供了一個CursorLoader類,它是AsyncTaskLoader的子類,一個異步的加載數據的類,通過ContentResolver的標准查詢並返回一個Cursor。這個類實現了Loader的協議,以一種標准的方式查詢Cursor。
CursorLoader類有兩個構造函數,推薦使用第二個,因為使用第一個構造函數,需要還需要通過CursorLoader提供的一些了getXXX()方法設置對應的屬性。兩個構造方法如下:
- CursorLoader(Context context)
- CursorLoader(Context context,Uri uri,String[] projection,String selection ,String[] selectionArgs,String sortOrder)
二、代碼舉例:
在這個例子中,數據使用SQLite數據庫保存,然后用ContentProvider進行數據的請求與訪問。在SQLite數據庫中,已經存在一個Student表,它有兩個字段:_id,name。在本例中,使用一個ListView展示數據,使用LoaderManager管理一個Loader,並通過這個Loader的回調接口進行加載ListView的數據顯示並實時刷新,最終進行完成對SQLite數據庫中的數據進行增加與刪除。
整個工程文件的目錄結構如下:
具體步驟如下:
步驟(1):新建類PersonDao,用於進行對SQLite的CRUD操作
步驟(2):新建類DBHelper,用於初始化SQLiate數據庫
步驟(3):新建類PersonContentProvider,繼承ContetProvider,記得聲明權限。
步驟(4):添加單元測試類。我們在單元測試里向SQLite中添加一些記錄。
注:上述步驟是ContentProvider中的知識,和上一篇博文:ContentProvider內容提供者中的步驟一模一樣。所以這里就不列出代碼了,如果不明白的話,可以回去回顧一下,或者在本文的最后下載源碼也行。
我們在步驟(4)的單元測試里向數據庫中添加一些數據之后,可以開始接下來最關鍵的步驟了:
步驟(5):
MainActivity.java:
1 package com.example.loadermanagertest; 2 3 import android.annotation.SuppressLint; 4 import android.app.Activity; 5 import android.app.AlertDialog; 6 import android.app.LoaderManager; 7 import android.app.LoaderManager.LoaderCallbacks; 8 import android.content.ContentResolver; 9 import android.content.ContentValues; 10 import android.content.CursorLoader; 11 import android.content.Loader; 12 import android.database.Cursor; 13 import android.net.Uri; 14 import android.os.Bundle; 15 import android.util.Log; 16 import android.view.ContextMenu; 17 import android.view.ContextMenu.ContextMenuInfo; 18 import android.view.LayoutInflater; 19 import android.view.Menu; 20 import android.view.MenuInflater; 21 import android.view.MenuItem; 22 import android.view.View; 23 import android.widget.AdapterView.AdapterContextMenuInfo; 24 import android.widget.Button; 25 import android.widget.EditText; 26 import android.widget.ListView; 27 import android.widget.SimpleCursorAdapter; 28 import android.widget.TextView; 29 30 @SuppressLint("InflateParams") public class MainActivity extends Activity { 31 private LoaderManager manager; 32 private ListView listview; 33 private AlertDialog alertDialog; 34 private SimpleCursorAdapter mAdapter; 35 private final String TAG="MainActivity"; 36 37 @Override 38 protected void onCreate(Bundle savedInstanceState) { 39 super.onCreate(savedInstanceState); 40 setContentView(R.layout.activity_main); 41 listview = (ListView) findViewById(R.id.listView1); 42 //使用一個SimpleCursorAdapter,布局使用android自帶的布局資源simple_list_item_1, android.R.id.text1 為simple_list_item_1中TextView的Id 43 mAdapter = new SimpleCursorAdapter(MainActivity.this, 44 android.R.layout.simple_list_item_1, null, 45 new String[] { "name" }, new int[] { android.R.id.text1 },0); 46 47 // 獲取Loader管理器。 48 manager = getLoaderManager(); 49 // 初始化並啟動一個Loader,設定標識為1000,並制定一個回調函數。 50 manager.initLoader(1000, null, callbacks); 51 52 // 為ListView注冊一個上下文菜單 53 registerForContextMenu(listview); 54 } 55 56 @Override 57 public void onCreateContextMenu(ContextMenu menu, View v, 58 ContextMenuInfo menuInfo) { 59 super.onCreateContextMenu(menu, v, menuInfo); 60 // 聲明一個上下文菜單,contentmenu中聲明了兩個菜單,添加和刪除 61 MenuInflater inflater = getMenuInflater(); 62 inflater.inflate(R.menu.contentmenu, menu); 63 } 64 65 //單擊單個的item,彈出菜單選項,讓你選擇是添加還是刪除 66 @Override 67 public boolean onContextItemSelected(MenuItem item) { 68 69 switch (item.getItemId()) { 70 //當用戶點擊菜單中的“添加”選項是,彈出對話框,在對話框里添加name的值 71 case R.id.menu_add: 72 // 添加一個對話框 73 AlertDialog.Builder builder = new AlertDialog.Builder( 74 MainActivity.this); 75 // 加載一個自定義布局,add_name中有一個EditText和Button控件。 76 final View view = LayoutInflater.from(MainActivity.this).inflate( 77 R.layout.add_name, null); 78 Button btnAdd = (Button) view.findViewById(R.id.btnAdd); 79 btnAdd.setOnClickListener(new View.OnClickListener() { 80 81 @Override 82 public void onClick(View v) { 83 EditText etAdd = (EditText) view 84 .findViewById(R.id.username); 85 String name = etAdd.getText().toString(); 86 // 使用ContentResolver進行刪除操作,根據name字段。 87 ContentResolver contentResolver = getContentResolver(); 88 ContentValues contentValues = new ContentValues(); 89 contentValues.put("name", name); 90 Uri uri = Uri 91 .parse("content://com.example.loadermanagertest.PersonContentProvider/person"); 92 Uri result = contentResolver.insert(uri, contentValues); 93 if (result != null) { 94 //result不為空證明添加成功,重新啟動Loader,注意標識需要和之前init的標識一致。 95 manager.restartLoader(1000, null, callbacks); 96 } 97 // 關閉對話框 98 alertDialog.dismiss(); 99 100 Log.i(TAG, "--->>添加數據成功,name="+name); 101 } 102 }); 103 builder.setView(view); 104 alertDialog = builder.show(); 105 return true; 106 case R.id.menu_delete: 107 // 獲取菜單選項的信息 108 AdapterContextMenuInfo info = (AdapterContextMenuInfo) item 109 .getMenuInfo(); 110 // 獲取到選項的TextView控件,並得到選中項的內容 111 TextView tv = (TextView) info.targetView; 112 String name = tv.getText().toString(); 113 // 使用ContentResolver進行刪除操作 114 Uri url = Uri 115 .parse("content://com.example.loadermanagertest.PersonContentProvider/person"); 116 ContentResolver contentResolver = getContentResolver(); 117 String where = "name=?"; 118 String[] selectionArgs = { name }; 119 int count = contentResolver.delete(url, where, selectionArgs); 120 if (count == 1) { 121 //這個操作僅刪除單條記錄,如果刪除行為1 ,則重新啟動Loader 122 manager.restartLoader(1000, null, callbacks); 123 } 124 Log.i(TAG, "--->>刪除數據成功,name="+name); 125 return true; 126 default: 127 return super.onContextItemSelected(item); 128 } 129 130 } 131 132 // Loader的回調接口,在這里異步加載數據庫的內容,顯示在ListView上,同時能夠自動更新 133 private LoaderManager.LoaderCallbacks<Cursor> callbacks = new LoaderCallbacks<Cursor>() { 134 135 @Override 136 public Loader<Cursor> onCreateLoader(int id, Bundle bundle) { 137 // 在Loader創建的時候被調用,這里使用一個ContentProvider獲取數據,所以使用CursorLoader返回數據 138 Uri uri = Uri 139 .parse("content://com.example.loadermanagertest.PersonContentProvider/person"); 140 CursorLoader loader = new CursorLoader(MainActivity.this, uri, 141 null, null, null, null); 142 Log.i(TAG, "--->>onCreateLoader被執行。"); 143 return loader; 144 } 145 146 //完成對UI的數據提取,更新UI 147 @Override 148 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 149 //把數據提取出來,放到適配器中完成對UI的更新操作(刷新SimpleCursorAdapter的數據) 150 mAdapter.swapCursor(cursor); 151 // 為ListView綁定適配器 152 listview.setAdapter(mAdapter); 153 Log.i(TAG, "--->>onLoadFinished被執行。"); 154 } 155 156 @Override 157 public void onLoaderReset(Loader<Cursor> loader) { 158 // 當Loader被從LoaderManager中移除的時候,被執行,清空SimpleCursorAdapter適配器的Cursor 159 mAdapter.swapCursor(null); 160 Log.i(TAG, "--->>onLoaderReset被執行。"); 161 } 162 }; 163 164 @Override 165 public boolean onCreateOptionsMenu(Menu menu) { 166 getMenuInflater().inflate(R.menu.main, menu); 167 return true; 168 } 169 170 }
核心代碼:132行至162行、95行和122行的實時刷新
注意為ListView綁定適配器的代碼:listview.setAdapter(mAdapter)是在Loader的回調接口中(132行)進行的,也就是在這里更新UI,這樣就能夠實現自動刷新UI。
43行:新建一個SimpleCursorAdapter適配器
先通過getLoaderManager()方法獲取LoaderManager對象(48行),然后通過manager.initLoader(1000, null, callbacks)初始化一個Loader(50行)。其方法的完整版是:
public abstract <D> Loader<D> initLoader(int id,Bundle args, LoaderManager.LoaderCallbacks<D> callback)
- 第一個參數id:一個Acticity中可以加載多個Loader,所以要給每個Loader制定一個唯一的標識符id。第二個參數可以置空。
- 第三個參數callback:回調。
Loader的回調接口是在132行至162行定義的,也就是在這里異步加載數據庫的內容,顯示在ListView上,同時能夠自動更新。
我們在第53行為ListView注冊一個上下文菜單,上下文的菜單布局是在62行的R.menu.contentmenu.xml中定義的(稍后給出代碼)。
當用戶單擊單個的item時,彈出菜單選項,讓你選擇是添加還是刪除(67行定義的方法)。如果是選擇添加內容,則彈出一個對話框(71行)(對話框的布局文件R.layout.add_name稍后給出),輸入需要加入的內容,單擊確定,就會更新到UI(95行的manager.restartLoader(1000, null, callbacks)方法);如果選擇刪除內容,則直接刪除,並更新UI(122行的manager.restartLoader(1000, null, callbacks)方法)。
R.menu.contentmenu.xml:用於定義上下文菜單的布局
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@+id/menu_add" android:orderInCategory="100" android:showAsAction="never" android:title="添加"> </item> <item android:id="@+id/menu_delete" android:orderInCategory="100" android:showAsAction="never" android:title="刪除"> </item> </menu>
R.layout.add_name.xml:定義添加內容的布局
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="240dp" 4 android:layout_height="wrap_content" 5 android:orientation="vertical" > 6 7 <TextView 8 android:id="@+id/textView1" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" 11 android:text="姓名:" /> 12 13 <EditText 14 android:id="@+id/username" 15 android:layout_width="match_parent" 16 android:layout_height="wrap_content" 17 android:layout_marginBottom="4dp" 18 android:layout_marginLeft="4dp" 19 android:layout_marginRight="4dp" 20 android:layout_marginTop="16dp" 21 android:hint="username" 22 android:inputType="textEmailAddress" /> 23 24 <Button 25 android:id="@+id/btnAdd" 26 android:layout_width="match_parent" 27 android:layout_height="wrap_content" android:text="確定"> 28 </Button> 29 30 </LinearLayout>
布局效果如下:
運行程序,動態演示效果如下:
圖文分解如下:
初始界面為:
單擊長按第二個item,會彈出一個菜單:
如果我們選擇上圖菜單中的“添加”,會彈出一個對話框:
我們在上面的對話框中輸入smyhvae,點擊“確定”,就會將內容添加到ListView中,並自動更新UI:
如果單擊菜單中的“刪除”,會直接刪除。
注:本文采用的適配器是SimpleCursorAdapter,可以參考老羅的老版視頻,采用的是自定義適配器BaseAdapter。
參考鏈接:http://www.cnblogs.com/plokmju/p/android_Loaders.html