StoreFlowable.swift

GitHub tag (latest by date) Test License Swift Version Compatibility Platform Compatibility

用于 Swift 的、支持并发的 Repository 模式库。
适用于 iOS 或任何 Swift 项目。

相关项目

概述

该库使用 Swift Concurrency 提供了远程和本地缓存的抽象和观察机制。
根据以下 5 项策略创建。

以下是使用该库的 Repository 模式的类结构。

README-SWIFT-1 (1)

以下是使用 LoadingState 进行屏幕显示的示例。

https://user-images.githubusercontent.com/7742104/125714730-381eee65-4126-4ee8-991a-7fc64dfb325c.jpg

安装

通过 Swift Package Manager 安装,并将 *.*.* 替换为最新的标签。 GitHub tag (latest by date)

dependencies: [
    .package(url: "https://github.com/KazaKago/StoreFlowable.swift.git", from: "*.*.*"),
],

开始使用

您只需要实现两件事

1. 创建一个类来管理应用内缓存

首先,创建一个继承自 Cacher<PARAM: Hashable, DATA> 的类。
将您想用作参数的类型放在 <PARAM: Hashable> 中。如果您不需要参数,请放入 UnitHash

class UserCacher: Cacher<UserId, UserData> {
    static let shared = UserCacher()
    private override init() {}
}

Cacher<PARAM: Hashable, DATA> 需要在单例模式中使用。

2. 创建一个类来从原始服务器获取数据

接下来,创建一个实现 Fetcher 的类。
将您想用作 Data 的类型放在 DATA associatedtype 中。

下面展示了一个示例。

struct UserFetcher : Fetcher {

    typealias PARAM = UserId
    typealias DATA = UserData

    private let userApi = UserApi()

    func fetch(param: UserId) async throws -> UserData {
        userApi.fetch(userId: param)
    }
}

您需要准备 API 访问类。
在本例中,是 UserApi 类。

3. 从 Cacher 和 Fetcher 类构建 StoreFlowable

之后,您可以从 AnyStoreFlowable.from() 方法中获取 StoreFlowable<DATA> 类。
在获取/更新数据时,请务必通过创建的 StoreFlowable<DATA> 类。

let userFlowable: AnyStoreFlowable<UserData> = AnyStoreFlowable.from(cacher: userCacher, fetcher: userFetcher, param: userId)
let userStateSequence: LoadingStateSequence<UserData> = userFlowable.publish()

您可以使用 publish() 方法以 LoadingStateSequence<DATA> (与 AsyncSequence<LoadingState<DATA>> 相同)的形式获取数据。
LoadingState 是一个持有原始数据的 enum

4. 订阅 FlowLoadingState<DATA>

您可以通过 for-in AsyncSequence 来观察数据。
并使用 doAction() 方法或 switch 语句来分支数据状态。

for await userState in userStateSequence {
    userState.doAction(
        onLoading: { (content: UserData?) in
            ...
        },
        onCompleted: { (content: UserData, _, _) in
            ...
        },
        onError: { (error: Error) in
            ...
        }
    )
}

示例

有关详细信息,请参阅 example module。该模块作为一个 Android 应用程序运行。
请参阅 GithubMetaCacher + GithubMetaFetcherGithubUserCacher + GithubUserFetcher

此示例访问 Github API

StoreFlowable 类的其他用法

获取不带 LoadingState enum 的数据

如果您不需要值流和 LoadingState enum,您可以使用 requireData()getData()
如果不存在有效缓存且获取新数据失败,requireData() 将抛出 Error。
getData() 返回 nil 而不是 Error。

public extension StoreFlowable {
    func getData(from: GettingFrom = .both) async -> DATA?
    func requireData(from: GettingFrom = .both) async throws -> DATA
}

GettingFrom 参数指定从哪里获取数据。

public enum GettingFrom {
    // Gets a combination of valid cache and remote. (Default behavior)
    case both
    // Gets only remotely.
    case origin
    // Gets only locally.
    case cache
}

但是,仅在一次性数据获取时使用 requireData()getData(),并尽可能考虑使用 publish()

刷新数据

如果您想忽略缓存并获取新数据,请将 forceRefresh 参数添加到 publish()

public extension StoreFlowable {
    func publish(forceRefresh: Bool = false) -> LoadingStateSequence<DATA>
}

或者,如果您已经观察 Publisher,则可以使用 refresh()

public protocol StoreFlowable {
    func refresh() async
}

验证缓存数据

如果您想验证本地缓存是否有效,请使用 validate()
如果无效,则从远程获取新数据。

public protocol StoreFlowable {
    func validate() async
}

更新缓存数据

如果您想更新本地缓存,请使用 update() 方法。
Publisher 观察者将被通知。

public protocol StoreFlowable {
    func update(newData: DATA?) async
}

LoadingStateSequence<DATA> 操作符

映射 LoadingStateSequence<DATA>

使用 mapContent(transform) 来转换 LoadingStateSequence<DATA> 中的内容。

let state: LoadingStateSequence<Int> = ...
let mappedState: LoadingStateSequence<String> = state.mapContent { value: Int in
    value.toString()
}

组合多个 LoadingStateSequence<DATA>

使用 combineState(state, transform) 来组合多个 LoadingStateSequence<DATA>

let state1: LoadingStateSequence<Int> = ...
let state2: LoadingStateSequence<Int> = ...
let combinedState: LoadingStateSequence<Int> = state1.combineState(state2) { value1: Int, value2: Int in
    value1 + value2
}

管理缓存

管理缓存过期时间

您可以轻松设置缓存过期时间。在您的 Cacher<PARAM: Hashable, DATA> 类中重写 expireSeconds 变量。默认值是 TimeInterval.infinity(= 永不过期)。

class UserCacher: Cacher<UserId, UserData> {
    static let shared = UserCacher()
    private override init() {}

    override var expireSeconds: TimeInterval {
        get { TimeInterval(60 * 30) } // expiration time is 30 minutes.
    }
}

持久化数据

如果您想使缓存的数据持久化,请重写您的 Cacher<PARAM: Hashable, DATA> 类的方法。

class UserCacher: Cacher<UserId, UserData> {
    static let shared = UserCacher()
    private override init() {}

    override var expireSeconds: TimeInterval {
        get { TimeInterval(60 * 30) } // expiration time is 30 minutes.
    }

    // Save the data for each parameter in any store.
    override func saveData(data: GithubMeta?, param: UnitHash) async {
        ...
    }

    // Get the data from the store for each parameter.
    override func loadData(param: UnitHash) async -> GithubMeta? {
        ...
    }

    // Save the epoch time for each parameter to manage the expiration time.
    // If there is no expiration time, no override is needed.
    override func saveDataCachedAt(epochSeconds: Double, param: UnitHash) async {
        ...
    }

    // Get the date for managing the expiration time for each parameter.
    // If there is no expiration time, no override is needed.
    override func loadDataCachedAt(param: UnitHash) async -> Double? {
        ...
    }
}

分页支持

该库包含分页支持。

继承 PaginationCacher<PARAM: Hashable, DATA>PaginationFetcher,而不是 Cacher<PARAM: Hashable, DATA>Fetcher

下面展示了一个示例。

class UserListCacher: PaginationCacher<UnitHash, UserData> {
    static let shared = UserListCacher()
    private override init() {}
}

struct UserListFetcher : PaginationFetcher {

    typealias PARAM = UnitHash
    typealias DATA = UserData

    private let userListApi = UserListApi()

    func fetch(param: UnitHash) async throws -> PaginationFetcher.Result<UserData> {
        let fetched = userListApi.fetch(pageToken: nil)
        return PaginationFetcher.Result(data: fetched.data, nextKey: fetched.nextPageToken)
    }

    func fetchNext(nextKey: String, param: UnitHash) async throws -> PaginationFetcher.Result<UserData> {
        let fetched = userListApi.fetch(pageToken: nextKey)
        return PaginationFetcher.Result(data: fetched.data, nextKey: fetched.nextPageToken)
    }
}

您需要额外实现 fetchNext(nextKey: String, param: PARAM)

然后,您可以从 onCompleted {}next 参数中获取附加加载的状态。

let userFlowable = AnyStoreFlowable.from(cacher: userListCacher, fetcher: userListFetcher)
for await loadingState in userFlowable.publish() {
    loadingState.doAction(
        onLoading: { (content: UserData?) in
            // Whole (Initial) data loading.
        },
        onCompleted: { (content: UserData, next: AdditionalLoadingState, _) in
            // Whole (Initial) data loading completed.
            next.doAction(
                onFixed: { (canRequestAdditionalData: Bool) in
                    // No additional processing.
                },
                onLoading: {
                    // Additional data loading.
                },
                onError: { (error: Error) in
                    // Additional loading error.
                }
            )
        },
        onError: { (error: Error) in
            // Whole (Initial) data loading error.
        }
    )
}

为了在 UITableView 中显示,请使用差异更新功能。另请参阅 UITableViewDiffableDataSource

请求附加数据

您可以使用 requestNextData() 方法请求用于分页的附加数据。

public extension PaginationStoreFlowable {
    func requestNextData(continueWhenError: Bool = true) async
}

分页示例

example module 中的 GithubOrgsCacher + GithubOrgsFetcherGithubReposCacher + GithubReposFetcher 类实现了分页。

许可证

本项目根据 Apache-2.0 许可证获得许可 - 有关详细信息,请参阅 LICENSE 文件。