安卓开发实战:从零构建一个天气应用

本文将带你全面了解现代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天气应用开发流程,涵盖了以下关键点:

  1. 现代化架构: 使用MVVM模式结合Clean Architecture原则

  2. 声明式UI: 采用Jetpack Compose构建响应式界面

  3. 依赖注入: 使用Hilt管理依赖关系

  4. 异步处理: 使用Kotlin协程处理异步操作

  5. 数据持久化: 使用Room进行本地数据存储

  6. 网络请求: 使用Retrofit进行API调用

  7. 后台任务: 使用WorkManager调度定期任务

最佳实践建议:

  • 使用Resource类包装网络请求状态

  • 在ViewModel中使用State管理UI状态

  • 为不同的屏幕尺寸提供响应式布局

  • 实现适当的错误处理和加载状态

  • 编写单元测试和仪器测试

  • 使用ProGuard或R8进行代码优化和混淆