Ink API が ベータ版となり、アプリに統合できるようになりました。このマイルストーンは、デベロッパーの皆様からの貴重なフィードバックによって実現しました。このフィードバックにより、API のパフォーマンス、安定性、画質が継続的に改善されました。
Google ドキュメント、Pixel Studio、Google フォト、Chrome PDF、YouTube エフェクト メーカーなどの Google アプリや、かこって検索などの Android の独自の機能はすべて、最新の API を使用しています。
このマイルストーンを記念して、あらゆるサイズの Android デバイス(特にタブレットと折りたたみ式スマートフォン)向けに最適化された包括的なメモアプリのサンプルである Cahier をリリースしました。
Cahier とは何ですか?
Cahier(フランス語で「ノート」)は、テキスト、図面、画像を組み合わせてユーザーが考えを記録し整理できるアプリケーションを構築する方法を示すように設計されたサンプルアプリです。
このサンプルは、大画面でのユーザーの生産性と創造性を高めるためのリファレンスとして活用できます。このようなエクスペリエンスを構築するためのベスト プラクティスを紹介し、関連する強力な API と手法に対するデベロッパーの理解と導入を促進します。この投稿では、Cahier のコア機能、主要な API、サンプルを独自のアプリの優れたリファレンスにするアーキテクチャ上の決定事項について説明します。
サンプルで確認できる主な機能は次のとおりです。
- 汎用性の高いメモ作成: テキスト、フリーフォームの描画、画像の添付ファイルなど、1 つのメモ内で複数の形式をサポートする柔軟なコンテンツ作成システムを実装する方法を示します。
- クリエイティブな手書きツール: Ink API を使用して、高性能で低レイテンシの描画エクスペリエンスを実装します。このサンプルでは、さまざまなブラシ、カラー選択ツール、元に戻す/やり直す機能、消しゴムツールを統合する実用的な例を示します。
- ドラッグ&ドロップによる流動的なコンテンツの統合: ドラッグ&ドロップを使用して、受信コンテンツと送信コンテンツの両方を処理する方法を示します。これには、他のアプリからドロップされた画像を受け入れることや、ユーザーがアプリからコンテンツをドラッグしてシームレスに共有できるようにすることが含まれます。
- メモの整理: メモをお気に入りとしてマークして、すばやくアクセスできます。ビューをフィルタして、整理された状態を維持します。
- オフライン ファーストのアーキテクチャ: Room を使用したオフライン ファーストのアーキテクチャで構築されており、すべてのデータがローカルに保存され、インターネット接続がなくてもアプリが完全に機能します。
- 強力なマルチウィンドウとマルチインスタンスのサポート: マルチインスタンスをサポートする方法を紹介します。アプリを複数のウィンドウで起動できるようにすることで、ユーザーが異なるメモを並べて作業できるようになり、大画面での生産性と創造性が向上します。
- すべての画面に対応するアダプティブ UI: ユーザー インターフェースは、ListDetailPaneScaffold と NavigationSuiteScaffold を使用して、さまざまな画面サイズと画面の向きにシームレスに適応し、スマートフォン、タブレット、折りたたみ式デバイスで最適なユーザー エクスペリエンスを提供します。
- システムとの深い統合: システム全体のメモ インテントに応答して、Android 14 以降でアプリをデフォルトのメモアプリにする方法について説明します。これにより、さまざまなシステム エントリ ポイントからコンテンツをすばやくキャプチャできるようになります。
大画面での生産性と創造性を追求
最初のリリースでは、生産性と創造性の両方のユースケースで Cahier を重要な学習リソースにするいくつかのコア機能に焦点を当てて発表します。
適応性の基盤
Cahier は、最初から適応型になるように構築されています。このサンプルでは、material3-adaptive ライブラリの ListDetailPaneScaffold と NavigationSuiteScaffold を使用して、さまざまな画面サイズや画面の向きにアプリのレイアウトをシームレスに適合させています。これは最新の Android アプリにとって重要な要素であり、Cahier は、これを効果的に実装する方法の明確な例を示しています。
マテリアル 3 アダプティブ ライブラリで構築された Cahier アダプティブ UI
主な API と統合を紹介する
このサンプルは、独自のアプリケーションで活用できる強力な生産性向上 API を紹介することに重点を置いています。具体的には、次の API が含まれます。
主な API の詳細
Cahier が統合して優れたメモ作成エクスペリエンスを実現している 2 つの基盤となる API について詳しく見ていきましょう。
Ink API を使用して自然なインク エクスペリエンスを作成する
タッチペン入力により、大画面デバイスがデジタル ノートブックやスケッチブックに変わります。スムーズで自然な手書き入力エクスペリエンスを構築できるよう、Ink API をサンプルの基盤にしました。Ink API を使用すると、クラス最高の低レイテンシで美しいインク ストロークを簡単に作成、レンダリング、操作できます。
Ink API はモジュール式アーキテクチャを採用しているため、アプリの特定のスタックやニーズに合わせてカスタマイズできます。API モジュールには次のものがあります。
- オーサリング モジュール(Compose - views): リアルタイムの手書き入力処理を行い、デバイスが提供できる最低のレイテンシで滑らかなストロークを作成します。
- DrawingSurface では、Cahier は新しく導入された InProgressStrokes コンポーザブルを使用して、スタイラスまたはタッチのリアルタイム入力を処理します。このモジュールは、ポインタ イベントをキャプチャし、可能な限り低いレイテンシでウェットインク ストロークをレンダリングします。
- Strokes モジュール: インク入力とその視覚的表現を表します。ユーザーが線の描画を完了すると、onStrokesFinished コールバックが最終的な Stroke オブジェクトをアプリに提供します。この不変オブジェクトは、完了したインク ストロークを表し、DrawingCanvasViewModel で管理されます。
- レンダリング モジュール: インクストロークを効率的に表示し、Jetpack Compose または Android ビューと組み合わせることができます。
- 既存のストロークと新しく描画されたストロークの両方を表示するために、Cahier はアクティブな描画には DrawingSurface の CanvasStrokeRenderer を使用し、メモの静的なプレビューを表示するには DrawingDetailPanePreview を使用します。このモジュールは、Stroke オブジェクトを Canvas に効率的に描画します。
- ブラシ モジュール(Compose - ビュー): ストロークのビジュアル スタイルを宣言的に定義する方法を提供します。最近のアップデート(alpha03 リリース以降)には、新しい破線ブラシが含まれています。これは、なげなわ選択などの機能で特に便利です。DrawingCanvasViewModel は、currentBrush の状態を保持します。DrawingCanvas のツールボックスを使用すると、ユーザーはさまざまなブラシ ファミリー(StockBrushes.pressurePen() や StockBrushes.highlighter() など)を選択して色を変更できます。ViewModel が Brush オブジェクトを更新します。このオブジェクトは、新しいストロークの InProgressStrokes コンポーザブルで使用されます。
- ジオメトリ モジュール(Compose - ビュー): 消去や選択などの機能でストロークの操作と分析をサポートします。
- ツールボックス内の消しゴムツールと DrawingCanvasViewModel の機能は、geometry モジュールに依存しています。消しゴムが有効になっている場合、ユーザーのジェスチャーのパスの周りに MutableParallelogram が作成されます。消しゴムは、既存のストロークのシェイプと境界ボックスの交差をチェックして、どのストロークを消去するかを判断します。これにより、直感的で正確な消しゴムを実現しています。
- Storage モジュール: インクデータの効率的なシリアル化とデシリアル化の機能を提供し、ディスクとネットワークのサイズを大幅に削減します。描画を保存するために、Cahier は Room データベースに Stroke オブジェクトを永続化します。Converters では、サンプルはストレージ モジュールの encode 関数を使用して、StrokeInputBatch(未加工のポイントデータ)を ByteArray にシリアル化します。バイト配列は、ブラシのプロパティとともに JSON 文字列として保存されます。decode 関数は、メモが読み込まれたときにストロークを再構築するために使用されます。
これらのコアモジュールに加えて、最近のアップデートでは Ink API の機能が拡張されています。
- カスタム
BrushFamilyオブジェクト用の新しい試験運用版 API により、デベロッパーはクリエイティブでユニークなブラシタイプを作成できるようになり、鉛筆やレーザー ポインターなどのブラシツールを作成できるようになります。
Cahier は、高度なクリエイティブの可能性を示すために、以下に示す独自の音楽ブラシなど、カスタム ブラシを活用しています。
Ink API のカスタム ブラシで作成された虹色のレーザー
Ink API のカスタム ブラシで作成された音楽ブラシ
- ネイティブの Jetpack Compose 相互運用性モジュールにより、Compose UI 内に直接インク機能が統合され、より慣用的で効率的な開発エクスペリエンスが実現します。
Ink API には、カスタム実装よりも生産性と創造性を高めるアプリに適した、次のような利点があります。
- 使いやすさ: Ink API はグラフィックとジオメトリの複雑さを抽象化するため、Cahier のコア機能に集中できます。
- パフォーマンス: 低レイテンシのサポートが組み込まれ、レンダリングが最適化されているため、スムーズでレスポンスの良いインク エクスペリエンスが実現します。
- 柔軟性: モジュール式の設計により、必要なコンポーネントを選択できるため、Ink API を Cahier のアーキテクチャにシームレスに統合できます。
Ink API は、ドキュメントのマークアップ、かこって検索、パートナー アプリ(Orion Notes、PDF Scanner など)を含め、多くの Google アプリですでに採用されています。
「かこって検索(CtS)には Ink API を最初に選択しました。豊富なドキュメントを活用して Ink API を簡単に統合できたため、わずか 1 週間で最初の動作プロトタイプを作成できました。Ink のカスタム ブラシのテクスチャとアニメーションのサポートにより、ストローク デザインを迅速に反復処理できました。」 - Jordan Komoda(ソフトウェア エンジニア - Google)
メモの役割を持つデフォルトのメモアプリになる
メモ作成は、大画面デバイスでユーザーの生産性を向上させる重要な機能です。メモのロール機能を使用すると、ロック画面からでも、他のアプリの実行中でも、対応するアプリにアクセスできます。この機能は、システム全体のデフォルトのメモ作成アプリを特定して設定し、コンテンツをキャプチャするために起動する権限を付与します。
Cahier での実装
メモロールの実装には、いくつかの重要なステップがあります。これらはすべてサンプルで示されています。
- マニフェストの宣言: まず、アプリはメモ作成インテントを処理する機能を宣言する必要があります。AndroidManifest.xml で、Cahier は android.intent.action.CREATE_NOTE アクションの
<intent-filter>を含んでいます。これにより、アプリがメモのロールの候補になる可能性があることがシステムに通知されます。 - ロールのステータスの確認: SettingsViewModel は、Android の RoleManager を使用して現在のステータスを判断します。SettingsViewModel は、デバイスでメモのロールが利用可能かどうか(isRoleAvailable)と、Cahier が現在そのロールを保持しているかどうか(isRoleHeld)を確認します。この状態は、Kotlin フローを使用して UI に公開されます。
- ロールのリクエスト: Settings.kt ファイルで、ロールが利用可能だが保持されていない場合、ユーザーに Button が表示されます。ボタンがクリックされると、ViewModel の
requestNotesRole関数が呼び出されます。この関数は、デフォルトのアプリ設定画面を開くインテントを作成します。この画面で、ユーザーは Cahier を選択できます。このプロセスは、インテントの起動と結果の受信を処理する rememberLauncherForActivityResult API を使用して管理されます。 - UI の更新: ユーザーが設定画面から戻ると、ActivityResultLauncher コールバックが ViewModel の関数をトリガーして、役割のステータスを更新します。これにより、アプリがデフォルトになったかどうかを UI に正確に反映できます。
アプリにメモの役割を統合する方法については、メモ作成アプリの作成ガイドをご覧ください。
Lenovo タブレットのデフォルトのメモ作成アプリとしてフローティング ウィンドウで起動する Cahier
大きな進歩: Lenovo がメモのロールを有効化
大画面 Android の生産性向上に向けた大きな一歩として、Android 15 以降を搭載したタブレットでメモロールのサポートが有効になったことをお知らせします。このアップデートにより、メモアプリを更新して、対応する Lenovo デバイスのユーザーがメモアプリをデフォルトに設定できるようになりました。これにより、ロック画面からシームレスにアクセスできるようになり、システムレベルのコンテンツ キャプチャ機能のロックが解除されます。
大手 OEM によるこの取り組みは、Android で真に統合された生産性の高いユーザー エクスペリエンスを実現するうえで、メモの役割がますます重要になっていることを示しています。
マルチインスタンス、マルチウィンドウ、デスクトップ ウィンドウ
大画面での生産性は、情報とワークフローを効率的に管理することにあります。そのため、Cahier は Android の高度なウィンドウ機能を最大限に活用するように構築されており、ユーザーのニーズに合わせて調整できる柔軟なワークスペースを提供します。このアプリは以下をサポートしています。
- マルチ ウィンドウ: 分割画面モードまたはフリーフォーム モードで他のアプリと並行して実行する基本的な機能。これは、Cahier でメモを取りながらウェブページを参照するなどのタスクに不可欠です。
- マルチインスタンス: マルチタスクの真価が発揮される機能です。Cahier を使用すると、アプリの複数の独立したウィンドウを同時に開くことができます。2 つの異なるメモを並べて比較したり、別のウィンドウで図形を描画しながら、一方のウィンドウでテキストメモを参照したりできます。Cahier は、それぞれ独自の状態を持つこれらの個別のインスタンスを管理し、アプリを強力で多面的なツールに変える方法を示しています。
- デスクトップ ウィンドウ: 外部ディスプレイに接続すると、Android デスクトップ モードでタブレットや折りたたみ式スマートフォンをワークステーションに変身させることができます。Cahier はアダプティブ UI で構築され、マルチインスタンスをサポートしているため、この環境でアプリが美しく動作します。ユーザーは、従来のデスクトップと同じように複数の Cahier ウィンドウを開き、サイズ変更や位置調整を行うことができます。これにより、以前はモバイル デバイスでは実現できなかった複雑なワークフローが可能になります。
Google Pixel Tablet でデスクトップ ウィンドウ モードで実行されている Cahier
Cahier でこれらの機能を実装する方法は次のとおりです。
マルチインスタンスを有効にするには、まず AndroidManifest の MainActivity の宣言に PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI プロパティを追加して、アプリが複数回起動されることをサポートしていることをシステムに通知する必要がありました。
<activity android:name="com.example.cahier.MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.MyApplication" android:showWhenLocked="true" android:turnScreenOn="true" android:resizeableActivity="true" android:launchMode="singleInstancePerTask"> <property android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI" android:value="true"/> ... </activity>
次に、アプリの新しいインスタンスを起動するロジックを実装しました。CahierHomeScreen.kt では、ユーザーが新しいウィンドウでメモを開くことを選択すると、新しいアクティビティの起動をシステムに指示する特定のフラグを含む新しいインテントを作成します。FLAG_ACTIVITY_NEW_TASK、FLAG_ACTIVITY_MULTIPLE_TASK、FLAG_ACTIVITY_LAUNCH_ADJACENT を組み合わせることで、メモが既存のウィンドウと並んで新しい別のウィンドウで開きます。
fun openNewWindow(activity: Activity?, note: Note) {
val intent = Intent(activity, MainActivity::class.java)
intent.putExtra(AppArgs.NOTE_TYPE_KEY, note.type)
intent.putExtra(AppArgs.NOTE_ID_KEY, note.id)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK or
Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
activity?.startActivity(intent)
}マルチ ウィンドウ モードをサポートするには、マニフェストの <activity> 要素または <application> 要素を設定して、アプリがサイズ変更可能であることをシステムに通知する必要がありました。
<activity android:name="com.example.cahier.MainActivity" android:resizeableActivity="true" ...> </activity>
UI 自体が Material 3 アダプティブ ライブラリで構築されているため、Android の分割画面モードなどのマルチ ウィンドウ シナリオでシームレスに適応できます。
ユーザー エクスペリエンスを向上させるため、ドラッグ&ドロップのサポートを追加しました。Cahier での実装方法については、以下をご覧ください。
ドラッグ&ドロップ
生産性や創造性を高めるアプリは、単独で機能するのではなく、デバイスのエコシステムの他の部分とシームレスに連携します。ドラッグ&ドロップは、特に大画面でユーザーが複数のアプリ ウィンドウを操作することが多い場合に、このインタラクションの要となります。Cahier は、コンテンツの追加と共有の両方で直感的なドラッグ&ドロップ機能を実装することで、このコンセプトを完全に採用しています。
- 簡単なインポート: ウェブブラウザ、フォト ギャラリー、ファイル マネージャーなどの他のアプリから画像をドラッグして、メモ キャンバスに直接ドロップできます。このため、Cahier は dragAndDropTarget 修飾子を使用してドロップ ゾーンを定義し、互換性のあるコンテンツ(
image/*など)を確認して、受信した URI を処理します。 - シンプルな共有: Cahier 内のコンテンツは、他のアプリのコンテンツと同じように簡単に共有できます。テキストメモ内の画像を長押しするか、描画メモと画像合成のキャンバス全体を長押しして、別のアプリケーションにドラッグできます。
技術的な詳細: 描画キャンバスからのドラッグ
描画キャンバスにドラッグ操作を実装するには、特有の課題があります。DrawingSurface では、ライブ描画入力(Ink API の InProgressStrokes)を処理するコンポーザブルと、ドラッグを開始する長押し操作を検出する Box が兄弟コンポーザブルです。
デフォルトでは、Jetpack Compose ポインタ入力システムは、タッチ位置と重なる宣言順序の最初の兄弟コンポーザブルのみがイベントを受け取るように設計されています。Cahier の場合、InProgressStrokes コンポーザブルが描画に使用されていないすべての入力を消費してから、その入力を消費する前に、ドラッグ&ドロップ入力処理ロジックが実行され、入力を消費する可能性があります。順序が正しくないと、Box はドラッグを開始する長押しジェスチャーを検出できず、InProgressStrokes は描画する入力を受け取れません。
この問題を解決するために、カスタムの pointerInputWithSiblingFallthrough 修飾子を作成し、その修飾子を使用する Box をコンポーザブル コードの InProgressStrokes の前に配置しました。このユーティリティは、標準の pointerInput システムの薄いラッパーですが、1 つの重要な変更があります。それは、sharePointerInputWithSiblings() 関数をオーバーライドして true を返すことです。これにより、Compose フレームワークは、ポインタ イベントが消費された後でも、兄弟コンポーザブルに渡されることを許可します。
internal fun Modifier.pointerInputWithSiblingFallthrough(
pointerInputEventHandler: PointerInputEventHandler
) = this then PointerInputSiblingFallthroughElement(pointerInputEventHandler)
private class PointerInputSiblingFallthroughModifierNode(
pointerInputEventHandler: PointerInputEventHandler
) : PointerInputModifierNode, DelegatingNode() {
var pointerInputEventHandler: PointerInputEventHandler
get() = delegateNode.pointerInputEventHandler
set(value) {
delegateNode.pointerInputEventHandler = value
}
val delegateNode = delegate(
SuspendingPointerInputModifierNode(pointerInputEventHandler)
)
override fun onPointerEvent(
pointerEvent: PointerEvent,
pass: PointerEventPass,
bounds: IntSize
) {
delegateNode.onPointerEvent(pointerEvent, pass, bounds)
}
override fun onCancelPointerInput() {
delegateNode.onCancelPointerInput()
}
override fun sharePointerInputWithSiblings() = true
}
private data class PointerInputSiblingFallthroughElement(
val pointerInputEventHandler: PointerInputEventHandler
) : ModifierNodeElement<PointerInputSiblingFallthroughModifierNode>() {
override fun create() = PointerInputSiblingFallthroughModifierNode(pointerInputEventHandler)
override fun update(node: PointerInputSiblingFallthroughModifierNode) {
node.pointerInputEventHandler = pointerInputEventHandler
}
override fun InspectorInfo.inspectableProperties() {
name = "pointerInputWithSiblingFallthrough"
properties["pointerInputEventHandler"] = pointerInputEventHandler
}
}DrawingSurface での使用方法は次のとおりです。
Box(
modifier = Modifier
.fillMaxSize()
// Our custom modifier enables this gesture to coexist with the drawing input.
.pointerInputWithSiblingFallthrough {
detectDragGesturesAfterLongPress(
onDragStart = { onStartDrag() },
onDrag = { _, _ -> /* consume drag events */ },
onDragEnd = { /* No action needed */ }
)
}
)
// The Ink API's composable for live drawing sits here as a sibling.
InProgressStrokes(...)この設定により、システムは描画ストロークと長押しドラッグ ジェスチャーの両方を同時に正しく検出します。ドラッグが開始されると、FileProvider を使用して共有可能な content:// URI を作成し、view.startDragAndDrop() を使用して URI をシステムのドラッグ&ドロップ フレームワークに渡します。このソリューションは、堅牢で直感的なユーザー エクスペリエンスを保証し、レイヤード UI で複雑な操作の競合を克服する方法を示しています。
モダンな建築様式で建てられた
特定の API を超えて、Cahier は高品質で適応性のあるアプリケーションを構築するための重要なアーキテクチャ パターンを示しています。
プレゼンテーション レイヤ: Jetpack Compose と適応性
プレゼンテーション レイヤは Jetpack Compose で完全に構築されています。前述のとおり、Cahier は UI の適応性に material3-adaptive ライブラリを採用しています。状態管理は厳密な単方向データフロー(UDF)パターンに従い、ViewModel インスタンスはメモ情報と UI の状態を保持するデータ コンテナとして使用されます。
データレイヤ: リポジトリと Room
データレイヤでは、Cahier は NoteRepository インターフェースを使用してすべてのデータ オペレーションを抽象化します。この設計により、アプリはローカル データソース(Room)と将来のリモート バックエンドを簡単に切り替えることができます。メモの編集などのアクションのデータフローは次のとおりです。
- Jetpack Compose UI が ViewModel のメソッドをトリガーします。
- ViewModel は NoteRepository からメモを取得し、ロジックを処理して、更新されたメモをリポジトリに渡します。
- NoteRepository は、更新を Room データベースに保存します。
包括的な入力サポート
生産性を真に高めるアプリは、さまざまな入力方法を完璧に処理する必要があります。Cahier は、大画面入力に関するガイドラインに準拠するように構築されており、次の機能をサポートしています。
- タッチペン: Ink API との統合、パーム リジェクション、メモロールの登録、テキスト フィールドでのタッチペン入力、没入モード。
- キーボード: 最も一般的なキーボード ショートカットと組み合わせ(Ctrl+クリック、Meta+クリックなど)をサポートし、キーボード フォーカスを明確に示します。
- マウスとトラックパッド: 右クリックとホバー状態をサポートします。
キーボード、マウス、トラックパッドの高度な操作のサポートは、今後の改善の重要な焦点です。
今すぐ始めましょう
Cahier が、次なる優れたアプリの出発点となることを願っています。このアプリは、適応型 UI、Ink やメモの役割などの強力な API、最新の適応型アーキテクチャを組み合わせる方法を示す包括的なオープンソース リソースとして構築されました。
準備が整ったら
- コードを調べる: GitHub リポジトリにアクセスして、Cahier コードベースを調べ、設計原則がどのように機能しているかを確認します。
- 独自に構築する: Cahier を独自のメモ取り、ドキュメント マークアップ、クリエイティブ アプリケーションの基盤として使用します。
- 投稿: 皆様からの投稿をお待ちしております。Cahier を Android デベロッパー コミュニティにとってさらに優れたリソースにするため、ご協力をお願いいたします。
公式デベロッパー ガイドを確認して、次世代の生産性と創造性を高めるアプリの構築を今すぐ始めましょう。ぜひお試しください。
続きを読む
-
ハウツー
バッテリーの消耗が Android ユーザーにとって最も重要な問題であると認識し、Google はデベロッパーがより省電力なアプリを構築できるよう、さまざまな取り組みを行ってきました。
Alice Yuan • 所要時間: 8 分
-
ハウツー
オンデバイス モデルとクラウドモデルの両方を使用する AI 対応機能の例をご紹介し、ユーザーに喜ばれるエクスペリエンスの作成に役立てていただきたいと考えています。
Thomas Ezan, Ivy Knight • 所要時間: 2 分
-
ハウツー
パフォーマンス レベリング ガイドには 5 つのレベルがあります。レベル 1 から始めます。レベル 1 では、導入の労力が最小限で済むパフォーマンス ツールが導入されます。レベル 5 は、カスタム パフォーマンス フレームワークを維持するためのリソースがあるアプリに最適です。
Alice Yuan • 所要時間: 9 分
メールを受け取る
Android 開発に関する最新の分析情報を毎週メールでお届けします。