我如何在 swift Kotlin 多平台上使用 Flow?

Posted

技术标签:

【中文标题】我如何在 swift Kotlin 多平台上使用 Flow?【英文标题】:How i can use Flow on swift Kotlin multiplatform? 【发布时间】:2021-08-08 21:13:31 【问题描述】:

我正在创建我的第一个 kotlin 多平台项目,但在快速使用 kotlin 流程时遇到了一些困难。我使用 kotlin flow 和 ktor 创建了模型、服务器数据请求作为通用文件、视图模型和我创建为本机的 ui 层。所以,我没有快速开发的经验,除此之外,我在快速视图模型上使用流程时遇到了很多麻烦。在寻找我的问题的答案时,我发现了一个描述为 CommonFlow 的类,它的目的是用作两种语言的通用代码(kotlin、swift,但我遇到了一个错误,让我很少或根本不知道为什么会这样发生了,或者,可能只是我缺乏对 xcode 和 swift 编程的支配:

所以这是 xcode 指出错误的代码部分: Obs:对图片感到抱歉,我认为这次可能更具描述性

这就是我从错误中得到的全部

Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee5928ff8)

我的 ios 视图模型:

class ProfileViewModel: ObservableObject 
    private let repository: ProfileRepository
    
    init(repository: ProfileRepository) 
        self.repository = repository
    
    
    @Published var authentication: Authetication = Authetication.unauthenticated(false)
    @Published var TokenResponse: ResponseDTO<Token>? = nil
    @Published var loading: Bool = false
    
    func authenticate(email: String, password: String) 
        DispatchQueue.main.async 
            if(self.isValidForm(email: email, password: password))
                self.repository.getTokenCFlow(email: email, password: password).watch response in
                    switch response?.status 
                    
                        case StatusDTO.success:
                            self.loading = false
                                let token: String = response!.data!.accessToken!
                                SecretStorage().saveToken(token: token)
                                self.authentication = Authetication.authenticated
                                break;
                            
                            case StatusDTO.loading:
                                self.loading = true
                            break;
                                
                            case StatusDTO.error:
                                print("Ninja request error \(String(describing: response!.error!))")
                                break;
                                
                            default:
                                break
                    
                
            
        
    
    
    private func isValidForm(email: String, password: String) -> Bool 
        var invalidFields = [Pair]()
        if(!isValidEmail(email))
            invalidFields.append(Pair(first:"email invalido",second: "email invalido"))
        
            
        if(password.isEmpty) 
            invalidFields.append(Pair(first:"senha invalida",second: "senha invalida"))
        
            
        if(!invalidFields.isEmpty)
            self.authentication = Authetication.invalidAuthentication(invalidFields)
            return false
        
        return true
    
    
    private func isValidEmail(_ email: String) -> Bool 
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]2,64"

        let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: email)
    
    


class Pair 
    let first: String
    let second: String
    init(first:String, second: String) 
        self.first = first
        self.second = second
    

enum Authetication 
    case invalidAuthentication([Pair])
    case authenticated
    case persistentAuthentication
    case unauthenticated(Bool)
    case authenticationFailed(String)
    

存储库方法:

override fun getToken(email: String, password: String): Flow<ResponseDTO<Token>> = flow 
        emit(ResponseDTO.loading<Token>())
        try 
            val result = api.getToken(GetTokenBody(email, password))
            emit(ResponseDTO.success(result))
         catch (e: Exception) 
            emit(ResponseDTO.error<Token>(e))
        
    

@InternalCoroutinesApi
    override fun getTokenCFlow(email: String, password: String): CFlow<ResponseDTO<Token>> 
        return wrapSwift(getToken(email, password))
    

类 CFLOW:

@InternalCoroutinesApi
class CFlow<T>(private val origin: Flow<T>): Flow<T> by origin 
    fun watch(block: (T) -> Unit): Closeable 
        val job = Job()
        onEach 
            block(it)
        .launchIn(CoroutineScope(Dispatchers.Main + job))

        return object: Closeable 
            override fun close() 
                job.cancel()
            
        
    


@FlowPreview
@ExperimentalCoroutinesApi
@InternalCoroutinesApi
fun <T> ConflatedBroadcastChannel<T>.wrap(): CFlow<T> = CFlow(asFlow())

@InternalCoroutinesApi
fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this)

@InternalCoroutinesApi
fun <T> wrapSwift(flow: Flow<T>): CFlow<T> = CFlow(flow)

【问题讨论】:

【参考方案1】:

有一个在 KampKit 中使用流的例子

https://github.com/touchlab/KaMPKit

我将粘贴来自 NativeViewModel (iOS) 的摘录

class NativeViewModel(
    private val onLoading: () -> Unit,
    private val onSuccess: (ItemDataSummary) -> Unit,
    private val onError: (String) -> Unit,
    private val onEmpty: () -> Unit
) : KoinComponent 

    private val log: Kermit by inject  parametersOf("BreedModel") 
    private val scope = MainScope(Dispatchers.Main, log)
    private val breedModel: BreedModel = BreedModel()
    private val _breedStateFlow: MutableStateFlow<DataState<ItemDataSummary>> = MutableStateFlow(
        DataState.Loading
    )

    init 
        ensureNeverFrozen()
        observeBreeds()
    

    @OptIn(FlowPreview::class)
    fun observeBreeds() 
        scope.launch 
            log.v  "getBreeds: Collecting Things" 
            flowOf(
                breedModel.refreshBreedsIfStale(true),
                breedModel.getBreedsFromCache()
            ).flattenMerge().collect  dataState ->
                _breedStateFlow.value = dataState
            
        



This ViewModel is consumed in swift like this:

lazy var adapter: NativeViewModel = NativeViewModel(
    onLoading:  /* Loading spinner is shown automatically on iOS */
        [weak self] in
        guard let self = self else  return 
        if (!(self.refreshControl.isRefreshing)) 
            self.refreshControl.beginRefreshing()
        
    ,
    onSuccess: 
        [weak self] summary in self?.viewUpdateSuccess(for: summary)
        self?.refreshControl.endRefreshing()
    ,
    onError:  [weak self] error in self?.errorUpdate(for: error)
        self?.refreshControl.endRefreshing()
    ,
    onEmpty:  /* Show "No doggos found!" message */
        [weak self] in self?.refreshControl.endRefreshing()
    
)

In short, the flow is kept wrapped in kotlin mp land and leveraged in iOS by using traditional callback interfaces.

【讨论】:

以上是关于我如何在 swift Kotlin 多平台上使用 Flow?的主要内容,如果未能解决你的问题,请参考以下文章

如何将字节从 Swift (iOS) 传递到 Kotlin 通用模块?

如何在 Kotlin 标准库(多平台)上获取当前的 unixtime

如何在 kotlin Multiplatform 和 Swift 中使用默认接口实现

Kotlin 与 Objective-C 框架的多平台/本机互操作性

Android平台的Swift—Kotlin

使用多平台模拟 kotlin 中的常见测试