Once是一个开源的用于管理一些只需要进行一次(或几次)的操作的库。比如说只显示一次引导页,每个版本只显示一次更新说明等等。

使用方式

添加依赖

1
2
3
dependencies {
    compile 'com.jonathanfinerty.once:once:1.2.2'
}

初始化

1
Once.initialise(this);

使用

检测某项操作是否已进行:

1
2
3
4
5
6
7
8
String showWhatsNew = "showWhatsNewTag";

// 检测是否需要进行操作
if (!Once.beenDone(Once.THIS_APP_VERSION, showWhatsNew)) {
    startActivity(new Intent(this, WhatsNewActivity.class));
    // 完成后进行标记
    Once.markDone(showWhatsNew);
}

Once支持3种尺度:

1
2
3
THIS_APP_INSTALL: 安装到卸载前,例如引导页只出现一次,升级也不重复出现。
THIS_APP_VERSION: 当前版本,例如更新说明,一个版本出现一次。
THIS_APP_SESSION: 本次使用。

此外,也支持如果时间,在设定的时间内进行一次操作。

1
if (!Once.beenDone(TimeUnit.HOURS, 1, phonedHome) { ... }

Once还支持将某件事标记为”to do”,之后检查是否需要做某项操作。Once给我们举了个例子,有时你想在用户看到基础功能后,在MainActivity中显示一些高级功能。

1
2
3
4
5
6
7
8
9
10
11
12
13

// in the basic functionality activity
Once.toDo(Once.THIS_APP_INSTALL, "show feature onboarding");
...

// back in the home activity
if (Once.needToDo(showAppTour)) {
    // do some operations
    ...

    // after task has been done, mark it as done as normal
    Once.markDone(showAppTour);
}

除了进行一次,Once也支持第N次后进行某些操作。例如,在用户使用3次App后弹出让用户评分的dialog。

1
2
3
4
5
6
7
8
9
10
// Once again in the basic functionality activity
Once.markDone("action");
if (Once.beenDone("action", Amount.exactly(3))) {
    showRateTheAppDialog();
}

// 支持下面这三种
Amount.exactly(int x)   // 第x次时
Amount.lessThan(int x)  // 小于x次时
Amount.moreThan(int x)  // 大于x次时

具体实现

Once的功能很简单,如果不使用Once而是自己实现的话,我们必然会用SharedPreferences去记录是否进行过某些操作,Once的实现也是类似的。

PersistedMap和PersistedSet

这两个类是Once中保存和处理SharedPreferences。PersistedMap对应名为PersistedMapTagLastSeenMap的sp,用于保存那些被markDone的tag的时间戳,sp中保存的key为tag,value为时间戳拼成的字符串,以逗号分隔各时间戳,在PersistedMap中保存在Map<String, List<Long>> map对象中,map的key为tag,List即为由时间戳字符串还原成的list。PersistedSet对应名为PersistedSetToDoSet的sp,用于保存被标记为todo的tag,Android3.0以上的版本直接已Set的形式保存在sp中,3.0以下以字符串形式保存,用逗号分隔。

PersistedMap和PersistedSet的实现类似,初始化时通过AsyncTask异步去加载各自的SharedPreferences,提供put、remove等方法,调用这些方法会更新内存中的Map/Set,并更新sp。

AsyncSharedPreferenceLoader

供PersistedMap和PersistedSet加载SharedPreferences使用。内部实现了一个AsyncTask用于异步加载SharedPreferences,并提供get()方法用于获取加载后的SharedPreferences。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private final AsyncTask<String, Void, SharedPreferences> asyncTask = new AsyncTask<String, Void, SharedPreferences>() {
    @Override
    protected SharedPreferences doInBackground(String... names) {
        return context.getSharedPreferences(names[0], Context.MODE_PRIVATE);
    }
};

SharedPreferences get() {
    try {
        return asyncTask.get();
    } catch (InterruptedException | ExecutionException ignored) {
        return null;
    }
}

Once

Once的主类,提供了Once能实现的所有功能。这里只介绍一些主要方法的实现。

initialise

初始化方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void initialise(Context context) {
    // 初始化PersistedMap和PersistedSet
    tagLastSeenMap = new PersistedMap(context, "TagLastSeenMap");
    toDoSet = new PersistedSet(context, "ToDoSet");

    // sessionList用于保存处理THIS_APP_SESSION级别的tag。
    if (sessionList == null) {
        sessionList = new ArrayList<>();
    }

    // 获取lastAppUpdatedTime,用于处理THIS_APP_VERSION级别。
    PackageManager packageManager = context.getPackageManager();
    try {
        PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
        lastAppUpdatedTime = packageInfo.lastUpdateTime;
    } catch (PackageManager.NameNotFoundException ignored) {

    }
}

markDone

用于记录某个tag对应的操作的完成。会将当前时间戳增加进PersistedMap中,向sessionList中添加该tag,并从PersistedSet中将该tag移除。

1
2
3
4
5
public static void markDone(String tag) {
    tagLastSeenMap.put(tag, new Date().getTime());
    sessionList.add(tag);
    toDoSet.remove(tag);
}

beenDone

用于判断某个tag是否已完成。有一系列重载的方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// THIS_APP_INSTALL级别,该tag是否满足moreThan(0)。
beenDone(String tag);
// THIS_APP_INSTALL级别,该tag是否满足传入的numberOfTimes条件。
beenDone(String tag, CountChecker numberOfTimes);
// 自定义scope,该tag是否满足moreThan(0)。
beenDone(@Scope int scope, String tag);
// 自定义scope,该tag是否满足传入的numberOfTimes条件。
beenDone(@Scope int scope, String tag, CountChecker numberOfTimes);

// amount * timeUnit之前至今,该tag是否满足moreThan(0)。
beenDone(TimeUnit timeUnit, long amount, String tag);
// amount * timeUnit之前至今,该tag是否满足传入的numberOfTimes条件。
beenDone(TimeUnit timeUnit, long amount, String tag, CountChecker numberOfTimes);
// timeSpanInMillis之前至今,该tag是否满足moreThan(0)。
beenDone(long timeSpanInMillis, String tag);
// timeSpanInMillis之前至今,该tag是否满足传入的numberOfTimes条件。
beenDone(long timeSpanInMillis, String tag, CountChecker numberOfTimes);

前4个方法以scope为维度,最终都会调用到beenDone(@Scope int scope, String tag, CountChecker numberOfTimes),后4个方法以时间为维度,最终都会调用beenDone(long timeSpanInMillis, String tag, CountChecker numberOfTimes),我们来看一下这2个方法即可。

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
public static boolean beenDone(@Scope int scope, String tag, CountChecker numberOfTimes) {
    // 获取该tag所有的记录。
    List<Long> tagSeenDates = tagLastSeenMap.get(tag);

    if (tagSeenDates.isEmpty()) {
        return false;
    }

    //noinspection SimplifiableIfStatement
    if (scope == THIS_APP_INSTALL) {
        // THIS_APP_INSTALL级别,直接和记录的size比较。
        return numberOfTimes.check(tagSeenDates.size());
    } else if (scope == THIS_APP_SESSION) {
        // THIS_APP_SESSION级别,和sessionList中该tag的个数比较。
        int counter = 0;
        for (String tagFromList : sessionList) {
            if (tagFromList.equals(tag)) {
                counter++;
            }
        }
        return numberOfTimes.check(counter);
    } else {
        // THIS_APP_VERSION级别,记录中大于lastAppUpdatedTime的才计算进去。
        int counter = 0;
        for (Long seenDate : tagSeenDates) {
            if (seenDate > lastAppUpdatedTime) {
                counter++;
            }
        }

        return numberOfTimes.check(counter);
    }
}

    public static boolean beenDone(long timeSpanInMillis, String tag, CountChecker numberOfTimes) {
    List<Long> tagSeenDates = tagLastSeenMap.get(tag);

    if (tagSeenDates.isEmpty()) {
        return false;
    }

    int counter = 0;
    for (Long seenDate : tagSeenDates) {
        // 计算出最小的有效时间
        long sinceSinceCheckTime = new Date().getTime() - timeSpanInMillis;
        // 比较记录的时间和有效时间,比有效时间大的才计入。
        if (seenDate > sinceSinceCheckTime) {
            counter++;
        }
    }

    return numberOfTimes.check(counter);
}

toDo/needToDo

toDo用于标记某个tag “need to do”。有toDo(@Scope int scope, String tag)toDo(String tag)2个方法。需要注意的是,使用toDo(@Scope int scope, String tag)时,如果该tag之前被markDone过,则仅当传入的scope为THIS_APP_VERSION且该tag上次被markDone的时间在lastAppUpdatedTime之前才会将该tag添加到PersistedSet中。而toDo(String tag)则无论该tag之前有没有被markDone,都会添加到PersistedSet中。
needToDo即用于检查PersistedSet中是否存在对应的tag。

总结

  1. Once可以帮我们方便地管理一些操作在什么时候进行。
  2. 利用SharedPreferences保存记录。
  3. 支持THIS_APP_INSTALLTHIS_APP_VERSIONTHIS_APP_SESSION三种级别。利用每次markDone的时间戳实现对三种级别的支持。