下路傳送眼:Android練手小項(xiàng)目(KTReader)基于mvp架構(gòu)(二)
前言
這是一個(gè)練手的小項(xiàng)目,權(quán)當(dāng)之前Android學(xué)習(xí)的總結(jié),項(xiàng)目里會(huì)有相同功能的不同實(shí)現(xiàn)
(從基本的代碼到熱門的開源項(xiàng)目實(shí)現(xiàn)础钠,比如網(wǎng)絡(luò)通信就會(huì)分別使用HttpURLConnection和retrofit2+OKhttp3實(shí)現(xiàn))入愧。
如果你在閱讀中發(fā)現(xiàn)了錯(cuò)誤或是需要改進(jìn)之處贬堵,歡迎指正划纽,謝謝。
ps:之前在P層做了model層的事情瞎领,感謝 別問了我去EDG了的指正泌辫,已改正。
GIthub地址: https://github.com/yiuhet/KTReader
目錄結(jié)構(gòu):
創(chuàng)建基類
- BaseActivity
- BasePresenter
- MVPBaseActivity
- BaseFragment
添加依賴 (之后會(huì)逐步添加)
基類創(chuàng)建會(huì)用到ButterKnife
compile 'com.jakewharton:butterknife:8.6.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0
1. BaseActivity
Activity基類里定義了以下方法(方法的作用如名所示):
- getLayoutRes()(抽象)
- showProgress(String msg)
- hideProgress()
- startActivity(Class activity)
- toast(String msg)
public abstract class BaseActivity extends AppCompatActivity {
private ProgressDialog mProgressDialog;
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayoutRes());
//android 5.0 以上設(shè)置直接狀態(tài)欄透明
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
WindowManager.LayoutParams localLayoutParams = getWindow().getAttributes();
localLayoutParams.flags = (WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | localLayoutParams.flags);
}
}
protected void showProgress(String msg) {
if (mProgressDialog == null) {
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setCancelable(true);
}
mProgressDialog.setMessage(msg);
mProgressDialog.show();
}
protected void hideProgress() {
if (mProgressDialog != null) {
mProgressDialog.dismiss();
}
}
protected void startActivity(Class activity) {
startActivity(activity, true);
}
protected void startActivity(Class activity, boolean finish) {
Intent intent = new Intent(this, activity);
startActivity(intent);
if (finish) {
finish();
}
}
protected abstract int getLayoutRes();
protected void toast(String msg) {
CommonUtils.ShowTips(this, msg); //方法類里的一個(gè)方法九默,封裝了Toast,后文會(huì)提到.
}
}
2. BasePresenter
BasePresenter有四個(gè)方法:
- attachView(T view) —— 建立關(guān)聯(lián)
- getView() —— 獲取View
- isViewAttached() —— 判斷是否與View建立了關(guān)聯(lián)
- detachView() —— 解除關(guān)聯(lián)
Presenter對(duì)通過泛型傳進(jìn)來的VIew持有弱引用震放,防止內(nèi)存泄漏。
public abstract class BasePresenter<T> {
protected Reference<T> mViewRef; //View 接口類型的弱引用
public void attachView(T view) {
mViewRef = new WeakReference<T>(view); //建立關(guān)聯(lián)
}
protected T getView() {
return mViewRef.get();
}
public boolean isViewAttached() {
return mViewRef != null && mViewRef.get() != null;
}
public void detachView() {
if (mViewRef != null) {
mViewRef.clear();
mViewRef = null;
}
}
}
3. MVPBaseActivity
MVPBaseActivity繼承BaseActivity驼修,含有兩個(gè)泛型參數(shù):
- 第一個(gè)是View接口類型 V
- 第二個(gè)是Presenter的具體類型 T
public abstract class MVPBaseActivity<V, T extends BasePresenter<V>> extends BaseActivity {
protected T mPresenter;
protected abstract T createPresenter();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPresenter = createPresenter();
mPresenter.attachView((V) this);
}
@Override
protected void onDestroy() {
super.onDestroy();
mPresenter.detachView();
}
}
4. BaseFragment
泛型參數(shù)與MVPBaseActivity一樣
- 第一個(gè)是View接口類型 V
- 第二個(gè)是Presenter的具體類型殿遂。T
public abstract class BaseFragment<V, T extends BasePresenter<V>> extends Fragment {
protected T mPresenter;
private ProgressDialog mProgressDialog;
protected abstract T createPresenter();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(getLayoutRes(), null);
ButterKnife.bind(this,root);
return root;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPresenter = createPresenter();
mPresenter.attachView((V)this);
}
protected abstract int getLayoutRes();
protected void setTitle(String title) {
getActivity().getActionBar().setTitle(title);
}
protected void showProgress(String msg) {
if (mProgressDialog == null) {
mProgressDialog = new ProgressDialog(getContext());
mProgressDialog.setCancelable(true);
}
mProgressDialog.setMessage(msg);
mProgressDialog.show();
}
protected void hideProgress() {
if (mProgressDialog != null) {
mProgressDialog.dismiss();
}
}
protected void toast(String msg) {
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
}
protected void startActivity(Class activity) {
startActivity(activity, true);
}
protected void startActivity(Class activity, boolean finish) {
Intent intent = new Intent(getContext(), activity);
startActivity(intent);
if (finish) {
getActivity().finish();
}
}
protected void startActivity(Class activity, String key, String extra) {
Intent intent = new Intent(getContext(),activity);
intent.putExtra(key, extra);
startActivity(intent);
}
@Override
public void onDestroy() {
super.onDestroy();
mPresenter.detachView();
mProgressDialog = null;
}
}
創(chuàng)建一個(gè)漂亮的啟動(dòng)界面(Splash Screen)
這個(gè)項(xiàng)目的啟動(dòng)界面有以下特點(diǎn):
- 啟動(dòng)App時(shí)不會(huì)出現(xiàn)白屏。
- 背景圖有放大的動(dòng)畫效果乙各。
- 從網(wǎng)上拉取一句勵(lì)志語錄并加載出來墨礁。
1. 不會(huì)出現(xiàn)白屏
Android是先渲染window再渲染activity,而業(yè)務(wù)邏輯(比如初始化用戶信息等)是在activity里耳峦,這就會(huì)導(dǎo)致渲染出activity的布局變慢,如果不做任何操作恩静,這時(shí)候在activity的頁面渲染出來前就會(huì)有個(gè)黑色或者白色的狀態(tài)。
所以嚴(yán)格來說妇萄,我并不是消除了白屏蜕企,而是設(shè)置window的background替換了那個(gè)階段的顯示,實(shí)際上沒有加速冠句,但從用戶角度確實(shí)提高了感知轻掩。
下面附上代碼:
styles.xml:
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="SplashTheme" parent="AppTheme.NoActionBar">
<item name="android:windowBackground">@drawable/splash_layers</item>
</style>
AndroidManifest.xml:
<activity android:name=".ui.activity.SplashActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
splash_layers.xml:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/bg_splash_default" />
</item>
<item>
<shape>
<gradient
android:angle="90"
android:startColor="@android:color/black"
android:endColor="@android:color/transparent"
/>
</shape>
</item>
</layer-list>
activity_splash.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_show_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/splash_layers"
android:scaleType="fitXY"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="100dp"
android:gravity="center"
android:text="KTReader"
android:textColor="@android:color/white"
android:textSize="23sp"/>
<TextView
android:id="@+id/tv_show_saying"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="60dp"
android:layout_centerInParent="true"
android:textColor="@android:color/white" />
</RelativeLayout>
2. 放大的動(dòng)畫效果
SplashActivity里定義一個(gè)startAnim(Class act)方法:
private void startAnim(final Class act) {
//傳入一個(gè)ImageView對(duì)象,圍繞X,Y進(jìn)行2D縮放,由原始的大小方法到原來的1.15倍
ObjectAnimator animatorX = ObjectAnimator.ofFloat(mIvShowPic, "scaleX", 1f, 1.15f);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(mIvShowPic, "scaleY", 1f, 1.15f);
//多個(gè)動(dòng)畫的協(xié)同工作
AnimatorSet set = new AnimatorSet();
set.setDuration(2000).play(animatorX).with(animatorY);
set.start();
//對(duì)動(dòng)畫的監(jiān)聽,動(dòng)畫結(jié)束后立馬跳轉(zhuǎn)到主頁面上
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
startActivity(act); //基類里的方法
}
});
}
3. 從網(wǎng)上拉取一句勵(lì)志語錄并加載出來:
啟動(dòng)界面里的數(shù)據(jù)通信是用基本的HttpURLConnection類實(shí)現(xiàn)的,數(shù)據(jù)解析也是用JSONObject類實(shí)現(xiàn)的懦底。
勵(lì)志語錄是易源數(shù)據(jù)由提供的,返回?cái)?shù)據(jù)格式如下:
{
"showapi_res_error": "",
"showapi_res_code": 0,
"showapi_res_body": {
"ret_code": 0,
"ret_message": "Success",
"data": [
{
"english": "Let the right one in. Let the old dreams die. Let the wrong ones go.",
"chinese": "讓適合的人走進(jìn)你的生活吧唇牧,讓舊夢(mèng)逝去吧,讓不合適的那個(gè)離開吧聚唐。"
}
]
}
}
Model層 :
由于歡迎界面的數(shù)據(jù)只需要字符串丐重,所以不需要實(shí)體類。
先寫一個(gè)接口
- 易源api勵(lì)志語句Model接口
public interface SplashModel {
void loadSaying(OnSplashListener listener);
}
然后寫Model層的實(shí)現(xiàn)類
- 獲取易源api勵(lì)志語句的Model實(shí)現(xiàn)
public class SplashModelImp1 implements SplashModel {
/*獲取易源api勵(lì)志語句的Model實(shí)現(xiàn)*/
private OnSplashListener listener; //回調(diào)接口
@Override
public void loadSaying(OnSplashListener listener) {
this.listener = listener;
new ShowAsyncTask().execute(ShowApiUtils.SAYING);
}
//使用基本的AsyncTask處理網(wǎng)絡(luò)請(qǐng)求
class ShowAsyncTask extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... params) {
return ShowApiUtils.parseJsonFromSaying(ShowApiUtils.getData(params[0]));
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
if (s != null) {
listener.onSuccess(s); //網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)不為空時(shí) 回調(diào)成功方法杆查。
} else {
listener.onError(); //回調(diào)失敗方法扮惦。
}
}
}
}
- 加載數(shù)據(jù)的方法類 —— ShowApiUtils:
public class ShowApiUtils {
private final static String SHOWAPI_APPID = "38473";
private final static String SHOWAPI_SECRT= "cb7ffb4054924ba2b2933a6834069bb1";
public final static String BING_PIC = "1377-1";
public final static String SAYING = "1211-1"; //勵(lì)志語錄的api
// 解析url網(wǎng)址
public static String getApiRequest(String address) {
String url = Uri.parse("http://route.showapi.com/" + address)
.buildUpon()
.appendQueryParameter("showapi_appid", SHOWAPI_APPID)
.appendQueryParameter("showapi_sign", SHOWAPI_SECRT)
.build().toString();
return url;
}
//獲取數(shù)據(jù),使用HttpURLConnection實(shí)現(xiàn)
public static String getData(String httpUrl) {
String jsonResult ;
BufferedReader reader = null;
StringBuffer sbf = new StringBuffer();
try {
URL url = new URL(getApiRequest(httpUrl));
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
InputStream is = connection.getInputStream();
reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String strRead = null;
while ((strRead = reader.readLine()) != null) {
sbf.append(strRead);
sbf.append("\r\n");
}
reader.close();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
jsonResult =sbf.toString();
return jsonResult;
}
//從返回的Json數(shù)據(jù)解析出結(jié)果
public static String parseJsonFromSaying(String jsonResult) {
String resultEnglish = null;
String resultChinese = null;
try {
JSONObject jsonBody = new JSONObject(jsonResult);
JSONObject resBody = jsonBody.getJSONObject("showapi_res_body");
JSONArray resDataArray = resBody.getJSONArray("data");
JSONObject result = resDataArray.getJSONObject(0);
resultEnglish = result.getString("english");
resultChinese = result.getString("chinese");
} catch (JSONException e) {
e.printStackTrace();
}
return resultChinese;
}
}
__View層 __
- SplashView接口
public interface SplashView {
void onGetSayingSuccess(String string);
void onGetSayingFailed();
}
- SplashActivity:
public class SplashActivity extends MVPBaseActivity<SplashView, SplashPresenterImp1> implements SplashView {
@BindView(R.id.iv_show_pic)
ImageView mIvShowPic;
@BindView(R.id.tv_show_saying)
TextView mTvShowSaying;
@Override
protected int getLayoutRes() {
return R.layout.activity_splash;
}
@Override
protected SplashPresenterImp1 createPresenter() {
return new SplashPresenterImp1(this);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife.bind(this);
//保持全屏窗口
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
mPresenter.loadSaying();
startAnim(MainActivity.class);
}
private void startAnim(final Class act) {
//傳入一個(gè)ImageView對(duì)象,圍繞X,Y進(jìn)行2D縮放,由原始的大小方法到原來的1.15倍
ObjectAnimator animatorX = ObjectAnimator.ofFloat(mIvShowPic, "scaleX", 1f, 1.15f);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(mIvShowPic, "scaleY", 1f, 1.15f);
//多個(gè)動(dòng)畫的協(xié)同工作
AnimatorSet set = new AnimatorSet();
set.setDuration(2000).play(animatorX).with(animatorY);
set.start();
//對(duì)動(dòng)畫的監(jiān)聽,動(dòng)畫結(jié)束后立馬跳轉(zhuǎn)到主頁面上
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
startActivity(act);
}
});
}
@Override
public void onGetSayingSuccess(String string) {
mTvShowSaying.setText(string);
}
@Override
public void onGetSayingFailed() {
mTvShowSaying.setText(getString(R.string.default_saying));
}
}
Presenter層
先寫一個(gè)回調(diào)接口(在Presenter層實(shí)現(xiàn)亲桦,給Model層回調(diào)崖蜜,更改View層的狀態(tài),確保Model層不直接操作View層)
- OnSplashListener
public interface OnSplashListener {
/**
* 成功時(shí)回調(diào)
* @param saying
*/
void onSuccess(String saying);
/**
* 失敗時(shí)回調(diào)
*/
void onError();
}
再寫一個(gè)presenter接口:
- SplashPresenter
public interface SplashPresenter {
void loadSaying();
}
最后寫Prestener實(shí)現(xiàn)類
- SplashPresenterImp1:
public class SplashPresenterImp1 extends BasePresenter<SplashView> implements SplashPresenter,OnSplashListener{
/*Presenter作為中間層客峭,持有View和Model的引用*/
private SplashView mSplashView;
private SplashModelImp1 splashModelImp1;
public SplashPresenterImp1(SplashView splashView) {
mSplashView = splashView;
splashModelImp1 = new SplashModelImp1();
}
@Override
public void loadSaying() {
splashModelImp1.loadSaying(this);
}
@Override
public void onSuccess(String saying) {
mSplashView.onGetSayingSuccess(saying);
}
@Override
public void onError() {
mSplashView.onGetSayingFailed();
}
}