Вступ
У 2025 році розробка Android повністю базується на Kotlin + Jetpack Compose. Coroutines та Flow роблять асинхронні операції чистими, Hilt автоматизує впорскування залежностей, Room для локальної бази даних, Clean Architecture максимізує тестування. З Kotlin 2.3+ та Android Studio Meerkat+ навіть інтеграція KMP стала простою, але ми фокусуємося на чистому Android.
У цьому посібнику ми створимо з нуля додаток Погоди:
Вимоги
Android Studio 2025.x, Kotlin 2.3+, minSdk 26+
Залежності Gradle (build.gradle.kts модуль app):
Для Hilt додайте клас Application та @HiltAndroidApp.
1. Структура проекту (Clean Architecture)
2. Шар Domain (Найчистіший)
domain/model/Weather.kt
domain/repository/WeatherRepository.kt
Приклад UseCase: domain/usecase/GetWeatherUseCase.kt
3. Шар Data
Віддалений API (Retrofit)
data/remote/WeatherApi.kt
Room Локальний
data/local/FavoriteCityEntity.kt
data/local/FavoriteDao.kt
Реалізація Repository
data/repository/WeatherRepositoryImpl.kt
4. Модулі Hilt
di/AppModule.kt
5. ViewModel (StateFlow + Coroutines)
presentation/viewmodel/WeatherViewModel.kt
6. Екран Jetpack Compose
presentation/ui/WeatherScreen.kt
7. Поради для просунутих (2025)
У 2025 році розробка Android повністю базується на Kotlin + Jetpack Compose. Coroutines та Flow роблять асинхронні операції чистими, Hilt автоматизує впорскування залежностей, Room для локальної бази даних, Clean Architecture максимізує тестування. З Kotlin 2.3+ та Android Studio Meerkat+ навіть інтеграція KMP стала простою, але ми фокусуємося на чистому Android.
У цьому посібнику ми створимо з нуля додаток Погоди:
- Пошук міста + додавання до улюблених
- Інтеграція OpenWeatherMap API (з Flow)
- Локальний кеш з Room + підтримка офлайн
- UI Jetpack Compose (Material 3)
- Hilt DI + Coroutines + StateFlow
- Clean Architecture (data/domain/presentation)
Вимоги
Android Studio 2025.x, Kotlin 2.3+, minSdk 26+
Залежності Gradle (build.gradle.kts модуль app):
Код Select
dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6")
implementation("androidx.activity:activity-compose:1.9.3")
implementation(platform("androidx.compose:compose-bom:2025.xx.xx"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
// Hilt
implementation("com.google.dagger:hilt-android:2.52")
ksp("com.google.dagger:hilt-compiler:2.52")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// Coroutines + Flow
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
// Retrofit + Converter
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// Coil for images
implementation("io.coil-kt:coil-compose:2.7.0")
}Для Hilt додайте клас Application та @HiltAndroidApp.
1. Структура проекту (Clean Architecture)
Код Select
app/
|-- data/
| |-- local/ # Room DAO, Entity
| |-- remote/ # Retrofit API, DTO
| |-- repository/# Impl Repository
|-- domain/
| |-- model/ # Domain Entities
| |-- repository/# Abstract Repository
| |-- usecase/ # UseCases
|-- di/ # Hilt modules
|-- presentation/
| |-- ui/ # Screens, Composables
| |-- viewmodel/ # ViewModels
|-- util/2. Шар Domain (Найчистіший)
domain/model/Weather.kt
Код Select
data class Weather(
val city: String,
val temperature: Double,
val description: String,
val icon: String,
val timestamp: Long = System.currentTimeMillis()
)domain/repository/WeatherRepository.kt
Код Select
interface WeatherRepository {
suspend fun getWeather(city: String): Result<Weather>
fun getFavoriteCities(): Flow<List<String>>
suspend fun addFavorite(city: String)
suspend fun removeFavorite(city: String)
}Приклад UseCase: domain/usecase/GetWeatherUseCase.kt
Код Select
class GetWeatherUseCase @Inject constructor(
private val repository: WeatherRepository
) {
suspend operator fun invoke(city: String): Result<Weather> = repository.getWeather(city)
}3. Шар Data
Віддалений API (Retrofit)
data/remote/WeatherApi.kt
Код Select
interface WeatherApi {
@GET("weather")
suspend fun getWeather(
@Query("q") city: String,
@Query("appid") apiKey: String,
@Query("units") units: String = "metric"
): WeatherResponse
}
// DTO
data class WeatherResponse(
val main: Main,
val weather: List<WeatherItem>,
val name: String
) {
data class Main(val temp: Double)
data class WeatherItem(val description: String, val icon: String)
}Room Локальний
data/local/FavoriteCityEntity.kt
Код Select
@Entity(tableName = "favorite_cities")
data class FavoriteCityEntity(
@PrimaryKey val city: String
)data/local/FavoriteDao.kt
Код Select
@Dao
interface FavoriteDao {
@Query("SELECT city FROM favorite_cities")
fun getAll(): Flow<List<String>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(city: FavoriteCityEntity)
@Delete
suspend fun delete(city: FavoriteCityEntity)
}Реалізація Repository
data/repository/WeatherRepositoryImpl.kt
Код Select
@Singleton
class WeatherRepositoryImpl @Inject constructor(
private val api: WeatherApi,
private val dao: FavoriteDao,
private val apiKey: String // @Named або з BuildConfig
) : WeatherRepository {
override suspend fun getWeather(city: String): Result<Weather> = try {
val response = api.getWeather(city, apiKey)
val weather = Weather(
city = response.name,
temperature = response.main.temp,
description = response.weather.firstOrNull()?.description ?: "",
icon = response.weather.firstOrNull()?.icon ?: ""
)
Result.success(weather)
} catch (e: Exception) {
Result.failure(e)
}
override fun getFavoriteCities(): Flow<List<String>> = dao.getAll()
override suspend fun addFavorite(city: String) {
dao.insert(FavoriteCityEntity(city))
}
override suspend fun removeFavorite(city: String) {
dao.delete(FavoriteCityEntity(city))
}
}4. Модулі Hilt
di/AppModule.kt
Код Select
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.openweathermap.org/data/2.5/")
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun provideWeatherApi(retrofit: Retrofit): WeatherApi = retrofit.create(WeatherApi::class.java)
// Room database vs.
}5. ViewModel (StateFlow + Coroutines)
presentation/viewmodel/WeatherViewModel.kt
Код Select
@HiltViewModel
class WeatherViewModel @Inject constructor(
private val getWeatherUseCase: GetWeatherUseCase,
private val repository: WeatherRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(WeatherUiState())
val uiState: StateFlow<WeatherUiState> = _uiState.asStateFlow()
private val _favorites = MutableStateFlow<List<String>>(emptyList())
val favorites: StateFlow<List<String>> = _favorites.asStateFlow()
init {
viewModelScope.launch {
repository.getFavoriteCities().collect { cities ->
_favorites.value = cities
}
}
}
fun searchCity(city: String) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
getWeatherUseCase(city).fold(
onSuccess = { weather ->
_uiState.update { it.copy(weather = weather, error = null) }
},
onFailure = { e ->
_uiState.update { it.copy(error = e.message, isLoading = false) }
}
)
}
}
fun toggleFavorite(city: String) {
viewModelScope.launch {
if (_favorites.value.contains(city)) {
repository.removeFavorite(city)
} else {
repository.addFavorite(city)
}
}
}
}
data class WeatherUiState(
val weather: Weather? = null,
val isLoading: Boolean = false,
val error: String? = null
)6. Екран Jetpack Compose
presentation/ui/WeatherScreen.kt
Код Select
@Composable
fun WeatherScreen(viewModel: WeatherViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val favorites by viewModel.favorites.collectAsStateWithLifecycle()
var city by remember { mutableStateOf("") }
Column(modifier = Modifier
.fillMaxSize()
.padding(16.dp)) {
TextField(
value = city,
onValueChange = { city = it },
label = { Text("Пошук міста (наприклад: Київ)") },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
Button(onClick = { viewModel.searchCity(city) }) {
Text("Пошук")
}
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
}
state.weather?.let { weather ->
Card(modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)) {
Column(modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Text(weather.city, style = MaterialTheme.typography.headlineMedium)
AsyncImage(
model = "https://openweathermap.org/img/wn/${weather.icon}@2x.png",
contentDescription = null,
modifier = Modifier.size(100.dp)
)
Text("${weather.temperature.toInt()}°C", style = MaterialTheme.typography.bodyLarge)
Text(weather.description.capitalize())
IconButton(onClick = { viewModel.toggleFavorite(weather.city) }) {
Icon(
imageVector = if (favorites.contains(weather.city)) Icons.Filled.Favorite else Icons.Outlined.Favorite,
contentDescription = "Улюблене"
)
}
}
}
}
state.error?.let {
Text(it, color = MaterialTheme.colorScheme.error)
}
// Список улюблених...
}
}7. Поради для просунутих (2025)
- Flow з offline-first: API > кеш > emit
- Додати стрічку новин з Paging 3 + RemoteMediator
- Прискорити процесор hilt з KSP
- Додати Baseline Profiles + Macrobenchmark
- Кастомні анотації з Kotlin Symbol Processing (KSP)

