安卓开发实战:从零构建一个天气应用
本文将带你全面了解现代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 = 34defaultConfig {applicationId = "com.example.weatherapp"minSdk = 24targetSdk = 34versionCode = 1versionName = "1.0"}buildFeatures {compose = true}composeOptions {kotlinCompilerExtensionVersion = "1.5.4"} }dependencies {// Core Androidimplementation("androidx.core:core-ktx:1.12.0")implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")// Composeimplementation("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")// ViewModelimplementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")// Retrofitimplementation("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")// Roomimplementation("androidx.room:room-runtime:2.6.0")implementation("androidx.room:room-ktx:2.6.0")kapt("androidx.room:room-compiler:2.6.0")// Hiltimplementation("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")// WorkManagerimplementation("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@Singletonfun provideOkHttpClient(): OkHttpClient {return OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {level = HttpLoggingInterceptor.Level.BODY}).build()}@Provides@Singletonfun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().baseUrl(BASE_URL).client(okHttpClient).addConverterFactory(GsonConverterFactory.create()).build()}@Provides@Singletonfun 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)@Deletesuspend 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(): WeatherDaocompanion object {@Volatileprivate var INSTANCE: AppDatabase? = nullfun getDatabase(context: Context): AppDatabase {return INSTANCE ?: synchronized(this) {val instance = Room.databaseBuilder(context.applicationContext,AppDatabase::class.java,"weather_database").build()INSTANCE = instanceinstance}}} }
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> = _weatherStateprivate val _favoritesState = mutableStateOf(FavoritesState())val favoritesState: State<FavoritesState> = _favoritesStateinit {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.weatherStatevar city by remember { mutableStateOf("") }Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {// HeaderRow(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 BarOutlinedTextField(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 Contentwhen {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 detailsRow(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@Singletonfun 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进行代码优化和混淆