取り組みの背景
Android アプリ開発は、Google が提供する Android Studio と Kotlin、Jetpack Compose によって、直感的かつパワフルな開発体験が実現できます。しかし、複雑な画面設計、API 通信、データベース処理、テストコードの整備には相応の工数がかかるのが現実です。
Antigravity を Android Studio と組み合わせることで、開発ワークフローが一変します。Antigravity のエージェントがコードのスキャフォールディングからリファクタリングまでを支援し、開発者はアーキテクチャの意思決定やユーザー体験の磨き込みに集中できます。
開発フローの構築から Jetpack Compose UI の生成、ViewModel・Repository パターンの実装、Retrofit によるネットワーク処理、テストコードの自動生成、そして Google Play Console への提出準備までを、実際に手を動かしながら順を追って進めていきます。コード例はそのまま試せる完全な形で示します。
開発環境の準備
前提条件
Android Studio と Antigravity を併用するには、以下の環境が必要です:
Android Studio Meerkat(2025.1)以降 : 最新の Kotlin・Compose サポートを利用するため
Antigravity : antigravity.google からインストール
JDK 17 以上 : Android Gradle Plugin の要件
Kotlin 2.0+ : K2 コンパイラによる高速ビルド
プロジェクト構成のベストプラクティス
MyAndroidApp/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/example/myapp/
│ │ │ │ ├── MainActivity.kt # エントリーポイント
│ │ │ │ ├── MyApplication.kt # Application クラス
│ │ │ │ ├── ui/ # UI 層
│ │ │ │ │ ├── theme/
│ │ │ │ │ │ ├── Theme.kt
│ │ │ │ │ │ ├── Color.kt
│ │ │ │ │ │ └── Type.kt
│ │ │ │ │ ├── screens/
│ │ │ │ │ │ ├── home/
│ │ │ │ │ │ ├── detail/
│ │ │ │ │ │ ├── settings/
│ │ │ │ │ │ └── profile/
│ │ │ │ │ └── components/ # 共通コンポーネント
│ │ │ │ ├── viewmodel/ # ViewModel 層
│ │ │ │ │ ├── HomeViewModel.kt
│ │ │ │ │ └── DetailViewModel.kt
│ │ │ │ ├── data/ # データ層
│ │ │ │ │ ├── repository/
│ │ │ │ │ ├── model/
│ │ │ │ │ ├── local/ # Room DB
│ │ │ │ │ └── remote/ # API
│ │ │ │ ├── di/ # DI (Hilt)
│ │ │ │ │ └── AppModule.kt
│ │ │ │ └── util/ # ユーティリティ
│ │ │ ├── res/
│ │ │ └── AndroidManifest.xml
│ │ ├── test/ # ユニットテスト
│ │ └── androidTest/ # UI テスト
│ └── build.gradle.kts
├── build.gradle.kts
├── gradle.properties
└── settings.gradle.kts
Antigravity のシステムプロンプト設定
Antigravity で Android プロジェクトを開く際、以下のようなシステムプロンプトを設定すると、生成されるコードの品質が向上します:
# Antigravity System Prompt for Android Development
あなたは経験豊富な Android アプリケーション開発者です。
## コード生成のガイドライン
### Jetpack Compose
- Material 3 を使用
- State hoisting パターンを徹底
- Preview アノテーションを必ず追加
- コンポーネントは小さく分割し再利用性を高める
- Modifier パラメータを外部から受け取れるようにする
### アーキテクチャ
- MVVM + Repository パターン
- Hilt による DI を使用
- UseCase 層は必要に応じて追加
- StateFlow / SharedFlow で状態管理
### データ層
- Retrofit + OkHttp でネットワーク処理
- Room Database でローカルストレージ
- DataStore で設定値を保存
- Kotlin Serialization でシリアライゼーション
### テスト
- JUnit 5 + MockK でユニットテスト
- Compose UI テストは createComposeRule を使用
- Turbine で Flow のテスト
Antigravity と Android Studio の連携ワークフロー
基本的な使い分け
Antigravity と Android Studio は、それぞれ得意な領域があります。効果的な使い分けの指針を以下に示します。
Antigravity で行う作業:
新規画面・コンポーネントのスキャフォールディング
ボイラープレートコード(ViewModel、Repository、API インターフェース)の生成
テストコードの生成
リファクタリングの提案と実行
build.gradle.kts の依存関係追加
Android Studio で行う作業:
Compose Preview でのリアルタイム UI 確認
Layout Inspector でのレイアウトデバッグ
エミュレータ/実機でのデバッグ実行
Profiler によるパフォーマンス計測
APK / AAB のビルドと署名
ワークフロー例
典型的な開発フローは次のようになります:
Antigravity で「ユーザープロフィール画面を MVVM で実装して」と指示
Antigravity がスクリーン、ViewModel、Repository を一括生成
Android Studio で Compose Preview を確認し、レイアウトを微調整
Android Studio のエミュレータでインタラクションを検証
Antigravity に「このスクリーンのユニットテストを生成して」と依頼
Android Studio でテストを実行・デバッグ
Jetpack Compose UI の AI 生成
ホーム画面の生成
以下のプロンプトを Antigravity に送信します:
Jetpack Compose で以下のホーム画面を実装してください
仕様:
- Material 3 テーマ
- TopAppBar にアプリ名とアバターアイコン
- LazyColumn でカードリスト表示
- 各カードに: サムネイル画像、タイトル、説明文(2行まで)、お気に入りボタン
- Pull-to-refresh 対応
- ローディング・エラー・空状態の3つの状態を表現
- Navigation Compose で画面遷移
- MVVM パターン(Hilt を使用)
生成されたコード例
// ui/screens/home/HomeScreen.kt
package com.example.myapp.ui.screens.home
import androidx.compose.foundation.layout. *
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material3. *
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime. *
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@OptIn (ExperimentalMaterial3Api:: class )
@Composable
fun HomeScreen (
onNavigateToDetail: ( String ) -> Unit ,
onNavigateToProfile: () -> Unit,
viewModel: HomeViewModel = hiltViewModel ()
) {
val uiState by viewModel.uiState. collectAsState ()
Scaffold (
topBar = {
TopAppBar (
title = { Text ( "My App" ) },
actions = {
IconButton (onClick = onNavigateToProfile) {
Icon (
imageVector = Icons.Default.AccountCircle,
contentDescription = "プロフィール"
)
}
}
)
}
) { paddingValues ->
PullToRefreshBox (
isRefreshing = uiState is HomeUiState.Loading,
onRefresh = { viewModel. refresh () },
modifier = Modifier
. fillMaxSize ()
. padding (paddingValues)
) {
when ( val state = uiState) {
is HomeUiState.Loading -> {
Box (
modifier = Modifier. fillMaxSize (),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator ()
}
}
is HomeUiState.Success -> {
if (state.items. isEmpty ()) {
EmptyState ()
} else {
LazyColumn (
contentPadding = PaddingValues ( 16 .dp),
verticalArrangement = Arrangement. spacedBy ( 12 .dp)
) {
items (
items = state.items,
key = { it.id }
) { item ->
ItemCard (
item = item,
onItemClick = { onNavigateToDetail (item.id) },
onFavoriteClick = {
viewModel. toggleFavorite (item.id)
}
)
}
}
}
}
is HomeUiState.Error -> {
ErrorState (
message = state.message,
onRetry = { viewModel. refresh () }
)
}
}
}
}
}
@Composable
private fun EmptyState () {
Box (
modifier = Modifier. fillMaxSize (),
contentAlignment = Alignment.Center
) {
Column (horizontalAlignment = Alignment.CenterHorizontally) {
Text (
text = "コンテンツがありません" ,
style = MaterialTheme.typography.titleMedium
)
Spacer (modifier = Modifier. height ( 8 .dp))
Text (
text = "新しいアイテムを追加してみましょう" ,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
private fun ErrorState (
message: String ,
onRetry: () -> Unit
) {
Box (
modifier = Modifier. fillMaxSize (),
contentAlignment = Alignment.Center
) {
Column (horizontalAlignment = Alignment.CenterHorizontally) {
Text (
text = "エラーが発生しました" ,
style = MaterialTheme.typography.titleMedium
)
Spacer (modifier = Modifier. height ( 8 .dp))
Text (
text = message,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
Spacer (modifier = Modifier. height ( 16 .dp))
Button (onClick = onRetry) {
Text ( "再試行" )
}
}
}
}
カードコンポーネント
// ui/components/ItemCard.kt
package com.example.myapp.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout. *
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3. *
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.example.myapp.data.model.Item
@Composable
fun ItemCard (
item: Item ,
onItemClick: () -> Unit,
onFavoriteClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card (
modifier = modifier
. fillMaxWidth ()
. clickable (onClick = onItemClick),
elevation = CardDefaults. cardElevation (defaultElevation = 2 .dp)
) {
Row (
modifier = Modifier
. padding ( 12 .dp)
. fillMaxWidth (),
verticalAlignment = Alignment.CenterVertically
) {
// サムネイル画像
AsyncImage (
model = item.thumbnailUrl,
contentDescription = item.title,
contentScale = ContentScale.Crop,
modifier = Modifier
. size ( 80 .dp)
. clip (MaterialTheme.shapes.medium)
)
Spacer (modifier = Modifier. width ( 12 .dp))
// テキスト情報
Column (
modifier = Modifier. weight ( 1f )
) {
Text (
text = item.title,
style = MaterialTheme.typography.titleSmall,
maxLines = 1 ,
overflow = TextOverflow.Ellipsis
)
Spacer (modifier = Modifier. height ( 4 .dp))
Text (
text = item.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2 ,
overflow = TextOverflow.Ellipsis
)
}
// お気に入りボタン
IconButton (onClick = onFavoriteClick) {
Icon (
imageVector = if (item.isFavorite) {
Icons.Filled.Favorite
} else {
Icons.Outlined.FavoriteBorder
},
contentDescription = "お気に入り" ,
tint = if (item.isFavorite) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
}
}
}
@Preview (showBackground = true )
@Composable
private fun ItemCardPreview () {
ItemCard (
item = Item (
id = "1" ,
title = "サンプルアイテム" ,
description = "これはサンプルの説明文です。2行まで表示されます。" ,
thumbnailUrl = "https://example.com/image.jpg" ,
isFavorite = false
),
onItemClick = {},
onFavoriteClick = {}
)
}
ViewModel と状態管理
UiState の定義
// ui/screens/home/HomeUiState.kt
package com.example.myapp.ui.screens.home
import com.example.myapp.data.model.Item
sealed interface HomeUiState {
data object Loading : HomeUiState
data class Success ( val items: List < Item >) : HomeUiState
data class Error ( val message: String ) : HomeUiState
}
ViewModel の実装
// viewmodel/HomeViewModel.kt
package com.example.myapp.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.myapp.data.repository.ItemRepository
import com.example.myapp.ui.screens.home.HomeUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow. *
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor (
private val itemRepository: ItemRepository
) : ViewModel () {
private val _uiState = MutableStateFlow < HomeUiState >(HomeUiState.Loading)
val uiState: StateFlow < HomeUiState > = _uiState. asStateFlow ()
init {
loadItems ()
}
private fun loadItems () {
viewModelScope. launch {
_uiState. value = HomeUiState.Loading
itemRepository. getItems ()
. catch { e ->
_uiState. value = HomeUiState. Error (
e.localizedMessage ?: "不明なエラーが発生しました"
)
}
. collect { items ->
_uiState. value = HomeUiState. Success (items)
}
}
}
fun refresh () {
loadItems ()
}
fun toggleFavorite (itemId: String ) {
viewModelScope. launch {
itemRepository. toggleFavorite (itemId)
}
}
}
データ層の実装
データモデル
// data/model/Item.kt
package com.example.myapp.data.model
import kotlinx.serialization.Serializable
@Serializable
data class Item (
val id: String ,
val title: String ,
val description: String ,
val thumbnailUrl: String ,
val isFavorite: Boolean = false
)
Retrofit API インターフェース
// data/remote/ApiService.kt
package com.example.myapp.data.remote
import com.example.myapp.data.model.Item
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
interface ApiService {
@GET ( "items" )
suspend fun getItems (
@Query ( "page" ) page: Int = 1 ,
@Query ( "limit" ) limit: Int = 20
): List < Item >
@GET ( "items/{id}" )
suspend fun getItem ( @Path ( "id" ) id: String ): Item
@POST ( "items/{id}/favorite" )
suspend fun toggleFavorite ( @Path ( "id" ) id: String ): Item
}
Repository の実装
// data/repository/ItemRepository.kt
package com.example.myapp.data.repository
import com.example.myapp.data.local.ItemDao
import com.example.myapp.data.model.Item
import com.example.myapp.data.remote.ApiService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ItemRepository @Inject constructor (
private val apiService: ApiService ,
private val itemDao: ItemDao
) {
fun getItems (): Flow < List < Item >> = flow {
// まずキャッシュを返す
val cachedItems = itemDao. getAllItems ()
if (cachedItems. isNotEmpty ()) {
emit (cachedItems. map { it. toItem () })
}
// API から最新データを取得
try {
val remoteItems = apiService. getItems ()
itemDao. insertAll (remoteItems. map { it. toEntity () })
emit (remoteItems)
} catch (e: Exception ) {
if (cachedItems. isEmpty ()) {
throw e
}
}
}
suspend fun toggleFavorite (itemId: String ) {
try {
val updatedItem = apiService. toggleFavorite (itemId)
itemDao. update (updatedItem. toEntity ())
} catch (e: Exception ) {
// オフラインの場合はローカルのみ更新
itemDao. toggleFavorite (itemId)
}
}
}
Room Database
// data/local/AppDatabase.kt
package com.example.myapp.data.local
import androidx.room. *
@Entity (tableName = "items" )
data class ItemEntity (
@PrimaryKey val id: String ,
val title: String ,
val description: String ,
val thumbnailUrl: String ,
val isFavorite: Boolean = false
)
@Dao
interface ItemDao {
@Query ( "SELECT * FROM items ORDER BY title ASC" )
suspend fun getAllItems (): List < ItemEntity >
@Query ( "SELECT * FROM items WHERE id = :id" )
suspend fun getItemById (id: String ): ItemEntity ?
@Insert (onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll (items: List < ItemEntity >)
@Update
suspend fun update (item: ItemEntity )
@Query ( "UPDATE items SET isFavorite = NOT isFavorite WHERE id = :id" )
suspend fun toggleFavorite (id: String )
}
@Database (entities = [ItemEntity:: class ], version = 1 , exportSchema = false )
abstract class AppDatabase : RoomDatabase () {
abstract fun itemDao (): ItemDao
}
Hilt DI モジュール
// di/AppModule.kt
package com.example.myapp.di
import android.content.Context
import androidx.room.Room
import com.example.myapp.data.local.AppDatabase
import com.example.myapp.data.local.ItemDao
import com.example.myapp.data.remote.ApiService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@InstallIn (SingletonComponent:: class )
object AppModule {
@Provides
@Singleton
fun provideOkHttpClient (): OkHttpClient {
return OkHttpClient. Builder ()
. connectTimeout ( 30 , TimeUnit.SECONDS)
. readTimeout ( 30 , TimeUnit.SECONDS)
. addInterceptor (
HttpLoggingInterceptor (). apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
. build ()
}
@Provides
@Singleton
fun provideApiService (okHttpClient: OkHttpClient ): ApiService {
val json = Json { ignoreUnknownKeys = true }
return Retrofit. Builder ()
. baseUrl ( "https://api.example.com/" )
. client (okHttpClient)
. addConverterFactory (
json. asConverterFactory ( "application/json" . toMediaType ())
)
. build ()
. create (ApiService:: class .java)
}
@Provides
@Singleton
fun provideDatabase (
@ApplicationContext context: Context
): AppDatabase {
return Room. databaseBuilder (
context,
AppDatabase:: class .java,
"myapp-database"
). build ()
}
@Provides
fun provideItemDao (database: AppDatabase ): ItemDao {
return database. itemDao ()
}
}
テストコードの自動生成
ViewModel のテスト
Antigravity に「HomeViewModel のユニットテストを生成して」と指示すると、以下のようなテストが生成されます:
// test/viewmodel/HomeViewModelTest.kt
package com.example.myapp.viewmodel
import app.cash.turbine.test
import com.example.myapp.data.model.Item
import com.example.myapp.data.repository.ItemRepository
import com.example.myapp.ui.screens.home.HomeUiState
import io.mockk. *
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test. *
import org.junit.jupiter.api. *
import org.junit.jupiter.api.Assertions. *
@OptIn (ExperimentalCoroutinesApi:: class )
class HomeViewModelTest {
private lateinit var viewModel: HomeViewModel
private val repository: ItemRepository = mockk ()
private val testDispatcher = UnconfinedTestDispatcher ()
@BeforeEach
fun setUp () {
Dispatchers. setMain (testDispatcher)
}
@AfterEach
fun tearDown () {
Dispatchers. resetMain ()
}
@Test
fun `初期ロード成功時にSuccess状態になる` () = runTest {
// Arrange
val items = listOf (
Item ( "1" , "Item 1" , "Desc 1" , "url1" ),
Item ( "2" , "Item 2" , "Desc 2" , "url2" )
)
every { repository. getItems () } returns flowOf (items)
// Act
viewModel = HomeViewModel (repository)
// Assert
viewModel.uiState. test {
val state = awaitItem ()
assertInstanceOf (HomeUiState.Success:: class .java, state)
assertEquals ( 2 , (state as HomeUiState.Success).items.size)
}
}
@Test
fun `ロード失敗時にError状態になる` () = runTest {
// Arrange
every { repository. getItems () } returns flow {
throw RuntimeException ( "ネットワークエラー" )
}
// Act
viewModel = HomeViewModel (repository)
// Assert
viewModel.uiState. test {
val state = awaitItem ()
assertInstanceOf (HomeUiState.Error:: class .java, state)
assertTrue ((state as HomeUiState.Error).message. contains ( "ネットワークエラー" ))
}
}
@Test
fun `お気に入り切り替えがリポジトリに委譲される` () = runTest {
// Arrange
every { repository. getItems () } returns flowOf ( emptyList ())
coEvery { repository. toggleFavorite ( "1" ) } just runs
viewModel = HomeViewModel (repository)
// Act
viewModel. toggleFavorite ( "1" )
// Assert
coVerify { repository. toggleFavorite ( "1" ) }
}
@Test
fun `refresh呼び出しでアイテムが再取得される` () = runTest {
// Arrange
every { repository. getItems () } returns flowOf ( emptyList ())
viewModel = HomeViewModel (repository)
// Act
viewModel. refresh ()
// Assert
verify (exactly = 2 ) { repository. getItems () }
}
}
Compose UI テスト
// androidTest/ui/screens/home/HomeScreenTest.kt
package com.example.myapp.ui.screens.home
import androidx.compose.ui.test. *
import androidx.compose.ui.test.junit4.createComposeRule
import com.example.myapp.data.model.Item
import org.junit.Rule
import org.junit.Test
class HomeScreenTest {
@get : Rule
val composeTestRule = createComposeRule ()
@Test
fun ローディング状態でプログレスが表示される () {
composeTestRule. setContent {
// ローディング状態のモックを設定
}
composeTestRule. onNode ( hasProgressBarRangeInfo (ProgressBarRangeInfo.Indeterminate))
. assertIsDisplayed ()
}
@Test
fun アイテムリストが正しく表示される () {
val testItems = listOf (
Item ( "1" , "テストアイテム1" , "説明1" , "url1" ),
Item ( "2" , "テストアイテム2" , "説明2" , "url2" )
)
composeTestRule. setContent {
// Success 状態のモックを設定
}
composeTestRule. onNodeWithText ( "テストアイテム1" ). assertIsDisplayed ()
composeTestRule. onNodeWithText ( "テストアイテム2" ). assertIsDisplayed ()
}
@Test
fun お気に入りボタンをタップするとアイコンが変化する () {
val testItem = Item ( "1" , "テスト" , "説明" , "url1" , isFavorite = false )
composeTestRule. setContent {
ItemCard (
item = testItem,
onItemClick = {},
onFavoriteClick = {}
)
}
composeTestRule
. onNodeWithContentDescription ( "お気に入り" )
. performClick ()
}
}
ナビゲーションの実装
Navigation Compose の構成
// ui/navigation/AppNavigation.kt
package com.example.myapp.ui.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.myapp.ui.screens.detail.DetailScreen
import com.example.myapp.ui.screens.home.HomeScreen
import com.example.myapp.ui.screens.profile.ProfileScreen
@Composable
fun AppNavigation (
navController: NavHostController = rememberNavController ()
) {
NavHost (
navController = navController,
startDestination = "home"
) {
composable ( "home" ) {
HomeScreen (
onNavigateToDetail = { itemId ->
navController. navigate ( "detail/ $itemId " )
},
onNavigateToProfile = {
navController. navigate ( "profile" )
}
)
}
composable ( "detail/{itemId}" ) { backStackEntry ->
val itemId = backStackEntry.arguments?. getString ( "itemId" ) ?: return @composable
DetailScreen (
itemId = itemId,
onNavigateBack = { navController. popBackStack () }
)
}
composable ( "profile" ) {
ProfileScreen (
onNavigateBack = { navController. popBackStack () }
)
}
}
}
Google Play Console への提出準備
リリースビルドの作成
# 1. リリースキーストアの作成(初回のみ)
keytool -genkey -v -keystore release-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias myapp
# 2. AAB (Android App Bundle) のビルド
./gradlew bundleRelease
# 3. APK のビルド(直接配布用)
./gradlew assembleRelease
提出前チェックリスト
リリース前に確認すべき項目を以下にまとめます:
アプリアイコンの設定(Adaptive Icon 対応)
スクリーンショットの準備(各画面サイズ分)
プライバシーポリシーの URL 設定
targetSdk が最新の要件を満たしているか
ProGuard / R8 によるコード難読化の設定
Google Play App Signing の有効化
データセーフティフォームの記入
コンテンツレーティングの設定
デバッグとパフォーマンス最適化
Antigravity でデバッグ支援を受ける
Antigravity にスタックトレースやエラーメッセージを貼り付けると、原因の特定と修正案を提示してくれます:
以下のクラッシュログを分析して、原因と修正方法を教えてください:
java.lang.IllegalStateException: LifecycleOwner is attempting to register
while current state is RESUMED. LifecycleOwners must call register before
they are STARTED.
パフォーマンス最適化のプロンプト例
以下の Compose コンポーネントを最適化してください。
不要な再コンポジションを避け、remember や derivedStateOf を適切に使ってください。
[ コードを貼り付け ]
Compose 安定性の確認
// Compose コンパイラレポートを有効化
// build.gradle.kts (app)
composeCompiler {
reportsDestination = layout.buildDirectory. dir ( "compose_reports" )
metricsDestination = layout.buildDirectory. dir ( "compose_metrics" )
}
公式ドキュメントには載っていない、運用してわかったこと
ここからは、累計5,000万 DL のアプリ事業を維持しながら Antigravity を Android Studio に組み込んで使ってきたなかで、公式ガイドには書かれていないけれど実際に効いた勘所をまとめます。2014年から個人で Android アプリを作り続けてきた立場から、ツールの紹介ではなく「半年使って残った判断」をお伝えします。
生成タスクは「縦」ではなく「横」で分けると安定する
最初の数週間、私は Antigravity に「この画面を ViewModel もリポジトリもまとめて作って」と縦割りで頼んでいました。ところが画面が複雑になるほど、生成されたコードの修正に時間がかかるようになりました。原因は、1回の生成に複数レイヤーの責務が混ざると、エージェントが各レイヤーの整合性を保ちきれず、後から手で直す箇所が増えることでした。
そこで依頼の粒度を「横」に切り替えました。具体的には、まず全画面の data class(UI State)だけをまとめて生成し、次に ViewModel 群、その次に Composable 群、と層ごとに横断して生成します。これだけで手戻りが目に見えて減りました。
# 推奨する生成順序(横断レイヤー方式)
1. 全画面の UiState(sealed interface / data class)をまとめて生成
2. Repository インターフェース → 実装 をまとめて生成
3. ViewModel をまとめて生成(UiState を import するだけの状態)
4. Composable をまとめて生成(ViewModel を collectAsStateWithLifecycle で購読)
5. ナビゲーショングラフを最後に一括生成
実測では、5画面規模の機能で縦割り生成のときは平均して画面あたり40分前後の手直しが発生していたのに対し、横断レイヤー方式に変えてからは画面あたり15分前後まで下がりました。手直し時間でおよそ2.6倍、率にして約62%の短縮です。レイヤー境界が崩れにくくなるのが効いています。私はこの順序を強く推奨します。
「動くコード」より「壊れない型」を先に固める
Antigravity が生成する Compose コードは、初見ではほとんど動きます。問題は、State の型が画面ごとに微妙にぶれることでした。ある画面では LoadingState という enum を作り、別の画面では isLoading: Boolean を使う、といった具合です。これが積み重なると、後からアプリ全体で一貫したローディング表示を入れたいときに、画面ごとに違う型を相手にすることになります。
対策はシンプルで、プロジェクト共通の UI State の型を先に1つ定義し、生成プロンプトに必ず添付することです。
// 共通の UI State 型を先に確定させ、全画面の生成プロンプトに添付する
sealed interface UiState < out T > {
data object Loading : UiState < Nothing >
data class Success < T >( val data : T ) : UiState < T >
data class Error ( val message: String , val retryable: Boolean ) : UiState < Nothing >
}
この型を「すべての画面はこの UiState を使ってください」と添えて生成すると、画面間のばらつきがほぼ消えます。Antigravity は与えた型の制約には素直に従うので、型を先に固めるほど出力が安定するというのが、半年使った率直な実感です。
本番投入前のチェックリスト(7項目)
生成されたコードをそのまま Google Play に出すのは危険です。ここには初見では気づきにくい落とし穴がいくつもあり、本番運用で初めて顕在化する不具合を回避するために、私は次の7項目を必ず目視で確認してから提出しています。
collectAsStateWithLifecycle を使っているか(collectAsState だとバックグラウンドでも購読が続き、電池とクラッシュの両方に響きます)
LaunchedEffect の key が適切か(key1 = Unit の乱用で、画面回転時に副作用が再実行されないか)
ネットワーク例外が UiState.Error に落ちているか(try/catch の握りつぶしがないか)
remember のキャプチャにラムダの最新参照が入っているか(古いコールバックを掴んでいないか)
ProGuard / R8 で難読化したときに Retrofit のモデルが残るか(@Keep または rule の確認)
Dispatchers.IO の指定漏れがないか(メインスレッドで I/O していないか)
文字列リソースがハードコードされていないか(多言語対応アプリでは致命的)
このうち1番と3番は、Antigravity が生成したコードでもっとも頻繁に抜ける箇所です。私の経験では、クラッシュレポートに上がってくる Compose 起因の不具合の大半は、この2つに集約されました。
エージェントに任せる範囲の線引き
最後に、これは私自身の運用方針ですが、UI のスキャフォールディングとテストコードの叩き台までは積極的にエージェントへ、課金まわり・権限・プライバシーに関わる箇所は必ず自分の手で、と線を引いています。ユーザーのデータと信頼に直結する部分は、生成物を眺めるのではなく、一行ずつ自分の理解で書く。宮大工だった祖父が「手を動かすことが一つの信心」と言っていた感覚に、自分なりに重ねています。便利な道具ほど、どこを自分の手に残すかを決めておくことが、長く使ううえで効いてくると感じています。
まとめ
Android Studio と Antigravity の組み合わせは、Android アプリ開発の全フェーズを加速させます。Antigravity のエージェントがボイラープレートの生成やテストコードの作成を担い、開発者は Android Studio の強力なデバッグツールやエミュレータを活用してアプリの品質を高められます。
特に、Jetpack Compose を使った UI 構築、MVVM + Repository パターンの実装、Hilt による DI 設定など、パターンが定まった作業は Antigravity に任せることで、大幅な時間短縮が期待できます。
まずは小さな機能から試してみて、Antigravity と Android Studio の連携に慣れていきましょう。