Android练手小项目(KTReader)基于mvp架构(一)

下路传送眼:Android练手小项目(KTReader)基于mvp架构(二)

前言

这是一个练手的小项目,权当之前Android学习的总结,项目里会有相同功能的不同实现
(从基本的代码到热门的开源项目实现,比如网络通信就会分别使用HttpURLConnection和retrofit2+OKhttp3实现)。

如果你在阅读中发现了错误或是需要改进之处,欢迎指正,谢谢。

ps:之前在P层做了model层的事情,感谢 别问了我去EDG了的指正,已改正。

GIthub地址: https://github.com/yiuhet/KTReader

启动界面

目录结构:

包结构

创建基类

  • BaseActivity
  • BasePresenter
  • MVPBaseActivity
  • BaseFragment

添加依赖 (之后会逐步添加)
基类创建会用到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 以上设置直接状态栏透明
        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); //方法类里的一个方法,封装了Toast,后文会提到.
    }
}

2. BasePresenter

BasePresenter有四个方法:

  • attachView(T view) —— 建立关联
  • getView() —— 获取View
  • isViewAttached() —— 判断是否与View建立了关联
  • detachView() —— 解除关联

Presenter对通过泛型传进来的VIew持有弱引用,防止内存泄漏。

public abstract class BasePresenter<T> {

    protected Reference<T> mViewRef; //View 接口类型的弱引用

    public void attachView(T view) {
        mViewRef = new WeakReference<T>(view);  //建立关联
    }

    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,含有两个泛型参数:

  • 第一个是View接口类型 V
  • 第二个是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

泛型参数与MVPBaseActivity一样

  • 第一个是View接口类型 V
  • 第二个是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;
    }
}

创建一个漂亮的启动界面(Splash Screen)

这个项目的启动界面有以下特点:

  • 启动App时不会出现白屏。
  • 背景图有放大的动画效果。
  • 从网上拉取一句励志语录并加载出来。

1. 不会出现白屏

白屏出现的原因:

Android是先渲染window再渲染activity,而业务逻辑(比如初始化用户信息等)是在activity里,这就会导致渲染出activity的布局变慢,如果不做任何操作,这时候在activity的页面渲染出来前就会有个黑色或者白色的状态。

所以严格来说,我并不是消除了白屏,而是设置window的background替换了那个阶段的显示,实际上没有加速,但从用户角度确实提高了感知。

下面附上代码:

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. 放大的动画效果

SplashActivity里定义一个startAnim(Class act)方法:

private void startAnim(final Class act) {
        //传入一个ImageView对象,围绕X,Y进行2D缩放,由原始的大小方法到原来的1.15倍
        ObjectAnimator animatorX = ObjectAnimator.ofFloat(mIvShowPic, "scaleX", 1f, 1.15f);
        ObjectAnimator animatorY = ObjectAnimator.ofFloat(mIvShowPic, "scaleY", 1f, 1.15f);
        //多个动画的协同工作
        AnimatorSet set = new AnimatorSet();
        set.setDuration(2000).play(animatorX).with(animatorY);
        set.start();
        //对动画的监听,动画结束后立马跳转到主页面上
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startActivity(act); //基类里的方法
            }
        });
    }

3. 从网上拉取一句励志语录并加载出来:

启动界面里的数据通信是用基本的HttpURLConnection类实现的,数据解析也是用JSONObject类实现的。

励志语录是易源数据由提供的,返回数据格式如下:

{
    "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": "让适合的人走进你的生活吧,让旧梦逝去吧,让不合适的那个离开吧。"
            }
        ]
    }
}

Model层 :

由于欢迎界面的数据只需要字符串,所以不需要实体类。

先写一个接口

  • 易源api励志语句Model接口
public interface SplashModel {
    void loadSaying(OnSplashListener listener);
}

然后写Model层的实现类

  • 获取易源api励志语句的Model实现
public class SplashModelImp1 implements SplashModel {
    /*获取易源api励志语句的Model实现*/

    private OnSplashListener listener; //回调接口
    @Override
    public void loadSaying(OnSplashListener listener) {
        this.listener = listener;
        new ShowAsyncTask().execute(ShowApiUtils.SAYING);
    }

    //使用基本的AsyncTask处理网络请求
    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); //网络请求数据不为空时 回调成功方法。
            } else {
                listener.onError(); //回调失败方法。
            }
        }
    }
}
  • 加载数据的方法类 —— 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";  //励志语录的api
    // 解析url网址
    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;
    }
    //获取数据,使用HttpURLConnection实现
    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数据解析出结果
    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) {
        //传入一个ImageView对象,围绕X,Y进行2D缩放,由原始的大小方法到原来的1.15倍
        ObjectAnimator animatorX = ObjectAnimator.ofFloat(mIvShowPic, "scaleX", 1f, 1.15f);
        ObjectAnimator animatorY = ObjectAnimator.ofFloat(mIvShowPic, "scaleY", 1f, 1.15f);
        //多个动画的协同工作
        AnimatorSet set = new AnimatorSet();
        set.setDuration(2000).play(animatorX).with(animatorY);
        set.start();
        //对动画的监听,动画结束后立马跳转到主页面上
        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层

先写一个回调接口(在Presenter层实现,给Model层回调,更改View层的状态,确保Model层不直接操作View层)

  • OnSplashListener
public interface OnSplashListener {
    /**
     * 成功时回调
     * @param saying
     */
    void onSuccess(String saying);
    /**
     * 失败时回调
     */
    void onError();
}

再写一个presenter接口:

  • SplashPresenter
public interface SplashPresenter {
    void loadSaying();
}

最后写Prestener实现类

  • 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();
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容