剛開始來公司的時候,接手了兩個項目,兩個項目的架構是用的同一個,雖然做了一些封裝,但在我這個菜鳥的眼裏,也覺得不好,單其中一個項目在不久後就上線了,因爲百分之八十以上的頁面在我來之前就寫好了,而且之前也上傳了幾個版本了,在這種情況下,雖然後來的擴展和修改bug讓我很頭疼,也還是沒有想着做一些改變。另外一個項目至今沒有上線,還有一些東西沒有完成,而另一個開發者又離職了,UI給我列出來一大堆要改的頁面和邏輯,在縫縫補補做了半個月的時候,我終於終於沒辦法忍受,於是毅然決定重構整個項目,哦,不對,是重新做這個項目。
那麼,既然重新做,就不能用之前的架構了,縫縫補補太痛苦,所以開始了這篇博客的主題所指,當然,在我這個閱歷下,能想到的封裝思路和能夠封裝的程度都是有限的,只是給初級的開發者一些參考,大神,請繞步。。。
一、網絡加載
首先,選擇網絡加載框架,恩,之前就一直聽說Retrofit,是最近非常流行的一個框架,但是一直沒有機會自己來好好玩一次,所以在研究了一段時間之後毅然決定就它了。因爲自己研究的程度目前還不能給大家分析,所以,詳情請看下後面的代碼吧!如果你都還沒聽說過,就去看看官方文檔,這個不是今天的重點。
二、圖片加載
然後就是圖片加載框架,現在流行的有三大圖片框架:
1) Picasso
2) Glide
3) Fresco
當然,ImageLoader是歷史舞臺的大佬,不過我好久沒用過了。。。
先簡單的介紹下這三個框架:
Picasso :和Square的網絡庫一起能發揮最大作用,因爲Picasso可以選擇將網絡請求的緩存部分交給了okhttp實現。
Glide:模仿了Picasso的API,而且在他的基礎上加了很多的擴展(比如gif等支持),Glide默認的Bitmap格式是RGB_565,比 Picasso默認的ARGB_8888格式的內存開銷要小一半;Picasso緩存的是全尺寸的(只緩存一種),而Glide緩存的是跟ImageView尺寸相同的(即56*56和128*128是兩個緩存) 。
FB的圖片加載框架Fresco:最大的優勢在於5.0以下(最低2.3)的bitmap加載。在5.0以下系統,Fresco將圖片放到一個特別的內存區域(Ashmem區)。當然,在圖片不顯示的時候,佔用的內存會自動被釋放。這會使得APP更加流暢,減少因圖片內存佔用而引發的OOM。爲什麼說是5.0以下,因爲在5.0以後系統默認就是存儲在Ashmem區了。
再看下三者的比較:
Picasso所能實現的功能,Glide都能做,無非是所需的設置不同。但是Picasso體積比起Glide小太多如果項目中網絡請求本身用的就是okhttp或者retrofit(本質還是okhttp),那麼建議用Picasso,體積會小很多(Square全家桶的幹活)。Glide的好處是大型的圖片流,比如gif、Video,如果你們是做美拍、愛拍這種視頻類應用,建議使用。
Fresco在5.0以下的內存優化非常好,代價就是體積也非常的大,按體積算Fresco>Glide>Picasso,不過在使用起來也有些不便(小建議:他只能用內置的一個ImageView來實現這些功能,用起來比較麻煩,我們通常是根據Fresco自己改改,直接使用他的Bitmap層)
因爲使用了retrofit,再加上本應用對圖片的要求不高,所以我們這裏最完美的選擇就是Picasso了,那就決定了,就用它。
三、其他
做完了這兩個選擇後,接下來要考慮的事情是對基類的封裝。當然最基礎的基類有BaseApplication,BaseActivity和BaseFragment三個,對於BaseApplication的話,我沒有做太多的處理,現在想到一個對網絡狀態進行監聽的方法,然後在當網絡由不可用轉爲確實可用的時候,發送一個消息給所有加載失敗的頁面,重新加載數據,這個在我寫完這篇博客後會加上去的。
BaseActivity和的ParentActivity封裝
至於ParentActivity是幹嘛的?因爲我需要在BaseActivity中封裝好對錯誤頁面,未加載完成的頁面,加載成功後的頁面和對標題欄等的一些頁面的處理,但是很多頁面其實是不需要加載網絡的,所以不需要加載錯誤,未加載完成的頁面,他只需要直接顯示出來一些界面就好,所以ParentActivity就誕生了,在這個類裏面,就是單單的做了除了與View有關的其他所有操作。
ParentActivity.java
/**
* Created by cretin on 16/10/27.
*/
public abstract class ParentActivity extends AppCompatActivity {
//記錄下所有的Activity
public final static List<ParentActivity> mActivities = new LinkedList<ParentActivity>();
public static boolean isKitkat;
public static ParentActivity mActivity;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().hide();
synchronized (mActivities) {
mActivities.add(this);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
isKitkat = true;
}
initView(null);
}
protected abstract void initView(View view);
@Override
protected void onResume() {
super.onResume();
mActivity = this;
}
@Override
protected void onPause() {
super.onPause();
mActivity = null;
}
@Override
protected void onDestroy() {
super.onDestroy();
synchronized (mActivities) {
mActivities.remove(this);
}
ButterKnife.unbind(this);
}
}
在上面的代碼中,首先定義了一個List<ParentActivity> mActivities的集合,用於存儲所有的已打開的Activtiy,便於以後統一的去處理和查詢;還定義了一個public static boolean isKitkat;這個是用來記錄當前Android版本支不支持沉浸式狀態欄,如果支持,直接使用沉浸式狀態欄;還有一個抽象方法initView(null);是用來暴露給用戶,讓他去處理View相關的事情,但是由於ParentActivity沒有處理View相關的操作,所以傳入了null;在onDestroy方法裏面,移除掉了當前的Activity,並且ButterKnife.unbind(this);由於整個架構默認使用ButterKnife來對View進行綁定,所以在退出的時候主動unbind掉,免得每個子類都要各自去處理。
在BaseActivity裏面,就需要處理上面ParentActivity沒有能處理的事情,所以BaseActivity是繼承自ParentActivity的。
具體如下:
BaseActivity.java
package com.cretin.www.createnewprojectdemo.base;
import android.graphics.Color;
import android.graphics.drawable.AnimationDrawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.cretin.www.createnewprojectdemo.R;
import com.cretin.www.createnewprojectdemo.utils.ViewUtils;
import com.cretin.www.createnewprojectdemo.view.CustomProgressDialog;
import butterknife.ButterKnife;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import rx.Subscription;
import rx.subscriptions.CompositeSubscription;
/**
* Created by cretin on 16/10/27.
* 如果是需要處理自己的邏輯 則繼承這個
* 如果只是給加載Fragment提供一個容器 則繼承ParentActivity
*/
public abstract class BaseActivity extends ParentActivity {
private CustomProgressDialog dialog;
private OnTitleAreaCliclkListener onTitleAreaCliclkListener;
private TextView tvMainTitle;
private ImageView ivMainBack;
private ImageView ivMainRight;
private TextView tvMainRight;
private RelativeLayout relaLoadContainer;
private TextView tvLoadingMsg;
private ImageView ivBack;
private CompositeSubscription mCompositeSubscription;
protected void addSubscription(Subscription s) {
if ( this.mCompositeSubscription == null ) {
this.mCompositeSubscription = new CompositeSubscription();
}
this.mCompositeSubscription.add(s);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = getLayoutInflater().inflate(R.layout.layout_base_activity, null);
setContentView(view);
initHeadView(view);
initContentView(view);
if ( isKitkat ) {
view.findViewById(R.id.ll_main_title).setPadding(0, ViewUtils.getStatusBarHeights(), 0, 0);
}
initData();
initEvent();
}
private AnimationDrawable animationDrawable;
private void initContentView(View view) {
RelativeLayout container = ( RelativeLayout ) view.findViewById(R.id.main_container);
relaLoadContainer = ( RelativeLayout ) view.findViewById(R.id.load_container);
tvLoadingMsg = ( TextView ) view.findViewById(R.id.loading_msg);
ImageView imageView = ( ImageView ) view
.findViewById(R.id.loading_image);
animationDrawable = ( AnimationDrawable ) imageView
.getBackground();
animationDrawable.start();
View v = getLayoutInflater().inflate(getContentViewId(), null);
ButterKnife.bind(this, v);
container.addView(v);
initView(v);
}
//onResponse子類去實現
public abstract class ResultCall<T> implements Callback<T> {
@Override
public void onResponse(Call<T> call, Response<T> response) {
hidProgressView();
onResponse(response);
}
protected abstract void onResponse(Response<T> response);
@Override
public void onFailure(Call<T> call, Throwable t) {
showErrorView();
onError(call, t);
}
protected abstract void onError(Call<T> call, Throwable t);
}
//隱藏正在加載視圖
public void hidProgressView() {
if ( relaLoadContainer != null )
relaLoadContainer.setVisibility(View.GONE);
if ( animationDrawable != null )
animationDrawable.stop();
}
//顯示正在加載視圖
public void showProgressView() {
if ( relaLoadContainer != null && relaLoadContainer.getVisibility() == View.GONE )
relaLoadContainer.setVisibility(View.VISIBLE);
if ( animationDrawable != null )
animationDrawable.start();
}
//隱藏返回按鈕
public void hidBackIv() {
if ( ivBack != null && ivBack.getVisibility() == View.VISIBLE )
ivBack.setVisibility(View.GONE);
}
//顯示加載錯誤
public void showErrorView() {
if ( relaLoadContainer != null && relaLoadContainer.getVisibility() == View.GONE )
relaLoadContainer.setVisibility(View.VISIBLE);
if ( animationDrawable != null )
animationDrawable.stop();
tvLoadingMsg.setText("加載錯誤");
}
//初始化頭部視圖
private void initHeadView(View view) {
tvMainTitle = ( TextView ) view.findViewById(R.id.tv_title_info);
ivMainBack = ( ImageView ) view.findViewById(R.id.iv_back);
ivMainRight = ( ImageView ) view.findViewById(R.id.iv_right);
tvMainRight = ( TextView ) view.findViewById(R.id.tv_right);
ivBack = ( ImageView ) view.findViewById(R.id.iv_back);
ivMainBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
if ( onTitleAreaCliclkListener != null )
onTitleAreaCliclkListener.onTitleAreaClickListener(v);
}
});
ivMainRight.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if ( onTitleAreaCliclkListener != null )
onTitleAreaCliclkListener.onTitleAreaClickListener(v);
}
});
tvMainRight.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if ( onTitleAreaCliclkListener != null )
onTitleAreaCliclkListener.onTitleAreaClickListener(v);
}
});
}
/**
* 顯示加載對話框
*
* @param msg
*/
public void showDialog(String msg) {
if ( dialog == null ) {
dialog = CustomProgressDialog.createDialog(this);
if ( msg != null && !msg.equals("") ) {
dialog.setMessage(msg);
}
}
dialog.show();
}
/**
* 關閉對話框
*/
public void stopDialog() {
if ( dialog != null && dialog.isShowing() ) {
dialog.dismiss();
}
}
public void setOnTitleAreaCliclkListener(OnTitleAreaCliclkListener onTitleAreaCliclkListener) {
this.onTitleAreaCliclkListener = onTitleAreaCliclkListener;
}
//設置Title
protected void setMainTitle(String title) {
if ( !TextUtils.isEmpty(title) )
tvMainTitle.setText(title);
}
//設置TitleColor
protected void setMainTitleColor(String titleColor) {
if ( !TextUtils.isEmpty(titleColor) )
setMainTitleColor(Color.parseColor(titleColor));
}
//設置TitleColor
protected void setMainTitleColor(int titleColor) {
tvMainTitle.setTextColor(titleColor);
}
//設置右邊TextView顏色
protected void setMainTitleRightColor(int tvRightColor) {
tvMainRight.setTextColor(tvRightColor);
}
//設置右邊TextView顏色
protected void setMainTitleRightColor(String tvRightColor) {
if ( !TextUtils.isEmpty(tvRightColor) )
setMainTitleRightColor(Color.parseColor(tvRightColor));
}
//設置右邊TextView大小
protected void setMainTitleRightSize(int size) {
tvMainRight.setTextSize(size);
}
//設置右邊TextView內容
protected void setMainTitleRightContent(String content) {
if ( !TextUtils.isEmpty(content) ) {
if ( tvMainRight.getVisibility() == View.GONE )
tvMainRight.setVisibility(View.VISIBLE);
tvMainRight.setText(content);
}
}
//設置左邊ImageView資源
protected void setMainLeftIvRes(int res) {
if ( ivMainBack.getVisibility() == View.GONE )
ivMainBack.setVisibility(View.VISIBLE);
ivMainBack.setImageResource(res);
}
//設置又邊ImageView資源
protected void setMainRightIvRes(int res) {
if ( ivMainRight.getVisibility() == View.GONE )
ivMainRight.setVisibility(View.VISIBLE);
ivMainRight.setImageResource(res);
}
interface OnTitleAreaCliclkListener {
void onTitleAreaClickListener(View view);
}
protected abstract void initData();
protected abstract int getContentViewId();
protected void initEvent() {
}
@Override
protected void onDestroy() {
super.onDestroy();
ButterKnife.unbind(this);
if ( this.mCompositeSubscription != null ) {
this.mCompositeSubscription.unsubscribe();
}
}
}
layouy_base_activity.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_main_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:clickable="true"
android:src="@mipmap/arrow_left"/>
<TextView
android:id="@+id/tv_title_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="13dp"
android:textColor="@color/font_black3"
android:textSize="@dimen/text_size_17"
tools:text="標題" />
<TextView
android:id="@+id/tv_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerInParent="true"
android:layout_marginRight="5dp"
android:clickable="true"
android:drawablePadding="5dp"
android:textColor="@color/font_black3"
android:textSize="@dimen/text_size_17"
android:visibility="gone"
tools:text="修改" />
<ImageView
android:id="@+id/iv_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:visibility="gone"
tools:src="@mipmap/ic_launcher" />
</RelativeLayout>
<include layout="@layout/line" />
</LinearLayout>
<RelativeLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/ll_main_title" />
<RelativeLayout
android:id="@+id/load_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/ll_main_title"
android:background="@color/white">
<LinearLayout
android:layout_width="120dip"
android:layout_height="120dip"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/loading_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal"
android:background="@drawable/load_progress_bar" />
<TextView
android:id="@+id/loading_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal"
android:padding="10dip"
android:text="正在加載..."
android:textSize="12sp" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
先看上面的佈局文件,分爲兩個大的部分,一個是標題欄,一個是內容區域,標題欄左邊有一個默認的返回ImageView,中間是一個TextView,用於顯示標題,右邊有一個TextView和一個ImageView,TextView用來顯示比如修改的操作,ImageView用來顯示設置什麼的圖標,默認左邊的返回按鈕個標題欄可見狀態,右邊兩個都是隱藏的,因爲用到他們的機會還是挺少的。在內用區域,有一個顯示內容的佈局,到時候用來顯示需要加載的佈局,還有一個正在加載的佈局,用來顯示正在加載中。。這個可自己去根據需求定製,這個頁面也可以作爲加載失敗的頁面,根據需求來就可以,當這個界面的網絡加載完成之後,就可以主動的隱藏掉這個正在加載的頁面就可以了,用戶體驗相對也會好一點。
然後來看下BaseActivity中的操作,首先,我們避免寫findViewById的操作,所以我們在BaseActivity中封裝好了對ButterKnife的bind和unbind操作,這樣在子類中就只需要簡單的操作就行,不需要再bind和unbind了,然後提供了一系列的對標題欄自定義的操作,比如設置標題內容,文字大小顏色,設置標題欄右邊的圖標的資源。。等等,這些根據需求,不夠的還可以自己再自行添加就好;還有在前面我們判斷了Android版本是否支持沉浸式狀態欄,如果支持的話,我在BaseActivity中也對標題欄進行了setPadding的操作,因爲不設置的話,標題會跟狀態欄的文字重疊,就會比較難看,這種操作不需要子類來理會,所以封裝出來會比較好;另外我還提供了一個比較方便的的方法,就是showDialog和stopDialog的操作,當頁面需要二次請求網路的時候,顯示showDialog,加載完就stopDialog即可,這樣用戶體驗就會好一點;還有一個很大的點就是對網絡請求的封裝,這個因爲我現在對Retrofit還不是非常熟練,所以只進行了簡單的封裝,到時候經過再研究之後,再來進行完善!在目前的代碼中,我寫了一個抽象方法,在抽象方法中對請求的響應做出一個初級的判斷,如果請求是成功的,並且數據的返回也是正常的,那麼回調給調用者,否則直接顯示網絡加載錯誤的頁面,統一的對網絡錯誤進行處理,不過,這個要能真正完美的使用,需要後臺不要太差。。不然老是返回亂七八糟的錯誤給你,就不太好統一處理了。。到此,BaseActivity的處理也要到此爲止了。
BaseFragment的封裝
對於BaseFragment來說,其封裝跟BaseActtivity是一模一樣的,本質的差別也就是一個是Activity一個是Fragment,所以就不再贅述了。
BaseFragmentActivity和BackFragmentActivity的封裝
爲什麼會有這兩個類的存在?我現在比較喜歡的一種加載界面方式是使用Fragment碎片化的方式,就是開一個Activity,這個Activity就是承載Fragment的一個容器,所以這個Activity啥都不做,就只作用於添加Fragment,顯然,這個Activity是不需要加載網絡和一些其他操作的,所以我把他們倆都是集成自ParentActivity,這個具體看下項目中的操作。看這兩個類的名字也很清楚,繼承BaseFragmentActivity的時候,代表要加載的Fragment是不需要假如回退棧的,繼承BackFragmentActivity的時候,代表要加載的Fragment是需要假如回退棧的,當然,在計入Fragment的時候,也可以使用動畫,具體的使用,看Demo就好,在此不再贅述了,代碼什麼的還是要自己看,理解才快。
工具類
最後再來說下項目中默認添加使用的一些其他好用的東西:
本地數據的存儲:Hawk
這個工具是當年一個叫Calvin的大哥告訴我的,一用起來就愛不釋手。
Hawk 是一個非常便捷的數據庫 . 操作數據庫只需一行代碼 , 能存任何數據類型 .
github 地址: https://github.com/orhanobut/hawk
Hawk 是一個簡單的 key-value 數據庫
它使用:
AES 加密
能選擇使用SharedPreferences 或者 SQLite
Gson解析
提供:
安全數據持久化
能存儲任何類型
具體的使用,大家去Github上去看吧,相信只要你用過你就會喜歡上他的,信我。
Android事件總線:EventBus
這個不多說,已經非常成熟了。簡單介紹下
實際項目開發過程中,經常遇到如下場景:不同的應用程序組件的控件間具有一定的相互關聯性,其中用戶對後者進行的某種操作會引起前者的相應改變。舉一個具體的場景:以糗事百科爲例,在糗事列表頁和詳情頁頁,對於每個糗事而言,佈局基本一致,在詳情頁點擊了個贊,讚的數量增加,同時讚的圖標發生了變化,此時返回到列表頁,此糗事上的贊圖標以及數量與剛剛詳情頁的需要保持一致。在舉一個例子,對於多個底部導航tab下的資訊類閱讀app,在諮詢詳情頁點擊了收藏,然後收藏成功,此時回到底部tab中的個人中心,假如個人中心中有我的收藏,同時後面顯示的是收藏數量,此時此收藏數量需要同於於剛剛用戶所進行的收藏/取消收藏而即時更改數字。凡此種種,類似需求場景非常常見。
有時候,當此類需求相對簡單時,通過接口以實現回調等方式可以完成,但是當不同組件/控件之間的關係紛繁複雜時,基於接口的方案不僅使得代碼非常繁瑣,同時是的程序邏輯很混亂,基於此,EventBus,爲此類需求的實現提供了非常方便的方案。
butterknife註解框架的偷懶插件
這個也不多說,已經非常成熟了。簡單介紹下
事實上這是個Android Studio的plugin,他可以讓你在添加Butterkinfe註解時偷偷懶,直接點擊幾下鼠標既可以完成註解的增加,同時還是圖形化的操作,可以說,大大的減輕了開發負擔。尤其是當你的layout中有很多很多的view需要通過findviewbyid來獲得引用時。實際上如果不用這個插件而通過手打加ButtefKnife註解的方式,要是view很多啓示也挺麻煩的,不是嗎?
到此這篇博客也就到了尾聲了,可能有很多地方還不是很準確,希望大家多提提意見,而且,這個封裝也還很基礎,希望大佬有時間看下,多提提意見和建議,再次謝過了。如果覺得不是太爛,記得Star一下哦。
我是Cretin,一個可愛的小男孩。
下面是Github的地址:
https://github.com/MZCretin/CreateNewProjectDemo