app 老司机来手把手教你半天搞定一个免费下载小说

本贴最后更新于 2923 天前,其中的信息可能已经时移世异

前言

使用这个标题先表示对老王的尊敬
api全部开放 但是服务器使用的是美国服务器 访问速度特别慢 只用于学习
快速开发框架是我整理出来的一套框架 使用简单 实现快速 GitHub地址,喜欢的童鞋欢迎star
MVP是一种开发模式 按照你自己理解和编程习惯的去实现就好 没有必要一股脑的照搬
可能理论什么的我也不蛮会说,接下来了部分,我带你真正的打一场战役
看到这里如果你感兴趣我建议你先下载app跑一遍,知道我们需要做的是什么
项目的源码地址Freebook

这里有那么一群志同道合的人在等你加入 QQ群:173999252




效果图




目录

  • 底层框架搭建

  • 网络请求框架搭建

  • MVP模式实现

  • 使用的第三方框架介绍


底层框架搭建

万事开头难,实质上只要你走出第一步了,后面的路就能迎刃而解

在这里我要先介绍一下我的底层框架LCRapidDevelop,这个框架能干嘛呢?

  • 异常奔溃统一友好管理 无需担心程序出现异常而遗失用户

  • 页面状态 加载中 加载失败 无数据快速实现

  • 下拉刷新以及自动加载

  • RecyclerView的相关封装快速实现item动画adapter的编写

  • Tab+Fragment快速实现效果Duang Duang Duang 可按照需求随意修改成自己想要的

  • 视频播放快速实现 这个功能是今天我们需要编写的app唯一一个用不到的东西 我会考虑去除这个东西

功能呢列举到这里就差不多了,接下来我们需要把LCRapidDevelop添加到我们的项目里并编译项目


 

导入后编译一下如果没有报错我们进行下一步,新建好相应的文件夹



  • Constant --- 用于存放常量数据

  • Data --- 编写数据请求相关代码

  • Dialog ---编写自定义对话框

  • MVP --- 所有页面都些这里 等等我会针对这个进行解释

  • MyApplication ---存放自定义Application

  • Util ---存放工具类

  • Widget --存在自定义view

然后就是Application的编写了

 /**
 *自定义Application
 * 用于初始化各种数据以及服务
 **/
public class MyApplication extends Application {   
   //记录当前栈里所有activity
    private Listactivities = new ArrayList();
    @Override
    public void onCreate() {   
        super.onCreate();
        instance = this;     
        //异常友好管理初始化
        Recovery.getInstance()
                .debug(true)
                .recoverInBackground(false)
                .recoverStack(true)
                .mainPage(WelcomeActivity.class)
              //  .skip(H5PayActivity.class) //如果应用集成支付宝支付,记得加上这句代码,没时间解释了,快上车,老司机发车了
                .init(this);
    } 
   /**
     * 应用实例
     **/
    private static MyApplication instance;   
    /**
     * 获得实例
     *
     * @return
     */
    public static MyApplication getInstance() {  
          return instance;
    }  
    /**
     * 新建了一个activity
     *
     * @param activity
     */
    public void addActivity(Activity activity) {
        activities.add(activity);
    }
    /**
     * 结束指定的Activity
     *
     * @param activity
     */
    public void finishActivity(Activity activity) { 
         if (activity != null) {       
            this.activities.remove(activity);
            activity.finish();
            activity = null;
        }
    } 
    /**
     * 应用退出,结束所有的activity
     */
    public void exit() {      
         for (Activity activity : activities) {    
            if (activity != null) {
                activity.finish();
              }
           }
        System.exit(0);
      }
}

并且在AndroidManifest.xml中使用这个android:name=".MyApplication.MyApplication"

然后就是BaseActivity和BaseFragment的编写了
在MVP文件夹内新建文件夹Base 然后新建BaseActivity.class

public abstract class BaseActivity extends AppCompatActivity implements View.OnClickListener {
    protected Context mContext;
    private ConnectivityManager manager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {   
        super.onCreate(savedInstanceState);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);// 锁定竖屏
        mContext = getActivityContext();
        initView();
        ButterKnife.bind(this);
        initdata();
        MyApplication.getInstance().addActivity(this);
    }   
    /**
      初始 activity 方法
     
/
    private void initView() {
        loadViewLayout();
    }
    private void initdata(){
        findViewById();
        setListener();
        processLogic();
    }
    @Override
    protected void onDestroy() {   
        super.onDestroy();
        MyApplication.getInstance().finishActivity(this);
    }
    /**
      加载页面 layout
     
/
    protected abstract void loadViewLayout(); 
    /**
      加载页面元素
     
/
    protected abstract void findViewById(); 
    /**
      设置各种事件的监听器
     
/
    protected abstract void setListener(); 
    /**
      业务逻辑处理,主要与后端交互
     
/
    protected abstract void processLogic();  
   /**
      Activity.this
     
/
    protected abstract Context getActivityContext();
   /**
      弹出 Toast
     
 
      @param text
     
/
    public void showToast(String text) {
        Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
        toast.setGravity(Gravity.CENTER, 0, 0);
        toast.show();
    }
    public boolean checkNetworkState() {
        boolean flag = false;       
        //得到网络连接信息
        manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        //去进行判断网络是否连接
        if (manager.getActiveNetworkInfo() != null) {
            flag = manager.getActiveNetworkInfo().isAvailable();
        }       
        return flag;
    }
}

然后就是 BaseFragment.class

/**
 * 这个是最简单的 大家实际使用时 可添加我自想要的元素
 */
 public abstract class BaseFragment extends Fragment{

    private View mRootView;
    @Nullable
    @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            mRootView = initView(inflater,container);
            ButterKnife.bind(this, mRootView);//绑定到 butterknife
            return mRootView;
        }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {  
        super.onActivityCreated(savedInstanceState);
        initListener();
        initData();
    }

    protected abstract View initView(LayoutInflater inflater,ViewGroup container);
    protected abstract void initListener();
    protected abstract void initData();
}

到这里基本上底层框架搭建就搭建好了,如果熟练了的话,这个过程复制粘贴不到两分钟就能搞定, 第一次搭建的话算个 10 分钟吧


网络请求框架搭建

网络请求框架实质上就是上面我们提到的 Data 文件

  • 网络请求框架结构



  • api -- 编写网络请求的 api 以及缓存 api

  • db --SQLite 数据的相关处理 这里主要用于存储下载信息

  • HttpData ---统一网络请求处理

  • Retrofit ---是相关的配置 包括请求时弹出加载中对话框什么的

网络请求采用的是 RxJava +Retrofit2.0 + okhttp +RxCache ,是目前最为主流也是个人认为最好用最高效的网络请求 首先相应的包先导好

    compile 'io.reactivex:rxjava:1.1.8'
    compile 'io.reactivex:rxandroid:1.2.1'
    compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
    compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4'
    compile 'com.github.VictorAlbertos.RxCache:core:1.4.6'

关于这个网络请求框架的搭建我就不细说了,掘金的文章太多了,懒得去了解的朋友呢直接复制我的代码,我教你怎么使用好了,就跟我赐予你一把宝剑,知道使用就好干嘛还要去了解宝剑是怎么制造的,哈哈 当然这是一句玩笑话啦
首先是 BookService.class 的编写 api 文档地址

/**
  API 接口
 
 因为使用 RxCache 作为缓存策略 所以这里不需要写缓存信息
 /
public interface BookService {    
    //获取首页详情
    @GET("api/getHomeInfo")
    Observable<httpresult> getHomeInfo();    
   
    //获取书籍详情
    @GET("api/getBookInfo")
    Observable<httpresult> getBookInfo(@Query("id") int id);    
   
    //获取类别列表
    @GET("api/getTypeConfigList")
    Observable<httpresult<list>> getTypeList();    
   
    //根据类别获取书籍列表
    @GET("api/getTypeBooks")
     Observable<httpresult<list>> getBookList(@Query("type")int type,@Query("pageIndex")int pageIndex);    
   
    //根据关键词获取搜索书籍列表
    @GET("api/getSearchList")
    Observable<httpresult<list>> getSearchList(@Query("key")String key);    
   
    //获取热门搜索标签
    @GET("api/getSearchLable")
    Observable<httpresult<list<String>>> getHotLable();
}

然后就是缓存 api 的编写 CacheProviders.class

/**
 
 缓存 API 接口
  @LifeCache 设置缓存过期时间. 如果没有设置 @LifeCache,数据将被永久缓存理除非你使用了,EvictProvider,EvictDynamicKey or EvictDynamicKeyGroup 
 
 EvictProvider 可以明确地清理清理所有缓存数据.
  EvictDynamicKey 可以明确地清理指定的数据 DynamicKey.
 
 EvictDynamicKeyGroup 允许明确地清理一组特定的数据. DynamicKeyGroup.
  DynamicKey 驱逐与一个特定的键使用 EvictDynamicKey 相关的数据。比如分页,排序或筛选要求
 
 DynamicKeyGroup。驱逐一组与 key 关联的数据,使用 EvictDynamicKeyGroup。比如分页,排序或筛选要求
 */ 
public interface CacheProviders { 
   //获取书库对应类别书籍列表  缓存时间 1 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<reply<list>> getBookList(Observable<list> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);  
  
    //获取书库分类信息缓存数据 缓存时间 永久
    Observable<reply<list>> getTypeList(Observable<list> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);  
  
    //获取首页配置数据 banner 最热 最新  缓存时间 7 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<reply> getHomeInfo(ObservableoRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);   
  
    //获取搜索标签  缓存时间 7 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<reply<list<String>>> getHotLable(Observable<list<String>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);  
   
    //获取书籍详情  缓存时间 7 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<reply> getBookInfo(ObservableoRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);  
  
   //根据关键词获取搜素列表  缓存时间 1 天
    @LifeCache(duration = 1, timeUnit = TimeUnit.DAYS)
    Observable<reply> getSearchList(ObservableoRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);
}

最后就是 HttpData.class 的使用了

/**
 *所有的请求数据的方法集中地
 * 根据 MovieService 的定义编写合适的方法
 * 其中 observable 是获取 API 数据
 * observableCahce 获取缓存数据
 * new EvictDynamicKey(false) false 使用缓存  true 加载数据不使用缓存
 **/
public class HttpData extends RetrofitUtils {

    private static File cacheDirectory = FileUtil.getcacheDirectory();
    private static final CacheProviders providers = new RxCache.Builder()
            .persistence(cacheDirectory)
            .using(CacheProviders.class);
    protected static final BookService service = getRetrofit().create(BookService.class);   

   //在访问 HttpMethods 时创建单例
    private static class SingletonHolder {
        private static final HttpData INSTANCE = new HttpData();
    }    
   
   //获取单例
    public static HttpData getInstance() {   
        return SingletonHolder.INSTANCE;
    }   
    
    //获取 app 书本类别
    public void getBookTypes(Observer<list> observer){
        Observable observable=service.getTypeList().map(new HttpResultFunc<list>());
        Observable observableCahce=providers.getTypeList(observable,new DynamicKey("书本类别"),
                         new EvictDynamicKey(false)).map(new HttpResultFuncCcche<list>());
        setSubscribe(observableCahce,observer);
    }    
    
    //获取 app 首页配置信息  banner  最新 最热
    public void getHomeInfo(boolean isload,Observerobserver){
        Observable observable=service.getHomeInfo().map(new HttpResultFunc());;
        Observable observableCache=providers.getHomeInfo(observable,new DynamicKey("首页配置"),
                        new EvictDynamicKey(isload)).map(new HttpResultFuncCcche());
        setSubscribe(observableCache,observer);
    } 
  
  
   //获得搜索热门标签
    public void getSearchLable(Observer<list<String>> observer){
        Observable observable=service.getHotLable().map(new HttpResultFunc<list<String>>());;
        Observable observableCache=providers.getHotLable(observable,new DynamicKey("搜索热门标签"), 
                     new EvictDynamicKey(false)).map(new HttpResultFuncCcche<list<String>>());
        setSubscribe(observableCache,observer);
    }
    
    //根据类型获取书籍集合
    public void getBookList(int bookType, int pageIndex, Observer<list> observer) {
        Observable observable = service.getBookList(bookType,pageIndex).map(new HttpResultFunc<list>());
        Observable observableCache=providers.getBookList(observable,new DynamicKey("getStackTypeHtml"+bookType+pageIndex), 
                            new EvictDynamicKey(false)).map(new HttpResultFuncCcche<list>());
        setSubscribe(observableCache, observer);
    }
    
    //根据关键字搜索书籍
    public void getSearchList(String key,Observer<list> observer){ 
        try {       
            //中文记得转码  不然会乱码  搜索不出想要的效果
            key = URLEncoder.encode(key, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        Observable observable=service.getSearchList(key).map(new HttpResultFunc<list>());
        Observable observableCache=providers.getSearchList(observable,new DynamicKey("getSearchList&"+key), 
                               new EvictDynamicKey(false)).map(new HttpResultFuncCcche<list>());
        setSubscribe(observableCache, observer);
    } 
   
    //获取书籍详情
    public void getBookInfo(int id, Observerobserver){
        Observable observable=service.getBookInfo(id).map(new HttpResultFunc());
        Observable observableCache=providers.getBookInfo(observable,new DynamicKey("getBookInfo&"+id),
                            new EvictDynamicKey(false)).map(new HttpResultFuncCcche());
        setSubscribe(observableCache, observer);
    }  
    
    /**
     * 插入观察者
     *
     * @param observable
     * @param observer
     * @param
     **/
    public static void setSubscribe(Observableobservable, Observerobserver) {
        observable.subscribeOn(Schedulers.io())
                .subscribeOn(Schedulers.newThread())//子线程访问网络
                .observeOn(AndroidSchedulers.mainThread())//回调到主线程
                .subscribe(observer);
    }  
    
    /**
     * 用来统一处理 Http 的 resultCode,并将 HttpResult 的 Data 部分剥离出来返回给 subscriber
     *
     * @paramSubscriber 真正需要的数据类型,也就是 Data 部分的数据类型
     **/
    private class HttpResultFunc<T> implements Func1<HttpResult<T>, T> {

        @Override
        public T call(HttpResulthttpResult) { 
           if (httpResult.getCode() !=1 ) {    
              throw new ApiException(httpResult);
           }          
            return httpResult.getData();
          }
    }    
    
    /**
     * 用来统一处理 RxCacha 的结果
     **/
    private class HttpResultFuncCcche<T> implements Func1<Reply<T>, T> {

        @Override
        public T call(ReplyhttpResult) {       
             return httpResult.getData();
        }
    } 
}

到这里呢网络框架的搭建和使用介绍完了,这里如果是复制粘贴只需要修改 api 相关资料的还是不需要多少时间的,这里我们算 1 小时吧


MVP 模式实现

之前的项目结构我们也看到了,其中有一个文件夹就叫 MVP



  • Adapter ---存放适配器

  • Base ---存放 BaseActivity 等等

  • Entity ---存放实体

  • BookInfo Home Search 本应该是单独存放到一起的 这个是功能 但是这个 app 呢比较小功能也少我就懒得分开了

mvp 的实现方式很多很多,我呢是通过功能区分 对于 MVP 我想说的是 我是用的最好理解的方式去实现 老司机绕路 新手可以先在这个基础上跑通 再去学习进阶 学一个新东西最怕的就是在你接触的时候 技术深度太深 让你放弃治疗



这一个查看书籍详情的功能

  • model---用于请求数据

  • view ---用户交互和视图显示

  • presenter --负责完成 View 于 Model 间的逻辑和交互

首先我们需要确定 BookInfoActivity 有一些什么样的交互,比如说在加载的时候显示加载页面 网络异常时显示异常页面等等

当我们清这个压面的交互和视图的显示是,我们就可以编写 BookInfoView.class (其实不是蛮清楚也可以的 你先把知道的加上,后面想起来了在添加就好了)

public interface BookInfoView {    
     //显示加载页
    void showProgress();    
    //关闭加载页
    void hideProgress();    
    //数据加载成功
    void newData(BookInfoDto data);   
    //显示加载失败
    void showLoadFailMsg();
}

然后呢我们就要开始去编写 BookInfoModel.class 网络请求我们写到这个里面

/**
  获取书籍详情数据
 
/
public class BookInfoModel {
    public void loadData(int id, final OnLoadDataListListener listener){
        HttpData.getInstance().getBookInfo(id, new Observer() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                listener.onFailure(e);
            }

            @Override
            public void onNext(BookInfoDto bookInfoDto) {
                listener.onSuccess(bookInfoDto);
            }
        });
    }
}

最后就是 BookInfoPresenter

public class BookInfoPresenter implements OnLoadDataListListener<BookInfoDto>{
    private BookInfoView mView;
    private BookInfoModel mModel;

    public BookInfoPresenter(BookInfoView mView) {  
        this.mView = mView;
        mModel=new BookInfoModel();
    }

    public void loadData(int id){
        mModel.loadData(id,this);
        mView.showProgress();
    }

    @Override
    public void onSuccess(BookInfoDto data) {     
        if(data.getBookName().equals("")){
            mView.showLoadFailMsg();
        }else{
            mView.newData(data);
            mView.hideProgress();
        }
    }

    @Override
    public void onFailure(Throwable e) {
        mView.showLoadFailMsg();
    }
}

最后就是 BookInfoActivity 对这些进行使用了,仔细看代码,Activity 里面将不会出现任何数据逻辑

public class BookInfoActivity extends BaseActivity implements BookInfoView {

    @BindView(R.id.book_info_toolbar_textview_title)
    TextView bookInfoToolbarTextviewTitle;
    .....
    private int  bookid;
    private BookInfoPresenter presenter;

    @Override
    protected void loadViewLayout() {
        setContentView(R.layout.activity_book_info);
    }

    @Override
    protected void findViewById() {
        Intent intent = getIntent();
        bookid = intent.getIntExtra("bookid",0);
    }

    public void initview(BookInfoDto data) {

        bookInfoTextviewName.setText(data.getBookName());
        .....数据显示
    }

    @Override
    protected void setListener() {

    }

    @Override
    protected void processLogic() {
        presenter = new BookInfoPresenter(this);
        presenter.loadData(bookid);
    }

    @Override
    protected Context getActivityContext() {   
        return this;
    }   
    /*
    以下是 BookInfoView 定义的相关接口  activity 是需要实现就好了
    */
    @Override
    public void showProgress() {
        //显示加载页
        bookInfoProgress.showLoading();
    }

    @Override
    public void hideProgress() {
        //显示数据页
        bookInfoProgress.showContent();
    }

    @Override
    public void newData(BookInfoDto data) {
        //
        initview(data);
    }

    @Override
    public void showLoadFailMsg() {
        toError();
    }

    public void toError() {
        bookInfoProgress.showError(getResources().getDrawable(R.mipmap.load_error), Constant.ERROR_TITLE,
                             Constant.ERROR_CONTEXT, Constant.ERROR_BUTTON, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                bookInfoProgress.showLoading();             
                   //重试
                presenter.loadData(bookid);
            }
        });
    }
}

MVP 的使用大概就是这样,新司机可以先按照我这种比较简单易理解的方式先实现,当你实现了再去看深度比较深的 MVP 相关文章是,你就不会觉得很难理解了,这里话的把 app 的功能都实现差不多要话 2 个小时左右


使用的第三方框架介绍

首页介绍一下我们这个 app 都用到了哪些第三方框架

  • LCRapidDevelop ---底层框架

  • butterknife ---这个不需要解释了吧

  • glide---图片显示缓存框架

  • BGABanner ---支持大于等于 1 页时的无限循环自动轮播、手指按下暂停轮播、抬起手指开始轮播

  • FileDownloader ---Android 文件下载引擎,稳定、高效、简单易用

每个框架我都提供了链接,感兴趣的直接点进去查看,毕竟人家写的好详细的,我就不多嘴了, 这些框架使用到项目里主要是 BGABanner 和 FileDownloader 加上页面的编写以及适配器的编写,差不哦两小时左右


结语

从你开始构建这个项目开始,到这个项目结束,半天时间足以
先把东西先玩起来再去细致的了解,会比你先详细了解在开发要轻松的多
我没有太多的耐心去写的很细致,但是你们有任何疑问可以发邮件给我 mychinalance@gmail.com
api 大家可以随意使用 但是用的是美国服务器,会比较的慢,api 是用 spring mvc 写,需要源码的可以联系我

转自:https://gold.xitu.io/post/58297a042f301e00585c7a21



  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3454 回帖 • 189 关注
  • App

    App(应用程序,Application 的缩写)一般指手机软件。

    91 引用 • 384 回帖 • 3 关注
  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    334 引用 • 323 回帖 • 4 关注
  • 猿码阁
    19 引用 • 14 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...