https://kotlinlang.org logo
Channels
100daysofcode
100daysofkotlin
100daysofkotlin-2021
advent-of-code
aem
ai
alexa
algeria
algolialibraries
amsterdam
android
android-architecture
android-databinding
android-studio
androidgithubprojects
androidthings
androidx
androidx-xprocessing
anime
anko
announcements
apollo-kotlin
appintro
arabic
argentina
arkenv
arksemdevteam
armenia
arrow
arrow-contributors
arrow-meta
ass
atlanta
atm17
atrium
austin
australia
austria
awesome-kotlin
ballast
bangladesh
barcelona
bayarea
bazel
beepiz-libraries
belgium
benchmarks
berlin
big-data
books
boston
brazil
brikk
budapest
build
build-tools
bulgaria
bydgoszcz
cambodia
canada
carrat
carrat-dev
carrat-feed
chicago
chile
china
chucker
cincinnati-user-group
cli
clikt
cloudfoundry
cn
cobalt
code-coverage
codeforces
codemash-precompiler
codereview
codingame
codingconventions
coimbatore
collaborations
colombia
colorado
communities
competitive-programming
competitivecoding
compiler
compose
compose-android
compose-desktop
compose-hiring
compose-ios
compose-mp
compose-ui-showcase
compose-wear
compose-web
confetti
connect-audit-events
corda
cork
coroutines
couchbase
coursera
croatia
cryptography
cscenter-course-2016
cucumber-bdd
cyprus
czech
dagger
data2viz
databinding
datascience
dckotlin
debugging
decompose
decouple
denmark
deprecated
detekt
detekt-hint
dev-core
dfw
docs-revamped
dokka
domain-driven-design
doodle
dsl
dublin
dutch
eap
eclipse
ecuador
edinburgh
education
effective-kotlin
effectivekotlin
emacs
embedded-kotlin
estatik
event21-community-content
events
exposed
failgood
fb-internal-demo
feed
firebase
flow
fluid-libraries
forkhandles
forum
fosdem
fp-in-kotlin
framework-elide
freenode
french
fritz2
fuchsia
functional
funktionale
gamedev
ge-kotlin
general-advice
georgia
geospatial
german-lang
getting-started
github-workflows-kt
glance
godot-kotlin
google-io
gradle
graphic
graphkool
graphql
graphql-kotlin
graviton-browser
greece
grpc
gsoc
gui
hackathons
hacktoberfest
hamburg
hamkrest
helios
helsinki
hexagon
hibernate
hikari-cp
hire-me
hiring
hiring-french
hongkong
hoplite
http4k
hungary
hyderabad
image-processing
india
indonesia
inkremental
intellij
intellij-plugins
intellij-tricks
internships
introduce-yourself
io
ios
iran
israel
istanbulcoders
italian
jackson-kotlin
jadx
japanese
jasync-sql
java-to-kotlin-refactoring
javadevelopers
javafx
javalin
javascript
jdbi
jhipster-kotlin
jobsworldwide
jpa
jshdq
juul-libraries
jvm-ir-backend-feedback
jxadapter
k2-early-adopters
kaal
kafka
kakao
kalasim
kapt
karachi
karg
karlsruhe
kash_shell
kaskade
kbuild
kdbc
kgen-doc-tools
kgraphql
kinta
klaxon
klock
kloudformation
kmdc
kmm-español
kmongo
knbt
knote
koalaql
koans
kobalt
kobweb
kodein
kodex
kohesive
koin
koin-dev
komapper
kondor-json
kong
kontent
kontributors
korau
korean
korge
korim
korio
korlibs
korte
kotest
kotest-contributors
kotless
kotlick
kotlin-asia
kotlin-beam
kotlin-by-example
kotlin-csv
kotlin-data-storage
kotlin-foundation
kotlin-fuel
kotlin-in-action
kotlin-inject
kotlin-latam
kotlin-logging
kotlin-multiplatform-contest
kotlin-mumbai
kotlin-native
kotlin-pakistan
kotlin-plugin
kotlin-pune
kotlin-roadmap
kotlin-samples
kotlin-sap
kotlin-serbia
kotlin-spark
kotlin-szeged
kotlin-website
kotlinacademy
kotlinbot
kotlinconf
kotlindl
kotlinforbeginners
kotlingforbeginners
kotlinlondon
kotlinmad
kotlinprogrammers
kotlinsu
kotlintest
kotlintest-devs
kotlintlv
kotlinultimatechallenge
kotlinx-datetime
kotlinx-files
kotlinx-html
kotrix
kotson
kovenant
kprompt
kraph
krawler
kroto-plus
ksp
ktcc
ktfmt
ktlint
ktor
ktp
kubed
kug-leads
kug-torino
kvision
kweb
lambdaworld_cadiz
lanark
language-evolution
language-proposals
latvia
leakcanary
leedskotlinusergroup
lets-have-fun
libgdx
libkgd
library-development
lincheck
linkeddata
lithuania
london
losangeles
lottie
love
lychee
macedonia
machinelearningbawas
madrid
malaysia
mathematics
meetkotlin
memes
meta
metro-detroit
mexico
miami
micronaut
minnesota
minutest
mirror
mockk
moko
moldova
monsterpuzzle
montreal
moonbean
morocco
motionlayout
mpapt
mu
multiplatform
mumbai
munich
mvikotlin
mvrx
myndocs-oauth2-server
naming
navigation-architecture-component
nepal
new-mexico
new-zealand
newname
nigeria
nodejs
norway
npm-publish
nyc
oceania
ohio-kotlin-users
oldenburg
oolong
opensource
orbit-mvi
osgi
otpisani
package-search
pakistan
panamá
pattern-matching
pbandk
pdx
peru
philippines
phoenix
pinoy
pocketgitclient
polish
popkorn
portugal
practical-functional-programming
proguard
prozis-android-backup
pyhsikal
python
python-contributors
quasar
random
re
react
reaktive
realm
realworldkotlin
reductor
reduks
redux
redux-kotlin
refactoring-to-kotlin
reflect
refreshversions
reports
result
rethink
revolver
rhein-main
rocksdb
romania
room
rpi-pico
rsocket
russian
russian_feed
russian-kotlinasfirst
rx
rxjava
san-diego
science
scotland
scrcast
scrimage
script
scripting
seattle
serialization
server
sg-user-group
singapore
skia-wasm-interop-temp
skrape-it
slovak
snake
sofl-user-group
southafrica
spacemacs
spain
spanish
speaking
spek
spin
splitties
spotify-mobius
spring
spring-security
squarelibraries
stackoverflow
stacks
stayhungrystayfoolish
stdlib
stlouis
strife-discord-lib
strikt
students
stuttgart
sudan
swagger-gradle-codegen
swarm
sweden
swing
swiss-user-group
switzerland
talking-kotlin
tallinn
tampa
teamcity
tegal
tempe
tensorflow
terminal
test
testing
testtestest
texas
tgbotapi
thailand
tornadofx
touchlab-tools
training
tricity-kotlin-user-group
trójmiasto
truth
tunisia
turkey
turkiye
twitter-feed
uae
udacityindia
uk
ukrainian
uniflow
unkonf
uruguay
utah
uuid
vancouver
vankotlin
vertx
videos
vienna
vietnam
vim
vkug
vuejs
web-mpp
webassembly
webrtc
wimix_sentry
wwdc
zircon
Powered by
Title
g

Gurupad Mamadapur [FH]

02/23/2021, 4:22 PM
What is the best way to consume flowables from my shared module in ios to be used on SwiftUI views. Right now I am doing something like this -
class AppState: ObservableObject {

    private var closeables = Array<Closeable>()
    private let viewmodel = AppViewModel()
    
    @Published private(set) var data:AppData? = nil
    
    init() {
        let close:Closeable = UtilsKt.watch(viewmodel.data) { any in. // watch is an extension function on flow that returns a closeable
            self.data = any as? AppData
        }
        
        closeables.append(close)
        
        viewmodel.load() // some api call
    }
    
    deinit {
        closeables.forEach { it in
            it.onClose()
        }
    }
    
}
Then use the object of AppState (ignore the naming) on SwiftUI view, marked with @ObservedObject. This is a bit tedious as I'll have to copy and mark (@published) for each exposed property from my shared viewmodel. Could directly use the viewmodel in a view but there is no lifecycle I know of where I can call close.
l

Lammert Westerhoff

02/24/2021, 2:39 PM
The way I do it: First of all I’m using
CFlow
from the KotlinConf app (https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/mobileMain/kotlin/org/jetbrains/kotlinconf/FlowUtils.kt) to have a generic Flow in iOS. I also have the following utility which creates a publisher from the flow:
func asPublisher<T>(_ flow: CFlow<T>) -> AnyPublisher<T, Never> {
    return Deferred<Publishers.HandleEvents<PassthroughSubject<T, Never>>> {
        let subject = PassthroughSubject<T, Never>()
        let closable = flow.watch { next in
            if let next = next {
                subject.send(next)
            }
        }
        return subject.handleEvents(receiveCancel: {
            closable.close()
        })
    }.eraseToAnyPublisher()
}
Now I could subscribe to that in an ObservableObject similar as you. However I created a wrapper view with a content builder in which I just pass the publisher:
public struct ObservingView<Observed, Content>: View where Content: View {

    @State private var model: Observed? = nil

    private let publisher: AnyPublisher<Observed, Never>
    private let content: (Observed) -> Content

    public init(publisher: AnyPublisher<Observed, Never>, @ViewBuilder content: @escaping (Observed) -> Content) {
        self.publisher = publisher
        self.content = content
    }

    public var body: some View {
        let view: AnyView
        if let model = self.model {
            view = AnyView(content(model))
        } else {
            view = AnyView(EmptyView())
        }
        return view.onReceive(publisher) { self.model = $0 }
    }
}
And then I use it all as following:
private struct ViewWithObserver: View {

    let viewModel: ViewModel // has output wich is a CFlow of ViewModelOutput

    var body: some View {
        ObservingView(publisher: asPublisher(viewModel.output))
    }
}

private struct InnerView: View {
    let output: ViewModelOutput // is a data class with all properties used in the view

    var body: some View {
        Text(output.title)
    }
}
Here the InnerView automatically gets updated by the
ObservingView
whenever something from the
ViewModelOutput
changes.
👍 1
l

leandro

02/27/2021, 5:13 PM
This is super interesting! have you expanded on this somewhere else (eg. blog post)?
l

Lammert Westerhoff

02/27/2021, 7:29 PM
Not yet but I was planning to write a blog post on it. Will keep this thread up to date when I do so
👍 2
:thank-you: 1
l

leandro

02/27/2021, 9:29 PM
Looking forward to it! Thanks
g

Gurupad Mamadapur [FH]

03/01/2021, 5:38 AM
@Lammert Westerhoff Thanks! I am very new to ios so looking forward to the blog post.
I have a simple question, my viewmodel output has multiple cflow properties, I do not want to club them into one data class. Would you say in that case your approach can still be useful?
l

Lammert Westerhoff

03/02/2021, 1:12 PM
@Gurupad Mamadapur [FH] I guess if you use those in different parts of your View you could still use them as separate flows. What’s the reason you don’t want them together in a single data class?
@leandro I’ve written a draft for the blog post (in markdown). it hasn’t been published anywhere yet. I’d love to get some feedback on it.
r

russhwolf

03/03/2021, 4:30 AM
Hi! Looks like a nice article. I’ve been looking for something like
asPublisher()
and it’s neat how compact your implementation is. Here’s a couple questions I came away with if it’s helpful feedback, though maybe they get beyond your intended scope: 1. What does typical error-handling look like at the Swift level? My Kotlin instincts get uncomfortable with
eraseToAnyPublisher()
because it sounds like it would discard error information, but from what I can tell it still throws if an error occurs, so maybe it’s fine. What’s the standard best practice for such things in Combine/SwiftUI? 2. Does it make sense to combine
asPublisher()
and
ObservingView
so your View could just call something like
ObservingView(cflow: viewModel.output)
? Or do the two live in different places in a more complete app? 3. Are there any useful patterns for passing interactions back to the viewmodel?
l

leandro

03/03/2021, 5:06 PM
great article! I also had the same #1 question that Russell has. For the #3, I wonder if we could have a
SendChannel<UiEvent>
and have the click listeners send typed events to the view model
r

russhwolf

03/07/2021, 6:00 PM
@Lammert Westerhoff Do you plan to publish your article soon? I’ve been putting a little sample app together using similar patterns and would love to be able to cite it.
For the record, I’ve answered my question 2 by realizing that (at least for my use-case) I wanted to be able to perform some Combine operations on my publisher before the view observes it, meaning having those pieces separate is helpful.
l

Lammert Westerhoff

03/08/2021, 6:12 PM
thank you for the feedback. to answer the questions: 1. Using
eraseToAnyPublisher()
does not erase error/failing cases as the name might suggest. It just erases the concrete type. All publishers are concrete types of the
Publisher
protocol. But since protocols in Swift have associated types instead of generic types using
Publisher
is not very useful. You can’t for example do something like
Publisher<String, Never>
.
AnyPublisher
is used instead. It’s a concrete type that you can convert any other publisher to so you can use
AnyPublisher
everywhere as if you would a protocol with a generic type. It’s a bit out of scope of the article since it’s more of a Swift thing, commonly misunderstood especially by those new to Swift. More related to this article: I’m using
Never
in
AnyPublisher<T, Never>
since I prefer to already do the error handing in my shared viewmodel and then for example add things like
val errorMessage: String?
to the view model output. 2. You could indeed create
ObservingView
with a cflow directly but since it’s only one additional function
asPublisher
I like to keep them separate to avoid adding initializers for all kind of different ways of constructing publishers. And like you mentioned you will soon find situations in which you need them to be separate things. 3. there are and I’ve used a variety of different ways now. some work better than others with SwiftUI. I’m planning to write follow up articles for error handling and for 3. the interactions between the view model. And I’ve just published the post on Medium: https://lwesterhoff.medium.com/using-swiftui-with-view-model-written-in-kotlin-multiplatform-mobile-67cf7b6da551
r

russhwolf

03/08/2021, 6:13 PM
nice!
Yeah I also realized after-the-fact that
eraseToAnyPublisher()
isn't really the source of my error-handling confusion. But it still seemed like SwiftUI wants a
Publisher<String, Never>
so I'm not clear on what the standard Swift-level error-handling looks like.
This was my sample if you're interested: https://github.com/russhwolf/To-Do
l

Lammert Westerhoff

03/08/2021, 6:38 PM
nice. seeing your example I see in the FlowAdapter that you also complete it. I guess the CFlow should also actually do that but in practice I’m never using flows that are really completed. do you have any use case for this?
r

russhwolf

03/08/2021, 6:42 PM
You could use the same adapter for something like RxSwift and that lets you forward completion to an Observable there. I talk about the pattern in this blog post: https://dev.to/touchlab/working-with-kotlin-coroutines-and-rxswift-24fa
l

Lammert Westerhoff

03/08/2021, 6:45 PM
Thanks, I’ll have a look. The project I’ve been working on the last 2 years actually also uses RxSwift instead of Combine. In there I have functions like
asDriver
and
asSignal
which essentially do the same as
asPublisher
public func asDriver<T>(_ flow: CFlow<T>) -> Driver<T?> {
    return asObservable(flow).asDriver(onErrorDriveWith: .empty())
}

public func asSignal<T>(_ flow: CFlow<T>) -> Signal<T?> {
    return asObservable(flow).asSignal(onErrorSignalWith: .empty())
}

public func asObservable<T>(_ flow: CFlow<T>) -> Observable<T?> {
    return Observable.create { observer in
        let closable = flow.watch(block: { object in
            observer.onNext(object)
        })
        return Disposables.create {
            closable.close()
        }
    }
}
r

russhwolf

03/08/2021, 6:48 PM
yeah, looks similar. I was just trying to avoid discarding error/completion info. But depending on use-case it might not be necessary.
l

Lammert Westerhoff

03/08/2021, 7:02 PM
in your sample project, have you considered something like the following to keep error information?
class PublishedFlow<T> : ObservableObject {
    @Published
    var output: Result<T, KotlinError>

    init(_ publisher: AnyPublisher<T, KotlinError>, defaultValue: T) {
        output = .success(defaultValue)

        publisher
            .map { Result.success($0) }
            .catch { Result.Publisher(.failure($0)) }
            .replaceError(with: nil)
            .compactMap { $0 }
            .receive(on: DispatchQueue.main)
            .assign(to: &$output)
    }
}
r

russhwolf

03/08/2021, 8:39 PM
Could do that but then also need to adjust the views that consume it so they process the error. That's the piece where I'm not sure if there's any sort of SwiftUI best-practice. Otherwise I'll just throw or discard the error in a different place instead of there, and I don't know what's better to a typical iOS developer.