怎樣使用ListView實現一個帶有網絡請求,解析,分頁,緩存的公共的List頁面來大大的提升工做效率

在尋常的開發中經常會有很是多列表頁面。每作一個列表頁就需要建立這個佈局文件那個Adapter適配器文件等等一大堆與之相關的附屬的沒必要要的冗餘文件。java

假設版本號更新迭代比較頻繁,如此以往,就會使項目project變得無比龐大臃腫。git

假設看過這篇文章或者在使用過這樣的方式以後呢,所有的工做都可以被壓縮成僅僅有兩個文件,一個JAVA文件一個XML佈局文件。數據庫

而且代碼還少少的。json


我們來看看實際狀況:緩存

尋常的一個列表頁面的生成需要下面文件:安全

  • 一個Activity文件。有時候可能還會忘記註冊
  • 一個包括上下拉刷新控件以及無數據時提示的佈局文件
  • 一個Listview的item的佈局文件
  • 一個Adapter適配器文件
  • 一個需要被解析的Bean文件

固然在Activity中還需要處理下面功能:
  • 數據解析
  • 分頁載入
  • 數據緩存
  • 網絡請求
固然,以上功能可以經過頻繁的複製粘貼來實現,但是這會給之後的維護留下很多不小的坑,比方忽然需要更換Listview爲Recyclerview,你是否是頓時就想哭了呢?是否是就得一個個文件去改呢?現在不用了,改倆地方便可了。

現在你可能想知道一個公共的List頁有什麼特色呢?
  • 無需再關心網絡請求、數據解析、分頁、緩存等一樣的功能
  • 不需要寫那麼多的一樣的佈局文件,僅僅用寫那些不一樣的item佈局文件就可以。僅僅需要關心你關心的
  • 僅僅會有一個Adapter適配器,一個ViewHolder存在,Activity也可以僅僅有一個
  • 可複用性超強,不論是Activity中展現,仍是在被要求放在ViewPager中顯示都沒問題
  • 大大減少項目的project文件數量。提升編譯速度,不用再把一天的時間都浪費在編譯時間上
  • 提升你的工做效率。不用再複製粘貼,那個時間沒有這個快。僅僅用實現你的getView方法就可以
  • 下降維護成本。假設某一天需要在網絡請求加上某個參數,曾經的方法需要改無數個地方,而現在僅僅用改一個地方就OK,假設如探需要更改上下拉刷新控件,比方需要將XListView改爲PullToRefreshListView。你是否是就苦逼了?很是多地方都需要跟着改,現在不用了,僅僅用動一個地方全都OK
  • 還有很是多我一時間想不起來等你去發掘的功能

好。BB了這麼多,到底是怎麼實現和怎麼使用呢?容我慢慢道來:

好,先來看看使用起來有多便捷:
/**
 * 演示樣例代碼,將關鍵的部分放在fragment中。不論是viewpager仍是Activity。仍是其餘容器,都可以將fragment嵌入當中顯示
 * 
 * @author Sahadev
 *
 */
public class ExampleFragment extends SuperAbstListFragment<ExampleBean> {

	public static AbstListFragment getInstance(String requestUrl) {
		AbstListFragment fragment = new ExampleFragment();
		Bundle bundle = new Bundle();
		bundle.putString(AbstListFragment.URL, requestUrl);
		fragment.setArguments(bundle);
		return fragment;
	}

	@Override
	public Type getInstanceType() {
		// 返回需要實例化的對象類型
		return new TypeToken<List<ExampleBean>>() {
		}.getType();
	}

	/**
	 * 需要實例化的類,這裏僅用一個屬性作樣例
	 * 
	 * @author Work
	 *
	 */
	public static class ExampleBean implements Serializable {

		/**
		 * 
		 */
		private static final long serialVersionUID = 7624541082621792974L;

		@SerializedName("title")
		public String title;

	}

	//在這裏完畢數據綁定就可以了,支持鏈式調用
	@Override
	public void setView(ViewHolder viewHolder, ExampleBean t) {
		viewHolder.setText(R.id.title, t.title);
	}

	@Override
	public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

	}
}


這是實際執行效果圖:只經過短短的幾行代碼就可以實現強大的功能。是否是很是方便?


好,效果先看到了,接下來描寫敘述一下是怎樣完畢這麼多功能的:
固然,子類寫的代碼少,那說明父類已經幫它完畢了很多功能,因此我們先看看整體的文件夾結構:


這個圖可能畫的有些毛糙,仍是再用小文來簡述一下,BaseFragment含有一些主要的功能,比方高速彈出一個toast,顯示一個等待對話框等待,它還有子類常用的一些屬性,activity。LayoutInflater,ImageLoader 等等:
/**
 * 基本類。提供一些常用的主要的方法屬性供子類使用
 * 
 * @author Sahadev
 *
 */
public class BaseFragment extends Fragment {
	/**
	 * 圖片載入工具
	 */
	protected ImageLoader mImageLoader;
	/**
	 * 等待對話框
	 */
	private LoadingDialog mLoadingDialog;
	/**
	 * 佈局填充器
	 */
	protected LayoutInflater mInflater;
	/**
	 * context
	 */
	protected Activity mContext;

	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
		mContext = activity;
		mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	}

	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		// 在此處初始化mImageLoader,mLoadingDialog等屬性
		mLoadingDialog = LoadingDialog.getInstance(mContext);
		// imageLoader屬性可在本身定義的Application中設置全局的單例。由本身定義Application暴露接口獲取單例,比方
		// mImageLoader = CustomApplication.getImageLoaderInstance();
	}

	/**
	 * 吐司
	 * 
	 * @param message
	 */
	protected void toast(String message) {
		Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
	}

	/**
	 * 顯示等待對話框。顯示默認文本
	 */
	protected void showLoadingDialog() {
		if (mLoadingDialog != null) {
			mLoadingDialog.show();
		}
	}

	/**
	 * 顯示等待對話框,顯示傳入的文本
	 * 
	 * @param message
	 */
	public void showLoadingDialog(String message) {
		if (mLoadingDialog != null) {
			mLoadingDialog.setMessage(message);
			mLoadingDialog.show();
		}
	}

	/**
	 * 關閉等待對話框
	 */
	protected void dismissLoadingDialog() {
		if (mLoadingDialog != null) {
			mLoadingDialog.dismiss();
		}
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		dismissLoadingDialog();
	}

}

AbstListFragment則是我們項目的關鍵部分了。它集成了界面生成、空數據展現界面、網絡請求及分頁請求。網絡請求回調,item點擊回調。界面主動刷新廣播接收器等功能,
可以使用戶自定義適配器:
/**
 * 含有ListView的Fragment
 * 抽取公共的含有ListView的Fragment。此Fragment已經包含主要的下拉刷新,網絡載入,分頁載入等公共功能,僅僅需要關心實現 推薦使用
 * {@link #SuperAbstListFragment}實例化子類方式參見{@link #ExampleFragment}
 * 
 * @author Sahadev
 * 
 */
public abstract class AbstListFragment extends BaseFragment implements OnItemClickListener, OnClickListener,
		Listener<JSONObject>, ErrorListener, OnRefreshListener2<ListView> {
	protected PullToRefreshListView mListView;
	protected ImageView emptyView;
	private AnimationDrawable rocketAnimation;
	private View rootView;
	protected int page = 0;

	public static final String URL = "ABST_LIST_FRAGMENT_URL";
	public static final String NEED_REFRESH_BROADCAST_RECEIVER = "NEED_REFRESH_BROADCAST_RECEIVER";

	/**
	 * 請求的連接地址
	 */
	protected String requestUrl;

	/**
	 * 由子類實現。安全傳參
	 * 
	 * @param requestUrl
	 * @return
	 */
	public static AbstListFragment getInstance(String requestUrl) {
		return null;
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		if (rootView == null) {
			rootView = inflater.inflate(R.layout.activity_list_layout, container, false);
			emptyView = (ImageView) rootView.findViewById(R.id.empty_view);
			mListView = (PullToRefreshListView) rootView.findViewById(R.id.list);
			mListView.setEmptyView(emptyView);
			mListView.getRefreshableView().setLayoutTransition(new LayoutTransition());

			mListView.setOnRefreshListener(this);
			mListView.setOnItemClickListener(this);
			mListView.getRefreshableView().setOnItemClickListener(this);

			emptyView.setOnClickListener(this);

			Bundle bundle = getArguments();
			if (bundle != null) {
				requestUrl = getArguments().getString(AbstListFragment.URL);
			}
		}

		ViewGroup parent = (ViewGroup) rootView.getParent();
		if (parent != null) {
			parent.removeView(rootView);
		}
		return rootView;
	}

	@Override
	public void onStart() {
		super.onStart();
		if (mListView != null) {
			mListView.setRefreshing(false);
		}
	}

	@Override
	public void onPullDownToRefresh(PullToRefreshBase<ListView> refreshView) {
		page = 0;
		getData(requestUrl + "&p=" + page);// 這裏可以加入分頁和其餘請求server所需要的必要參數。比方token或者其餘什麼的,因此在傳入的地方僅僅用傳入必要的參數就OK
	}

	@Override
	public void onPullUpToRefresh(PullToRefreshBase<ListView> refreshView) {
		page++;
		getData(requestUrl + "&p=" + page);
	}

	protected void getData(String requestUrl) {
		if (isNeedLoadDataFromNet()) {
			if (page == 0) {
				// 可以在這裏設置載入動畫
				emptyView.setImageResource(R.drawable.loading_animation);// R.drawable.loading_animation表明動畫資源
				rocketAnimation = (AnimationDrawable) emptyView.getDrawable();
				rocketAnimation.start();
			}
			RequestUtils.requesGet(requestUrl, this, this);
		}
	}

	/**
	 * 這種方法用於返回是不是從網絡載入,有些數據是需要從本地載入的。這個 方法就可以由子類來控制詳細是什麼
	 * 
	 * @return
	 */
	protected boolean isNeedLoadDataFromNet() {
		return true;
	}

	@Override
	public void onResponse(JSONObject response) {
		// 設置請求完成以後的狀態
		rocketAnimation.stop();
		emptyView.setImageResource(R.drawable.nocontent);
		mListView.onRefreshComplete();
	}

	@Override
	public void onErrorResponse(VolleyError error) {
		// 設置請求完成以後的狀態
		rocketAnimation.stop();
		emptyView.setImageResource(R.drawable.nocontent);
		toast("咦?網絡情況貌似出了點問題.");
		mListView.onRefreshComplete();
	}

	@Override
	public void onClick(View v) {
		switch (v.getId()) {
		// 當點擊無數據提示的時候又一次載入
		case R.id.empty_view:
			mListView.setRefreshing();
			break;
		default:
			break;
		}
	}

	private BroadcastReceiver receiver;

	@Override
	public void onResume() {
		super.onResume();
		receiver = new NeedRefreshBroadcastReceiver();
		IntentFilter filter = new IntentFilter(NEED_REFRESH_BROADCAST_RECEIVER);
		filter.addCategory(Intent.CATEGORY_DEFAULT);
		mContext.registerReceiver(receiver, filter);
	}

	@Override
	public void onPause() {
		super.onPause();
		mContext.unregisterReceiver(receiver);
	}

	/**
	 * 主動刷新廣播接收器,當數據發生改變的時候(比方加入或者刪除)主動刷新
	 * 
	 * @author Work
	 *
	 */
	private class NeedRefreshBroadcastReceiver extends BroadcastReceiver {

		@Override
		public void onReceive(Context context, Intent intent) {
			mListView.setCurrentMode(Mode.PULL_FROM_START);
			mListView.setRefreshing(false);
		}

	}

}

SuperAbstListFragment<T> 是對父類AbstListFragment的進一步抽象。它裏面集成了一個適配器與一個萬能的ViewHolder,使子類僅僅用
實現幾個主要的方法就可以,比方要解析的類型、當item點擊以後的處理方式、數據與界面怎樣綁定等等,來看看這個
類都有什麼:

/**
 * 抽象的AbstListFragment中間層,具備更強大的功能
 * 
 * @author Work
 *
 */
public abstract class SuperAbstListFragment<T> extends AbstListFragment {
	protected AbstBaseAdapter<T> adapter;

	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		try {
			// 假設有些Adapter中不知足實際狀況的話,可以使用反射來實例化
			// adapter = (AbstBaseAdapter<T>)
			// getAdapterClass().getConstructor(Context.class).newInstance(mContext);
			adapter = new SuperAdapter(mContext);
			mListView.setAdapter(adapter);
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 萬能適配器,它僅僅是個中間件
	 * 
	 * @author Work
	 *
	 */
	public class SuperAdapter extends AbstBaseAdapter<T> {

		public SuperAdapter(Context context) {
			super(context);
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			return SuperAbstListFragment.this.getView(position, convertView, parent);
		}
	}

	/**
	 * 
	 * 
	 * @param position
	 * @param convertView
	 * @param parent
	 * @return
	 */
	public View getView(int position, View convertView, ViewGroup parent) {
		// 這裏使用的是萬能的ViewHolder
		ViewHolder viewHolder = ViewHolder.get(mContext, convertView, parent, R.layout.fragment_list_item, position);// 這一行可以進一步的抽取到父類中

		T t = adapter.getData().get(position);

		setView(viewHolder, t);

		return viewHolder.getConvertView();
	}

	/**
	 * 綁定數據,使用戶真正關心的僅僅有他們想要關心的
	 * 
	 * @param viewHolder
	 * @param t
	 */
	public abstract void setView(ViewHolder viewHolder, T t);

	/**
	 * 假設單單的getView方法不知足需求的話。可以經過本身定義Adapter的方法來實現,該方法用來返回需要實例化的Adapter的類名
	 * 
	 * @return
	 */
	public Class<?> getAdapterClass() {
		return null;
	}

	/**
	 * 需要解析的數據類型是一個對象仍是對象的集合,由這個返回
	 * 
	 * @return
	 */
	public abstract Type getInstanceType();

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.sahadev.general_assembly.base.AbstListFragment#onResponse(org.json
	 * .JSONObject) 當網絡請求成功以後回調該方法,開始解析數據
	 */
	@Override
	public void onResponse(JSONObject response) {
		super.onResponse(response);
		if (response != null && response.optBoolean("success")) {
			Gson gson = new Gson();
			List<T> datas = gson.fromJson(response.optJSONArray("data").toString(), getInstanceType());
			initAdapter(datas);
		}
	}

	/**
	 * 數據解析完成以後刷新數據
	 * 
	 * @param list
	 */
	protected void initAdapter(List<T> list) {
		if (page == 0) {
			adapter.addFirstPageData(list);
		} else {
			adapter.addOtherPageData(list);
		}
	}

}


經過以上幾個類的不斷抽取。當最後在使用的時候。實現類僅僅用簡單的幾行代碼就可以完畢很是多很是多的功能。怎麼樣,是否是很是easy?

接下來簡介一下怎樣在各類頁面僅僅用一個Activity來裝載不一樣頁面的Fragment呢:
/**
 * 含有ListFragment的Activity
 * 
 * @author 尚斌
 * 
 */
public class IncludeListFragmentActivity extends FragmentActivity {
	private String mFragmentClass = "x.x.x.x.x.x";
	private String mRequestUrl = "http://www.baidu.com";
	private String title = "標題沒有定義";

	public static final String TITLE = "TITLE";
	public static final String CLASS = "CLASS";
	public static final String URL = "URL";

	/**
	 * @param context
	 * @param fragmentClass
	 *            需要實例化的Fragment的包名
	 * @param requestUrl
	 *            該Fragment內部的請求地址
	 * @return
	 */
	public static Intent getIntent(Context context, String fragmentClass, String requestUrl, String title) {
		Intent intent = new Intent(context, IncludeListFragmentActivity.class);
		Bundle bundle = new Bundle();
		bundle.putString(TITLE, title);
		bundle.putString(CLASS, fragmentClass);
		bundle.putString(URL, requestUrl);
		intent.putExtras(bundle);
		return intent;
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		requestWindowFeature(Window.FEATURE_NO_TITLE);
		super.onCreate(savedInstanceState);

		Intent intent = getIntent();
		Bundle bundle = intent.getExtras();
		mFragmentClass = bundle.getString(CLASS);
		mRequestUrl = bundle.getString(URL);
		title = bundle.getString(TITLE);

		// 設置標題
		setTitle(title);

		// 設置佈局文件
		setContentView(R.layout.activity_include_list_fragment);

		try {
			Class<BaseFragment> newInstance = (Class<BaseFragment>) Class.forName(mFragmentClass);

			Method method = null;
			BaseFragment fragment = null;

			method = newInstance.getMethod("getInstance", String.class);
			fragment = (BaseFragment) method.invoke(null, mRequestUrl);

			if (fragment != null) {
				getSupportFragmentManager().beginTransaction().add(R.id.container, fragment).commit();
			} else {
				throw new Exception("You must be have a named getInstance method!");
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

就是這個文件。可以經過傳入的標題,需要實現的Fragment類,需要請求的地址來生成多種多樣的界面,因此在實現子類的時候每個子類都需要重寫public static AbstListFragment getInstance(String requestUrl) 方法以供外部可以調用到它。


說了這麼多,在這裏面事實上是沒有緩存的。事實上這個公共的項目與緩存關係是不大的。既然提到就說一下是怎麼實現的。在項目開發的時候很是多時候都用到了第三方網絡框架,也是有源代碼的,這裏就用Volley舉個栗子:
在Volley請求的時候, 在請求的基類方法中,依據請求的URL去數據庫中尋找,數據庫這裏推薦使用xUtils提供的數據庫存儲。假設沒找到,則調用網絡請求,在網絡請求成功回調的部分將請求的數據存入數據庫。以便第二次查找。基本思路就是這樣:
使用代碼舉例:
網絡請求部分:
	public JsonRequest(int method, String url, Map<String, String> body, Listener<T> listener,
			ErrorListener errorListener, boolean isNeedCache, Type type) {
		super(method, url + "&token=token" + "&vid=vid"), errorListener);
		mListener = listener;
		mRequestBody = null;
		mType = type;
		if (isNeedCache && !(檢查網絡是否可用)) {
			url += "&token=token" + "&vid=vid";

			try {

				//使用本身定義的方式去數據庫中查找,這裏使用的是xUtils舉例:

				List<Cache> datas = xUtils
						.getInstance()
						.getDbUtils()
						.findAll(
								Selector.from(Cache.class).where("requestUrl", "=", url).orderBy("time", false)
										.limit(1));

				if (!netAccessed && datas != null & listener != null) {
					for (Cache cache : datas) {
						//假設查找成功就進行回調
						listener.onResponse((T) new JSONObject(cache.jsonString));
					}
				}

			} catch (DbException e) {
				e.printStackTrace();
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}
		bodyMap = body;

	}
網絡回調部分。將server數據存儲:
@Override
	protected void deliverResponse(final T response) {
		if (response != null
				&& (response.getClass().equals(String.class) || response.getClass().equals(JSONObject.class))) {
			new Thread(new Runnable() {

				@Override
				public void run() {
					Cache cache = new Cache(mUrl, response.toString());

					try {
						//使用xUtils進行存儲
						xUtils.getInstance().getDbUtils().save(cache);
					} catch (DbException e) {
						e.printStackTrace();
					}
				}
			}).start();
		}

		netAccessed = true;

		if (mListener != null && (假設網絡可用)) {
			if (mType != null) {
				try {
					String simString1 = response.getClass().getName();
					String simString2 = mType.toString();
					//有多種類型回調,假設僅僅是回調String類。則調用下面
					if (simString2.contains(simString1)) {
						mListener.onResponse(response);
					} else {
						mListener.onResponse(null);
					}
				} catch (Exception e1) {
				}
			} else {
				//另外一種是JsonObject類型,則調用下面
				mListener.onResponse(response);
			}
		}
	}
大夥可能實際狀況不是這個樣子。但是思路可能差點兒相同,僅供參考。



固然,在該項目中還集成了很多別的主要的東西,比方ImageLoader圖片載入。Volley請求工具,json解析工具等,假設是做爲一個新項目的話,本項目仍是可以做爲一個最主要的起始項目來用用。網絡


項目是一個開源項目,迫切的想要不少其它的人可以增長進來。將本身工做中可以提升工做效率的知識和成果分享出來,出一份力。假設想增長 請聯繫我:sahadev@foxmail.com,但願可以一塊兒發展壯大,擁有很是多爲你們減輕負擔的成果。
相關文章
相關標籤/搜索