Slide 1

Slide 1 text

Flutter × Kotlin Multiplatform by 2021/02/24 #2 #ca_flutter_kmm by Taiki Suzuki iOSΞϓϦʹKMMΛಋೖ͢Δtips

Slide 2

Slide 2 text

ࣗݾ঺հ #ca_flutter_kmm marty_suzuki marty-suzuki Taiki Suzuki

Slide 3

Slide 3 text

ΞδΣϯμ #ca_flutter_kmm • Suspend functionΛiOSͰΩϟϯηϧͰ͖ΔΑ͏ʹ͢Δ • ςετͷͱ͖͚ͩ೚ҙͷϞδϡʔϧΛimport͍ͨ͠ • KMMͷFrameworkΛϓϥΠϕʔτϦϙδτϦͷReleasesʹ
 Ξοϓϩʔυͯ͠CocoaPodsͰDLͰ͖ΔΑ͏ʹ͢Δ • KotlinͰ࣮૷͞ΕͨNSObjectͷαϒΫϥεΛSwizzle͢Δͱ
 Ϋϥογϡ͢Δ

Slide 4

Slide 4 text

Suspend functionΛ iOSͰΩϟϯηϧͰ͖ΔΑ͏ʹ͢Δ #ca_flutter_kmm

Slide 5

Slide 5 text

KotlinͰͷInterface #ca_flutter_kmm public interface VideoRankingApi { public suspend fun getGenreRanking( type: GenreRankingType, limit: Int? ): GenreRankingResponse }

Slide 6

Slide 6 text

SwiftͰݺͼग़ͤΔϝιου #ca_flutter_kmm videoRankingApi.getGenreRanking( type: .free, limit: nil completionHandler: { (response: GenreRankingResponse?, error: Error?) in } ) ※ϝιουͷฦΓ஋͕VoidͳͷͰAPI௨৴ͳͲΛΩϟϯηϧ͢Δज़͕Swift͔ΒΞΫηεͰ͖Δੈքʹ͸ͳ͍

Slide 7

Slide 7 text

SwiftͰҎԼͷΑ͏ʹݺͼग़ͤΔΑ͏ʹ͢Δ #ca_flutter_kmm let job = videoRankingApi.getGenreRanking(type: .free, limit: nil) .subscribe( onSuccess: { (response: GenreRankingResponse?) in }, onThrow: { (throwable: KotlinThrowable) in } ) job.cancel(cause_: nil)

Slide 8

Slide 8 text

Working with Kotlin Coroutines and RxSwift #ca_flutter_kmm https://touchlab.co/kotlin-coroutines-rxswift/

Slide 9

Slide 9 text

Suspend functionΛϥοϓ͢Δ #ca_flutter_kmm class SuspendWrapper(private val suspender: suspend () -> T) { suspend fun suspend(): T = suspender() fun subscribe( onSuccess: (item: T) -> Unit, onThrow: (error: Throwable) -> Unit ) : Job = CoroutineScope(SupervisorJob() + Dispatchers.Main).launch { try { onSuccess(suspender()) } catch (error: Throwable) { onThrow(error) } } }

Slide 10

Slide 10 text

Suspend functionΛϥοϓ͢Δ #ca_flutter_kmm public interface VideoRankingApi { public suspend fun getGenreRanking( type: GenreRankingType, limit: Int? ): SuspendWrapper } ˞%FGFSSFE5Ͱฦ͢ํ๏΋͋Δ͕ɺ%FGFSSFE͸JOUFSGBDFͰ͋ΔͨΊ0CKFDUJWF$ʹม׵͢ΔͱQSPUPDPMʹͳΔ ɹ0CKFDUJWF$ͷQSPUPDPM͸(FOFSJD"SHVNFOUΛ࣋ͨͳ͍ͨΊɺ5ͱఆ͍ٛͯͯ͠΋"OZʹͳͬͯ͠·͏

Slide 11

Slide 11 text

SwiftͰҎԼͷΑ͏ʹݺͼग़ͤΔ #ca_flutter_kmm let job = videoRankingApi.getGenreRanking(type: .free, limit: nil) .subscribe( onSuccess: { (response: GenreRankingResponse?) in }, onThrow: { (throwable: KotlinThrowable) in } ) job.cancel(cause_: nil) ˞σϝϦοτͱͯ͠J04Ͱ্هͷݺͼग़͠ΛͰ͖ΔΑ͏ʹ͢ΔͨΊʹɺ"OESPJEଆͰ͸ຊདྷෆཁͳTVTQFOE Λ ɹܦ༝ͯ͠TVTQFOEGVODUJPOΛݺͼग़͢Α͏ʹͳͬͯ͠·͏

Slide 12

Slide 12 text

Suspend functionΛ iOSͰΩϟϯηϧͰ͖ΔΑ͏ʹ͢Δ #ca_flutter_kmm SuspendWrapperΛ΋ͬͱ׆༻͍ͯ͘͠

Slide 13

Slide 13 text

Koru #ca_flutter_kmm https://github.com/FutureMind/koru

Slide 14

Slide 14 text

KoruͰࣗಈੜ੒͞ΕΔΫϥε #ca_flutter_kmm public class VideoRankingApiNative( private val wrapped: VideoRankingApi ) { public fun getGenreRanking( type: GenreRankingType, limit: Int? ): SuspendWrapper = SuspendWrapper(exportedScopeProvider_mainScopeProvider) { wrapped.getGenreRanking(type, limit) } }

Slide 15

Slide 15 text

KoruΛར༻͢Δ #ca_flutter_kmm @ToNativeClass(launchOnScope = MainScopeProvider::class) public interface VideoRankingApi { public suspend fun getGenreRanking( type: GenreRankingType, limit: Int? ): GenreRankingResponse }

Slide 16

Slide 16 text

KoruΛར༻͢Δ #ca_flutter_kmm @ExportedScopeProvider public class MainScopeProvider : ScopeProvider { override val scope: CoroutineScope = MainScope() }

Slide 17

Slide 17 text

Suspend functionͷؔ਺Λͦͷ··ݺΜͩ৔߹ #ca_flutter_kmm videoRankingApi.getGenreRanking( type: .free, limit: nil completionHandler: { (response: GenreRankingResponse?, error: Error?) in } )

Slide 18

Slide 18 text

KoruͰࣗಈੜ੒͞ΕͨΫϥεͰϥοϓͯ͠ݺͼग़͢ #ca_flutter_kmm let videoRankingApi: VideoRankingApi let api = VideoRankingApiNative(wrapped: videoRankingApi) let job = api.getGenreRanking(type: .free, limit: nil) .subscribe( onSuccess: { (response: GenreRankingResponse?) in }, onThrow: { (throwable: KotlinThrowable) in } ) job.cancel(cause_: nil)

Slide 19

Slide 19 text

SuspendWrapperΛRxSwiftͰදݱ͢ΔͱSingleͱͯ͠ѻ͑Δ #ca_flutter_kmm enum SuspendWrapperError: Error { case invalidResponse case throwable(KotlinThrowable) } extension Single where Element: AnyObject { static func create(_ suspendWrapper: SuspendWrapper) -> Single { Single.create { observer in let job = suspendWrapper.subscribe( onSuccess: { value in guard let value = value else { observer(.failure(SuspendWrapperError.invalidResponse)) return } observer(.success(value)) }, onThrow: { throwable in observer(.failure(SuspendWrapperError.throwable(throwable))) } ) return Disposables.create { job.cancel(cause_: nil) } } } }

Slide 20

Slide 20 text

೚ҙͷΫϥεͰར༻͢Δ৔߹ #ca_flutter_kmm final class VideoRankingRepository { private let videoRankingApi: VideoRankingApiNative init(videoRankingApi: VideoRankingApi) { self.videoRankingApi = VideoRankingApiNative(wrapped: videoRankingApi) } func getGenreRanking() -> Single<[GenreRanking]> { Single.create(videoRankingApi.getGenreRanking(type: .free, limit: nil)) .map { $0.genres.map(GenreRanking.init) } } }

Slide 21

Slide 21 text

Suspend functionΛ iOSͰΩϟϯηϧͰ͖ΔΑ͏ʹ͢Δ #ca_flutter_kmm SuspendWrapperΛར༻ͨ͠ΫϥεΛςετ͢Δ

Slide 22

Slide 22 text

iOSͰͷςετ༻ʹImmediateScopeΛ௥Ճ #ca_flutter_kmm @Suppress("FunctionName") public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main) https://github.com/Kotlin/kotlinx.coroutines/blob/1.4.2/kotlinx-coroutines-core/common/src/CoroutineScope.kt#L84-L104

Slide 23

Slide 23 text

MainScopeΛར༻͍ͯ͠ΔͷͰඇಉظͳςετΛॻ͘ඞཁ͕͋Δ #ca_flutter_kmm class SampleProjectTests: XCTestCase { func testGenreRanking() throws { let fakeApi = FakeVideoRankingApi() let suspender = MockSuspendFunction() fakeApi.genreRankingResponse = suspender.asSuspendWrapper() let repository = VideoRankingRepository(videoRankingApi: fakeApi) let expect = expectation(description: "Wait") var response: [GenreRanking]? let disposable = repository.getVideoGenreRanking() .subscribe(onSuccess: { (value: [GenreRanking]) in response = value expect.fulfill() }) defer { disposable.dispose() } DispatchQueue.main.async { suspender.respond(.success(RankingVideoSeriesResponse.Companion().fake())) } wait(for: [expect], timeout: 0.1) XCTAssertNotNil(response) } } ᶃ ᶄ ᶅ ᶆ ᶇ

Slide 24

Slide 24 text

Dispatchers.Main.immediate͕Main Thread͔ΒͰ΋dispatch͞ΕΔ #ca_flutter_kmm https://github.com/Kotlin/kotlinx.coroutines/issues/2283

Slide 25

Slide 25 text

iOSͰͷςετ༻ʹImmediateScopeΛ௥Ճ #ca_flutter_kmm @Suppress("FunctionName") public fun ImmediateScope() = CoroutineScope( SupervisorJob() + object : CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { block.run() } } )

Slide 26

Slide 26 text

iOSͰͷςετ༻ʹImmediateScopeΛ௥Ճ #ca_flutter_kmm @ExportedScopeProvider public class MainScopeProvider : ScopeProvider { override val scope: CoroutineScope get() = scopeHolder.access { it.scope } private class ScopeHolder(var scope: CoroutineScope) private val scopeHolder = IsolateState { ScopeHolder(MainScope()) } fun setScope(scope: CoroutineScope) { scopeHolder.access { it.scope = scope } } }

Slide 27

Slide 27 text

iOSͰͷςετ༻ʹImmediateScopeΛ௥Ճ #ca_flutter_kmm MainScopeProviderContainerKt .exportedScopeProvider_mainScopeProvider .setScope(scope: ImmediateScopeKt.ImmediateScope())

Slide 28

Slide 28 text

ςετͷSuspendFunction޲͚ͷScopeProviderΛ௥Ճ #ca_flutter_kmm final class ImmediateScopeProvider: ScopeProvider { let scope: CoroutineScope = ImmediateScopeKt.ImmediateScope() }

Slide 29

Slide 29 text

ςετ༻ͷKotlinSuspendFunctionͷϞοΫΛ௥Ճ #ca_flutter_kmm final class MockSuspendFunction: KotlinSuspendFunction0 { private var handler: ((Result) -> Void)? func invoke(completionHandler: @escaping (Any?, Error?) -> Void) { handler = { result in do { completionHandler(try result.get(), nil) } catch { completionHandler(nil, error) } } } func respond(_ result: Result) { handler?(result) } func asSuspendWrapper() -> SuspendWrapper { SuspendWrapper(scopeProvider: ImmediateScopeProvider(), suspender: self) } }

Slide 30

Slide 30 text

ImmediateScopeΛར༻ͯ͠ಉظతʹϢχοτςετΛ࣮ߦ͢Δ #ca_flutter_kmm class SampleProjectTests: XCTestCase { func testGenreRanking() throws { MainScopeProviderContainerKt.exportedScopeProvider_mainScopeProvider .setOverrideScope(scope: ImmediateScopeKt.ImmediateScope()) let fakeApi = FakeVideoRankingApi() let suspender = MockSuspendFunction() fakeApi.genreRankingResponse = suspender.asSuspendWrapper() let repository = VideoRankingRepository(videoRankingApi: fakeApi) let response = BehaviorRelay<[GenreRanking]?>(value: nil) let disposable = repository.getGenreRanking().asObservable().bind(to: response) defer { disposable.dispose() } suspender.respond(.success(GenreRankingResponse.Companion().fake())) XCTAssertNotNil(response.value) } }

Slide 31

Slide 31 text

ςετͷͱ͖͚ͩ ೚ҙͷϞδϡʔϧΛimport͍ͨ͠ #ca_flutter_kmm

Slide 32

Slide 32 text

KMMͷߏ੒ #ca_flutter_kmm module A iOSKMM.framwork

Slide 33

Slide 33 text

KMMͷߏ੒ #ca_flutter_kmm ios-kmm module A module B iOSKMM.framwork

Slide 34

Slide 34 text

KMMͷߏ੒ #ca_flutter_kmm ios-kmm module A module B ios-kmm-mock iOSKMM.framwork iOSKMMMock.framwork

Slide 35

Slide 35 text

KMMͷߏ੒ #ca_flutter_kmm ios-kmm module A module B ios-kmm-mock iOSKMM.framwork iOSKMMMock.framwork ❌

Slide 36

Slide 36 text

KMMͷߏ੒ #ca_flutter_kmm ios-kmm module A module B ios-kmm-mock iOSKMMMock.framwork ios-kmm module A module B iOSKMM.framwork

Slide 37

Slide 37 text

== KMMͷߏ੒ #ca_flutter_kmm iOSKMM.framwork iOSKMMMock.framwork ❌ module A class ApiClient module A class ApiClient

Slide 38

Slide 38 text

ଥڠҊ: ReleaseͱDebugͰϦϯΫ͢ΔFrameworkΛ෼ذ͢Δ #ca_flutter_kmm target ‘SampleProject’ do pod 'iOSKMM', '0.1.0', :configuration => 'Release' pod 'iOSKMMMock', ‘0.1.0', :configuration => 'Debug' end ※ςετλʔήοτͷϏϧυ࣌ʹϦϯΫ͢ΔFrameworkΛมߋ͢ΔεΫϦϓτΛ࣮ߦ͢Δͱ͍͏ख΋͋Γ·͢

Slide 39

Slide 39 text

KMMͷFrameworkΛϓϥΠϕʔτϦϙδτϦͷReleasesʹ
 Ξοϓϩʔυͯ͠CocoaPodsͰDLͰ͖ΔΑ͏ʹ͢Δ #ca_flutter_kmm

Slide 40

Slide 40 text

Use a Kotlin Gradle project as a CocoaPods dependency #ca_flutter_kmm

Slide 41

Slide 41 text

Use a Kotlin Gradle project as a CocoaPods dependency #ca_flutter_kmm https://kotlinlang.org/docs/native-cocoapods.html#add-a-dependency-between-a- kotlin-pod-and-xcode-project-with-one-target

Slide 42

Slide 42 text

όΠφϦΛCocoaPodsܦ༝Ͱ؅ཧ͢Δ #ca_flutter_kmm Private Repository Releases iOSKMM.zip target ‘SampleProject’ do pod 'iOSKMM', '0.1.0' end iOSKMM.framwork iOSKMM.zip iOSKMM.zip Private Specs iOSKMM.podspec iOSKMM.podspec.json iOSKMM.framwork

Slide 43

Slide 43 text

௨ৗͷpodspecͷఆٛ #ca_flutter_kmm Pod::Spec.new do |spec| spec.name = 'iOSKMM' spec.version = '0.1.0' spec.homepage = 'https://github.com/marty-suzuki/kmm' spec.authors = 'marty-suzuki' spec.license = 'LICENSE' spec.summary = 'SUMMARY' spec.source = { :http => 'https://github.com/marty-suzuki/kmm/releases/download/0.1.0/iOSKMM.zip' } spec.vendored_frameworks = 'iOSKMM.framework' end

Slide 44

Slide 44 text

Private RepositoryͷReleases #ca_flutter_kmm ˞1SJWBUF3FQPTJUPSZͳͷͰʮIUUQTHJUIVCDPNNBSUZTV[VLJLNNSFMFBTFTEPXOMPBEJ04,..[JQʯʹ ɹೝূঢ়ଶͰΞΫηεͨ͠ͱͯ͠΋ɺΞΫηεՄೳͳ63-͸BTTFUͷVSMͱͳΔͨΊΤϥʔʹͳͬͯ͠·͏

Slide 45

Slide 45 text

assetsͷURLͷऔಘॲཧΛؚΜͩpodspec #ca_flutter_kmm Pod::Spec.new do |spec| module PrivateRepository class Spec attr_reader :version attr_reader :homepage attr_reader :source def initialize(owner, name, version, source) @version = version @homepage = "https://github.com/#{owner}/#{name}" @source = source end end class SpecLoader def initialize(owner, name, version, token) @owner = owner @name = name @version = version if !token.nil? && !token.empty? @authorizationToken = "Authorization: token #{token}" else @authorizationToken = nil end end def loadPrivateSpec tagResponse = loadTag assetURL = getAssetURL(tagResponse) headers = ["Accept: application/octet-stream"] if [email protected]? headers.push(@authorizationToken) end source = { :http => assetURL, :headers => headers, :type => "zip" } return PrivateRepository::Spec.new(@owner, @name, @version, source) end private def loadTag requestURL = "https://api.github.com/repos/#{@owner}/#{@name}/releases/tags/#{@version}" cmdElements = ["curl"] if [email protected]? cmdElements.push("-sL") cmdElements.push("-H \"#{@authorizationToken}\"") else cmdElements.push("-nsL") end cmdElements.push(requestURL) cmd = cmdElements.join(" ") response = %x[#{cmd}] if response.nil? puts "Failed Request: #{requestURL}" exit! else return response end end def getAssetURL(tagResponse) tag_hash = JSON.load(tagResponse) asset = tag_hash["assets"]&.first assetURL = asset && asset["url"] if assetURL.nil? puts "Can't get asset URL." exit! else return assetURL end end end end privateSpecLoader = PrivateRepository::SpecLoader.new("marty-suzuki", "kmm", "0.1.0", ENV["GITHUB_ACCESS_TOKEN"]) privateSpec = privateSpecLoader.loadPrivateSpec spec.name = 'iOSKMM' spec.version = privateSpec.version spec.homepage = privateSpec.homepage spec.authors = 'marty-suzuki' spec.license = 'LICENSE' spec.summary = 'SUMMARY' spec.source = privateSpec.source spec.vendored_frameworks = 'iOSKMM.framework' end

Slide 46

Slide 46 text

pod repo push࣌ʹpodspec಺ͰassetsͷURLΛऔಘ͢Δ #ca_flutter_kmm iOSKMM.podspec GitHub API GET /repos/{OWNER}/{REPO}/releases/tag/{NAME} ೚ҙͷtagͷreleasesͷ৘ใ Ϩεϙϯεͷassets͔Β೚ҙͷzipϑΝΠϧ໊ͷassetΛऔಘ͢Δ asset[“url”]Λऔಘ͠ɺspecͷsourceʹ୅ೖ͢Δ

Slide 47

Slide 47 text

࠷ऴతͳpodspecͷ಺༰ #ca_flutter_kmm Pod::Spec.new do |spec| spec.name = 'iOSKMM' spec.version = '0.1.0' spec.homepage = 'https://github.com/marty-suzuki/kmm' spec.authors = 'marty-suzuki' spec.license = 'LICENSE' spec.summary = 'SUMMARY' spec.source = { :http => 'https://api.github.com/repos/marty-suzuki/kmm/releases/assets/12345678', :headers => ["Accept: application/octet-stream"], :type => "zip" } spec.vendored_frameworks = 'iOSKMM.framework' end

Slide 48

Slide 48 text

Private RepositoryͷReleases༻ͷpodspec template #ca_flutter_kmm IUUQTHJTUHJUIVCDPNNBSUZTV[VLJ EGFDDCFFDGEEDBCEB

Slide 49

Slide 49 text

Private RepositoryͷReleasesΛར༻͢Δ৔߹ͷ஫ҙ #ca_flutter_kmm • v1.9.xͷCocoaPodsͰ͸ɺ࣮ࡍʹpod installՄೳͳpodspecͰ͋ͬͯ΋pod lib lint࣌ʹ
 vendored_frameworksͷvalidationͰΤϥʔʹͳͬͯ͠·͏ͷͰɺlintΛ࣮ߦ͠ͳ͍
 ͱ͍͏ରԠํ๏͕ݱঢ়ͷճආࡦʢ࠷৽൛Ͱ΋௚ͬͯͳͦ͞͏ʣ • GITHUB_ACESS_TOKENΛར༻ͭͭ͠pod repo push࣌ʹ—use-jsonΛࢦఆ͢Δͱ
 Private Specsͷjsonͷheadersʹ௚઀GITHUB_ACESS_TOKEN͕هࡌ͞Εͯ͠·͏ͷͰ
 .netrcΛར༻͢Δ͜ͱΛਪ঑ • xcframeworkΛར༻͍ͯ͠Δͱ࣮ࡍʹϦϯΫՄೳͰ΋pod repo push࣌ʹlinkerͷΤϥʔ
 ʹͳΔ৔߹͕͋ΔͷͰɺ—skip-import-validationͱ—skip-testsΛࢦఆ͢Δ͜ͱͰճආ • pod install࣌ʹ΋.netrc·ͨ͸GITHUB_ACCESS_TOKEN͕ඞཁʢpod repo pushΛ
 ࣮ߦͨ͠ͱ͖ʹར༻ͨ͠ํʹ߹ΘͤΔʣ

Slide 50

Slide 50 text

KotlinͰ࣮૷͞ΕͨNSObjectͷαϒΫϥεΛ Swizzle͢ΔͱΫϥογϡ͢Δ #ca_flutter_kmm

Slide 51

Slide 51 text

Fixed since kotlin-native v1.4.30 #ca_flutter_kmm https://github.com/JetBrains/kotlin-native/pull/4569

Slide 52

Slide 52 text

e.g. ktor IosResponseReader #ca_flutter_kmm https://github.com/ktorio/ktor/blob/1.5.1/ktor-client/ktor-client-ios/darwin/ src/io/ktor/client/engine/ios/IosResponseReader.kt

Slide 53

Slide 53 text

e.g. ktor IosResponseReader #ca_flutter_kmm

Slide 54

Slide 54 text

e.g. ktor IosResponseReader #ca_flutter_kmm https://github.com/ktorio/ktor/blob/master/ktor-client/ktor-client-ios/ darwin/src/io/ktor/client/engine/ios/IosClientEngine.kt ~ ~

Slide 55

Slide 55 text

e.g. ktor IosResponseReader #ca_flutter_kmm internalͳͷͰKMMଆͰ΋ରԠ͢Δ͜ͱ͕Ͱ͖ͳ͍ forkͯ͠मਖ਼ରԠ͢Δ͔͠ͳ͍?

Slide 56

Slide 56 text

SwiftଆͰURLSessionͷinitializerΛswizzleͯ͠delegateΛೖΕସ͑Δ #ca_flutter_kmm extension URLSession { @objc dynamic fileprivate class func kotlinWorkaroundInitializer( configuration: URLSessionConfiguration, delegate: URLSessionDelegate?, delegateQueue queue: OperationQueue? ) -> URLSession { kotlinWorkaroundInitializer( configuration: configuration, delegate: delegate.map { // xxxio.ktor.client.engine.ios.IosResponseReader0 guard NSStringFromClass(type(of: $0)).contains("ktor") else { return $0 } return WorkaroundDelegateProxy(delegate: $0) }, delegateQueue: queue ) } }

Slide 57

Slide 57 text

SwiftଆͰURLSessionͷinitializerΛswizzleͯ͠delegateΛೖΕସ͑Δ #ca_flutter_kmm fileprivate final class KotlinWorkaroundDelegateProxy: NSObject, URLSessionDataDelegate { private let delegate: URLSessionDataDelegate? init(delegate: URLSessionDelegate) { self.delegate = delegate as? URLSessionDataDelegate } … func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data ) { delegate?.urlSession?(session, dataTask: dataTask, didReceive: data) } }

Slide 58

Slide 58 text

SwiftଆͰURLSessionͷinitializerΛswizzleͯ͠delegateΛೖΕସ͑Δ #ca_flutter_kmm extension ApiClient { private static var _once: () = { typealias FunctionType = (URLSessionConfiguration, URLSessionDelegate?, OperationQueue?) -> URLSession let from = #selector(URLSession.init(configuration:delegate:delegateQueue:) as FunctionType) let to = #selector(URLSession.kotlinWorkaroundInitializer(configuration:delegate:delegateQueue:)) guard let fromMethod = class_getClassMethod(URLSession.self, from), let toMethod = class_getClassMethod(URLSession.self, to) else { return } method_exchangeImplementations(fromMethod, toMethod) }() static func swizzleURLSessionInitializer() { _ = _once } }

Slide 59

Slide 59 text

One more thing… #ca_flutter_kmm

Slide 60

Slide 60 text

Flutter × Kotlin Multiplatform by 2021/xx/xx #3 #ca_flutter_kmm KMMશମʹؔ͢ΔτʔΫ UBLBIJSPN !OFX@SVOOBCMF

Slide 61

Slide 61 text

#ca_flutter_kmm ͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ by Taiki Suzuki iOSΞϓϦʹKMMΛಋೖ͢Δtips