DT ツール・運用 Android アプリ開発完全ガイド

Android アプリ開発完全ガイド 2025年版 - Kotlin & Jetpack Compose から Google Play 公開まで

2025年最新のAndroidアプリ開発について、Kotlin、Jetpack Compose、Material Design 3から公開・配信まで実践的に解説します。初心者から上級者まで対応した包括的なガイドです。

約5分で読めます
技術記事
実践的

この記事のポイント

2025年最新のAndroidアプリ開発について、Kotlin、Jetpack Compose、Material Design 3から公開・配信まで実践的に解説します。初心者から上級者まで対応した包括的なガイドです。

この記事では、実践的なアプローチで技術的な課題を解決する方法を詳しく解説します。具体的なコード例とともに、ベストプラクティスを学ぶことができます。

はじめに

Androidアプリ開発は近年大きく進化し、2025年現在ではKotlinとJetpack Composeが標準となっています。本記事では、最新のAndroid 15とAndroid Studio Koalaを使用した現代的なアプリ開発手法を包括的に解説します。

開発環境のセットアップ

必要なツールと環境

graph TD
    A[JDK 17+] --> B[Android Studio Koala]
    B --> C[Android SDK]
    C --> D[Emulator/実機]
    B --> E[Kotlin 2.0]
    E --> F[Jetpack Compose]
    B --> G[Gradle 8.0+]
    G --> H[Google Play Console]

Android Studio の初期設定

// build.gradle.kts (Project level)
plugins {
    id("com.android.application") version "8.2.0" apply false
    id("org.jetbrains.kotlin.android") version "2.0.0" apply false
    id("com.google.dagger.hilt.android") version "2.48" apply false
}

// build.gradle.kts (Module level)
android {
    namespace = "com.example.myapp"
    compileSdk = 35
    
    defaultConfig {
        applicationId = "com.example.myapp"
        minSdk = 24
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"
        
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }
    
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    
    kotlinOptions {
        jvmTarget = "17"
    }
    
    buildFeatures {
        compose = true
    }
    
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.8"
    }
}

Jetpack Compose による UI 開発

基本的なComposable関数

@Composable
fun WelcomeScreen(
    userName: String,
    onLoginClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Icon(
            imageVector = Icons.Default.Person,
            contentDescription = null,
            modifier = Modifier.size(120.dp),
            tint = MaterialTheme.colorScheme.primary
        )
        
        Spacer(modifier = Modifier.height(24.dp))
        
        Text(
            text = "Welcome, $userName!",
            style = MaterialTheme.typography.headlineMedium,
            textAlign = TextAlign.Center
        )
        
        Spacer(modifier = Modifier.height(32.dp))
        
        Button(
            onClick = onLoginClick,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("ログイン")
        }
    }
}

Material Design 3 テーマ

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val lightColorScheme = lightColorScheme(
        primary = Color(0xFF006D40),
        onPrimary = Color.White,
        primaryContainer = Color(0xFF7DF8B1),
        onPrimaryContainer = Color(0xFF002111),
        secondary = Color(0xFF4F6355),
        onSecondary = Color.White,
        surface = Color(0xFFFBFDF8),
        onSurface = Color(0xFF191C19)
    )
    
    val darkColorScheme = darkColorScheme(
        primary = Color(0xFF61DB95),
        onPrimary = Color(0xFF003822),
        primaryContainer = Color(0xFF005230),
        onPrimaryContainer = Color(0xFF7DF8B1),
        secondary = Color(0xFFB7CCB8),
        onSecondary = Color(0xFF23352A),
        surface = Color(0xFF101411),
        onSurface = Color(0xFFE1E4DE)
    )
    
    val colorScheme = if (darkTheme) darkColorScheme else lightColorScheme
    
    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography(),
        content = content
    )
}

状態管理とナビゲーション

@Composable
fun TaskApp() {
    val navController = rememberNavController()
    val viewModel: TaskViewModel = hiltViewModel()
    
    NavHost(
        navController = navController,
        startDestination = "task_list"
    ) {
        composable("task_list") {
            TaskListScreen(
                viewModel = viewModel,
                onTaskClick = { taskId ->
                    navController.navigate("task_detail/$taskId")
                },
                onAddTask = {
                    navController.navigate("add_task")
                }
            )
        }
        
        composable(
            "task_detail/{taskId}",
            arguments = listOf(navArgument("taskId") { type = NavType.StringType })
        ) { backStackEntry ->
            val taskId = backStackEntry.arguments?.getString("taskId")
            TaskDetailScreen(
                taskId = taskId,
                onBackClick = { navController.popBackStack() }
            )
        }
        
        composable("add_task") {
            AddTaskScreen(
                onTaskAdded = { navController.popBackStack() },
                onCancelClick = { navController.popBackStack() }
            )
        }
    }
}

Room データベースとViewModel

Entityとアクセスの定義

@Entity(tableName = "tasks")
data class Task(
    @PrimaryKey
    val id: String = UUID.randomUUID().toString(),
    val title: String,
    val description: String,
    val isCompleted: Boolean = false,
    val priority: TaskPriority,
    val dueDate: Long? = null,
    val createdAt: Long = System.currentTimeMillis()
)

enum class TaskPriority {
    LOW, MEDIUM, HIGH
}

@Dao
interface TaskDao {
    @Query("SELECT * FROM tasks ORDER BY createdAt DESC")
    fun getAllTasks(): Flow<List<Task>>
    
    @Query("SELECT * FROM tasks WHERE id = :id")
    suspend fun getTaskById(id: String): Task?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertTask(task: Task)
    
    @Update
    suspend fun updateTask(task: Task)
    
    @Delete
    suspend fun deleteTask(task: Task)
    
    @Query("SELECT * FROM tasks WHERE isCompleted = 0 ORDER BY priority DESC, dueDate ASC")
    fun getIncompleteTasks(): Flow<List<Task>>
}

@Database(
    entities = [Task::class],
    version = 1,
    exportSchema = false
)
@TypeConverters(Converters::class)
abstract class TaskDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
}

Repository パターンの実装

@Singleton
class TaskRepository @Inject constructor(
    private val taskDao: TaskDao
) {
    fun getAllTasks(): Flow<List<Task>> = taskDao.getAllTasks()
    
    fun getIncompleteTasks(): Flow<List<Task>> = taskDao.getIncompleteTasks()
    
    suspend fun getTaskById(id: String): Task? = taskDao.getTaskById(id)
    
    suspend fun insertTask(task: Task) = taskDao.insertTask(task)
    
    suspend fun updateTask(task: Task) = taskDao.updateTask(task)
    
    suspend fun deleteTask(task: Task) = taskDao.deleteTask(task)
    
    suspend fun toggleTaskCompletion(task: Task) {
        taskDao.updateTask(task.copy(isCompleted = !task.isCompleted))
    }
}

ViewModelによる状態管理

@HiltViewModel
class TaskViewModel @Inject constructor(
    private val repository: TaskRepository
) : ViewModel() {
    
    private val _uiState = MutableStateFlow(TaskUiState())
    val uiState: StateFlow<TaskUiState> = _uiState.asStateFlow()
    
    val tasks = repository.getAllTasks()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
    
    fun addTask(title: String, description: String, priority: TaskPriority) {
        viewModelScope.launch {
            try {
                val task = Task(
                    title = title,
                    description = description,
                    priority = priority
                )
                repository.insertTask(task)
                _uiState.value = _uiState.value.copy(message = "タスクを追加しました")
            } catch (e: Exception) {
                _uiState.value = _uiState.value.copy(
                    error = "タスクの追加に失敗しました: ${e.message}"
                )
            }
        }
    }
    
    fun toggleTask(task: Task) {
        viewModelScope.launch {
            repository.toggleTaskCompletion(task)
        }
    }
    
    fun deleteTask(task: Task) {
        viewModelScope.launch {
            repository.deleteTask(task)
        }
    }
}

data class TaskUiState(
    val isLoading: Boolean = false,
    val error: String? = null,
    val message: String? = null
)

通信とAPI連携

Retrofit によるネットワーク通信

data class ApiTask(
    val id: String,
    val title: String,
    val description: String,
    val completed: Boolean,
    val priority: String,
    val dueDate: String?
)

interface TaskApiService {
    @GET("tasks")
    suspend fun getTasks(): Response<List<ApiTask>>
    
    @POST("tasks")
    suspend fun createTask(@Body task: ApiTask): Response<ApiTask>
    
    @PUT("tasks/{id}")
    suspend fun updateTask(
        @Path("id") id: String,
        @Body task: ApiTask
    ): Response<ApiTask>
    
    @DELETE("tasks/{id}")
    suspend fun deleteTask(@Path("id") id: String): Response<Unit>
}

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            })
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .build()
    }
    
    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    @Provides
    @Singleton
    fun provideTaskApiService(retrofit: Retrofit): TaskApiService {
        return retrofit.create(TaskApiService::class)
    }
}

オフライン対応の実装

@Singleton
class SyncRepository @Inject constructor(
    private val localTaskDao: TaskDao,
    private val apiService: TaskApiService,
    private val networkConnectivityManager: NetworkConnectivityManager
) {
    
    suspend fun syncTasks(): Result<Unit> {
        return try {
            if (!networkConnectivityManager.isConnected()) {
                return Result.failure(Exception("ネットワーク接続がありません"))
            }
            
            // ローカルの変更をサーバーに同期
            val localTasks = localTaskDao.getAllTasks().first()
            localTasks.filter { it.needsSync }.forEach { task ->
                val apiTask = task.toApiTask()
                apiService.updateTask(task.id, apiTask)
                localTaskDao.updateTask(task.copy(needsSync = false))
            }
            
            // サーバーから最新データを取得
            val response = apiService.getTasks()
            if (response.isSuccessful) {
                response.body()?.let { apiTasks ->
                    val localTaskEntities = apiTasks.map { it.toLocalTask() }
                    localTaskDao.deleteAllAndInsert(localTaskEntities)
                }
            }
            
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

カメラとギャラリー機能

CameraX の実装

@Composable
fun CameraScreen(
    onImageCaptured: (Uri) -> Unit,
    onError: (String) -> Unit
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    
    var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) }
    val imageCapture = remember { ImageCapture.Builder().build() }
    
    LaunchedEffect(Unit) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
        cameraProvider = cameraProviderFuture.get()
    }
    
    Box(modifier = Modifier.fillMaxSize()) {
        cameraProvider?.let { provider ->
            AndroidView(
                factory = { context ->
                    PreviewView(context).apply {
                        val preview = Preview.Builder().build()
                        preview.setSurfaceProvider(surfaceProvider)
                        
                        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
                        
                        try {
                            provider.unbindAll()
                            provider.bindToLifecycle(
                                lifecycleOwner,
                                cameraSelector,
                                preview,
                                imageCapture
                            )
                        } catch (e: Exception) {
                            onError("カメラの初期化に失敗しました")
                        }
                    }
                },
                modifier = Modifier.fillMaxSize()
            )
        }
        
        FloatingActionButton(
            onClick = {
                captureImage(context, imageCapture, onImageCaptured, onError)
            },
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(16.dp)
        ) {
            Icon(Icons.Default.Camera, contentDescription = "写真を撮る")
        }
    }
}

private fun captureImage(
    context: Context,
    imageCapture: ImageCapture,
    onImageCaptured: (Uri) -> Unit,
    onError: (String) -> Unit
) {
    val outputFileOptions = ImageCapture.OutputFileOptions.Builder(
        context.contentResolver,
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}")
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        }
    ).build()
    
    imageCapture.takePicture(
        outputFileOptions,
        ContextCompat.getMainExecutor(context),
        object : ImageCapture.OnImageSavedCallback {
            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                output.savedUri?.let { uri ->
                    onImageCaptured(uri)
                }
            }
            
            override fun onError(exception: ImageCaptureException) {
                onError("写真の保存に失敗しました")
            }
        }
    )
}

パフォーマンス最適化

LazyColumn の最適化

@Composable
fun OptimizedTaskList(
    tasks: List<Task>,
    onTaskClick: (String) -> Unit,
    onTaskToggle: (Task) -> Unit
) {
    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(
            items = tasks,
            key = { task -> task.id }
        ) { task ->
            TaskItem(
                task = task,
                onClick = { onTaskClick(task.id) },
                onToggle = { onTaskToggle(task) },
                modifier = Modifier.animateItemPlacement()
            )
        }
    }
}

@Composable
fun TaskItem(
    task: Task,
    onClick: () -> Unit,
    onToggle: () -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .clickable { onClick() },
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Row(
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Checkbox(
                checked = task.isCompleted,
                onCheckedChange = { onToggle() }
            )
            
            Spacer(modifier = Modifier.width(12.dp))
            
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = task.title,
                    style = MaterialTheme.typography.titleMedium,
                    textDecoration = if (task.isCompleted) {
                        TextDecoration.LineThrough
                    } else {
                        TextDecoration.None
                    }
                )
                
                if (task.description.isNotEmpty()) {
                    Text(
                        text = task.description,
                        style = MaterialTheme.typography.bodyMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                        maxLines = 2,
                        overflow = TextOverflow.Ellipsis
                    )
                }
            }
            
            PriorityIndicator(priority = task.priority)
        }
    }
}

メモリリーク対策

@Composable
fun ImageWithCache(
    imageUrl: String,
    contentDescription: String?,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    
    AsyncImage(
        model = ImageRequest.Builder(context)
            .data(imageUrl)
            .crossfade(true)
            .memoryCachePolicy(CachePolicy.ENABLED)
            .diskCachePolicy(CachePolicy.ENABLED)
            .build(),
        contentDescription = contentDescription,
        modifier = modifier,
        contentScale = ContentScale.Crop,
        placeholder = painterResource(R.drawable.placeholder),
        error = painterResource(R.drawable.error_image)
    )
}

テストの実装

Unit Test

class TaskRepositoryTest {
    
    @Mock
    private lateinit var taskDao: TaskDao
    
    private lateinit var repository: TaskRepository
    
    @Before
    fun setup() {
        MockitoAnnotations.openMocks(this)
        repository = TaskRepository(taskDao)
    }
    
    @Test
    fun `getAllTasks returns flow of tasks`() = runTest {
        // Given
        val expectedTasks = listOf(
            Task(id = "1", title = "タスク1", description = "説明1", priority = TaskPriority.HIGH),
            Task(id = "2", title = "タスク2", description = "説明2", priority = TaskPriority.LOW)
        )
        whenever(taskDao.getAllTasks()).thenReturn(flowOf(expectedTasks))
        
        // When
        val result = repository.getAllTasks().first()
        
        // Then
        assertEquals(expectedTasks, result)
    }
    
    @Test
    fun `insertTask calls dao insertTask`() = runTest {
        // Given
        val task = Task(title = "新しいタスク", description = "説明", priority = TaskPriority.MEDIUM)
        
        // When
        repository.insertTask(task)
        
        // Then
        verify(taskDao).insertTask(task)
    }
}

UI Test

@RunWith(AndroidJUnit4::class)
class TaskListScreenTest {
    
    @get:Rule
    val composeTestRule = createComposeRule()
    
    @Test
    fun taskListDisplaysTasks() {
        val testTasks = listOf(
            Task(id = "1", title = "タスク1", description = "説明1", priority = TaskPriority.HIGH),
            Task(id = "2", title = "タスク2", description = "説明2", priority = TaskPriority.LOW)
        )
        
        composeTestRule.setContent {
            MyAppTheme {
                TaskListScreen(
                    tasks = testTasks,
                    onTaskClick = {},
                    onTaskToggle = {},
                    onAddTask = {}
                )
            }
        }
        
        composeTestRule.onNodeWithText("タスク1").assertIsDisplayed()
        composeTestRule.onNodeWithText("タスク2").assertIsDisplayed()
    }
    
    @Test
    fun addButtonNavigatesToAddScreen() {
        composeTestRule.setContent {
            MyAppTheme {
                TaskListScreen(
                    tasks = emptyList(),
                    onTaskClick = {},
                    onTaskToggle = {},
                    onAddTask = {}
                )
            }
        }
        
        composeTestRule.onNodeWithContentDescription("タスクを追加").performClick()
        // ナビゲーションの検証
    }
}

Google Play での公開準備

リリース用ビルド設定

// build.gradle.kts
android {
    signingConfigs {
        create("release") {
            keyAlias = project.findProperty("MYAPP_RELEASE_KEY_ALIAS") as String
            keyPassword = project.findProperty("MYAPP_RELEASE_KEY_PASSWORD") as String
            storeFile = file(project.findProperty("MYAPP_RELEASE_STORE_FILE") as String)
            storePassword = project.findProperty("MYAPP_RELEASE_STORE_PASSWORD") as String
        }
    }
    
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
            signingConfig = signingConfigs.getByName("release")
        }
    }
}

ProGuard設定

# proguard-rules.pro

# Retrofit用の設定
-keepattributes Signature, InnerClasses, EnclosingMethod
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
-keepclassmembers,allowshrinking,allowobfuscation interface * {
    @retrofit2.http.* <methods>;
}

# Room用の設定
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
-dontwarn androidx.room.paging.**

# Gson用の設定
-keepattributes Signature
-keepattributes *Annotation*
-dontwarn sun.misc.**
-keep class com.google.gson.** { *; }

アプリアイコンとスクリーンショット

// アダプティブアイコンの設定
// res/mipmap-anydpi-v26/ic_launcher.xml
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
    <monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

Google Play Console での設定

アプリの基本情報

graph LR
    A[アプリ詳細] --> B[ストア掲載情報]
    B --> C[スクリーンショット]
    C --> D[評価とレビュー]
    D --> E[リリース管理]
    E --> F[アプリ署名]
    F --> G[公開]
    
    H[内部テスト] --> I[クローズドテスト]
    I --> J[オープンテスト]
    J --> G

段階的ロールアウト

// ビルドバリアント用の設定
android {
    buildTypes {
        debug {
            applicationIdSuffix = ".debug"
            versionNameSuffix = "-DEBUG"
        }
        
        create("staging") {
            applicationIdSuffix = ".staging"
            versionNameSuffix = "-STAGING"
            isMinifyEnabled = false
            signingConfig = signingConfigs.getByName("debug")
        }
        
        release {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
            signingConfig = signingConfigs.getByName("release")
        }
    }
}

まとめ

2025年のAndroidアプリ開発では、以下の点が特に重要です:

  1. モダンUI - Jetpack ComposeとMaterial Design 3による宣言的UI開発
  2. アーキテクチャ - MVVM + Repository パターンによる保守性の高い設計
  3. データ管理 - Room + Flowによる効率的なデータ操作
  4. パフォーマンス - LazyColumn最適化とメモリ管理
  5. テスト - Unit TestとUI Testによる品質保証
  6. 公開戦略 - 段階的リリースとユーザーフィードバックの活用

継続的な学習と実践により、ユーザーに価値を提供する高品質なAndroidアプリケーションの開発が可能になります。最新の技術動向を把握しながら、実用的なアプリ開発を心がけましょう。