プログラミング
モバイルアプリのためのGo言語活用
Using Go for Mobile Apps (davidsobsessions.com)
要約
この記事は、開発者が過去1年間、Go Mobileを使用してクロスプラットフォームアプリ「Digital Carrot」を開発した経験を共有するものです。Go言語の強力なライブラリとクロスプラットフォーム対応が選択の理由であり、UIにはFlutterを、ビジネスロジックにはGo Mobileを使用しています。Protobufによる効率的なデータ通信や、Goからネイティブコードへのコールバック実装方法についても詳細に解説されています。
全文翻訳
モバイルアプリのためのGo言語活用
過去1年間、私はGo Mobileを使用してDigital Carrotを開発しており、全体的に良好な経験でした。この記事は、そのプロセスで学んだすべてをまとめたものです。
なぜGo Mobileを選んだのか?
短い答えは、Goが本当に好きで使いたかったからです。より長い答えは、以下の要因の組み合わせです。
Digital Carrotはクロスプラットフォームアプリケーションであり、どこでも簡単にコンパイルして実行できるものを求めていました。
Goは、Digital Carrotをカスタマイズ可能でプラグイン可能にするために使用したいシステム機能の強力なライブラリを持っています。これらの中で最も重要なのは、式構築のためのExprと、JavaScriptプラグインを作成するためのGojaです。
Go Mobileをどのように使用しているか?
Go mobileは、Digital Carrotのすべてのビジネスロジックに使用されています。UIにはFyneを使いたかったのですが、試した結果、まだ成熟していないと判断しました。代わりに、Goバックエンドを持つFlutterを採用しました。
大まかには以下のようになっています。
Flutterはプラットフォームチャネルを介してSwift/Kotlinを呼び出します。
APIコールはバイナリエンコードされたprotobufメッセージです。
ネイティブコードは生のバイナリをGoに転送します。
Goはprotobufメッセージをデコードして応答します。
GoはネイティブAPIと対話する必要がある場合、インターフェースを介してネイティブコードを呼び出すこともできます。
```mermaid
flowchart TD
native["ネイティブコード (Swift, Kotlin)"]
ui["Flutter UI"]
go["Go Mobile Backend"]
ui -- ProtobufバイナリAPIコール --> native
native <-- ProtobufをGoに転送 --> go
go -- ネイティブAPIへのコール (スクリーンタイム、ヘルスケア) --> native
```
FlutterからGoへの通信
前述の通り、FlutterはProtobufメッセージを使用してGoと通信します。Protobufはここで必須です。これにより、JSONオブジェクトの手動マーシャリングやアンマーシャリングを大量に行うことなく、GoとDartでNiceな構造体/クラスを扱うことができます。メッセージをProtobufで定義すれば、DartとGoでNiceなオブジェクトを自動的に取得できます。Flutter、プラットフォームコード、Go間の通信は、基本的なデータ型(文字列、バイナリ、ブール値など)でのみ可能です。そのため、Protobufメッセージはこのユースケースに最適です。
Flutterで新しいプラットフォームチャネルを定義するのも大変な作業です。なぜなら、3つの場所(Flutter、プラットフォーム、Go)で定義する必要があるからです。そのため、すべてのAPIコールを処理する単一の関数を使用することにしました。この関数は、実際のAPIコールを定義するための大きなoneofブロックを持つメッセージを渡します。それは次のようなものになります。
```protobuf
message CarrotAPI {
oneof api {
Function1API function1 = 10;
Function2API function2 = 11;
}
}
```
oneofブロック内の各関数呼び出しは、次のようなものになります。
```protobuf
message Function1API {
message Request {}
message Response {}
Request request = 1;
Response response = 2;
}
```
Flutterはメッセージのリクエスト部分を埋め込み、CarrotAPIオブジェクトに入れてGoに送信します。GoはCarrotAPI.apiに対するswitch文を通じて呼び出される関数を判断し、適切な応答を生成できます。これは少し煩雑ですが、関数ごとに新しいプラットフォームチャネルを作成するよりもはるかに優れており、SwiftとKotlin間で重複する作業を大幅に節約できます。
これをどのように実装するかについては詳しく説明しません。他の記事でより良い説明があります。
GoからSwift/Kotlinへの通信
これは長い間苦労してきた分野です。Goではこのドキュメントがあまり充実していません。基本的に、Goからプラットフォームコードにメッセージを送信するには、プラットフォーム側で実装され、Goコードが呼び出されたときにGoに渡されるGoインターフェースを作成する必要があります。例を見てみましょう。
次のようにGoインターフェースを作成します。
```go
type IosMethods interface {
// Screentime
SetShields([]byte) bool
HasScreentimePermissions() bool
}
```
Go Mobileは、次のようなObjective-CまたはKotlinインターフェースを生成します。
```objc
@interface MobileIosMethods : NSObject <goSeqRefInterface, MobileIosMethods>
@property(strong, readonly) _Nonnull id _ref;
- (BOOL)hasScreentimePermissions;
- (BOOL)setShields:(NSData* _Nullable)p0;
@end
```
これをSwiftまたはKotlinで実装し、Goに渡すことができます。
```swift
class GoScreentime: NSObject, MobileIosMethodsProtocol {
public func setShields(_ p0: Data?) -> Bool {
return setShieldsFromJSONBytes(p0)
}
public func hasScreentimePermissions() -> Bool {
return AuthorizationCenter.shared.authorizationStatus == .approved
}
}
```
```swift
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool {
// ...
self.carrotApi = DigitalCarrot.MobileNewAppleMobileAPI( GoScreentime() )
// ...
}
}
```
ここでも、これらのインターフェースはプリミティブデータ型のみをサポートします。これはProtobufが役立つもう一つの分野ですが、Goからプラットフォームコードへの呼び出しはそれほど多くないため、使用しませんでした。
これに関する詳細情報は、この記事で見つけることができます。
このすべてはどのように機能しているか?
これまでのところ、このスタックは非常に気に入っています。Goでの作業は非常に快適であり、それだけでも私にとっては十分な価値があります。個人的なこだわりを超えて、このアプローチには真剣な長所と短所があります。
良い点
シームレスなサーバー統合
Digital Carrotの同期サーバーもGoで実装されています。これにより、同期テストが非常に簡単になります。クライアントコードをサーバー同期テストに直接インポートできるからです。クライアントとサーバーアプリケーションを異なるスタックで実行するためにDockerを使った複雑なテストハーネスを構築する必要がありません。Macで直接テストを実行でき、テストは20秒未満で完了します。
強力なビジネス/プレゼンテーション層の境界
これはほとんど自明です。ビジネスロジックとプレゼンテーションロジックを混同するのは困難です。なぜなら、それらは異なる言語で書かれているからです。アプリのFlutterコンポーネントはUI(および権限要求などのマイナーなプラットフォーム固有の処理)のみを処理し、Goコンポーネントはすべてのビジネスロジック(サーバーとの通信、データの保存、検証など)を処理します。
API契約のテストが非常に容易になるという大きな利点があります。すべてのビジネスロジックはGoでテストされるため、アプリをテストするために複雑なUIコンポーネントを起動する必要がありません。Goで統合テストを書き、go testで実行するだけです。ここでの予期せぬ利点は、リプレイテストでした。UIでアプリを手動でテストし、UIが行うAPIコールを記録してから、それらのAPIコールをプログラムで再生できます。一部のUIテストは依然として必要ですが、アプリのビジネスロジックが非常に堅牢であると仮定できるため、非常に最小限で済みます。
この関心の分離により、将来的にUIを別のものに置き換えることも非常に容易になります。よりネイティブなエクスペリエンスが必要な場合は、iOSアプリをSwift UIで、AndroidアプリをKotlinで実装できます。Googleがプロジェクトを終了させる傾向があることを考えると、これは非常に心強いです。Goが決して消えないと reasonably 確信できますが、UIの奇妙な世界で何が起こるかは誰にもわかりません。
プルできるライブラリの大規模なコレクション
このアーキテクチャは、GoとDartの両方で利用可能なツールの全スイートにアクセスできることを意味します。Dartエコシステムは、プラットフォーム固有の権限をインターフェースするためのほぼすべてのユーティリティを提供し、Goはシステムプログラミングおよびネットワーキングツールの豊富な品揃えを持っています。
ビジネスロジックはどのプロトコルでも実行可能
通信全体をプロトコルなどのネットワーク対応インターフェースに置くことで、バックエンドはどこでも実行できます。これはWindowsとMacで非常に有利でした。これらのプラットフォームでは、バックエンドはバックグラウンドでデーモンとして実行され、UIはソケットまたはパイプ経由でgRPCを介してそれに接続します。Digital Carrotは、プログラムやウェブサイトをブロックできるように、バックグラウンドで継続的に実行する必要があります。UIのすべての雑多なものをメモリにロードする必要がないため、Goデーモンを単独で実行できるのは素晴らしいことです。その結果、Digital Carrotはメモリ使用量が大幅に削減されます。