はじめまして.iOSエンジニア新卒一年目のk-koheyです.今回は,AndroidとiOSでコードを共通化したことについて書きます🙌
はじめに
ソースコードは時折負債的な側面を見せますが,一度書いただけで継続的に価値を生み出し続けるという点では資産と言えます.そのため,なるべく同じようなコードは書かずに再利用したいという思いは,エンジニアに限らず共感される方は多いのではないでしょうか.このような市場からの需要もあり,近年ではReact Native(以降,RNと呼びます)やFlutter等の数多くのマルチプラットフォーム技術が業界を賑わせ,実際に使用されるようになっています.そして弊社もその例にもれず,AndroidおよびiOSにおけるWantedly Visitの開発にKotlin Multiplatform Mobile(以降,KMM)を用いてコードの共通化をしています.
本記事ではiOS開発にKMMを用いてみて,開発体験がどのように変わったかをiOSエンジニアの視点から紹介します.なお,KMM導入の経緯や方法について知りたい方はぜひ下記の記事も合わせて読んでみてください.
RNと比べたKMMの特徴
まず,iOS版Wantedly Visit(以降,便宜上iOSアプリと呼びます)では一部の画面をRNを用いて開発していました.そのため,Swiftで書いた画面からRNで構成された画面へ遷移したり,その逆のようなことが起こりえます.そのためには,Swiftで書いたViewControllerでRNで構成された画面実装をブリッジする必要があり,実装のオーバーヘッドが大きくなるおよび他の画面とアーキテクチャを揃えられないという実装上の問題がありました.
一方でKMMを用いると,Kotlinを使って書いたコードがObjective-Cで構成されたフレームワークへトランスパイルされます.よって,単にそのフレームワークをインポートするだけで他のSwift/Objective-Cで書かれたライブラリと同様に扱うことができます.
開発フローの変化
それでは,KMMをiOSアプリへ導入した以前と以後で開発フローがどのように変わったかを紹介します.
iOSアプリの構成
まず,開発フローの説明のためにiOSアプリの構成(簡略版)を示します.Application本体がBusinessLogicモジュールに依存しており,更にBusinessLogicモジュールがLibraryモジュールに依存しています.また,それぞれのモジュール内における代表的なコンポーネント名とその意味の対応は以下のとおりです.
- Application
- UI : UIViewControllerやUIViewを継承して作られたクラス
- Reactor : Viewの状態の保存と更新を行うクラス.詳しくはこちらをご参照ください.
- BusinessLogic
- UseCase : アプリケーション固有のビジネスルールをカプセル化したクラス.
- Repository : バックエンドのDBやローカルストレージ等のリソースにアクセスクラス.
- Library
- Extension : Swiftの言語仕様によって既に実装されたクラスを拡張しているファイル群
- Tracker : ユーザの行動追跡をトラックするクラス
従来の開発工程
さて,この構成において新しい画面を追加するためにはざっくりと以下の工程が必要です.抽象化すると,下位のモジュールから上位のモジュールへ伝搬するように,既存の実装へ手を加えていきます.構成に関わらず,やっている事自体はどのアプリにも共通することだと思います.
- エンドポイントを叩くAPI Client,およびローカルストレージを参照する処理を書く
- 1の工程で作った処理をRepositoryに隠蔽する
- Repositoryや既存のRepositoryから得るデータを1つのユースケースにまとめあげる
- Reactorを作って,UIからのイベントを受け取ってUseCaseを呼び出し自身のStateを更新する.
- レイアウトを組む
- ViewからReactorを参照し,StateのバインディングやUIからのコントロールイベントを伝える.
KMM導入後の変化
ここで,私達のチームが運用しているKMMによって作られたAndroid/iOS共有ライブラリ(以降,便宜上このライブラリもKMMと呼びます)の構成を示します.図中のKotlin Multiplatformと書かれた領域がKMMの実装範囲です.その範囲の中にはiOSアプリにもあった,Reactor,UseCase,およびRepositoryが含まれていることが分かります.
https://www.wantedly.com/companies/wantedly/post_articles/300999 より引用
よって,このKMMをiOSアプリに適用した場合,次のような構成になります.ApplicationからUIを残して,他のコンポーネントは全てKMMに移りました.つまり,iOS側はViewを組むだけで特定の画面を実装することができるようになります.先程説明した工程でいうと,5と6の工程以外必要なくなります.必要なのはたったの2つの工程だけとなります.
実例の紹介
私たちのチームは段階的にKMMへの置き換えを進めています.例えば,下記の動画はKMMの移行を行った画面を動作させている様子です.コンテンツの読み込みや,ユーザからの入力を基にバックエンドの状態を更新するのはKMMの仕事ですが,問題なく動作しているのが分かります.
動作している画面のコード例も動画と併せて下記に掲載します.この実装に手を加えたのはViewやViewContollerのみで,もちろんBusinessLogicモジュールに手は加えていません.
この実装を含めた実験を通してKMMを使用しても問題ないと判断し,いくつかの画面はKMMを適用して既にリリースされており,多くのユーザに使われています.今の所,プロダクション環境におけるKMM要因の問題は報告されていません.
import RxSwift
import UIKit
import KMM
final class ViewController: UIViewController {
// =========省略=========
// KMMからReactorをコンストラクタインジェクションする
// (実際にはRxSwiftを使うためにラップしてから使う).
init(reactor: Reactor) { self.reactor = reactor }
private func bind() {
collectionView
.rx
.itemSelected
.subscribe(onNext: { [weak self] indexPath in
guard let content = self?.dataSource?.itemIdentifier(for: indexPath) else { return }
self?.didTapCell(
Int(content.id.value)
)
})
.disposed(by: disposeBag)
// KMMからimportしたReactorからコンテンツを取得できる
reactor.state.map { $0.contents }
.subscribe(onNext: { [weak self] in self?.update(contents: $0) })
.disposed(by: disposeBag)
}
// UIのイベントをReactorに伝える
private func didTapLikeButton(id: Int) {
reactor.send(Reactor.ActionToggleLike(id: id))
}
}
解決していきたい課題
先程のセクションではKMMを用いて開発フローが短縮されたと述べました.しかし,まだまだ改善していきたい点が残っています.このセクションではKMMおよびその運用によって生じている課題を紹介します.
KMMの実装を開発中にシュッと見れない
KMMはKotlinによって記述されています.そのため,iOSアプリから参照する前処理としてObjective-Cにトランスパイルされています.
普段開発している時は,Appleが提供しているライブラリ以外は基本的にXcodeのコードジャンプ機能を使えば瞬時に実装を見れます.しかし現状私達のチームでは,コードジャンプしても下記のようなトランスパイルされたObjective-Cのヘッダーファイルしか見ることが出来ません.具体的な実装は見ることが出来ませんし,機械的に生成されたものなので可読性もかなり低いです.
__attribute__((swift_name("Hoge")))
@protocol VASFuga <VASHoge>
@required
@property (readonly) int32_t hoge __attribute__((swift_name("hoge")));
@end;
KMMには重要なビジネスルールの実装が多く含まれています.その実装がすぐ確認できないと,KMMを多く触っている人とそれ以外でドメイン知識に差が出てくることを懸念しています.また,おかしな挙動に遭遇したときにすぐに実装を確認できないのは開発を進める上でも不便です.
クリーンビルド毎にKMMのフレームワークの生成が走るためビルド時間が長くなる
私達はKMMをCocoaPodsを使って管理しています.そのため,クリーンビルド後に必ずKMMのビルドが走ります.そして,KMMはビルドの際にフレームワークの生成するようにしているため,かなり時間を食ってしまっています.結果,CI上でのビルドが5分ほど伸びたことが報告されています.
これは,KMM自体の課題というよりはライブラリの管理・配布方法も含めた課題です.よって,ライブラリ管理・配布方法に関するナレッジ,例えばライブラリのPre Buildingやバイナリの配布などで解決できるかもしれません.
iOSエンジニアのKotlin経験が皆無
自分含め私達のチームのiOSエンジニアにKotlinの経験者がいません.その一方で,KMMを専任で保守するチームが居ない前提に立つと,KMMが爆誕した瞬間にiOSエンジニアもKotlinを書くことを強いられます.つまり,iOSエンジニアもKotlinを書けないと今後簡単なバグ修正すらできない可能性があります.
この課題に対してはペアプログラミングやKotlinの勉強会によって解決しようと考えています.少しずつそれらも進めており,新卒のiOSエンジニアがKMMにコントリビュートする等,着実にナレッジは広がっていっています.
オブジェクトの命名や型を不自然に感じることがややある
ネイティブ視点から見ると,プラットフォームの差異のせいで命名や型がおかしいと感じる時がたまにあります.documentを見ると良い感じですが,私達のチームでは下記の事象に悩んでいます.
- Kotlinでは予約語になっていないが,Swiftでは予約語になっている語をオブジェクトやそのプロパティに含まれている場合,自動で改名してくれる(が,結果不自然になる)
- Typeという型をトランスパイルすると,トランスパイル時にType_となる.
- Swiftで一般的に使うのと別の型にトランスパイルされている
- Swiftだと一般的に整数表現にInt型を用いるがトランスパイルされる整数表現はInt64
- KotlinのinterfaceをObjective-Cのinterfaceにトランスパイルする際にKotlin interfaceにあったジェネリクスの情報が消える
まとめ
本記事ではKMMを実際にプロダクションに導入し,どのように開発体験が変わったかについて説明しました.そして,実運用した上で課題と感じている点についても説明しました.使ってみた感想としては,Swiftに触れる機会が減ることに残念な気持ちもありつつですが,概ね良い印象を持っています.
最近は社内外問わずKMMに関する発表がより散見されるようになってきました.その追い風を受けてか,社内でも更にKMMの利用が促進されつつあります.例えば,今まで1つのプロダクトのみの利用でしたが,全社的に利用するKMMライブラリの開発アイデアもでてきています.
この記事を最後まで読んでくださったKotlin好きの方.ぜひ弊社に遊びに来てください.じっくりコトコト,Kotlin Multiplatformについて話しましょう.それでは.
サムネイル画像提供: フリー素材ぱくたそ(www.pakutaso.com)