【架构】一种Android界面数据同步刷新机制

我们来谈谈客户端界面的数据同步问题。

界面数据同步的需求

比如,下面的AB两个界面中都显示了学生Leslie的信息,当我们在A界面修改学生学号时,我们希望回到B界面时,学生的信息也能跟着改变,才能保证业务数据的正确和一致性。

这就涉及到数据的同步和刷新问题。

主界面显示了学生和老师的信息
另一个界面也显示了学生的信息

刷新数据时都要从数据源再次请求数据吗?

如今手机应用的数据几乎都来自网络(或者本地数据库)。假如我们在A界面上修改了学生的信息并同步到网络,若回到B界面需要刷新该学生的信息,再次调用网络请求得到学生的信息显示在B界面,这是可以的,但会大大增加服务器的负担或影响应用的响应速度,而且当用户到达一定的数量级,客户端频繁的网络请求迟早会把服务器搞垮。

如何避免频繁的网络请求同时也能实现客户端界面数据的正确性和一致性的呢?

对于上述问题,大多应用的解决方法是把网络请求得到的业务数据缓存在内存中,优先使用内存缓存数据

这样做的理由是:

我们假定一个正常的普通用户,同一时刻只会在同一个设备上打开应用并进行业务数据操作。在这个大前提下,我们可以保证在同一时间内,不会有其他设备对服务器的同个账号数据进行更新。
于是,我们可以在第一次请求网络数据时,将请求得到的数据存在内存中,当用户对业务数据做出了操作,我们在调用API将业务数据同步到服务器后,根据用户的操作对于内存数据进行对应的更新,保证了内存数据和服务器数据的一致,在之后,直接从内存数据源取出数据供界面显示即可

这样的方式避免了频繁、重复的API网络请求,同时使应用有较高的数据访问和响应速度。坏处是如果业务数据量庞大,应用将会占用大量的运行内存,可以通过优化来改善(我们日后再谈)。

总结一下。我们对客户端的业务数据进行刷新时,可以优先考虑从内存中读取缓存好的业务数据,特殊情况下,才考虑从网络或本地数据库请求数据。

客户端数据同步机制的设计

上面讨论好的数据存储机制为全局的数据刷新提供了一些基础。如何实现全局的数据同步机制呢?

先上架构大致的示意图。

架构的大致示意图(模糊的话请查看原图)

看完示意图你可能就大致明白了。

简单来讲。一个Actvity里包含了多个Fragment,而每个Fragment界面如果有数据同步的需要,就委托Activity向一个叫FragmentSyncManager的类注册一个特定类型的监听回调(如学生数据、教师数据类型)。在之后的任何界面中,只要我们调用同步指令并指明要同步的数据类型,FragmentSyncManager就会找到对应数据类型的所有同步回调,并全部执行同步刷新。

原理很简单,但把它们融进架构和妥善封装是开发和程序设计的问题了。

接下来我们简单分析一下架构的实现。


架构实现

1. 同步监听的定义和监听实例的管理

我们先看看同步监听类的定义:

/**
 * 同步回调类
 * @param <T> 这里的泛型就是用来标识数据类型的,为SyncedObject的子类
 * 比如学生数据类型我们就指定T为SyncedObject.Student,教师数据类型就指定T为SyncedObject.Teacher
 */
public abstract class OnRefreshDataListener<T extends SyncedObject> {

    /**
     * 返回泛型的类型
     * 假如这一个监听是用来刷新学生信息的,这个方法将会返回SyncedObject.Student的Class类型值
     */
    public final Class<?> getSyncedObjectClass() {
        return (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    // onRefresh方法便是同步要做的事情
    // 当FragmentSyncManager接收到同步指令时,会找到指定的数据类型并调用对应同步监听的这个方法
    public void onRefresh(T t) {
        // 这里内容可以是从内存缓存或网络得到数据并显示在界面
    }
}

上面的代码中,泛型T是SyncedObject的子类,SyncedObject类就是用来标明同步刷新的数据类型,我们用它来方便管理。比如学生和教师数据类型,继承这个类即可,可以不用包含什么信息,如下。

public class SyncedObject {

    // 学生数据类型标识类
    public static class Student extends SyncedObject {
    }

    // 教师数据类型标识类
    public static class Teacher extends SyncedObject {
    }
}

再看看刷新监听的管理类FragmentSyncManager,它包含了对同步监听的登记、注销(删除)、执行三个方法。

public class FragmentSyncManager {

    // 存入【刷新类型】->【所有界面的同步回调集合】,
    // 如【学生信息类型】->【A界面的同步回调, B界面的同步回调】
    // 如【教师信息类型】->【B界面的同步回调, C界面的同步回调】
    private Map<Class, List<OnRefreshDataListener>> mOnUIThreadSyncedMap = new HashMap<>();

    private static FragmentSyncManager sFragmentSyncManager = new FragmentSyncManager();

    public static FragmentSyncManager getInstance() {
        return sFragmentSyncManager;
    }

    // 注册一个回调
    public void register(OnRefreshDataListener onRefreshDataListener) {
        if (onRefreshDataListener != null) {

            // 得到要同步的数据类型,随后根据类型把本次注册的同步监听添加到对应的集合
            Class syncedObjectClass = onRefreshDataListener.getSyncedObjectClass();

            List<OnRefreshDataListener> onRefreshDataListeners = mOnUIThreadSyncedMap.get(syncedObjectClass);
            if (onRefreshDataListeners == null) {
                onRefreshDataListeners = new ArrayList<>();
            }
            onRefreshDataListeners.add(onRefreshDataListener);
            mOnUIThreadSyncedMap.put(syncedObjectClass, onRefreshDataListeners);
        }
    }

    // 删除掉一个监听,这个方法在界面销毁时执行。
    // 原因很简单,界面销毁掉了,不再需要对这个界面上的数据进行同步刷新了。
    public void deregister(OnRefreshDataListener onRefreshDataListener) {
        mOnUIThreadSyncedMap.get(onRefreshDataListener.getSyncedObjectClass()).remove(onRefreshDataListener);
    }

    /**
     * 传入特定的数据类型,执行这个数据类型对应的所有同步监听
     *
     * @param syncedObject 要同步的数据类型。
     */
    public void synced(SyncedObject syncedObject) {
        if (syncedObject != null && mOnUIThreadSyncedMap.containsKey(syncedObject.getClass())) {
            List<OnRefreshDataListener> onRefreshDataListeners = mOnUIThreadSyncedMap.get(syncedObject.getClass());
            for (int index = 0; index < onRefreshDataListeners.size(); index++) {
                OnRefreshDataListener onRefreshUIListener = onRefreshDataListeners.get(index);
                if (onRefreshUIListener != null) {
                    
                    // 执行同步监听的onRefresh回调方法
                    onRefreshUIListener.onRefresh(syncedObject);
                    
                }
            }
        }
    }
}

到这里,刷新监听的定义和管理便完成了。接下来谈谈封装方法。

2. Activity和Fragment的封装

我们再回顾一下这个示意图。

架构的大致示意图(模糊的话请查看原图)

可以得到:每个Fragment对同步监听有三种操作,为在登记、注销、执行;同时,一个Actvity里会包含多个Fragment。

我们本可以把这三类操作放在Fragment的封装基类,但我们把同步监听的操作放到BaseActivity中,原因有几个,一是Actvity作为粒度比Fragment大的组件,应当是封装内容的最顶部,这样也可以减少创建Fragment的运行代码的内存占用;二是Activity作为系统主要的组件,需要在后台进行更复杂的同步任务(比如可以用Handler的postDelayed()定时循环同步刷新、第三方SDK监控Crash等)。对此,将同步监听的操作放在Activity更有长远的意义。如下。

public abstract class BaseActivity extends AppCompatActivity implements OnFragmentSyncedListener {

    @Override
    public void registerListener(OnRefreshDataListener onRefreshUIListener) {
        FragmentSyncManager.getInstance().register(onRefreshUIListener);  // 登记监听
    }

    @Override
    public void unRegisterListener(OnRefreshDataListener onRefreshUIListener) {
        FragmentSyncManager.getInstance().deregister(onRefreshUIListener);  // 注销监听
    }

    @Override
    public void onSynced(SyncedObject syncedObject) {
        FragmentSyncManager.getInstance().synced(syncedObject);  // 执行同步
    }
}

显然,这三个方法是需要在Fragment中被调用的。Fragment里用getActivity()方法可以得到所在的activity,那如何让Fragment调用得到BaseActivity里的方法呢?

在上面代码中,我们让BaseActivity实现接口OnFragmentSyncedListener(这个接口包含了增、删、执行那三个方法),随后在BaseFragment里把activity转为这个接口的实例,就可以实现调用BaseActivity这三个方法了,如下。

Activity activity = (Activity) context;
mOnFragmentSyncedListener = activity instanceof OnFragmentSyncedListener ? (OnFragmentSyncedListener) activity : null;

我们看看BaseFragment的总实现。

public abstract class BaseFragment extends Fragment {

    // 通过本Fragment对应的Activity转换后得到的OnFragmentSyncedListener实例
    private OnFragmentSyncedListener mOnFragmentSyncedListener;

    // 存储本Fragment登记过的所有同步监听,这个字段用于在Fragment销毁时注销掉所有的同步监听
    private List<OnRefreshDataListener> onRefreshDataListeners = new ArrayList<>();

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Activity activity = (Activity) context;
        mOnFragmentSyncedListener = activity instanceof OnFragmentSyncedListener ? (OnFragmentSyncedListener) activity : null;
    }

    // 登记监听
    protected void registerRefreshDataListener(OnRefreshDataListener onRefreshDataListener) {
        if (mOnFragmentSyncedListener != null) {
            mOnFragmentSyncedListener.registerListener(onRefreshDataListener);  // 这个方法就是Activity实现的registerListener方法
            onRefreshDataListeners.add(onRefreshDataListener);
        }
    }

    // 同步监听
    protected void syncedRefreshDataListener(SyncedObject syncedObject) {
        if (mOnFragmentSyncedListener != null) {
            mOnFragmentSyncedListener.onSynced(syncedObject);  // 这个方法就是Activity实现的onSynced方法
        }
    }

    // 注销所有监听
    private void unRegisterRefreshDataListeners(List<OnRefreshDataListener> onRefreshDataListeners) {
        if (mOnFragmentSyncedListener != null) {
            for (int index = 0; index < onRefreshDataListeners.size(); index++) {
                if (onRefreshDataListeners.get(index) != null) {
                    mOnFragmentSyncedListener.unRegisterListener(onRefreshDataListeners.get(index));  // 这个方法就是Activity实现的unRegisterListener方法
                }
            }
        }
    }

    protected void unRegisterALLRefreshDataListeners() {
        unRegisterRefreshDataListeners(onRefreshDataListeners);
    }

    // Fragment销毁时,注销掉所有的同步监听
    @Override
    public final void onDestroy() {
        unRegisterALLRefreshDataListeners();
        super.onDestroy();
    }
}

之后,每个BaseActivity子类中的BaseFragment子类就可以使用数据同步的功能了。

如Fragment A中登记了同步监听:

registerRefreshDataListener(new OnRefreshDataListener<SyncedObject.Student>() {
    @Override
    public void onRefresh(SyncedObject.Student o) {
        refreshStudentData();  // 从数据源得到学生数据并显示在界面上,执行全局刷新时,这个方法将会被调用
    }
});
Fragment A 显示了学生和老师的信息

在Fragment B 中也显示了学生的信息,当我们点击按钮【Update Student】更新学生信息后,Fragment A 界面上的学生信息也会改变。

Fragment B 也显示了学生的信息

按钮【Update Student】的响应代码如下。

view.findViewById(R.id.btn_update_student).setOnClickListener(v -> {

    // 改变业务数据。实际应用中,可能是通过调用网络请求去更新业务信息,调用成功后再更新内存数据
    StudentRepository.getInstance().getStudent().setNum("1234567890");

    showStudentData();  // 显示界面学生的信息

    // 针对学生数据类型,执行全局刷新操作
    syncedRefreshDataListener(new SyncedObject.Student());  // 这个方法是BaseFragment里的同步方法

});

上面的代码中,当执行

SyncedObject.Student studentFromB = new SyncedObject.Student();
syncedRefreshDataListener(studentFromB)); 

后,如果Fragment A 此时还没有销毁的话,Fragment A 中的

void onRefresh(SyncedObject.Student obj)

方法将会被执行(因为它是学生数据类型的同步监听),即实现了刷新界面。

这里还有一个更高阶的使用方法。我们可以看到,上面的onRefresh方法的形参obj就是B界面传过来的实例studentFromB,这也说明,这个架构也实现了Fragment之间值的传递,它可以用于Fragment之间的交互

比如,我们实现了一个商品列表Fragment,也实现了一个商品搜索的Fragment,现在在商品搜索界面设定了搜索条件,点击搜索按钮后,我们希望在商品列表界面实现商品查询和加载。除了使用回调,也可以用一个夹着搜索条件的SyncedObject实例调用同步操作,在商品列表Fragment中的onRefresh方法接收到实例,并触发查询操作。

Demo源码:GitHub地址


现在安卓官方出了一个叫LiveData的组件,也同样是基本内存缓存数据实现了全局刷新机制的,还加入了Fragment和Activity生命周期的控制。它的实现方式和设计原理和本文提到的内容大致相同,但更加的傻瓜化和易用,朋友们可以自行搜索查看。

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