本文将带你全面了解现代Android开发的核心技术和最佳实践,通过构建一个功能完整的天气应用来展示实际开发流程。
1. 现代Android开发技术栈
1.1 核心技术组件
-
Kotlin: 官方推荐的编程语言
-
Jetpack Compose: 声明式UI工具包
-
ViewModel: 管理UI相关数据
-
Room: 本地数据库
-
Retrofit: 网络请求
-
Hilt: 依赖注入
-
WorkManager: 后台任务调度
2. 项目结构与配置
2.1 build.gradle.kts (Module级)
kotlin
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
}
android {
namespace = "com.example.weatherapp"
compileSdk = 34
defaultConfig {
applicationId = "com.example.weatherapp"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
}
dependencies {
// Core Android
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
// Compose
implementation("androidx.activity:activity-compose:1.8.0")
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.navigation:navigation-compose:2.7.4")
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
// Room
implementation("androidx.room:room-runtime:2.6.0")
implementation("androidx.room:room-ktx:2.6.0")
kapt("androidx.room:room-compiler:2.6.0")
// Hilt
implementation("com.google.dagger:hilt-android:2.48.1")
kapt("com.google.dagger:hilt-compiler:2.48.1")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// Coil (Image loading)
implementation("io.coil-kt:coil-compose:2.4.0")
// WorkManager
implementation("androidx.work:work-runtime-ktx:2.8.1")
}
3. 数据模型与API集成
3.1 天气数据模型
kotlin
// WeatherResponse.kt
data class WeatherResponse(
val name: String,
val main: Main,
val weather: List<Weather>,
val wind: Wind,
val sys: Sys
)
data class Main(
val temp: Double,
val feels_like: Double,
val humidity: Int,
val pressure: Int
)
data class Weather(
val id: Int,
val main: String,
val description: String,
val icon: String
)
data class Wind(
val speed: Double,
val deg: Int
)
data class Sys(
val country: String,
val sunrise: Long,
val sunset: Long
)
3.2 Retrofit API服务
kotlin
// WeatherApiService.kt
interface WeatherApiService {
@GET("weather")
suspend fun getWeatherByCity(
@Query("q") city: String,
@Query("appid") apiKey: String,
@Query("units") units: String = "metric"
): WeatherResponse
@GET("weather")
suspend fun getWeatherByLocation(
@Query("lat") lat: Double,
@Query("lon") lon: Double,
@Query("appid") apiKey: String,
@Query("units") units: String = "metric"
): WeatherResponse
}
// ApiModule.kt
@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
private const val BASE_URL = "https://api.openweathermap.org/data/2.5/"
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideWeatherApiService(retrofit: Retrofit): WeatherApiService {
return retrofit.create(WeatherApiService::class.java)
}
}
4. 本地数据库与Repository模式
4.1 Room实体与DAO
kotlin
// FavoriteCity.kt
@Entity(tableName = "favorite_cities")
data class FavoriteCity(
@PrimaryKey val name: String,
val timestamp: Long = System.currentTimeMillis()
)
// WeatherDao.kt
@Dao
interface WeatherDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertFavorite(city: FavoriteCity)
@Delete
suspend fun deleteFavorite(city: FavoriteCity)
@Query("SELECT * FROM favorite_cities ORDER BY timestamp DESC")
fun getFavorites(): Flow<List<FavoriteCity>>
}
// AppDatabase.kt
@Database(
entities = [FavoriteCity::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun weatherDao(): WeatherDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"weather_database"
).build()
INSTANCE = instance
instance
}
}
}
}
4.2 Repository实现
kotlin
// WeatherRepository.kt
class WeatherRepository @Inject constructor(
private val weatherApiService: WeatherApiService,
private val weatherDao: WeatherDao,
private val context: Context
) {
suspend fun getWeatherByCity(city: String): Resource<WeatherResponse> {
return try {
val apiKey = context.getString(R.string.weather_api_key)
val response = weatherApiService.getWeatherByCity(city, apiKey)
Resource.Success(response)
} catch (e: Exception) {
Resource.Error(e.message ?: "An error occurred")
}
}
suspend fun addFavorite(city: String) {
weatherDao.insertFavorite(FavoriteCity(city))
}
suspend fun removeFavorite(city: String) {
weatherDao.deleteFavorite(FavoriteCity(city))
}
fun getFavorites(): Flow<List<FavoriteCity>> {
return weatherDao.getFavorites()
}
}
5. ViewModel与状态管理
kotlin
// WeatherViewModel.kt
@HiltViewModel
class WeatherViewModel @Inject constructor(
private val repository: WeatherRepository
) : ViewModel() {
private val _weatherState = mutableStateOf(WeatherState())
val weatherState: State<WeatherState> = _weatherState
private val _favoritesState = mutableStateOf(FavoritesState())
val favoritesState: State<FavoritesState> = _favoritesState
init {
loadFavorites()
}
fun getWeather(city: String) {
_weatherState.value = _weatherState.value.copy(
isLoading = true,
error = null
)
viewModelScope.launch {
when (val result = repository.getWeatherByCity(city)) {
is Resource.Success -> {
_weatherState.value = _weatherState.value.copy(
weatherData = result.data,
isLoading = false,
error = null
)
}
is Resource.Error -> {
_weatherState.value = _weatherState.value.copy(
isLoading = false,
error = result.message
)
}
}
}
}
fun addToFavorites(city: String) {
viewModelScope.launch {
repository.addFavorite(city)
}
}
private fun loadFavorites() {
viewModelScope.launch {
repository.getFavorites().collect { favorites ->
_favoritesState.value = FavoritesState(favorites = favorites)
}
}
}
}
// States.kt
data class WeatherState(
val weatherData: WeatherResponse? = null,
val isLoading: Boolean = false,
val error: String? = null
)
data class FavoritesState(
val favorites: List<FavoriteCity> = emptyList(),
val isLoading: Boolean = false
)
6. UI组件与Compose实现
6.1 主界面组件
kotlin
// MainScreen.kt
@Composable
fun MainScreen(
viewModel: WeatherViewModel = hiltViewModel(),
onNavigateToFavorites: () -> Unit
) {
val weatherState by viewModel.weatherState
var city by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Weather App",
style = MaterialTheme.typography.headlineMedium
)
IconButton(onClick = onNavigateToFavorites) {
Icon(Icons.Default.Favorite, contentDescription = "Favorites")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Search Bar
OutlinedTextField(
value = city,
onValueChange = { city = it },
label = { Text("Enter city name") },
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
IconButton(onClick = { viewModel.getWeather(city) }) {
Icon(Icons.Default.Search, contentDescription = "Search")
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = { viewModel.getWeather(city) }
)
)
Spacer(modifier = Modifier.height(16.dp))
// Weather Content
when {
weatherState.isLoading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
}
weatherState.error != null -> {
Text(
text = "Error: ${weatherState.error}",
color = MaterialTheme.colorScheme.error
)
}
weatherState.weatherData != null -> {
WeatherCard(
weatherData = weatherState.weatherData,
onAddFavorite = { viewModel.addToFavorites(weatherState.weatherData.name) }
)
}
else -> {
Text(
text = "Search for a city to see weather information",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
}
}
6.2 天气信息卡片组件
kotlin
// WeatherCard.kt
@Composable
fun WeatherCard(
weatherData: WeatherResponse,
onAddFavorite: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(
text = "${weatherData.name}, ${weatherData.sys.country}",
style = MaterialTheme.typography.headlineSmall
)
Text(
text = weatherData.weather.firstOrNull()?.description?.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
} ?: "",
style = MaterialTheme.typography.bodyLarge
)
}
IconButton(onClick = onAddFavorite) {
Icon(Icons.Default.FavoriteBorder, contentDescription = "Add to favorites")
}
}
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${weatherData.main.temp.toInt()}°C",
style = MaterialTheme.typography.displaySmall
)
weatherData.weather.firstOrNull()?.icon?.let { iconCode ->
val iconUrl = "https://openweathermap.org/img/wn/$iconCode@2x.png"
AsyncImage(
model = iconUrl,
contentDescription = "Weather icon",
modifier = Modifier.size(64.dp)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Weather details
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
WeatherDetailItem(
icon = Icons.Default.WaterDrop,
title = "Humidity",
value = "${weatherData.main.humidity}%"
)
WeatherDetailItem(
icon = Icons.Default.Air,
title = "Wind",
value = "${weatherData.wind.speed} m/s"
)
WeatherDetailItem(
icon = Icons.Default.Speed,
title = "Pressure",
value = "${weatherData.main.pressure} hPa"
)
}
}
}
}
@Composable
fun WeatherDetailItem(icon: ImageVector, title: String, value: String) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(icon, contentDescription = title, modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.height(4.dp))
Text(text = title, style = MaterialTheme.typography.labelSmall)
Text(text = value, style = MaterialTheme.typography.bodySmall)
}
}
7. 导航与应用入口点
kotlin
// Navigation.kt
@Composable
fun WeatherNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "main") {
composable("main") {
MainScreen(onNavigateToFavorites = {
navController.navigate("favorites")
})
}
composable("favorites") {
FavoritesScreen(onNavigateBack = { navController.popBackStack() })
}
}
}
// MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WeatherAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WeatherNavigation()
}
}
}
}
}
8. 后台任务与WorkManager
kotlin
// WeatherWorker.kt
class WeatherWorker(
context: Context,
params: WorkerParameters,
private val repository: WeatherRepository
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// 获取收藏的城市并更新天气数据
val favorites = repository.getFavorites().first()
favorites.forEach { favorite ->
when (val result = repository.getWeatherByCity(favorite.name)) {
is Resource.Success -> {
// 这里可以发送通知或更新本地数据库
Log.d("WeatherWorker", "Updated weather for ${favorite.name}")
}
is Resource.Error -> {
Log.e("WeatherWorker", "Failed to update ${favorite.name}: ${result.message}")
}
}
}
Result.success()
} catch (e: Exception) {
Result.failure()
}
}
}
// WorkerModule.kt
@Module
@InstallIn(SingletonComponent::class)
object WorkerModule {
@Provides
@Singleton
fun provideWeatherWorkerFactory(
repository: WeatherRepository
): WeatherWorkerFactory {
return WeatherWorkerFactory(repository)
}
}
class WeatherWorkerFactory(
private val repository: WeatherRepository
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
return when (workerClassName) {
WeatherWorker::class.java.name -> {
WeatherWorker(appContext, workerParameters, repository)
}
else -> null
}
}
}
9. 总结与最佳实践
本文展示了一个完整的现代Android天气应用开发流程,涵盖了以下关键点:
-
现代化架构: 使用MVVM模式结合Clean Architecture原则
-
声明式UI: 采用Jetpack Compose构建响应式界面
-
依赖注入: 使用Hilt管理依赖关系
-
异步处理: 使用Kotlin协程处理异步操作
-
数据持久化: 使用Room进行本地数据存储
-
网络请求: 使用Retrofit进行API调用
-
后台任务: 使用WorkManager调度定期任务
最佳实践建议:
-
使用Resource类包装网络请求状态
-
在ViewModel中使用State管理UI状态
-
为不同的屏幕尺寸提供响应式布局
-
实现适当的错误处理和加载状态
-
编写单元测试和仪器测试
-
使用ProGuard或R8进行代码优化和混淆