当前位置: 首页 > news >正文

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

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

  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进行代码优化和混淆

http://www.dtcms.com/a/346733.html

相关文章:

  • 【Android】使用FragmentManager动态添加片段
  • C# 项目“交互式展厅管理客户端“针对的是“.NETFramework,Version=v4.8”,但此计算机上没有安装它。
  • week4-[字符数组]字符统计
  • STAR-CCM+|K-epsilon湍流模型溯源
  • c语言学习_数组使用_扫雷1
  • 【小沐学GIS】基于Godot绘制三维数字地球Earth(Godot)
  • HTTP的状态码有哪些,并用例子说明一下
  • 人工智能之数学基础:离散随机变量和连续随机变量
  • react中多个页面,数据相互依赖reducer解决方案
  • 变频器实习DAY35
  • 深入理解Java多线程:状态、安全、同步与通信
  • Day12 数据统计-Excel报表
  • 基于llama.cpp的量化版reranker模型调用示例
  • 目标跟踪 YOLO11 单目标跟踪
  • Uipath查找元素 查找子元素 获取属性活动组合使用示例
  • 【数据结构】线性表——链表
  • 基于springboot购物商城系统源码
  • 灵动AI:工业级商品图AI生成工具
  • 【剖析高并发秒杀】从流量削峰到数据一致性的架构演进与实践
  • GaussDB 数据库架构师修炼(十八) SQL引擎-解析器
  • 慢查询该怎么优化
  • 【文献阅读】Lossless data compression by large models
  • 【卷积神经网络详解与实例】2——卷积计算详解
  • Hive中的join优化
  • 解决散点图绘制算法单一导致的数据异常问题
  • DeepSpeed v0.17.5发布:优化性能与扩展功能的全新升级
  • Axure:有个特别实用的功能
  • 寻找AI——高保真还原设计图生成App页面
  • 【K8s】整体认识K8s之Docker篇
  • 完整实验命令解析:从集群搭建到负载均衡配置