一直想写一个类似于新闻客户端的APP,但是没有碰到免费的API。不知道什么时候在网上兜兜装转看到了gank.io有开放的API,立马着手准备写一个客户端。
一、客户端设计
主要框架:MVC、RxJava、Retrifit
使用MVC主要是因为整体APP体量较小,没有较多的业务逻辑,没有明显的模块化。网络请求选择Retrifit与RxJava相结合的写法,之前做项目一直都在这样用,不过RxJava没有使用2.0以后的版本。
二、产品设计
整理布局使用的类似DrawerLayout的SlidingRootNav,左侧为RecyclerView列表,显示所有分类,这里SlidingRootNav我稍微改了一下,用户选择分类的时候就把左侧隐藏掉,回到Activity的页面。数据列表是在Fragment中,根据用户选择的分类不同在Activity中加载不同Fragment。列表这里使用的都是RecyclerView,福利部分为瀑布流的形式展示。用户点击item跳转的WebViewActivity,可以复制链接,分享(第一版没有添加功能),收藏。在福利分类中,点击item会到一个图片预览的Fragment中,可以保存图片到相册中。
三、关键部分代码
***********************************网络请求处理************************************
public class InterfaceAPI {
private InterfaceService interfaceService;
public InterfaceAPI() {
interfaceService = new RetrofitClient().getInterfaceService();
}
public Observable<ResponseInfo> getAllData(String type,String pageSize ,String page) {
return interfaceService.getAllInfo(type,pageSize,page).onErrorResumeNext(new Func1<Throwable, Observable<? extends ResponseInfo>>() {
@Override
public Observable<? extends ResponseInfo> call(Throwable throwable) {
return Observable.error(RxHttpHelper.convertIOEError(throwable));
}
}).flatMap(new Func1<ResponseInfo, Observable<ResponseInfo>>() {
@Override
public Observable<ResponseInfo> call(ResponseInfo responseInfo) {
if (responseInfo == null) {
return Observable.error(new RequestErrorThrowable(HttpErrorInfo.CODE_OF_PARSE_REQUEST_FAILURE,
HttpErrorInfo.MSG_OF_PARSE_REQUEST_FAILURE));
}else {
if (responseInfo.isError){
return Observable.error(new RequestErrorThrowable("-1", "获取失败"));
}else {
return Observable.just(responseInfo);
}
}
}
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
}
}
***********************************发送网络请求************************************
public interface InterfaceService {
@GET("{type}/{pageSize}/{page}")
Observable<ResponseInfo> getAllInfo(@Path("type") String type,@Path("pageSize")String pageSize, @Path("page")String page);
}
**************************************首页***************************************
public class SampleActivity extends AppCompatActivity{
private String[] screenTitles;
private SlidingRootNavBuilder builder;
private WelfareFragment welfareFragment;
private AllListFragment allListFragment;
private CollectFragment collectFragment;
private MenuAdapter adapter;
private long exitTime = 0;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setTitleTextColor(ContextCompat.getColor(this,R.color.white));
setSupportActionBar(toolbar);
builder = new SlidingRootNavBuilder(this);
builder .withToolbarMenuToggle(toolbar)
.withMenuOpened(false)
.withSavedState(savedInstanceState)
.withMenuLayout(R.layout.menu_left_drawer)
.inject();
screenTitles = loadTitleString();
adapter = new MenuAdapter(this);
adapter.setOnItemClickListener(new MenuAdapter.ItemClickListener() {
@Override
public void onItemClick(int position) {
adapter.setSelected(position);
show(position);
}
});
RecyclerView list = (RecyclerView) findViewById(R.id.list);
list.setNestedScrollingEnabled(false);
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter);
show(0);
LinearLayout about = (LinearLayout)findViewById(R.id.about_layout);
about.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showDialog();
}
});
}
@Override
public void onBackPressed() {
if ((System.currentTimeMillis() - exitTime) > 2000) {
Snackbar.make(getWindow().getDecorView(),"再按一次退出", BaseTransientBottomBar.LENGTH_SHORT).show();
exitTime = System.currentTimeMillis();
} else {
quit();
}
}
protected void quit() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);
moveTaskToBack(true);
finish();
}
private void showDialog(){
MaterialDialog.Builder builder = new MaterialDialog.Builder(this);
builder.title("About");
builder.negativeText("关闭");
builder.negativeColorRes(R.color.colorAccent);
builder.content(R.string.about);
builder.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
dialog.dismiss();
}
});
builder.build().show();
}
public void show(int position){
adapter.setSelected(position);
Fragment selectedScreen;
if (position == 5){
if (welfareFragment == null){
welfareFragment = new WelfareFragment();
}
selectedScreen = welfareFragment;
}else if (position == 7){
if (collectFragment == null){
collectFragment = new CollectFragment();
}
selectedScreen = collectFragment;
}
else {
if (allListFragment == null) {
allListFragment = new AllListFragment();
}
selectedScreen = allListFragment;
if (allListFragment.isAdded()){
allListFragment.setType(screenTitles[position]);
}else {
Bundle bundle = new Bundle();
bundle.putString("type",screenTitles[position]);
allListFragment.setArguments(bundle);
}
}
if (getSupportActionBar() != null){
if (position == 0){
getSupportActionBar().setTitle(getString(R.string.app_name));
}else {
getSupportActionBar().setTitle(screenTitles[position]);
}
}
builder.hideMenu();
showFragment(selectedScreen);
}
private void showFragment(Fragment fragment) {
getFragmentManager().beginTransaction()
.replace(R.id.container, fragment)
.commit();
}
private String[] loadTitleString() {
return getResources().getStringArray(R.array.title_list);
}
}
**********************************自定义application********************************
//主要处理缓存,两次启动间隔12小时,会把除了“我的收藏”模块的其他缓存数据清除,这里也不包括图片缓存。
public class MyApplication extends Application{
private String [] title;
@Override
public void onCreate() {
super.onCreate();
DataCache.instance.init(MyApplication.this);
Long saveTime = DataCache.instance.getCacheData("fuliang","limitTime");
Long nowTime = System.currentTimeMillis();
if (saveTime == null){
DataCache.instance.saveCacheData("fuliang","limitTime",nowTime);
}else {
title = getResources().getStringArray(R.array.ld_activityScreenTitles);
int i = 0;
if (nowTime - saveTime > 12*60*60*1000){
while (i<title.length-1){
DataCache.instance.clearCacheData("fuliang",title[i]);
i++;
}
}
}
}
}
************************************列表适配器************************************
/**
* Created by lfu on 2017/6/8.
*/
public class AllListFragment extends Fragment implements WaveSwipeRefreshLayout.OnRefreshListener{
private WaveSwipeRefreshLayout swipeLayout;
private RecyclerView recyclerView;
private AllDataAdapter allDataAdapter;
private ArrayList<ResultsList> allDataList;
private String itemType;
private LoadingFragment loadingFragment;
private Animator spruceAnimator;
private int page = 1;
private boolean isLoadMore = false;
private boolean isHaveMore = true;
@Nullable
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.all_fragment_layout,container,false);
recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new MyLayoutManager(getActivity()){
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
spruceAnimator = new Spruce.SpruceBuilder(recyclerView)
.sortWith(new DefaultSort(100))
.animateWith(DefaultAnimations.shrinkAnimator(recyclerView, 800),
ObjectAnimator.ofFloat(recyclerView, "translationX", -recyclerView.getWidth(), 0f).setDuration(800))
.start();
}
};
recyclerView.setLayoutManager(layoutManager);
swipeLayout = (WaveSwipeRefreshLayout)view.findViewById(R.id.swipe_layout);
swipeLayout.setColorSchemeColors(ContextCompat.getColor(getActivity(),R.color.white),ContextCompat.getColor(getActivity(),R.color.white));
swipeLayout.setOnRefreshListener(this);
swipeLayout.setWaveColor(ContextCompat.getColor(getActivity(),R.color.blue));
allDataAdapter = new AllDataAdapter(getActivity());
allDataAdapter.setReloadListener(new AllDataAdapter.ReloadListener() {
@Override
public void onReload() {
page++;
isLoadMore = true;
getDataFromInternet();
allDataAdapter.startReload();
}
});
allDataAdapter.setOnItemClickListener(new AllDataAdapter.ItemClickListener() {
@Override
public void onItemClick(ResultsList model) {
Intent intent = new Intent(getActivity(), WebActivity.class);
intent.putExtra("model",model);
startActivity(intent);
}
});
recyclerView.addOnScrollListener(new EndLessOnScrollListener(layoutManager) {
@Override
public void onLoadMore() {
if (isHaveMore){
page++;
isLoadMore = true;
getDataFromInternet();
}
}
});
itemType = getArguments().getString("type");
setType(itemType);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
private void getDataFromInternet(){
BusinessHelper.getAllData(itemType,"30",String.valueOf(page)).subscribe(new Action1<ResponseInfo>() {
@Override
public void call(ResponseInfo responseInfo) {
if (swipeLayout.isRefreshing()) {
swipeLayout.setRefreshing(false);
}
if (loadingFragment != null){
loadingFragment.removeSelf(getFragmentManager());
}
if (responseInfo.results.size() < 30){
isHaveMore = false;
}
if (page == 1){
DataCache.instance.saveCacheData("fuliang", TypeHelper.getType(itemType),responseInfo.results);
}
allDataList = responseInfo.results;
setViewData(isLoadMore);
if (spruceAnimator != null){
spruceAnimator.start();
}
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
if (loadingFragment != null){
loadingFragment.loadFail();
}
if (isLoadMore){
page--;
isHaveMore = true;
allDataAdapter.loadMoreFail();
}
}
});
}
private void setViewData(boolean isLoadMore){
if (isLoadMore){
allDataAdapter.addData(allDataList);
}else {
allDataAdapter.setData(allDataList);
}
if (recyclerView.getAdapter() == null){
recyclerView.setAdapter(allDataAdapter);
}else {
if (!isLoadMore){
recyclerView.scrollTo(0,0);
}
allDataAdapter.notifyDataSetChanged();
}
}
public void setType(String type){
itemType = type;
isLoadMore = false;
page = 1;
if (recyclerView != null){
recyclerView.removeAllViews();
if (allDataList != null){
allDataList.clear();
allDataAdapter.notifyDataSetChanged();
}
}
if (loadingFragment != null){
loadingFragment.removeSelf(getFragmentManager());
}
allDataList = DataCache.instance.getCacheData("fuliang",TypeHelper.getType(type));
if (allDataList == null){
if (!type.equals("我的收藏")){
showFragment();
getDataFromInternet();
}else {
showFragment();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
loadingFragment.noData();
}
},500);
}
return;
}
if (allDataList.size()>0){
setViewData(isLoadMore);
if (spruceAnimator != null){
spruceAnimator.start();
}
}else {
showFragment();
getDataFromInternet();
}
}
private void showFragment() {
loadingFragment= new LoadingFragment();
loadingFragment.setReloadListener(new LoadingFragment.ReloadData() {
@Override
public void reloadData() {
loadingFragment.reloadData();
page = 1;
getDataFromInternet();
}
});
getFragmentManager().beginTransaction()
.replace(R.id.loading_layout, loadingFragment)
.commitAllowingStateLoss();
}
@Override
public void onRefresh() {
page = 1;
getDataFromInternet();
}
}
四、使用到的第三方类库
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.android.support:recyclerview-v7:25.3.1'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'
compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
compile 'com.squareup.retrofit2:converter-gson:2.0.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'
compile 'com.android.support:cardview-v7:25.3.1'
compile 'com.github.recruit-lifestyle:WaveSwipeRefreshLayout:1.6'
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
compile 'com.afollestad.material-dialogs:commons:0.9.4.5'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.android.support:support-v4:25.3.1'
compile 'com.willowtreeapps.spruce:spruce-android:1.0.1'
compile 'com.oguzdev:CircularFloatingActionMenu:1.0.2'
五、截图
六、项目地址