Android天气应用开发:从零搭建MVVM架构与Jetpack Compose实战 📅 2026/8/5 3:15:29 1. 项目概述为什么从“开发准备”开始做Android开发这些年我见过太多项目在起跑线上就栽了跟头。一个看似简单的天气APP如果前期准备不充分后面可能就是无尽的填坑和重构。很多人拿到一个想法比如“做个天气应用”第一反应就是打开Android Studio新建一个项目然后一头扎进代码里。结果往往是UI库选型不合适网络请求框架三天两头出问题项目结构混乱不堪最后要么半途而废要么变成一个难以维护的“屎山”。所以这个系列的第一篇我们不写一行代码只做一件事把开发的“地基”打牢。这个“地基”就是开发准备。它决定了你的项目能建多高能走多远。今天我们就来系统性地聊聊在动手开发一个Android天气APP之前你需要考虑清楚哪些事准备好哪些工具搭建好怎样的环境。这不仅仅是安装一个IDE那么简单它关乎技术选型、架构思路、团队协作哪怕你是一个人和未来的可扩展性。相信我花在准备上的每一分钟都会在后续开发中十倍地回报你。2. 核心需求与目标定义在打开任何开发工具之前我们必须先回答一个最根本的问题我们要做一个什么样的天气APP这个问题的答案将直接指导我们后续所有的技术决策。2.1 功能边界划定要做什么不做什么一个天气APP的核心功能看似简单获取并展示天气信息。但细究起来可以非常复杂。为了避免项目失控我们必须明确第一版本的MVP最小可行产品范围。核心必做功能地理位置获取与切换这是基础。APP需要能自动获取用户当前位置并允许用户手动搜索、添加、切换其他城市。基础天气信息展示包括当前温度、天气状况晴、阴、雨等图标、最高/最低温、湿度、风速、风向、体感温度、气压、能见度、日出日落时间等。数据来源的API决定了你能展示哪些字段。多日预报至少提供未来5-7天的天气预报包括日期、天气状况、温度范围。数据刷新支持手动下拉刷新并考虑后台定时更新的策略注意省电和用户流量。高级或后续迭代功能第一期可暂缓天气预警推送需要接入推送服务涉及后台服务、通知渠道复杂度较高。桌面小部件Widget提供桌面快捷查看需要单独编写Widget的Provider。动态壁纸/天气主题根据天气动态更换APP主题或提供动态壁纸对UI和动画要求高。空气质量AQI、生活指数穿衣、洗车、运动等这些是很好的增值点但依赖于数据源是否提供。天气地图雷达图需要集成地图SDK和特殊图层实现成本高。我的经验对于个人项目或小型团队强烈建议第一期只做核心功能。先让一个简洁、稳定、核心功能可用的版本跑起来获得正向反馈后再逐步添加高级功能。贪多嚼不烂是很多项目烂尾的开端。2.2 非功能性目标设定除了功能我们还要设定一些“隐性”目标它们决定了APP的质量和用户体验。性能与流畅度列表滑动必须流畅数据加载要有过渡动画如Shimmer效果避免卡顿。这是影响用户留存的关键。网络体验优化必须处理无网络、弱网络、服务器异常等各种情况。要有本地缓存策略确保上次查看的天气数据在无网时也能展示。耗电与流量控制后台更新频率要合理避免频繁唤醒和网络请求。可以使用WorkManager来调度可延迟的、省电的后台任务。兼容性需要确定最低支持到哪个Android版本API Level。目前主流是支持到Android 5.0 (API 21) 或更高。这会影响你能使用的库和系统特性。包体积APK Size从开始就要有意识控制。选择库时考虑其大小图片资源进行压缩WebP格式启用代码缩减R8/ProGuard。3. 技术选型与架构设计思路明确了目标我们就可以开始选择“武器”和设计“蓝图”了。这是开发准备中最具技术含量的一环。3.1 开发语言与框架Kotlin是现在Compose是未来开发语言Kotlin这已经毫无悬念。Google早在2019年就宣布Kotlin为Android开发的首选语言。它空安全、语法糖多、与Java完全互通能极大减少NullPointerException这类崩溃提升开发效率和代码健壮性。新项目没有任何理由再选择纯Java。UI框架Jetpack Compose 还是 View System这是一个关键抉择。传统 View System (XML布局)成熟、稳定、资料多有海量的第三方库支持。如果你或你的团队已有深厚积累或者项目需要快速上线且UI不太复杂继续用它没问题。Jetpack Compose声明式UI框架是Android UI开发的未来方向。代码即UI开发效率高预览功能强大更易于实现复杂动画和自定义交互。对于新项目尤其是个人学习或追求技术前沿的项目我强烈推荐从Compose开始。虽然生态还在完善中但官方支持力度大核心库已非常稳定用于开发天气APP这种UI驱动型应用非常合适。我的选择与理由在这个天气APP项目中我将选择Kotlin Jetpack Compose。原因有三第一这是学习未来技术的绝佳机会第二Compose的声明式特性与天气数据的状态驱动更新非常契合第三其强大的预览和实时交互能力能极大提升UI开发调试效率。3.2 架构模式MVVM是标配架构模式决定了代码的组织方式关乎可测试性、可维护性和团队协作。对于Android现代开发MVVM (Model-View-ViewModel)结合Jetpack组件已成为事实标准。Model负责数据和业务逻辑。这里包括从网络API获取天气数据的Repository仓库层以及可能存在的本地数据库如Room。ViewUI层负责显示数据。在Compose中就是一个个Composable函数。ViewModel作为View和Model之间的桥梁。它持有UI状态使用StateFlow或LiveData处理来自View的用户交互事件并调用Model层获取或处理数据。为什么是MVVM它实现了关注点分离。ViewModel独立于Android生命周期便于单元测试。Compose通过viewModel()函数可以轻松获取ViewModel并观察其状态状态一变UI自动重组非常优雅。3.3 关键依赖库选型选对库事半功倍。以下是针对天气APP的核心库推荐网络请求Retrofit OkHttp Kotlin协程Retrofit类型安全的HTTP客户端通过接口定义API用起来像调用本地方法。OkHttp强大的HTTP客户端Retrofit底层依赖它。我们可以通过OkHttp配置拦截器Interceptor方便地添加统一请求头、日志打印、缓存策略等。Kotlin协程用于处理异步操作。让异步代码写得像同步代码一样简洁避免回调地狱。Retrofit有专门的协程适配器。依赖注入Hilt管理类实例的创建和依赖关系。使用Hilt可以避免在代码中手动new对象让代码更松散耦合、更易测试。虽然初期学习有成本但对于任何稍具规模的项目都是值得的。本地持久化Room (如果需要复杂缓存)如果希望离线时能查看历史城市列表、或缓存详细的天气数据Room是官方首推的SQLite抽象层。如果只是简单存储几个城市名用DataStore或SharedPreferences更轻量。图片加载Coil (Compose项目首选)用于加载网络天气图标。Coil是为Kotlin协程和Compose量身定制的图片加载库API极其简洁性能优秀。在Compose中一行AsyncImage组件就能搞定。权限请求Accompanist Permissions (对于Compose)Jetpack Compose官方尚未提供权限请求的原生支持Google的accompanist-permissions库是目前的最佳实践。它提供了在Compose作用域内请求和处理权限的Composable函数。数据序列化kotlinx.serialization 或 Moshi用于将API返回的JSON字符串转换为Kotlin数据类。两者都很好kotlinx.serialization是Kotlin官方出品与语言集成度更高。4. 开发环境搭建与项目初始化工具链准备好了现在来搭建我们的“工作台”。4.1 Android Studio不只是安装那么简单版本选择务必使用最新稳定版的Android Studio。新版本对Compose的支持、编译速度、工具链都有优化。可以通过 官网 下载。安装与基础配置安装路径建议不要装在C盘根目录或带有中文、空格的路径下避免潜在的奇怪问题。SDK管理安装完成后打开SDK Manager。确保安装了项目所需版本的Android SDK Platform和SDK Build-Tools。例如我们目标API 33就需要安装“Android SDK Platform 33”。模拟器AVD创建在Device Manager中创建一个模拟器。建议选择Pixel系列的设备镜像并安装最新的系统版本如Tiramisu API 33和一个较低的版本如API 29用于测试兼容性。为模拟器分配足够的RAM如4GB和存储空间。插件安装检查并安装以下有用插件Kotlin通常已内置。Android APK Support、Google Developers Samples方便查看示例。JSON To Kotlin Class(可选)可以快速将JSON样例转换成Kotlin数据类初期效率神器。4.2 创建新项目关键步骤详解打开Android Studio选择“New Project”。选择模板这里有一个重要选择。如果你决定用Compose请直接选择Empty Compose Activity模板。不要选择Empty Views Activity否则后续引入Compose会比较麻烦。配置项目Name你的应用名称如“WeatherNow”。Package name应用包名通常是域名倒写如com.yourname.weather。一旦确定后续修改会很麻烦请想好。Save location项目存放路径同样避免中文和空格。Language选择Kotlin。Minimum SDK选择API 21: Android 5.0 (Lollipop)。这个版本能覆盖绝大多数现有设备且支持Compose所需的大部分现代特性。你可以点击旁边的“Help me choose”查看版本分布图。Build configuration language选择Kotlin DSL (build.gradle.kts)。这是新的构建脚本方式比Groovy更类型安全推荐新项目使用。点击Finish。Android Studio会自动创建项目并开始首次构建Gradle Sync。这个过程会下载Gradle wrapper和项目依赖耗时取决于网络请耐心等待。4.3 初始项目结构解析与调整项目创建完成后我们来看看生成的结构并做一些初步调整。WeatherNow/ ├── app/ │ ├── src/ │ │ ├── main/ │ │ │ ├── java/com/yourname/weather/ (实际是kotlin) │ │ │ │ └── MainActivity.kt // 入口Activity │ │ │ ├── res/ // 资源目录Compose项目这个目录内容会很少 │ │ │ └── AndroidManifest.xml // 应用清单文件 │ │ └── androidTest/ test/ // 测试目录 │ └── build.gradle.kts // Module级别的构建脚本**主要配置在这里** ├── gradle/ ├── build.gradle.kts // 项目级别的构建脚本 └── settings.gradle.kts // 项目设置文件关键文件配置 (app/build.gradle.kts) 打开这个文件我们需要确保依赖项正确。模板通常已经配置好了基础Compose依赖但我们还需要添加之前选定的库。plugins { id(com.android.application) id(org.jetbrains.kotlin.android) // 添加Hilt插件 id(kotlin-kapt) id(com.google.dagger.hilt.android) } android { namespace com.yourname.weather compileSdk 34 // 通常与最新稳定版SDK一致 defaultConfig { applicationId com.yourname.weather minSdk 21 targetSdk 34 // ... versionCode, versionName } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion 1.5.4 // 需与Compose版本对应 } // ... 其他配置 } dependencies { // 核心依赖 implementation(androidx.core:core-ktx:1.12.0) implementation(androidx.lifecycle:lifecycle-runtime-ktx:2.7.0) implementation(androidx.activity:activity-compose:1.8.0) // Compose BOM (Bill of Materials)统一管理Compose库版本 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) debugImplementation(androidx.compose.ui:ui-tooling) debugImplementation(androidx.compose.ui:ui-test-manifest) // 测试 testImplementation(junit:junit:4.13.2) androidTestImplementation(androidx.test.ext:junit:1.1.5) androidTestImplementation(androidx.test.espresso:espresso-core:3.5.1) androidTestImplementation(platform(androidx.compose:compose-bom:2023.10.01)) androidTestImplementation(androidx.compose.ui:ui-test-junit4) // --- 我们添加的库 --- // ViewModel implementation(androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0) // 网络请求 implementation(com.squareup.retrofit2:retrofit:2.9.0) implementation(com.squareup.retrofit2:converter-gson:2.9.0) // 或用kotlinx-serialization的转换器 implementation(com.squareup.okhttp3:logging-interceptor:4.12.0) // 协程 implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3) // 图片加载 implementation(io.coil-kt:coil-compose:2.5.0) // 依赖注入 implementation(com.google.dagger:hilt-android:2.48) kapt(com.google.dagger:hilt-compiler:2.48) implementation(androidx.hilt:hilt-navigation-compose:1.1.0) // 权限请求 implementation(com.google.accompanist:accompanist-permissions:0.32.0) }注意库版本号可能会更新请以官方文档或仓库最新版本为准。添加依赖后点击右上角的“Sync Now”进行同步。5. 天气数据源选择与API申请APP的灵魂是数据。选择一个稳定、免费或低成本、数据准确的天气API至关重要。5.1 主流免费天气API对比这里列举几个常见的选项并分析其优劣和风天气优点国内服务速度快数据准确免费额度较为充足开发者认证后QPM可达300-1000次提供非常全面的数据包括基础天气、逐小时、多日预报、生活指数、天气预警、空气质量等。文档为中文友好。缺点需要实名认证。免费版有调用频率和QPM限制。适合国内个人开发者、对数据全面性要求高的项目。OpenWeatherMap优点国际老牌服务免费版提供当前天气、5天预报3小时粒度。API设计较为通用。缺点免费版调用频率限制严格60次/分钟且返回的数据字段相比国内API可能对中文用户不够友好如天气描述为英文。速度可能不如国内服务。适合学习、练手或面向国际用户的应用。心知天气优点另一家国内优秀服务商数据源可靠免费额度也足够个人使用。提供分钟级降水、灾害预警等特色数据。缺点同样需要认证部分高级数据需付费。适合国内开发者可作为和风天气的备选。我的选择对于本系列教程我将以和风天气为例进行讲解。因为它数据全、速度快、文档清晰更符合国内开发者的实际使用场景。5.2 和风天气API申请与配置步骤注册与认证访问和风天气官网注册账号并完成开发者实名认证。这是获取更高免费额度的必要步骤。创建项目与Key在控制台创建一个新项目系统会为你生成一个唯一的KEY。这个KEY是调用所有API的凭证务必妥善保管。阅读文档仔细阅读“开发文档”了解如何调用“城市搜索”、“实时天气”、“7天预报”等核心接口。重点关注请求URL格式、必需参数如key、location、返回的JSON数据结构。本地配置KEY绝对不要将API KEY硬编码在代码中我们使用local.properties文件来存储敏感信息。在项目的根目录下找到或创建local.properties文件。添加一行WEATHER_API_KEY你的和风天气KEY在app/build.gradle.kts中读取这个属性android { defaultConfig { // ... // 从 local.properties 读取 API Key val localProperties java.util.Properties() val localPropertiesFile rootProject.file(local.properties) if (localPropertiesFile.exists()) { localProperties.load(localPropertiesFile.inputStream()) } buildConfigField(String, WEATHER_API_KEY, \${localProperties.getProperty(WEATHER_API_KEY)}\) } }这样在Java代码中可以通过BuildConfig.WEATHER_API_KEY在Kotlin中通过BuildConfig.WEATHER_API_KEY来安全地访问它。记得将local.properties加入.gitignore避免提交到版本库。6. 项目基础架构搭建在写业务逻辑之前我们先搭建好项目的骨架。好的架构能让后续开发如行云流水。6.1 包结构规划按照功能和架构分层来组织包而不是按类型如把所有Activity放一起。建议如下com.yourname.weather/ ├── di/ // 依赖注入模块 (Hilt) ├── data/ // 数据层 │ ├── local/ // 本地数据源 (数据库DataStore) │ ├── remote/ // 远程数据源 (网络API) │ └── repository/ // 仓库整合本地和远程数据 ├── domain/ // 领域层 (可选放置业务逻辑和实体模型) │ └── model/ // 领域模型 ├── network/ // 网络相关 (Retrofit实例拦截器等) ├── ui/ // UI层 │ ├── theme/ // Compose主题定义 │ ├── components/ // 可复用的Compose组件 │ ├── home/ // 首页功能模块 │ │ ├── HomeScreen.kt │ │ ├── HomeViewModel.kt │ │ └── ... │ └── citysearch/ // 城市搜索功能模块 │ ├── CitySearchScreen.kt │ ├── CitySearchViewModel.kt │ └── ... └── util/ // 工具类6.2 网络层封装在network包下创建WeatherApiService.kt和RetrofitInstance.kt。RetrofitInstance.kt创建Retrofit单例实例。import com.yourname.weather.BuildConfig import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit object RetrofitInstance { private const val BASE_URL https://devapi.qweather.com/v7/ // 和风天气开发环境地址 private val loggingInterceptor HttpLoggingInterceptor().apply { level if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY // 调试时打印完整日志 } else { HttpLoggingInterceptor.Level.NONE // 发布时关闭日志 } } private val okHttpClient OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() private val retrofit by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) // 使用Gson解析JSON .build() } val api: WeatherApiService by lazy { retrofit.create(WeatherApiService::class.java) } }WeatherApiService.kt定义API接口。import retrofit2.http.GET import retrofit2.http.Query interface WeatherApiService { // 城市搜索 GET(city/lookup) suspend fun searchCity( Query(location) location: String, Query(key) key: String BuildConfig.WEATHER_API_KEY, Query(adm) adm: String? null, // 城市所属行政区域可选 Query(range) range: String? null, // 搜索范围可选 Query(number) number: Int 20 // 返回数量 ): CitySearchResponse // 实时天气 GET(weather/now) suspend fun getRealtimeWeather( Query(location) locationId: String, // 城市ID Query(key) key: String BuildConfig.WEATHER_API_KEY, Query(lang) lang: String zh // 语言 ): RealtimeWeatherResponse // 7天预报 GET(weather/7d) suspend fun get7DayForecast( Query(location) locationId: String, Query(key) key: String BuildConfig.WEATHER_API_KEY, Query(lang) lang: String zh ): ForecastResponse // 注意需要根据和风天气官方文档定义对应的数据类CitySearchResponse, RealtimeWeatherResponse等 // 这些数据类放在 data/remote/model 或 domain/model 包下。 }6.3 依赖注入Hilt配置在项目根目录的build.gradle.kts中添加Hilt插件依赖plugins { // ... id(com.google.dagger.hilt.android) version 2.48 apply false }在app/build.gradle.kts中应用插件并添加依赖前面已做。创建Hilt应用类在com.yourname.weather包下创建WeatherApp.kt。package com.yourname.weather import android.app.Application import dagger.hilt.android.HiltAndroidApp HiltAndroidApp class WeatherApp : Application()在AndroidManifest.xml中声明这个应用类application android:name.WeatherApp ... ... /application创建AppModule在di包下创建AppModule.kt用于提供全局单例。package com.yourname.weather.di import com.yourname.weather.network.WeatherApiService import com.yourname.weather.network.RetrofitInstance import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton Module InstallIn(SingletonComponent::class) object AppModule { Singleton Provides fun provideWeatherApiService(): WeatherApiService { return RetrofitInstance.api } // 未来可以在这里提供Repository、Database等实例 }现在我们就可以在ViewModel或其他类中使用Inject注解来注入WeatherApiService了。7. 常见问题与排查技巧实录准备工作看似琐碎但每一步都可能埋着坑。这里记录一些我踩过的以及新手最容易遇到的问题。7.1 环境与构建问题问题1Gradle Sync失败提示“Connection timed out”或“Could not download xxx.jar”。原因网络问题无法从Maven仓库下载依赖。解决检查网络连接尝试切换网络如手机热点。配置国内镜像源。在项目根目录的settings.gradle.kts或build.gradle.kts中修改仓库地址。// settings.gradle.kts dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() // 添加阿里云镜像 maven { url uri(https://maven.aliyun.com/repository/public) } maven { url uri(https://maven.aliyun.com/repository/google) } maven { url uri(https://maven.aliyun.com/repository/gradle-plugin) } } }关闭Android Studio删除项目目录下的.gradle文件夹和~/.gradle用户目录下的.gradle缓存重新打开同步。问题2编译报错“Manifest merger failed”。原因AndroidManifest.xml文件中的配置如权限、uses-sdk与引入的第三方库的Manifest冲突。解决在app/build.gradle.kts的android块内添加以下配置通常可以自动解决大部分冲突android { // ... packaging { resources { excludes /META-INF/{AL2.0,LGPL2.1} merges META-INF/LICENSE.md merges META-INF/LICENSE-notice.md } } }如果冲突是关于android:allowBackup等属性可以在AndroidManifest.xml的application标签中添加tools:replaceandroid:allowBackup。7.2 API与网络问题问题3网络请求返回403或401错误。原因API KEY错误、过期、或调用频率超限。解决检查local.properties中的KEY是否正确以及BuildConfig.WEATHER_API_KEY是否成功生成可以在代码中打印一下。登录和风天气控制台检查KEY的状态、剩余调用次数和QPM限制。在OkHttp的日志拦截器中查看完整的请求URL确认参数尤其是location和key拼接正确。问题4Retrofit抛出“Expected BEGIN_OBJECT but was BEGIN_ARRAY”等JSON解析错误。原因定义的数据类data class结构与API返回的JSON结构不匹配。解决使用Postman或浏览器直接访问API接口查看完整的、正确的JSON响应体。对照响应体仔细检查你的数据类定义。注意字段名、类型是对象{}还是数组[]、嵌套关系。可以使用SerializedName注解来映射JSON字段名和Kotlin属性名。推荐使用kotlinx.serialization它的编译时错误信息有时更友好。7.3 Compose与UI相关问题问题5Compose预览Preview无法显示或报错。原因预览函数依赖的上下文如ViewModel、Hilt注入在预览环境中无法提供。解决为预览提供默认参数或模拟对象。例如Preview Composable fun WeatherCardPreview() { WeatherCard(weatherData sampleWeatherData, onClick {}) }将状态逻辑与UI展示分离。让Composable函数接收状态数据作为参数而不是在内部直接调用ViewModel。这样预览时只需传入模拟数据即可。检查是否使用了需要真实Android环境的功能如Context、Resources在预览中可能需要用LocalContext.current或提供模拟值。问题6Compose界面重组导致不必要的网络请求或计算。原因在Composable函数内部直接执行耗时操作或发起网络请求。解决所有业务逻辑和状态管理都应放在ViewModel中。使用remember、rememberSaveable、derivedStateOf等API来缓存计算结果避免重复计算。使用LaunchedEffect在安全的协程作用域中执行副作用操作如响应事件发起请求并指定正确的key以控制其执行时机。7.4 设备与权限问题问题7在模拟器或真机上获取不到地理位置。原因模拟器未设置虚拟位置或真机未开启定位权限/高精度模式。解决模拟器在Android Studio的模拟器控制面板中使用“Location”选项卡发送虚拟的经纬度坐标。真机确保APP已申请并获得了ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION权限。在手机系统设置中检查该应用的权限管理确保定位权限已授予。打开手机的位置信息开关并选择“高精度模式”。代码检查使用FusedLocationProviderClientGoogle Play服务的一部分来获取位置它比原生API更智能、更省电。记得处理权限被拒绝的情况。准备工作到这里其实已经完成了80%。你已经从一个模糊的想法走到了一个结构清晰、工具就绪、蓝图在握的起跑点。剩下的20%就是在后续开发中根据实际遇到的问题回头来微调这个“地基”。比如你可能发现某个库不如另一个好用或者架构的某个部分需要调整这都很正常。重要的是我们有了一个扎实的、可维护的起点而不是一堆混乱的代码。在下一篇我们将真正开始编写代码实现第一个功能城市搜索与定位。