最近做的比分更新功能,算是到一个完善的稳定版本了,将从以下部分由整体到具体细节一步一步实现更新比分功能,将了解到,Android的轮询实现,在长列表中如何优雅的更新局部数据,以及更新动画

  • 主体思想
  • Android服务端
  • Android客户端
  • 客服端更新数据
  • 重新进入应用

主体思想
Android端定时请求list_code.htm,获取当前服务器数据更新状态标志,如果本次请求得到的code标志与上一次请求的标志不一致,则请求list.htm,实时需要更新的数据列表。
如下图
主体流程

Android中的服务端

服务端需要实现功能

  1. 获得需要的参数
  2. 启动定时请求
  3. 将更新的数据传递到Activity
  4. 停止定时请求

服务端需要两个参数

  1. 更新时间间隔
  2. 需要更新的赛事类别

将其封装出一个CategoryServiceNeed

客户端每次bindService都需要将所需要的CategoryServiceNeed封装好,put到Inent中

准备客户端需要的Binder

ICategoryUpdate.aidl负责获得客户端实现的IDataCallback.aidl接口,启动更新Timer

1
2
3
4
5
6
7
8
9
10
11
12
13
// ICategoryUpdate.aidl
package com.tango.zhibodi;

// Declare any non-default types here with import statements
import com.tango.zhibodi.IDataCallback;
import com.tango.zhibodi.datasource.myentity.CategoryServiceNeed;
interface ICategoryUpdate {
void start(); // 设置定时器
void init(CategoryServiceNeed need); 重新设置更新参数
void setCallback(IDataCallback callback); // 设置定时器
void stop();
void clearCallback(IDataCallback callback);
}

onServiceConnected()的是时候,将以下Binder提供给客服端调用,类似RPC调用,而在客户端实现的IDataCallback接口提供给服务端调用,从而服务端到客户端数据能双向更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private ICategoryUpdate.Stub mBinder = new ICategoryUpdate.Stub() {
@Override
public void init(CategoryServiceNeed need) throws RemoteException {
mNeed = need;
if (mNeed != null &&mNeed.getRefreshTime() <= 0){
mNeed.setRefreshTime(5000);
}
}

@Override
public void start() throws RemoteException {
if (mNeed == null){
return;
}
if (mTimer == null){
mTimer = new Timer();
} else {
mTimer.cancel();
mTimer = null;
mTimer = new Timer();
}
doGetData();
mTimer.schedule(new TimerTask() {
@Override
public void run() {
RetrofitHelper.getCategoryCodeService()
.getUpdateCodeCategory(mNeed.getCateId())
.enqueue(new Callback<CategoryCode>() {
@Override
public void onResponse(Call<CategoryCode> call,
Response<CategoryCode> response) {
if (!mCode.equals(response.body().getCode())){
doGetData();
}
mCode = response.body().getCode();
}

@Override
public void onFailure(Call<CategoryCode> call, Throwable t) {

}
});
}
}, 0, mNeed.getRefreshTime());

}

@Override
public void setCallback(IDataCallback callback) throws RemoteException {
mListenerList.clear();
mListenerList.add(callback);
}

@Override
public void stop() throws RemoteException {
if (mTimer!= null){
mCode = "";
mTimer.cancel();
mTimer = null;
mCallback = null;
}
}

@Override
public void clearCallback(IDataCallback callback) throws RemoteException {
if (mListenerList.contains(callback)){
mListenerList.remove(callback);
}
}
};

更新数据的列表方法,得到正确的数据后,调用iDataCallback.onSuccess()将数据传递Activity层,实现服务层到Activity层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void doGetData() {
if (mNeed!=null) {
RetrofitHelper.getCategoryUpdateService()
.getUpdateListCategory(mNeed.getCateId())
.enqueue(new Callback<CategoryUpdateList>() {
@Override
public void onResponse(Call<CategoryUpdateList> call,
Response<CategoryUpdateList> response) {
mCode = response.body().getCode();
try {
if (mListenerList!=null&&mListenerList.size()>0) {
IDataCallback iDataCallback = mListenerList.get(0);
if (iDataCallback != null){
iDataCallback.onSuccess(response.body());
}
} else {
DebugLogger.debug("没有是实现该接口");
}
} catch (RemoteException e) {
e.printStackTrace();
}
}

@Override
public void onFailure(Call<CategoryUpdateList> call, Throwable t) {
if (mListenerList!=null&&mListenerList.size()>0) {
IDataCallback iDataCallback = mListenerList.get(0);
if (iDataCallback != null){
iDataCallback.onFailed("请求失败");
}
} else {
DebugLogger.debug("没有是实现该接口");
}
}
});
}
}

Note: 为什么在Serive使用异步,不是使用同步,因为,Serivce也是和Activity都是属于Appliction下的,是Appliction的组件,所以也不能使用做耗时的请求。参见深入理解Android源码第二章

自此我们完成了获取参数,启动更新,数据更新到Acivity层,还差停止定时请求,这个放在Service的onUnbind生命方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 当unbind的时候需要将初始化的变量清除
*/
@Override
public boolean onUnbind(Intent intent) {
mNeed = null;
if (mTimer!= null){
mCode = "";
mTimer.cancel();
mTimer = null;
mCallback = null;
}
return super.onUnbind(intent);
}

Android中的客户端
客户端实现的功能就比较简单了,需要

  1. 绑定服务
  2. 实现IDataCallback
  3. 取消绑定服务

在客户端中主要是Fragment绑定服务,和解除绑定,这两个动作需要与Fragment的生命周期一致并且得配对好,所以我就选择在onResume()绑定服务和onPause()中解除绑定。进入下一个Acitvity还是退出Activity必调用onPasue(),而从Activity回来也必调onResume()

绑定服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

@Override
public void onResume() {
super.onResume();
HomeCate homeCateByIndex =
mAdapter.getHomeCateByIndex(viewPager.getCurrentItem());
if (homeCateByIndex != null) {
bindToService(homeCateByIndex);
}
}
private void bindToService(HomeCate homeCate) {
Intent intent = new Intent(getActivity(), CategoryUpdateService.class);
CategoryServiceNeed need = new CategoryServiceNeed();
need.setRefreshTime(homeCate.getRefreshtime() * 1000);
need.setCateId(homeCate.getCateid());
intent.putExtra(CATEGORY_NEED, need);
getActivity().bindService(intent, mConnection, Activity.BIND_AUTO_CREATE);
}

实现IDataCallback接口,在onSuccess()方法中只需要数据包裹在Message中,然后交给Handler把数据添加到MessageQuene中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static IDataCallback.Stub mCallback = new IDataCallback.Stub() {
@Override
public void onSuccess(CategoryUpdateList list) throws RemoteException {
Message message = Message.obtain();
message.what = 0x1000;
Bundle bundle = new Bundle();
bundle.putParcelable("Update", list);
message.setData(bundle);
mUpdateUI.sendMessage(message);
}

@Override
public void onFailed(String msg) throws RemoteException {

}
};

将mConnection传到bindService方法中,一旦建立连接后,设置回调接口,启动更新(之前,不晓得RPC这个概念,这就相当于在客户端调用了服务端的方法,很给力)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 连接
*/
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mCategoryUpdate = ICategoryUpdate.Stub.asInterface(service);
try {
mCategoryUpdate.setCallback(mCallback);
mCategoryUpdate.start();
} catch (RemoteException e) {
e.printStackTrace();
}
}

@Override
public void onServiceDisconnected(ComponentName name) {
try {
mCategoryUpdate.clearCallback(mCallback);
} catch (RemoteException e) {
e.printStackTrace();
}
}
};

解除绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onPause() {
try {
if (mCategoryUpdate != null) {
if (mCategoryUpdate.asBinder().isBinderAlive()) {
getActivity().unbindService(mConnection);
}
}

} catch (Exception e) {
e.printStackTrace();
}
super.onPause();
}

Note: 为啥要加try catch捕获crash异常,及时是配对的bind和unbind,但是极少数情况下,就给你来个给个服务之前已经取消绑定了,不要再给我取消绑定,再给取消绑定,我崩给你看。

数据更新UI部分

终于到高潮了,马上就能看到UI更新的效果。好激动,还是先冷静一下,看看有哪几类客户端

  1. ViewPager Container类Fragment.
  2. 头部比分类Fragment

ViewPager Container类Fragment

我们这类app都是采用TabLayout+ViewPager来实现,所以我就选在TabLayout+ViewPager的Fragment作为客户端,而不是选择ViewPager中具体的Fragment来作为客户端
这样做的好处

  1. 避免了频繁的绑定,解除绑定
  2. 统一管理更新

这里里面涉及到,怎么调用ViewPager下的Fragment的更新方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static class UiUpdateHandler extends Handler {
SoftReference<HomeFragment> mFragmentReference;
UiUpdateHandler(SoftReference<HomeFragment> fragmentReference) {
mFragmentReference = fragmentReference;
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 0x1000) {
HomeFragment fragment = mFragmentReference.get();
CategoryUpdateList list = msg.getData().getParcelable("Update");
if (fragment != null) {
if (list != null && list.getList() != null &&
list.getList().size() > 0) {
Fragment item = fragment.adapter.getCurrentFragment();
if (item instanceof CateHomeHotFragment) {
((CateHomeHotFragment) item).update(list);
}
if (item instanceof CateHomeFragment) {
((CateHomeFragment) item).update(list);
}

}
}
}
}
}

我值ViewPager的Adapter中添加了getCurrentFragment()获取当前页的Fragment, 这个方法会被具体的Fragment向上转型成通用的Fragment,之后调用updata()方法更新,当然也可把CateHomeHotFragment和CateHomeHotFragment重构成一个抽象类。

1
2
3
4
5
6
7
8
9
10
11
 public Fragment getCurrentFragment() {
return mCurrentFragment;
}
@Override
public void setPrimaryItem(ViewGroup container,
int position, Object object) {
if (getCurrentFragment() != object) {
mCurrentFragment = ((Fragment) object);
}
super.setPrimaryItem(container, position, object);
}

Fragment调用的update方法,mGameRVAdapter.updateVisibleItem(mLiveIndex, first, last, list);
mLiveIndex记录第一个在直播中的位置,之后如果下拉加载之前的数据,需要mLiveIndex += 封装后的List的长度。
first列表中当前第一个可见的item的位置,last列表中当前最后一个可见的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 /**
* 刷新列表
* @param list
*/
public void update(CategoryUpdateList list) {
if (mGameRVAdapter!=null){
if (mRecyclerView.getScrollState() ==
RecyclerView.SCROLL_STATE_IDLE) {
int first =
linearLayoutManager.findFirstVisibleItemPosition();
int last =
linearLayoutManager.findLastVisibleItemPosition();
mGameRVAdapter.updateVisibleItem(mLiveIndex, first, last, list);
}
}
}

而在Adapter中更新的需要从mLiveIndex向下找的第一个是日期类型的item,通过id匹配,最后更新数据,但是notifyItemChanged的只有[first,last]中匹配的id
Note:直接调notifyItemChanged(postion)会使图片闪烁,所以需要用notifyItemChanged(postion, Object)和以下方法配合使用。

1
2
3
4
5
6
7
8
9
@Override
public void onBindViewHolder(final BaseViewHolder holder,
final int position, List playList) {
if (playList.isEmpty()){
onBindViewHolder(holder, position);
} else {
holder.bindingData(mList.get(position), context, isNoScore);
}
}

至此ViewPager Container类Fragment完成数据更新

头部比分类Fragment

这类的客户端就简单多了,分成两类

  1. 带小小比分的Fragment
  2. 不带小小比分的Fragment
  3. 不带局数的Fragment;

抽取公共方法

开始编写这个公能的时候,我将这个三个类都单独的,绑定服务,解除绑定,更新数据
但是绑定服务,解除绑定,不同就是更新数据的部分不同,这时候抽象类的功效就来了,绑定服务,解除绑定,初始化的一些数据全部放到,抽象类中
添加更新头部比分和小比分的抽象方法,在子类中初始化UI控件,将更新的数据类上ViewPager Container类Fragment完成数据更新,只不过这次
只需要遍历列表,找到比赛id,实现对应的部比分和小比分的抽象方法即可

RecyclerView更新动画

由于每次跟新数据,RecycelerView渐变动画,时间太短,我们需要设置Recycler的渐变动画
在RecyclerView如果没有设置动画,Recyceler设置一个默认250ms的渐变动画,这个动画对于用户来说,TMD什么时候变的,我都没看清楚就变了,
这时候,重新设置一下recyclerView的渐变动画时间即可,当然也可以重写SimpleItemAnimator实现自己的数据更新动画,在Adapter的 onAttachedToRecyclerView中设置动画时间为3秒

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
mSetsRV = recyclerView;
DefaultItemAnimator defaultItemAnimator = new DefaultItemAnimator();
defaultItemAnimator.setAddDuration(DURATION);
defaultItemAnimator.setChangeDuration(DURATION);
defaultItemAnimator.setMoveDuration(DURATION);
defaultItemAnimator.setRemoveDuration(DURATION);
mSetsRV.setItemAnimator(defaultItemAnimator);
mContext = recyclerView.getContext();
}

这两个客户端都完成数据跟新了,看看最终结果吧

首页
详情页

重新进入应用处理

我们这服务并不需要包活,app没处于活动状态,没有比要跟新比分,费流量
冲新进入应用,需要走SplashActivity–>到MainActivity.onRestart()方法
所以在重写的onRestart()方法中findFragmentByTag(“Tab1”),在调用fragment方法即可

MainAcitvity

1
2
3
4
5
6
7
8
9
@Override
protected void onRestart() {
super.onRestart();
Fragment tab1 = getSupportFragmentManager().findFragmentByTag("Tab1");
if (tab1!=null && tab1 instanceof HomeFragment){
HomeFragment home = (HomeFragment) tab1;
home.onRestart();
}
}

HomFragment,绑定以一下服务或者启动一下更新,就Over了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void onRestart() {
if (homeSoftReference == null){
homeSoftReference = new SoftReference<>(this);
}
if (mUpdateUI == null){
mUpdateUI = new UiUpdateHandler(homeSoftReference);
}

try {
if (!mCategoryUpdate.asBinder().isBinderAlive()) {
HomeCate homeCateByIndex =
adapter.getHomeCateByIndex(viewPager.getCurrentItem());
bindToService(homeCateByIndex);
} else {
mCategoryUpdate.start();
}
} catch (Exception e) {
e.printStackTrace();
}

}