Android Studio

開発ツール

Android Studio

概要

Android StudioはGoogle社が開発するAndroid専用の統合開発環境(IDE)です。Android アプリケーション開発に特化し、公式の開発ツールとして世界中のAndroid開発者に使用されています。

詳細

Android StudioはIntelliJ IDEAをベースに2013年にGoogle社によってリリースされたAndroid開発専用IDEです。現在の最新版はAndroid Studio Koala(2024.1.1)で、IntelliJ 2024.1プラットフォームをベースとしています。JavaとKotlinの完全サポート、Android SDKとの深い統合、高速エミュレーター、豊富なデバッグツールなど、Android開発に最適化された環境を提供します。

2024年版では重要な新機能が多数追加されています。生成AI統合により、Google AI SDKを使用したGemini APIテンプレートによってAI機能を持つアプリの開発が可能になりました。Running Devicesウィンドウの拡張により複数デバイスの同時表示、新しいターミナル、エディターの「sticky lines」機能などが追加されています。

Android Studioの最大の特徴は、Android開発に特化した包括的な機能セットです。スマートコードエディター、自動コード補完、リアルタイム監査、Gradle統合、レイアウトエディター、プロファイリングツール、Firebase統合、Google Play Console連携など、Android開発の全工程をカバーしています。AVDエミュレーターは6秒以内の高速起動を実現し、様々なデバイスでのテストを効率化します。

メリット・デメリット

メリット

  • Android公式IDE: Googleによる公式サポートとAndroid SDKとの完全統合
  • 軽量で高速: 軽量設計により高速起動とメモリ効率の良い動作
  • 直感的なUI: 初心者から上級者まで使いやすいインターフェース
  • 豊富なライブラリサポート: 他のIDEより多くのライブラリとの統合
  • 高速エミュレーター: 6秒以内の起動で多様なデバイステストが可能
  • チーム開発支援: コード共有、変更マージ、バージョン管理の優れたサポート
  • 完全無料: 全機能を無償で利用可能

デメリット

  • 高いシステム要件: 最低4GBのRAMと64ビットOSが必要
  • インストール時間: 初回インストールに時間がかかる
  • RAMの大量消費: 大量のメモリを使用し、古いPCでは動作が重い
  • プラグインの影響: 多数のプラグイン使用時にパフォーマンス低下
  • Android専用: Android以外の開発には使用できない
  • 学習コスト: 機能豊富ゆえに初期習得に時間が必要
  • ビルド時間: 大規模プロジェクトでは長いビルド時間

主要リンク

書き方の例

プロジェクト設定例

// MainActivity.kt - Kotlin Activityの基本構造
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapp.ui.theme.MyAppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MainScreen()
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
    var text by remember { mutableStateOf("") }
    val items by viewModel.items.collectAsState()
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        TopAppBar(
            title = { Text("My Android App") }
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        OutlinedTextField(
            value = text,
            onValueChange = { text = it },
            label = { Text("Enter item") },
            modifier = Modifier.fillMaxWidth()
        )
        
        Spacer(modifier = Modifier.height(8.dp))
        
        Button(
            onClick = {
                if (text.isNotEmpty()) {
                    viewModel.addItem(text)
                    text = ""
                }
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Add Item")
        }
        
        Spacer(modifier = Modifier.height(16.dp))
        
        LazyColumn {
            items(items) { item ->
                ItemCard(
                    item = item,
                    onDelete = { viewModel.removeItem(it) }
                )
            }
        }
    }
}

@Composable
fun ItemCard(
    item: String,
    onDelete: (String) -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = item,
                style = MaterialTheme.typography.bodyLarge
            )
            IconButton(onClick = { onDelete(item) }) {
                Icon(
                    imageVector = Icons.Default.Delete,
                    contentDescription = "Delete"
                )
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyAppTheme {
        MainScreen()
    }
}

Gradle ビルド設定

// build.gradle.kts (Module: app)
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("kotlin-kapt")
    id("dagger.hilt.android.plugin")
    id("androidx.navigation.safeargs.kotlin")
}

android {
    namespace = "com.example.myapp"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.myapp"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
        debug {
            isDebuggable = true
            applicationIdSuffix = ".debug"
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }

    buildFeatures {
        compose = true
        viewBinding = true
        dataBinding = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.8"
    }

    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
    implementation("androidx.activity:activity-compose:1.8.2")
    implementation("androidx.compose.ui:ui:1.5.8")
    implementation("androidx.compose.ui:ui-tooling-preview:1.5.8")
    implementation("androidx.compose.material3:material3:1.1.2")
    implementation("androidx.navigation:navigation-compose:2.7.6")
    
    // Hilt Dependency Injection
    implementation("com.google.dagger:hilt-android:2.48")
    kapt("com.google.dagger:hilt-compiler:2.48")
    
    // Network
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
    
    // Database
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    kapt("androidx.room:room-compiler:2.6.1")
    
    // Testing
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.5.8")
    debugImplementation("androidx.compose.ui:ui-tooling:1.5.8")
    debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.8")
}

ViewModel例

// MainViewModel.kt - MVVM アーキテクチャ
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(
    private val repository: ItemRepository
) : ViewModel() {
    
    private val _items = MutableStateFlow<List<String>>(emptyList())
    val items: StateFlow<List<String>> = _items.asStateFlow()
    
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
    
    private val _error = MutableStateFlow<String?>(null)
    val error: StateFlow<String?> = _error.asStateFlow()
    
    init {
        loadItems()
    }
    
    fun addItem(item: String) {
        viewModelScope.launch {
            try {
                _isLoading.value = true
                repository.addItem(item)
                loadItems()
            } catch (e: Exception) {
                _error.value = e.message
            } finally {
                _isLoading.value = false
            }
        }
    }
    
    fun removeItem(item: String) {
        viewModelScope.launch {
            try {
                repository.removeItem(item)
                loadItems()
            } catch (e: Exception) {
                _error.value = e.message
            }
        }
    }
    
    private fun loadItems() {
        viewModelScope.launch {
            try {
                _isLoading.value = true
                _items.value = repository.getAllItems()
                _error.value = null
            } catch (e: Exception) {
                _error.value = e.message
            } finally {
                _isLoading.value = false
            }
        }
    }
}

Room データベース設定

// AppDatabase.kt - Room データベース設定
import androidx.room.*
import androidx.room.Room

@Entity(tableName = "items")
data class ItemEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    @ColumnInfo(name = "name")
    val name: String,
    @ColumnInfo(name = "created_at")
    val createdAt: Long = System.currentTimeMillis()
)

@Dao
interface ItemDao {
    @Query("SELECT * FROM items ORDER BY created_at DESC")
    suspend fun getAllItems(): List<ItemEntity>
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertItem(item: ItemEntity)
    
    @Query("DELETE FROM items WHERE name = :name")
    suspend fun deleteItemByName(name: String)
    
    @Query("DELETE FROM items")
    suspend fun deleteAllItems()
}

@Database(
    entities = [ItemEntity::class],
    version = 1,
    exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun itemDao(): ItemDao
    
    companion object {
        const val DATABASE_NAME = "app_database"
    }
}

// データベースプロバイダー(Hilt)
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    
    @Provides
    @Singleton
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            AppDatabase.DATABASE_NAME
        ).build()
    }
    
    @Provides
    fun provideItemDao(database: AppDatabase): ItemDao {
        return database.itemDao()
    }
}

Android Manifest設定

<!-- AndroidManifest.xml -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApp"
        android:usesCleartextTraffic="true"
        tools:targetApi="31">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.MyApp"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".DetailActivity"
            android:exported="false"
            android:parentActivityName=".MainActivity" />

        <service
            android:name=".BackgroundService"
            android:enabled="true"
            android:exported="false" />

        <receiver
            android:name=".NotificationReceiver"
            android:enabled="true"
            android:exported="false" />
        
    </application>
</manifest>

Unit Test例

// MainViewModelTest.kt - Unit テスト
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import org.junit.*
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.junit.MockitoJUnitRunner

@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
    
    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()
    
    private val testDispatcher = UnconfinedTestDispatcher()
    
    @Mock
    private lateinit var repository: ItemRepository
    
    private lateinit var viewModel: MainViewModel
    
    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
        viewModel = MainViewModel(repository)
    }
    
    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }
    
    @Test
    fun `addItem should update items list`() = runTest {
        // Given
        val newItem = "Test Item"
        val expectedItems = listOf(newItem)
        `when`(repository.getAllItems()).thenReturn(expectedItems)
        
        // When
        viewModel.addItem(newItem)
        
        // Then
        verify(repository).addItem(newItem)
        verify(repository, times(2)).getAllItems() // 初期化時とaddItem後
        assert(viewModel.items.value == expectedItems)
    }
    
    @Test
    fun `removeItem should remove item from list`() = runTest {
        // Given
        val itemToRemove = "Item to remove"
        val updatedItems = emptyList<String>()
        `when`(repository.getAllItems()).thenReturn(updatedItems)
        
        // When
        viewModel.removeItem(itemToRemove)
        
        // Then
        verify(repository).removeItem(itemToRemove)
        verify(repository, times(2)).getAllItems()
        assert(viewModel.items.value == updatedItems)
    }
}