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アプリ開発では、以下の点が特に重要です:
- モダンUI - Jetpack ComposeとMaterial Design 3による宣言的UI開発
- アーキテクチャ - MVVM + Repository パターンによる保守性の高い設計
- データ管理 - Room + Flowによる効率的なデータ操作
- パフォーマンス - LazyColumn最適化とメモリ管理
- テスト - Unit TestとUI Testによる品質保証
- 公開戦略 - 段階的リリースとユーザーフィードバックの活用
継続的な学習と実践により、ユーザーに価値を提供する高品質なAndroidアプリケーションの開発が可能になります。最新の技術動向を把握しながら、実用的なアプリ開発を心がけましょう。