
1. 项目概述Android天气预报应用开发全流程解析天气预报应用作为移动开发领域的经典练手项目涵盖了网络请求、数据解析、UI设计、定位服务等Android开发的核心技术点。这个看似简单的应用实际上需要处理实时数据获取、多线程操作、本地缓存等一系列复杂问题。我最近用三周时间完成了一个完整的天气预报应用开发从API对接、数据模型设计到界面交互实现踩过不少坑也积累了些实用经验。这个应用支持实时天气查询、未来三天预报、城市搜索和自动定位功能UI采用Material Design风格适配不同尺寸的Android设备。2. 核心技术选型与架构设计2.1 基础技术栈选择开发Android应用首先需要确定技术框架。我选择的标准是官方支持、社区活跃、学习曲线平缓。最终确定的方案是开发工具Android Studio 2023.1.1稳定版编程语言KotlinGoogle官方推荐比Java更简洁架构模式MVVMModel-View-ViewModel构建工具Gradle 8.0提示新手建议从Kotlin开始学习它解决了Java的很多痛点如空指针问题和冗长的代码。Google官方文档中Kotlin的示例也越来越多。2.2 天气数据源对接可靠的天气API是应用的核心。经过对比多家服务商我选择了和风天气的免费API主要考虑免费套餐足够开发使用1000次/日数据准确度高更新及时提供完善的开发文档支持多种天气数据实时、预报、空气质量等API接入关键代码// 网络请求使用Retrofit interface WeatherService { GET(v7/weather/now) suspend fun getRealTimeWeather( Query(location) location: String, Query(key) apiKey: String ): ResponseWeatherResponse } // 数据模型示例 data class WeatherResponse( val code: String, val now: Now, val updateTime: String ) data class Now( val temp: String, val feelsLike: String, val icon: String, val text: String, val humidity: String, val vis: String )2.3 应用架构设计采用MVVM架构分离关注点Model层负责数据获取和持久化网络数据Retrofit本地缓存Room数据库ViewModel层处理业务逻辑数据格式化错误处理状态管理View层UI展示Activity/FragmentXML布局数据绑定架构优势各层职责清晰便于单元测试UI与业务逻辑解耦生命周期感知3. 关键功能实现详解3.1 实时天气展示模块实时天气界面需要展示当前温度、体感温度、天气状况、湿度、能见度等信息。我采用ConstraintLayout构建响应式布局确保在不同尺寸设备上都能良好显示。布局关键点androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightwrap_content !-- 天气图标 -- ImageView android:idid/ivWeatherIcon app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent android:layout_width100dp android:layout_height100dp/ !-- 温度显示 -- TextView android:idid/tvTemperature app:layout_constraintStart_toEndOfid/ivWeatherIcon app:layout_constraintTop_toTopOfid/ivWeatherIcon android:textSize48sp android:layout_marginStart16dp/ !-- 其他信息采用水平排列的LinearLayout -- LinearLayout android:orientationhorizontal app:layout_constraintTop_toBottomOfid/ivWeatherIcon android:layout_widthmatch_parent TextView android:idid/tvHumidity.../ TextView android:idid/tvVisibility.../ /LinearLayout /androidx.constraintlayout.widget.ConstraintLayout数据加载逻辑// ViewModel中 fun loadWeather(location: String) { viewModelScope.launch { _loadingState.value true try { val response weatherRepository.getRealTimeWeather(location) if (response.isSuccessful) { _weatherData.value response.body() } else { _errorMessage.value 获取天气数据失败 } } catch (e: Exception) { _errorMessage.value 网络请求异常${e.message} } finally { _loadingState.value false } } }3.2 多天预报功能实现未来天气预报需要展示多天数据我选择RecyclerView实现可滚动的列表展示。关键步骤创建天气预报数据模型data class Forecast( val date: String, val highTemp: Int, val lowTemp: Int, val dayIcon: String, val nightIcon: String, val condition: String )实现RecyclerView.Adapterclass ForecastAdapter(private val items: ListForecast) : RecyclerView.AdapterForecastAdapter.ViewHolder() { class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val dateView: TextView view.findViewById(R.id.tvDate) val iconView: ImageView view.findViewById(R.id.ivIcon) val tempView: TextView view.findViewById(R.id.tvTempRange) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_forecast, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item items[position] holder.dateView.text item.date holder.tempView.text ${item.lowTemp}°/${item.highTemp}° // 加载天气图标 Glide.with(holder.itemView) .load(getIconUrl(item.dayIcon)) .into(holder.iconView) } }在Fragment中设置RecyclerViewval forecastList findViewByIdRecyclerView(R.id.rvForecast) forecastList.layoutManager LinearLayoutManager(this) forecastList.adapter ForecastAdapter(forecastData)3.3 城市定位与搜索功能自动定位实现添加位置权限到AndroidManifest.xmluses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION/ uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/使用FusedLocationProviderClient获取位置private fun getLastLocation() { if (checkPermissions()) { fusedLocationClient.lastLocation .addOnSuccessListener { location - location?.let { val geocoder Geocoder(this, Locale.getDefault()) val addresses geocoder.getFromLocation( it.latitude, it.longitude, 1 ) addresses?.firstOrNull()?.let { address - val cityName address.locality ?: address.subAdminArea viewModel.loadWeather(cityName) } } } } else { requestPermissions() } }城市搜索实现使用Android的SearchView组件androidx.appcompat.widget.SearchView android:idid/searchView android:layout_widthmatch_parent android:layout_heightwrap_content app:queryHint输入城市名称/实现搜索逻辑searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { query?.let { viewModel.loadWeather(it) searchView.clearFocus() } return true } override fun onQueryTextChange(newText: String?) false })4. 用户体验优化技巧4.1 数据缓存策略为提高响应速度并减少网络请求我实现了两级缓存内存缓存使用LruCacheprivate val memoryCache LruCacheString, WeatherData(10) fun getWeather(location: String): WeatherData? { // 先检查内存缓存 memoryCache.get(location)?.let { return it } // 然后检查数据库 val cached database.weatherDao().getByLocation(location) cached?.let { memoryCache.put(location, it) } return cached }网络请求时更新缓存fun fetchAndCache(location: String) { viewModelScope.launch { val freshData api.getWeather(location) memoryCache.put(location, freshData) database.weatherDao().insert(freshData) } }4.2 界面状态管理良好的状态管理能显著提升用户体验。我定义了以下状态sealed class WeatherState { object Loading : WeatherState() data class Success(val data: WeatherData) : WeatherState() data class Error(val message: String) : WeatherState() object Empty : WeatherState() }在Activity/Fragment中观察状态变化viewModel.weatherState.observe(this) { state - when (state) { is WeatherState.Loading - showLoading() is WeatherState.Success - showWeather(state.data) is WeatherState.Error - showError(state.message) WeatherState.Empty - showEmptyView() } }4.3 天气图标动态加载使用Glide加载网络天气图标并添加过渡动画fun loadWeatherIcon(imageView: ImageView, iconCode: String) { Glide.with(imageView.context) .load(https://icons.qweather.com/$iconCode.png) .transition(DrawableTransitionOptions.withCrossFade()) .into(imageView) }5. 常见问题与解决方案5.1 网络请求失败处理问题现象应用在弱网环境下频繁崩溃解决方案添加网络状态检查fun isNetworkAvailable(): Boolean { val connectivityManager getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager return connectivityManager.activeNetwork?.let { connectivityManager.getNetworkCapabilities(it)?.hasCapability( NET_CAPABILITY_INTERNET ) ?: false } ?: false }实现重试机制private suspend fun T safeApiCall( apiCall: suspend () - ResponseT ): ResultT { return try { val response apiCall() if (response.isSuccessful) { Result.Success(response.body()!!) } else { Result.Error(API error: ${response.code()}) } } catch (e: Exception) { Result.Error(Network error: ${e.message}) } }5.2 内存泄漏预防问题场景旋转屏幕后应用变卡顿解决方案使用ViewModel代替Activity/Fragment保存数据在RecyclerView.Adapter中正确处理资源override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) Glide.with(holder.itemView).clear(holder.iconView) }避免在回调中直接引用View// 错误写法 api.getWeather { data - textView.text data.temp // 持有textView引用 } // 正确写法 api.getWeather { data - _weatherData.postValue(data) // 通过LiveData更新 }5.3 多分辨率适配问题问题现象在不同尺寸设备上布局错乱解决方案使用ConstraintLayout构建灵活布局为不同屏幕尺寸提供备用资源创建res/layout-sw600dp/目录用于平板布局提供多套图标mdpi, hdpi, xhdpi等使用尺寸资源而非硬编码数值dimen nameweather_icon_size100dp/dimen6. 性能优化实践6.1 启动时间优化应用启动时间直接影响用户体验。我采取的优化措施延迟初始化非关键组件// Application类中 override fun onCreate() { super.onCreate() // 关键组件立即初始化 initCrashReporting() // 非关键组件延迟初始化 Handler().postDelayed({ initAnalytics() }, 3000) }使用App Startup库统一管理初始化// 实现Initializer接口 class WeatherInitializer : InitializerUnit { override fun create(context: Context) { // 初始化工作 } override fun dependencies(): ListClassout Initializer* emptyList() }6.2 列表滚动优化RecyclerView滚动流畅度优化技巧使用DiffUtil高效更新数据class ForecastDiffCallback( private val oldList: ListForecast, private val newList: ListForecast ) : DiffUtil.Callback() { // 实现必要方法 } val result DiffUtil.calculateDiff(ForecastDiffCallback(oldList, newList)) adapter.submitList(newList) result.dispatchUpdatesTo(adapter)设置RecyclerView的setHasFixedSizerecyclerView.setHasFixedSize(true) // 当item尺寸固定时预加载机制recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val layoutManager recyclerView.layoutManager as LinearLayoutManager val lastVisible layoutManager.findLastVisibleItemPosition() if (lastVisible adapter.itemCount - 5) { viewModel.loadMoreData() } } })7. 测试与发布准备7.1 单元测试实践ViewModel测试示例RunWith(AndroidJUnit4::class) class WeatherViewModelTest { private lateinit var viewModel: WeatherViewModel private val mockRepository mockkWeatherRepository() Before fun setup() { viewModel WeatherViewModel(mockRepository) } Test fun load weather should update live data() runTest { val testData WeatherData(...) coEvery { mockRepository.getWeather(any()) } returns testData viewModel.loadWeather(Beijing) assertThat(viewModel.weatherData.value).isEqualTo(testData) } }7.2 UI自动化测试使用Espresso编写界面测试RunWith(AndroidJUnit4::class) class WeatherActivityTest { get:Rule val activityRule ActivityScenarioRule(WeatherActivity::class.java) Test fun displayWeather_whenDataLoaded() { // 模拟网络响应 val testData WeatherData(...) val mockResponse Response.success(testData) MockWebServer().apply { start() enqueue(MockResponse().setBody(mockResponse.toString())) } // 执行测试操作 onView(withId(R.id.searchView)).perform(typeText(Beijing)) onView(withId(R.id.btnSearch)).perform(click()) // 验证结果 onView(withText(testData.temp °)).check(matches(isDisplayed())) } }7.3 发布前检查清单代码混淆配置proguard-rules.pro-keep class com.example.weather.model.** { *; } -keep class com.example.weather.network.** { *; }应用签名配置android { signingConfigs { release { storeFile file(weather.jks) storePassword System.getenv(STORE_PASSWORD) keyAlias weather keyPassword System.getenv(KEY_PASSWORD) } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled true proguardFiles getDefaultProguardFile(proguard-android.txt), proguard-rules.pro } } }版本管理规范版本号格式主版本.次版本.补丁版本如1.0.0每次提交明确变更内容使用Git Tag标记发布版本8. 项目扩展方向完成基础版本后可以考虑以下扩展功能天气预警通知接入紧急天气预警API在状态栏显示通知桌面小部件开发天气信息桌面小部件天气地图集成地图SDK展示天气分布主题切换支持浅色/深色模式切换多语言支持适配不同语言用户实现桌面小部件的关键代码示例class WeatherWidget : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { appWidgetIds.forEach { widgetId - val views RemoteViews(context.packageName, R.layout.widget_weather) // 设置点击刷新按钮 val intent Intent(context, WeatherWidget::class.java).apply { action ACTION_REFRESH } val pendingIntent PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_IMMUTABLE ) views.setOnClickPendingIntent(R.id.btnRefresh, pendingIntent) // 更新小部件 appWidgetManager.updateAppWidget(widgetId, views) // 加载天气数据 updateWidgetData(context, widgetId) } } private fun updateWidgetData(context: Context, widgetId: Int) { // 实现数据加载逻辑 } }开发过程中最大的体会是看似简单的功能背后需要考虑很多边界情况。比如网络异常时的降级处理、不同Android版本的权限获取方式变化等。这些经验只有实际动手开发才能深刻理解。