第24回コベチケの会

コベリンの最近の取り組みとして、業務などで役立ちそうな知見を共有する会を開催することになりました。 そのついでに発表に使ったアジェンダもそのまま公開してしまおうという豪快な企画です。

※ アジェンダをそのままコピペして公開したものなので若干見にくい箇所もあるかもしれませんが、ご了承ください。

runCatching vs try-catch in kotlin @numa08

id:numanuma08

結論

どっちがベストってことはない。状況に応じて使い分けたら良いし、チーム内で合意をとっておく事が必要。lintで発見しにくい部分なので、PRのレビューのタイミングで話し合いができたら良い。

about try-catch

Exceptions | Kotlin

javaと似たようなシンタックスで例外をキャッチできるが、javaと違って式なので値を返す。

  • Example
val result = try {
  api.getData()
} (e: IOException) {
  Timber.e(e)
  null
} ?: return
print(result)

about runCatching

runCatching - Kotlin Programming Language

パラメータの無名関数の結果をResultに包む。Resut.fold等で結果を取得できる。

  • Example
val result = runCatching {
  api.getData()
}.fold(
  onSuccess : { it },
  onFailure: {
    Timber.e(it)
    null
  }
) ?: return
print(result)

which should be use ?

try-catchは例外をキャッチするにはややレガシーな記法で、例えば次の課題がある。

  • 複数の処理が連続する場合にどの例外をキャッチするのか分かりにくい
  • 例外クラスの継承関係を意識したcatchが必要

コードで示すと以下のような状態。

try {
  val classAtFirst = api.getClass(grade: 1, class: 1)
  api.getStudents(classAtFirst)
} catch(e: Exception) {
} catch(e: IOException){
}

try内で2つAPIアクセスを実行しているので例外をcatchするとどちらの操作で発生した例外かわかりにくい。またIOExceptionの前にExceptionでキャッチしているため、IOExceptionがスローされてもcatchされない(これはlintで怒られる気がする)

runCatchingによってある程度解決できる。

fun errorHandler(e: Exception, action: String) {
  when(e) {
    is IOException -> Timber.e("IOException at ${action}", e)
    is Exception -> Timber.e("some error at ${action}", e)
  }
}

val classAtFirst = runCatching {
  api.getClass(grade:1, class: 1)
}.fold(
  onSuccess: { it },
  onFailure { 
    errorHandler(it, "getClasses")
    null
  }
) ?: return
val students = runCatching {
  api.getStudents(classAtFirst)
}.fold(
  onSuccess: { it },
  onFailure: {
    errorHandler(it, "getStudents")
    null
  }
) ?: return

runCatchingを使うとtry-catchで実現可能な処理を楽に実装できるため、try-catchを廃してrunCatchingにするとプロジェクト内で統一感が出る。一方、try-catch内の処理が常に1つしか無いなど、runCatchingを使うメリットを感じられない場合はtry-catchでも問題ない。チーム内で話し合って結論を出すのが良い。

off topic

Arrow-ktを使うと例外発生時の処理中断を簡単に実装できるが、初見だと混乱する。

Λrrow Core

val classAtFirst = api.getClass(grade:1, class:1).bind()
// getClassで例外がスローされたら↓の処理は実行されない
val studens = api.getStudents(classAtFirst).bind()
// getStudentsで例外がスローされたら↓は実行されず、Failureが返される
return studens

最近の知見 @mironal

String → Data 変換

// これだと型が Data? なる
let data1 = "AA".data(using: .utf8)

// これだと型が Data になる
let data2 = Data("AA".utf8)

// "AA".data(using: .utf8) は絶対に nil にならない
// このメソッドが nil になるのは
// "ああ".data(using: .ascii) みたいな指定した encoding で表現出来ない文字を指定した場合
// UTF-8 ならすべての文字が表現できるので大丈夫.

UIApplication.sharedApplication.networkActivityIndicatorVisible 無くなってた

iOS 13 から非推奨。 iPhone X でノッチ付きの端末が出てから。

そういや見ないな... お前...

image.png (85.0 kB)

https://developer.apple.com/documentation/uikit/uiapplication/1623102-networkactivityindicatorvisible

null_resettable

最近 null_resettable なるものを見つけた。

image.png (134.6 kB)

Swift が導入されたときに Objc にも nullability の keyword (nonnull, nullable) が追加されたときに null_resettable も導入されてた。

https://nshipster.com/swift-1.2/

nil を set すると nil じゃなくて特定のデフォルト値にリセットされる挙動を示している(以下のような動作)。

if (newValue == nil) {
  value = "default value"
} else {
  value = newValue
}

戻り値は必ず non-nil なので swift では tintColor: UIColor!のような (Implicitly Unwrapped Optionals) な型で表現される。

image.png (129.1 kB)

ちなみにドキュメントには null_resettable キーワードは出てこない。 header にだけ書かれている。

image.png (109.0 kB)

実行中のprogressを表示する (Rx) @takkumattsu

API叩いてる時にProgressを表示したい。結論的にはActionを使うのがいい。

https://github.com/RxSwiftCommunity/Action/blob/master/Demo/ViewController.swift

こんな感じでexecutingで取れる

let sharedAction = Action<SharedInput, String> { input in
    switch input {
    case .barButton: return Observable.just("UIBarButtonItem with 3 seconds delay").delaySubscription(.seconds(3), scheduler: MainScheduler.instance)
    case .button(let title): return .just("UIButton " + title)
    }
}

sharedAction.executing.debounce(.seconds(0), scheduler: MainScheduler.instance).subscribe(onNext: { [weak self] executing in
    if (executing) {
        self?.activityIndicator.startAnimating()
    }
    else {
        self?.activityIndicator.stopAnimating()
    }
})

使うわない時(使わない理由はないけど)はRxSwfitのサンプルにあるようにActivityIndicatorというのを作成して利用するとよさそう。 ちなみにUIActivityIndicatorとは全く関係ないです

//
//  ActivityIndicator.swift
//  RxExample
//
//  Created by Krunoslav Zaher on 10/18/15.
//  Copyright © 2015 Krunoslav Zaher. All rights reserved.
//
import RxSwift
import RxCocoa

private struct ActivityToken<E> : ObservableConvertibleType, Disposable {
    private let _source: Observable<E>
    private let _dispose: Cancelable

    init(source: Observable<E>, disposeAction: @escaping () -> Void) {
        _source = source
        _dispose = Disposables.create(with: disposeAction)
    }

    func dispose() {
        _dispose.dispose()
    }

    func asObservable() -> Observable<E> {
        _source
    }
}

/**
Enables monitoring of sequence computation.
If there is at least one sequence computation in progress, `true` will be sent.
When all activities complete `false` will be sent.
*/
public class ActivityIndicator : SharedSequenceConvertibleType {
    public typealias Element = Bool
    public typealias SharingStrategy = DriverSharingStrategy

    private let _lock = NSRecursiveLock()
    private let _relay = BehaviorRelay(value: 0)
    private let _loading: SharedSequence<SharingStrategy, Bool>

    public init() {
        _loading = _relay.asDriver()
            .map { $0 > 0 }
            .distinctUntilChanged()
    }

    fileprivate func trackActivityOfObservable<Source: ObservableConvertibleType>(_ source: Source) -> Observable<Source.Element> {
        return Observable.using({ () -> ActivityToken<Source.Element> in
            self.increment()
            return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
        }) { t in
            return t.asObservable()
        }
    }

    private func increment() {
        _lock.lock()
        _relay.accept(_relay.value + 1)
        _lock.unlock()
    }

    private func decrement() {
        _lock.lock()
        _relay.accept(_relay.value - 1)
        _lock.unlock()
    }

    public func asSharedSequence() -> SharedSequence<SharingStrategy, Element> {
        _loading
    }
}

extension ObservableConvertibleType {
    public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable<Element> {
        activityIndicator.trackActivityOfObservable(self)
    }
}

https://github.com/ReactiveX/RxSwift/blob/1a1fa37b0d08e0f99ffa41f98f340e8bc60c35c4/RxExample/RxExample/Examples/GitHubSignup/UsingDriver/GithubSignupViewModel2.swift

// viewへのprogress用のプロパティ
let signingIn: Driver<Bool>

:
略
:

func init() {
    let signingIn = ActivityIndicator()
    self.signingIn = signingIn.asDriver()
    _ = input.loginTaps.withLatestFrom(usernameAndPassword)
        .flatMapLatest { pair in
            return API.signup(pair.username, password: pair.password)
                .trackActivity(signingIn)
                .asDriver(onErrorJustReturn: false)
        }
        .flatMapLatest { loggedIn -> Driver<Bool> in
            let message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed"
            return wireframe.promptFor(message, cancelAction: "OK", actions: [])
                // propagate original value
                .map { _ in
                    loggedIn
                }
                .asDriver(onErrorJustReturn: false)
        }
}