搜索
您的当前位置:首页正文

[转]如何用一周时间开发一款Android APP并在Googl

来源:二三娱乐

PaperPlane

本次教程分为7天,内容分别为:

第一天,准备

功能需求

可行性分析

其他准备

第二天,UI

选择合适的UI

第三天,整体架构

第四天,首页列表

界面编写

实体类

显示数据

缓存内容

第五天,详情页与其他

界面编写

实体类

显示数据

设置与关于

第六天,高级功能

夜间模式

版本适配

第七天,发布与开源

在Google Play上线

在GitHub开源

思考

好了,废话不多说了。现在就开始吧。

DAY 1

俗话说,万事开头难,准备工作做好了,可以起到事半功倍的作用。磨刀不误砍柴工嘛。

Day 1,功能需求

在开始正式编码之前,咱们还是得先把要实现的功能一一列出来,后面实现起来才有方向嘛。我认为咱们需要实现的功能有:

正确获取消息列表并展示

能够获取历史消息

展示内容详情

后台自动缓存内容详情,方便用户在无网络连接时查看

收藏特定消息

夜间模式

一共6个大的需求,不多,但是我们仔细的研究一下,实际上这6个需求涉及到了网络,UI,数据存储,后台服务等内容。相信对于聪明的你不算困难,现在我们来研究一下可行性。

Day 1,可行性分析

我们来粗略的看一下数据的内容。获取知乎日报2017年1月22日的消息列表:

服务器向我们返回JSON格式的内容:

获取到的内容为:

{

"body": "html格式的内容",

"image_source": "《帕特森》",

"title": "谁说普通人的生活就不能精彩有趣呢?",

"js": [],

"ga_prefix": "012121",

"section": {

"id": 28,

"name": "放映机"

},

"images": [

],

"type": 0,

"id": 9165434,

"css": [

]

}

Day 1,其他准备

工欲善其事,必先利其器。工具准备好总是没错的。

一台电脑 这个怎么说呢,没有这个的话,要进行开发工作还是很难的,咱们总不能用石器写代码吧。

软件:

最好是能有一台Android手机。

科学上网,确保能够正常访问Google和StackOverFlow。让百度去死吧。

好了,第一天的工作差不多就是这么多,熟悉一下API,把工具备好,收拾一下心情,准备明天的工作。

DAY 2

今天主要完成的是UI设计。你可能会问了,这不是设计师的工作么。然而,我在开发纸飞机的过程中,并没有射鸡湿这种生物,UI就我自己完成了。相信大多数的程序员,美术方面应该不是那么地擅长。

当然,有美术和相关基础的同学可以试试用Sketch或者PS把原型图画出来,对于没有美术基础的童鞋,最简单的方法当然就是模仿现成的APP了。当然,你也可以在下列网站寻找合适的设计图:

另外,还有一些小的注意事项:

纸飞机的最终设计效果如下:

PaperPlane

DAY 3

现在开始就要真正的写代码了。

新建Android Studio项目什么的就不说了,下面的是我的项目结构图:

项目结构

·

├── app

|  ├── libs 存放相关的jar文件等

|  ├── src

|  |  ├── androidTest 测试相关目录

|  |  ├── main

|  |  |  ├── assets 存放资源原文件

|  |  |  ├── java

|  |  |  |  ├── com.marktony.zhihudaily java包

|  |  |  |  |  ├── about 关于页面

|  |  |  |  |  ├── adapter RecyclerView与ViewPager等控件的Adapter

|  |  |  |  |  ├── app Application

|  |  |  |  |  ├── bean 存放实体类

|  |  |  |  |  ├── bookmarks 收藏页面

|  |  |  |  |  ├── customtabs Chrome Custom Tabs相关

|  |  |  |  |  ├── db 数据库相关

|  |  |  |  |  ├── detail 详细内容页面

|  |  |  |  |  ├── homepage 首页页面

|  |  |  |  |  ├── innerbrowser 内置浏览器页面

|  |  |  |  |  ├── interfaze 接口集合

|  |  |  |  |  ├── license 开源许可证页面

|  |  |  |  |  ├── search 搜索页面

|  |  |  |  |  ├── service Service集合

|  |  |  |  |  ├── settings 设置页面

|  |  |  |  |  ├── util 工具类集合

|  |  |  |  |  ├── BasePresenter.java Presenter基类

|  |  |  |  |  ├── BaseView.java View基类

|  |  |  ├── res

|  |  |  ├── AndroidManifest.xml 清单文件

(不难看出,我是按照页面和功能进行分包的。)

包建立完成后,我们开始导入第三方的开源库,便于简化代码的编写和实现特定的效果。找到工程目录下app文件夹,打开build.gradle文件,添加如下内容。

dependencies {

compile fileTree(include: ['*.jar'], dir: 'libs')

// 使用volley简化网络请求

compile files('libs/library-1.0.19.jar')

// appcompat兼容包

compile 'com.android.support:appcompat-v7:25.1.0'

// material design 设计包

compile 'com.android.support:design:25.1.0'

// recycler view控件

compile 'com.android.support:recyclerview-v7:25.1.0'

// preference screen 设置和关于页面的配置

compile 'com.android.support:preference-v14:25.1.0'

// 支持Chrome Custom Tabs

compile 'com.android.support:customtabs:25.1.0'

// card view 控件

compile 'com.android.support:cardview-v7:25.1.0'

// 解析JSON数据

compile 'com.google.code.gson:gson:2.7'

// 图片加载

compile 'com.github.bumptech.glide:glide:3.7.0'

// 为了保持在低版本SDK中的UI一致性,引入material data time picker库

compile 'com.wdullaer:materialdatetimepicker:2.5.0'

testCompile 'junit:junit:4.12'

由于一些历史遗留问题,我并没有使用OkHttp作为网络请求包,而是选择了volley。如果你有一定的基础,可以选择使用OkHttp。

导入volley有两种方式:

当然也可以通过gradle引入。

compile 'com.android.volley:volley:1.0.0'

然后点击Sync Project with Gradle files。

首先创建最基本的BaseView和BasePresenter,他们分别是所有View和Presenter的基类。

publicinterfaceBaseView{// 为View设置PresentervoidsetPresenter(T presenter);// 初始化界面控件voidinitViews(View view); }

publicinterfaceBasePresenter{// 获取数据并改变界面显示,在todo-mvp的项目中的调用时机为Fragment的OnResume()方法中voidstart(); }

然后创建一个契约类,用于同一管理View和Presenter。这里以知乎日报的部分为例(如果没有特别说明,后面的代码均以知乎日报的部分为例,果壳精选与豆瓣一刻的代码类似,详细代码可以在GitHub的repo中找到)。

publicinterfaceZhihuDailyContract{interfaceViewextendsBaseView{// 显示加载或其他类型的错误voidshowError();// 显示正在加载voidshowLoading();// 停止显示正在加载voidstopLoading();// 成功获取到数据后,在界面中显示voidshowResults(ArrayList list);// 显示用于加载指定日期的date picker dialogvoidshowPickDialog();    }interfacePresenterextendsBasePresenter{// 请求数据voidloadPosts(longdate,booleanclearing);// 刷新数据voidrefresh();// 加载更多文章voidloadMore(longdate);// 显示详情voidstartReading(intposition);// 随便看看voidfeelLucky();    } }

在上面已经分好的子包中,创建相应的子类View和Presenter。

```java

public class ZhihuDailyFragment extends Fragment

implements ZhihuDailyContract.View {

public ZhihuDailyFragment() {}

public static ZhihuDailyFragment newInstance() {

return new ZhihuDailyFragment();

}

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

}

@Nullable

@Override

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

return null;

}

@Override

public void setPresenter(ZhihuDailyContract.Presenter presenter) {

}

@Override

public void initViews(View view) {

}

@Override

public void showError() {

}

@Override

public void showLoading() {

}

@Override

public void stopLoading() {

}

@Override

public void showResults(ArrayList list) {

}

@Override

public void showPickDialog() {

}

}

```

```java

public class ZhihuDailyPresenter implements ZhihuDailyContract.Presenter {

public ZhihuDailyPresenter(Context context, ZhihuDailyContract.View view) {

}

@Override

public void loadPosts(long date, final boolean clearing) {

}

@Override

public void refresh() {

}

@Override

public void loadMore(long date) {

}

@Override

public void startReading(int position) {

}

@Override

public void feelLucky() {

}

@Override

public void start() {

}

}

```

然后完成果壳精选页面,豆瓣一刻的内容,就可以进行下面的工作了。

创建VolleySingleton,即Volley的单例。这样,整个应用就可以只维护一个请求队列,加入新的网络请求也会更加的方便。

publicclassVolleySingleton{privatestaticVolleySingleton volleySingleton;privateRequestQueue requestQueue;privateVolleySingleton(Context context){        requestQueue = Volley.newRequestQueue(context.getApplicationContext());    }publicstaticsynchronizedVolleySingletongetVolleySingleton(Context context){if(volleySingleton ==null){            volleySingleton =newVolleySingleton(context);        }returnvolleySingleton;    }publicRequestQueuegetRequestQueue(){returnthis.requestQueue;    }publicvoidaddToRequestQueue(Request req){        getRequestQueue().add(req);    } }

然后是Model层的实现。使用了Gson之后,对JSON的转换更加方便了,所以,我们只需要返回类型为String即可。

publicinterfaceOnStringListener{/**      * 请求成功时回调      *@paramresult      */voidonSuccess(String result);/**      * 请求失败时回调      *@paramerror      */voidonError(VolleyError error); }

定义了两个方法,分别为请求成功时和请求失败时的回调。

然后定义一个StringModel的实现类–StringModelImpl。

publicclassStringModelImpl{privateContext context;publicStringModelImpl(Context context){this.context = context;    }publicvoidload(String url,finalOnStringListener listener){        StringRequest request =newStringRequest(url,newResponse.Listener() {@OverridepublicvoidonResponse(String s){                listener.onSuccess(s);            }        },newResponse.ErrorListener() {@OverridepublicvoidonErrorResponse(VolleyError volleyError){                listener.onError(volleyError);            }        });        VolleySingleton.getVolleySingleton(context).addToRequestQueue(request);    } }

到这里,基本的架构就搭建完成了。现在可以喝杯咖啡,然后完成今天的最后一点工作,为后面的工作做准备。

创建Api.java文件,用于存储app所用到的所有API。

创建NetworkState.java文件,判断当前的网络状态,是否有网络连接,WiFi或者是移动数据。

publicclassNetworkState{// 检查是否连接到网络publicstaticbooleannetworkConnected(Context context){if(context !=null){            ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);            NetworkInfo info = manager.getActiveNetworkInfo();if(info !=null)returninfo.isAvailable();        }returnfalse;    }// 检查WiFi是否连接publicstaticbooleanwifiConnected(Context context){if(context !=null){            ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);            NetworkInfo info = manager.getActiveNetworkInfo();if(info !=null){if(info.getType() == ConnectivityManager.TYPE_WIFI)returninfo.isAvailable();            }        }returnfalse;    }// 检查移动网络是否连接publicstaticbooleanmobileDataConnected(Context context){if(context !=null){            ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);            NetworkInfo info = manager.getActiveNetworkInfo();if(info !=null){if(info.getType() == ConnectivityManager.TYPE_MOBILE)returntrue;            }        }returnfalse;    }}

创建DateFormatter .java文件,方便将long类型的日期转换为String类型。

publicclassDateFormatter{/**    * 将long类date转换为String类型    *@paramdate date    *@returnString date    */publicStringZhihuDailyDateFormat(longdate){        String sDate;        Date d =newDate(date +24*60*60*1000);        SimpleDateFormat format =newSimpleDateFormat("yyyyMMdd");        sDate = format.format(d);returnsDate;    }publicStringDoubanDateFormat(longdate){        String sDate;        Date d =newDate(date);        SimpleDateFormat format =newSimpleDateFormat("yyyy-MM-dd");        sDate = format.format(d);returnsDate;    }}

OK,day 3工作完成。

Day 4

今天的只要任务是完成首页。

Day 4,界面编写

我们的首页,使用的是Activity + Fragment搭配的方式,即一个MainActivity + MainFragment + BookmarksFragment的方式。其中,MainActivity的布局文件中包含了DrawerLayout, Toolbar以及Fragment所在的容器。

MainActivity对应布局文件如下:

android:id="@+id/drawer_layout"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:fitsSystemWindows="true"

tools:openDrawer="start">

android:id="@+id/nav_view"

android:layout_width="wrap_content"

android:layout_height="match_parent"

android:layout_gravity="start"

android:fitsSystemWindows="true"

app:headerLayout="@layout/nav_header_main"

app:menu="@menu/activity_main_drawer" />

android:layout_width="match_parent"

android:layout_height="@dimen/nav_header_height"

android:background="@drawable/nav_header"

android:gravity="bottom"

android:orientation="vertical"

android:theme="@style/ThemeOverlay.AppCompat.Dark">

nav_header实际上就只是一个简单的ImageView。

android:layout_width="match_parent"

android:layout_height="match_parent"

android:fitsSystemWindows="true"

tools:context=".homepage.MainActivity">

android:layout_width="match_parent"

android:layout_height="wrap_content"

app:elevation="0dp"

android:theme="@style/AppTheme.AppBarOverlay">

android:id="@+id/toolbar"

android:layout_width="match_parent"

android:layout_height="?attr/actionBarSize"

android:background="@color/colorPrimary"

app:popupTheme="@style/AppTheme.PopupOverlay" />

android:layout_width="match_parent"

android:layout_height="match_parent"

android:id="@+id/layout_fragment"

android:layout_marginTop="?actionBarSize"/>

OK,Activity的布局文件完成。然后就可以写java代码了。

从代码中可以看出,MainActivity负责处理DrawerLayout的点击事件,即控制显示或者隐藏特定的Fragment。而Fragment的状态的保存与恢复也是在这里进行的。

publicclassMainFragmentextendsFragment{privateContext context;privateMainPagerAdapter adapter;privateTabLayout tabLayout;privateZhihuDailyFragment zhihuDailyFragment;privateGuokrFragment guokrFragment;privateDoubanMomentFragment doubanMomentFragment;privateZhihuDailyPresenter zhihuDailyPresenter;privateGuokrPresenter guokrPresenter;privateDoubanMomentPresenter doubanMomentPresenter;publicMainFragment(){}publicstaticMainFragmentnewInstance(){returnnewMainFragment();    }@OverridepublicvoidonAttach(Context context){super.onAttach(context);    }@OverridepublicvoidonCreate(@Nullable Bundle savedInstanceState){super.onCreate(savedInstanceState);this.context = getActivity();// Fragment状态恢复if(savedInstanceState !=null) {            FragmentManager manager = getChildFragmentManager();            zhihuDailyFragment = (ZhihuDailyFragment) manager.getFragment(savedInstanceState,"zhihu");            guokrFragment = (GuokrFragment) manager.getFragment(savedInstanceState,"guokr");            doubanMomentFragment = (DoubanMomentFragment) manager.getFragment(savedInstanceState,"douban");        }else{// 创建View实例zhihuDailyFragment = ZhihuDailyFragment.newInstance();            guokrFragment = GuokrFragment.newInstance();            doubanMomentFragment = DoubanMomentFragment.newInstance();        }// 创建Presenter实例zhihuDailyPresenter =newZhihuDailyPresenter(context, zhihuDailyFragment);        guokrPresenter =newGuokrPresenter(context, guokrFragment);        doubanMomentPresenter =newDoubanMomentPresenter(context, doubanMomentFragment);    }@Nullable@OverridepublicViewonCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState){        View view = inflater.inflate(R.layout.fragment_main, container,false);// 初始化控件initViews(view);// 显示菜单setHasOptionsMenu(true);// 当tab layout位置为果壳精选时,隐藏fabtabLayout.addOnTabSelectedListener(newTabLayout.OnTabSelectedListener() {@OverridepublicvoidonTabSelected(TabLayout.Tab tab){                FloatingActionButton fab = (FloatingActionButton) getActivity().findViewById(R.id.fab);if(tab.getPosition() ==1) {                    fab.hide();                }else{                    fab.show();                }            }@OverridepublicvoidonTabUnselected(TabLayout.Tab tab){            }@OverridepublicvoidonTabReselected(TabLayout.Tab tab){            }        });returnview;    }// 初始化控件privatevoidinitViews(View view){        tabLayout = (TabLayout) view.findViewById(R.id.tab_layout);        ViewPager viewPager = (ViewPager) view.findViewById(R.id.view_pager);// 设置离线数为3viewPager.setOffscreenPageLimit(3);        adapter =newMainPagerAdapter(                getChildFragmentManager(),                context,                zhihuDailyFragment,                guokrFragment,                doubanMomentFragment);        viewPager.setAdapter(adapter);        tabLayout.setupWithViewPager(viewPager);    }@OverridepublicvoidonCreateOptionsMenu(Menu menu, MenuInflater inflater){super.onCreateOptionsMenu(menu, inflater);        inflater.inflate(R.menu.main, menu);    }@OverridepublicbooleanonOptionsItemSelected(MenuItem item){intid = item.getItemId();if(id == R.id.action_feel_lucky) {            feelLucky();        }returntrue;    }// 保存状态@OverridepublicvoidonSaveInstanceState(Bundle outState){super.onSaveInstanceState(outState);        FragmentManager manager = getChildFragmentManager();        manager.putFragment(outState,"zhihu", zhihuDailyFragment);        manager.putFragment(outState,"guokr", guokrFragment);        manager.putFragment(outState,"douban", doubanMomentFragment);    }// 随便看看publicvoidfeelLucky(){        Random random =newRandom();inttype = random.nextInt(3);switch(type) {case0:                zhihuDailyPresenter.feelLucky();break;case1:                guokrPresenter.feelLucky();break;default:                doubanMomentPresenter.feelLucky();break;        }    }publicMainPagerAdaptergetAdapter(){returnadapter;    }}

首页的MainFragment主要负责显示与TabLayout + ViewPager相关的内容。

OK,终于把首页的UI框架搭建好了,喝杯咖啡,休息一下,冷静冷静。

现在开始实现具体的ZhihuDailyFragment的布局。仔细观察,实际上,ZhihuDailyFragment所包含的控件就只有一个RecyclerView,将获取到的内容以列表的形式显示出来。并且,不难发现,果壳精选与豆瓣一刻的布局与知乎日报的列表布局相同,可以复用。

android:layout_width="match_parent"

android:layout_height="match_parent"

android:id="@+id/refreshLayout">

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:focusable="true"

android:clickable="true">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:id="@+id/recyclerView"

android:scrollbars="vertical"

android:scrollbarFadeDuration="1"

android:fadeScrollbars="true"/>

布局实际上还包含了SwipeRefreshLayout,用于显示正在加载和手动刷新。

列表子项的布局有很多种,分别是:

普通仅文字

普通文字 + 图片

头部项,用于显示子项类型(如知乎日报,在收藏页面会用到)

底部项,加载更多等

android:layout_height="96dp"

android:layout_width="match_parent"

android:focusable="true"

android:clickable="true"

android:foreground="?android:attr/selectableItemBackground"

app:cardCornerRadius="4dp"

app:cardElevation="1dp"

app:cardPreventCornerOverlap="true"

android:layout_marginTop="8dp"

android:layout_marginLeft="8dp"

android:layout_marginRight="8dp">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:id="@+id/textViewTitle"

android:paddingTop="8dp"

android:paddingBottom="8dp"

android:paddingLeft="8dp"

android:paddingRight="8dp"

android:gravity="center_vertical"

android:maxLines="3"

android:ellipsize="end"

android:textSize="18sp" />

android:layout_height="96dp"

android:layout_width="match_parent"

android:focusable="true"

android:clickable="true"

android:foreground="?android:attr/selectableItemBackground"

app:cardCornerRadius="4dp"

app:cardElevation="1dp"

app:cardPreventCornerOverlap="true"

android:layout_marginTop="8dp"

android:layout_marginLeft="8dp"

android:layout_marginRight="8dp">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="horizontal"

android:paddingLeft="8dp"

android:paddingRight="8dp" >

android:layout_width="0dp"

android:layout_height="match_parent"

android:layout_weight="1"

android:id="@+id/textViewTitle"

android:paddingTop="8dp"

android:paddingBottom="8dp"

android:layout_marginRight="8dp"

android:layout_marginEnd="8dp"

android:gravity="center_vertical"

android:maxLines="3"

android:ellipsize="end"

android:textSize="18sp" />

android:layout_width="80dp"

android:layout_height="80dp"

android:id="@+id/imageViewCover"

android:layout_gravity="center_vertical" />

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:id="@+id/textViewType"

android:paddingLeft="8dp"

android:paddingStart="8dp"

android:paddingRight="8dp"

android:paddingEnd="8dp"

android:paddingTop="8dp"

android:gravity="center_vertical"

android:textColor="@color/colorPrimary"

android:textAllCaps="true"/>

android:orientation="horizontal"

android:layout_width="match_parent"

android:layout_height="48dp"

android:layout_marginTop="8dp"

android:layout_marginBottom="8dp"

android:gravity="center_horizontal"

android:background="@color/viewBackground">

android:id="@+id/address_looking_up"

style="?android:attr/progressBarStyleInverse"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_gravity="center_horizontal"

android:visibility="visible" />

android:layout_width="wrap_content"

android:layout_height="match_parent"

android:text="@string/loading_more"

android:layout_marginLeft="16dp"

android:layout_marginStart="8dp"

android:gravity="center_vertical"/>

布局文件到这里基本就完成了。

Day 4,实体类

Json格式数据:

publicclassZhihuDailyNews{privateString date;privateArrayList stories;publicStringgetDate(){returndate;    }publicvoidsetDate(String date){this.date = date;    }publicArrayListgetStories(){returnstories;    }publicvoidsetStories(ArrayList stories){this.stories = stories;    }publicclassQuestion{privateArrayList images;privateinttype;privateintid;privateString ga_prefix;privateString title;publicArrayListgetImages(){returnimages;        }publicvoidsetImages(ArrayList images){this.images = images;        }publicintgetType(){returntype;        }publicvoidsetType(inttype){this.type = type;        }publicintgetId(){returnid;        }publicvoidsetId(intid){this.id = id;        }publicStringgetGa_prefix(){returnga_prefix;        }publicvoidsetGa_prefix(String ga_prefix){this.ga_prefix = ga_prefix;        }publicStringgetTitle(){returntitle;        }publicvoidsetTitle(String title){this.title = title;        }    }}

Day 4,显示数据

首先,我们得有一个adapter。

publicclassZhihuDailyNewsAdapterextendsRecyclerView.Adapter{privatefinalContext context;privatefinalLayoutInflater inflater;privateList list =newArrayList();privateOnRecyclerViewOnClickListener mListener;// 文字 + 图片privatestaticfinalintTYPE_NORMAL =0;// footer,加载更多privatestaticfinalintTYPE_FOOTER =1;publicZhihuDailyNewsAdapter(Context context, List list){this.context = context;this.list = list;this.inflater = LayoutInflater.from(context);    }@OverridepublicRecyclerView.ViewHolderonCreateViewHolder(ViewGroup parent,intviewType){// 根据ViewType加载不同布局switch(viewType) {caseTYPE_NORMAL:returnnewNormalViewHolder(inflater.inflate(R.layout.home_list_item_layout, parent,false), mListener);caseTYPE_FOOTER:returnnewFooterViewHolder(inflater.inflate(R.layout.list_footer, parent,false));        }returnnull;    }@OverridepublicvoidonBindViewHolder(RecyclerView.ViewHolder holder,intposition){// 对不同的ViewHolder做不同的处理if(holderinstanceofNormalViewHolder) {            ZhihuDailyNews.Question item = list.get(position);if(item.getImages().get(0) ==null){                ((NormalViewHolder)holder).itemImg.setImageResource(R.drawable.placeholder);            }else{                Glide.with(context)                        .load(item.getImages().get(0))                        .asBitmap()                        .placeholder(R.drawable.placeholder)                        .diskCacheStrategy(DiskCacheStrategy.SOURCE)                        .error(R.drawable.placeholder)                        .centerCrop()                        .into(((NormalViewHolder)holder).itemImg);            }            ((NormalViewHolder)holder).tvLatestNewsTitle.setText(item.getTitle());        }    }// 因为含有footer,返回值需要 + 1@OverridepublicintgetItemCount(){returnlist.size() +1;    }@OverridepublicintgetItemViewType(intposition){if(position == list.size()) {returnZhihuDailyNewsAdapter.TYPE_FOOTER;        }returnZhihuDailyNewsAdapter.TYPE_NORMAL;    }publicvoidsetItemClickListener(OnRecyclerViewOnClickListener listener){this.mListener = listener;    }publicclassNormalViewHolderextendsRecyclerView.ViewHolderimplementsView.OnClickListener{privateImageView itemImg;privateTextView tvLatestNewsTitle;privateOnRecyclerViewOnClickListener listener;publicNormalViewHolder(View itemView, OnRecyclerViewOnClickListener listener){super(itemView);            itemImg = (ImageView) itemView.findViewById(R.id.imageViewCover);            tvLatestNewsTitle = (TextView) itemView.findViewById(R.id.textViewTitle);this.listener = listener;            itemView.setOnClickListener(this);        }@OverridepublicvoidonClick(View v){if(listener !=null){                listener.OnItemClick(v,getLayoutPosition());            }        }    }publicclassFooterViewHolderextendsRecyclerView.ViewHolder{publicFooterViewHolder(View itemView){super(itemView);        }    }}

adapter中含有两个常量,TYPE_NORMAL,TYPE_FOOTER,用于区别item的类型,从而加载不同的布局。众所周知,RecyclerView原生并没有设置item点击事件的方法,所有我们需要自己定义一个接口--OnRecyclerViewOnClickListener。

packagecom.marktony.zhihudaily.interfaze;importandroid.view.View;publicinterfaceOnRecyclerViewOnClickListener{voidOnItemClick(View v,intposition);}

实现ZhihuDailyPresenter中的loadPosts方法,记得要在manifest清单文件中添加网络访问权限:

model.load(Api.ZHIHU_HISTORY + formatter.ZhihuDailyDateFormat(date),newOnStringListener() {@OverridepublicvoidonSuccess(String result){try{                        ZhihuDailyNews post = gson.fromJson(result, ZhihuDailyNews.class);if(clearing) {                            list.clear();                        }for(ZhihuDailyNews.Question item : post.getStories()) {                            list.add(item);                                                  }                        view.showResults(list);                    }catch(JsonSyntaxException e) {                        view.showError();                    }                    view.stopLoading();                }@OverridepublicvoidonError(VolleyError error){                    view.stopLoading();                    view.showError();                }            });

我们通过Gson,可以很简单将JSON格式数据转换为Java对象。

实现ZhihuDailyFragment的showResults方法。

@OverridepublicvoidshowResults(ArrayList list){if(adapter ==null) {        adapter =newZhihuDailyNewsAdapter(getContext(), list);        adapter.setItemClickListener(newOnRecyclerViewOnClickListener() {@OverridepublicvoidOnItemClick(View v,intposition){                presenter.startReading(position);            }        });        recyclerView.setAdapter(adapter);    }else{        adapter.notifyDataSetChanged();    }}

Day 4,缓存内容

首先当然是要建立数据库了(由于纸飞机已经进行多个版本的迭代,所以你创建数据库的SQL语句或其他内容和我的文件应该不完全相同)。

publicclassDatabaseHelperextendsSQLiteOpenHelper{publicDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,intversion){super(context, name, factory, version);    }@OverridepublicvoidonCreate(SQLiteDatabase db){        db.execSQL("create table if not exists Zhihu("+"id integer primary key autoincrement,"+"zhihu_id integer not null,"+"zhihu_news text,"+"zhihu_time real,"+"zhihu_content text)");        db.execSQL("alter table Zhihu add column bookmark integer default 0");    }@OverridepublicvoidonUpgrade(SQLiteDatabase db,intoldVersion,intnewVersion){  }}

相信大牛应该看出来了,这数据库设计的真心不怎么样😂,因为我数据库学的确实很一般。求大牛不喷。

字段类型含义备注

idinteger主键自增长

zhihu_idinteger知乎日报消息id由知乎提供

zhihu_newstext知乎日报消息内容与Java实体类对应

zhihu_timereal知乎日报消息发布的时间由知乎提供

zhihu_contenttext知乎日报消息详细内容与Java实体类对应

bookmarkinteger是否被收藏由于SQLite并没有boolean类型,使用integer的不同值代替

OK,当我们正确请求到数据后,就可以进行存储了。

if( !queryIfIDExists(item.getId())) {    db.beginTransaction();try{        DateFormat format =newSimpleDateFormat("yyyyMMdd");        Date date = format.parse(post.getDate());        values.put("zhihu_id", item.getId());        values.put("zhihu_news", gson.toJson(item));        values.put("zhihu_content","");        values.put("zhihu_time", date.getTime() /1000);        db.insert("Zhihu",null, values);        values.clear();        db.setTransactionSuccessful();    }catch(Exception e) {        e.printStackTrace();    }finally{        db.endTransaction();    }}// 查询数据库表中是否已经存在了此idprivatebooleanqueryIfIDExists(intid){    Cursor cursor = db.query("Zhihu",null,null,null,null,null,null);if(cursor.moveToFirst()){do{if(id == cursor.getInt(cursor.getColumnIndex("zhihu_id"))){returntrue;            }        }while(cursor.moveToNext());    }    cursor.close();returnfalse;}

细心的童鞋可能发现了,诶,数据表中还有一个字段--zhihu_content,你没有存储呀。这是因为我们在请求知乎消息列表的时候,并没有返回消息的详细内容呀。不过详细内容我们还是需要缓存的,网络请求在UI线程上进行可能会引起ANR,那更好的解决办法就是在Service里面完成了。

我们先将一些必须的数据通过本地广播的形式,发送出去。

Intent intent =newIntent("com.marktony.zhihudaily.LOCAL_BROADCAST");intent.putExtra("type", CacheService.TYPE_ZHIHU);intent.putExtra("id", item.getId());LocalBroadcastManager.getInstance(context).sendBroadcast(intent);

然后在CacheService里接收广播,获取传送的数据,然后进行网络请求和数据存储。

publicclassCacheServiceextendsService{privateDatabaseHelper dbHelper;privateSQLiteDatabase db;privatestaticfinalString TAG = CacheService.class.getSimpleName();publicstaticfinalintTYPE_ZHIHU =0x00;publicstaticfinalintTYPE_GUOKR =0x01;publicstaticfinalintTYPE_DOUBAN =0x02;@OverridepublicvoidonCreate(){super.onCreate();        dbHelper =newDatabaseHelper(this,"History.db",null,5);        db = dbHelper.getWritableDatabase();        IntentFilter filter =newIntentFilter();        filter.addAction("com.marktony.zhihudaily.LOCAL_BROADCAST");        LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);        manager.registerReceiver(newLocalReceiver(), filter);    }@Nullable@OverridepublicIBinderonBind(Intent intent){returnnull;    }@OverridepublicintonStartCommand(Intent intent,intflags,intstartId){returnsuper.onStartCommand(intent, flags, startId);    }@OverridepublicbooleanonUnbind(Intent intent){returnsuper.onUnbind(intent);    }/**    * 网络请求id对应的知乎日报的内容主体    * 当type为0时,存储body中的数据    * 当type为1时,再次请求share url中的内容并储存    *@paramid 所要获取的知乎日报消息内容对应的id    */privatevoidstartZhihuCache(finalintid){        Cursor cursor = db.query("Zhihu",null,null,null,null,null,null);if(cursor.moveToFirst()) {do{if((cursor.getInt(cursor.getColumnIndex("zhihu_id")) == id)                        && (cursor.getString(cursor.getColumnIndex("zhihu_content")).equals(""))) {                    StringRequest request =newStringRequest(Request.Method.GET, Api.ZHIHU_NEWS + id,newResponse.Listener() {@OverridepublicvoidonResponse(String s){                            Gson gson =newGson();                            ZhihuDailyStory story = gson.fromJson(s, ZhihuDailyStory.class);if(story.getType() ==1) {                                StringRequest request =newStringRequest(Request.Method.GET, story.getShare_url(),newResponse.Listener() {@OverridepublicvoidonResponse(String s){                                        ContentValues values =newContentValues();                                        values.put("zhihu_content", s);                                        db.update("Zhihu", values,"zhihu_id = ?",newString[] {String.valueOf(id)});                                        values.clear();                                    }                                },newResponse.ErrorListener() {@OverridepublicvoidonErrorResponse(VolleyError volleyError){                                    }                                });                                request.setTag(TAG);                                VolleySingleton.getVolleySingleton(CacheService.this).addToRequestQueue(request);                            }else{                                ContentValues values =newContentValues();                                values.put("zhihu_content", s);                                db.update("Zhihu", values,"zhihu_id = ?",newString[] {String.valueOf(id)});                                values.clear();                            }                        }                    },newResponse.ErrorListener() {@OverridepublicvoidonErrorResponse(VolleyError volleyError){                        }                    });                    request.setTag(TAG);                    VolleySingleton.getVolleySingleton(CacheService.this).addToRequestQueue(request);                }            }while(cursor.moveToNext());        }        cursor.close();    }@OverridepublicvoidonDestroy(){super.onDestroy();        VolleySingleton.getVolleySingleton(this).getRequestQueue().cancelAll(TAG);    }classLocalReceiverextendsBroadcastReceiver{@OverridepublicvoidonReceive(Context context, Intent intent){intid = intent.getIntExtra("id",0);switch(intent.getIntExtra("type", -1)) {caseTYPE_ZHIHU:                    startZhihuCache(id);break;caseTYPE_GUOKR:                    startGuokrCache(id);break;caseTYPE_DOUBAN:                    startDoubanCache(id);break;default:case-1:break;            }        }    }}

我们先遍历一下数据库,如果数据库中指定id的消息详情内容已经不为空,那我们就直接跳过了,可以节省用户的流量以及电量。

到这里,数据的存储是完成了。可是怎么读取出来呢?哈,其实也简单,我们判断一下当前的网络状态,如果用户设备没有连接到网路,我们就直接去数据库中读取,然后解析就行了。

到这里,今天的工作差不多已经完成了,等等,是不是忘了什么?我们的Service并没有启动呀。

@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    initViews();// 启动服务startService(newIntent(this, CacheService.class));}@OverrideprotectedvoidonDestroy(){    ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);for(ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {if(CacheService.class.getName().equals(service.service.getClassName())) {            stopService(newIntent(this, CacheService.class));        }    }super.onDestroy();}

到这里,今天的内容就算结束了,内容是一周之中最多的一天,可能比前几天的总和还要多,可能需要你加班才能完全完成,之前Activity, Presenter, Fragment中各还有一部分内容没有完成,需要你自行补充完成。不过,看到自己的App正确的跑了起来,有木有很兴奋呢?休息休息,准备明天的工作吧。

作者:TonnyL

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Top