您好,登录后才能下订单哦!
# 如何使用Android聚合数据实现天气预报
## 目录
1. [前言](#前言)
2. [开发环境准备](#开发环境准备)
3. [聚合数据API申请](#聚合数据api申请)
4. [Android项目基础配置](#android项目基础配置)
5. [网络请求框架集成](#网络请求框架集成)
6. [数据模型设计](#数据模型设计)
7. [API接口封装](#api接口封装)
8. [UI界面设计](#ui界面设计)
9. [数据绑定与展示](#数据绑定与展示)
10. [定位功能集成](#定位功能集成)
11. [数据缓存策略](#数据缓存策略)
12. [错误处理与用户体验](#错误处理与用户体验)
13. [性能优化建议](#性能优化建议)
14. [完整代码示例](#完整代码示例)
15. [总结](#总结)
## 前言
在移动应用开发中,天气预报功能是常见的需求场景。本文将详细介绍如何利用聚合数据平台提供的天气API,在Android应用中实现完整的天气预报功能。通过本教程,您将掌握从API申请到最终界面展示的全流程开发技术。
聚合数据(Juhe.cn)是国内知名的数据服务平台,提供包括天气、快递、股票等多种数据接口。其天气API具有以下优势:
- 数据覆盖全国3000+市县
- 提供7天预报、实时天气、生活指数等完整数据
- 免费套餐适合个人开发者
- 响应速度快,稳定性高
## 开发环境准备
### 基础环境要求
- Android Studio 2022.3.1或更高版本
- JDK 17
- Gradle 8.0
- 最低兼容API Level 23(Android 6.0)
### 项目创建步骤
1. 打开Android Studio选择"New Project"
2. 选择"Empty Activity"模板
3. 配置项目信息:
- 应用名称:WeatherDemo
- 包名:com.example.weather
- 语言:Kotlin
- 最低SDK:API 23
### 必要依赖配置
在app/build.gradle文件中添加以下依赖:
```gradle
dependencies {
// 网络请求
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
// 协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
// 生命周期
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
// 定位服务
implementation 'com.google.android.gms:play-services-location:21.0.1'
// UI组件
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}
审核通过后: 1. 进入”我的API” 2. 找到”天气预报”服务 3. 记录下分配的AppKey(如:a1b2c3d4e5f6g7h8i9j0)
关键接口参数说明: - 请求地址:http://apis.juhe.cn/simpleWeather/query - 请求方式:GET - 参数: - city: 城市名称或ID - key: 申请的AppKey
返回数据示例:
{
"reason": "查询成功",
"result": {
"city": "北京",
"realtime": {
"temperature": "23",
"humidity": "45",
"info": "晴",
"wid": "00",
"direct": "东南风",
"power": "3级",
"aqi": "65"
},
"future": [
{
"date": "2023-05-01",
"temperature": "12/24℃",
"weather": "晴",
"wid": {"day": "00", "night": "00"},
"direct": "东南风"
}
]
},
"error_code": 0
}
在AndroidManifest.xml中添加权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
创建res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">apis.juhe.cn</domain>
</domain-config>
</network-security-config>
然后在AndroidManifest.xml的application标签中添加:
android:networkSecurityConfig="@xml/network_security_config"
采用MVVM架构: - Model: 数据模型和网络请求 - View: Activity/Fragment和XML布局 - ViewModel: 业务逻辑处理
项目包结构:
com.example.weather
├── api
├── model
├── repository
├── view
├── viewmodel
└── utils
创建ApiService.kt:
interface ApiService {
@GET("simpleWeather/query")
suspend fun getWeather(
@Query("city") city: String,
@Query("key") key: String = API_KEY
): Response<WeatherResponse>
companion object {
private const val BASE_URL = "http://apis.juhe.cn/"
private const val API_KEY = "your_app_key_here" // 替换为实际key
fun create(): ApiService {
val logger = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.connectTimeout(15, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
}
创建扩展函数处理网络请求:
suspend fun <T> safeApiCall(
apiCall: suspend () -> Response<T>
): Result<T> {
return try {
val response = apiCall()
if (response.isSuccessful) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("API error: ${response.code()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
根据API返回结构创建数据类:
data class WeatherResponse(
val reason: String,
val result: WeatherResult,
val error_code: Int
)
data class WeatherResult(
val city: String,
val realtime: RealtimeWeather,
val future: List<FutureWeather>
)
data class RealtimeWeather(
val temperature: String,
val humidity: String,
val info: String,
val wid: String,
val direct: String,
val power: String,
val aqi: String
)
data class FutureWeather(
val date: String,
val temperature: String,
val weather: String,
val wid: DayNightWid,
val direct: String
)
data class DayNightWid(
val day: String,
val night: String
)
创建WeatherRepository处理业务逻辑:
class WeatherRepository {
private val apiService = ApiService.create()
suspend fun getWeatherByCity(city: String): Result<WeatherResult> {
return safeApiCall { apiService.getWeather(city) }.map { it.result }
}
suspend fun getWeatherByLocation(lat: Double, lon: Double): Result<WeatherResult> {
// 实际项目中需要先通过逆地理编码获取城市名称
val city = convertLocationToCity(lat, lon)
return getWeatherByCity(city)
}
private suspend fun convertLocationToCity(lat: Double, lon: Double): String {
// 这里简化为返回固定值,实际应使用地理编码API
return "北京"
}
}
res/layout/activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 当前天气 -->
<include layout="@layout/layout_current_weather"/>
<!-- 天气预报列表 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="未来预报"
android:textSize="18sp"
android:layout_marginTop="24dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvForecast"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"/>
</LinearLayout>
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
res/layout/layout_current_weather.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/bg_weather_card"
android:padding="16dp">
<TextView
android:id="@+id/tvCity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/ivWeatherIcon"
android:layout_width="64dp"
android:layout_height="64dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/tvTemperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="36sp"/>
<TextView
android:id="@+id/tvWeatherInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="16sp"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:weightSum="3">
<TextView
android:id="@+id/tvHumidity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawableTop="@drawable/ic_humidity"
android:gravity="center"/>
<!-- 类似添加风向、空气质量等视图 -->
</LinearLayout>
</LinearLayout>
创建WeatherViewModel:
class WeatherViewModel : ViewModel() {
private val repository = WeatherRepository()
private val _weatherData = MutableLiveData<WeatherResult>()
val weatherData: LiveData<WeatherResult> = _weatherData
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
fun fetchWeather(city: String) {
viewModelScope.launch {
_isLoading.value = true
when (val result = repository.getWeatherByCity(city)) {
is Result.Success -> {
_weatherData.value = result.data
}
is Result.Failure -> {
_errorMessage.value = result.exception.message
}
}
_isLoading.value = false
}
}
fun fetchWeather(lat: Double, lon: Double) {
viewModelScope.launch {
_isLoading.value = true
when (val result = repository.getWeatherByLocation(lat, lon)) {
is Result.Success -> {
_weatherData.value = result.data
}
is Result.Failure -> {
_errorMessage.value = result.exception.message
}
}
_isLoading.value = false
}
}
}
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: WeatherViewModel by viewModels()
private lateinit var forecastAdapter: ForecastAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupViews()
setupObservers()
loadInitialData()
}
private fun setupViews() {
forecastAdapter = ForecastAdapter()
binding.rvForecast.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = forecastAdapter
}
binding.swipeRefresh.setOnRefreshListener {
viewModel.weatherData.value?.let {
viewModel.fetchWeather(it.city)
} ?: loadInitialData()
}
}
private fun setupObservers() {
viewModel.weatherData.observe(this) { weather ->
updateCurrentWeather(weather)
forecastAdapter.submitList(weather.future)
}
viewModel.isLoading.observe(this) { isLoading ->
binding.swipeRefresh.isRefreshing = isLoading
}
viewModel.errorMessage.observe(this) { message ->
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
private fun loadInitialData() {
// 默认加载北京天气
viewModel.fetchWeather("北京")
}
private fun updateCurrentWeather(weather: WeatherResult) {
binding.tvCity.text = weather.city
binding.tvTemperature.text = "${weather.realtime.temperature}℃"
binding.tvWeatherInfo.text = weather.realtime.info
binding.tvHumidity.text = "${weather.realtime.humidity}%"
// 根据天气代码设置图标
val iconRes = when(weather.realtime.wid) {
"00" -> R.drawable.ic_sunny
"01" -> R.drawable.ic_cloudy
// 其他天气代码处理
else -> R.drawable.ic_unknown
}
binding.ivWeatherIcon.setImageResource(iconRes)
}
}
在Activity中添加权限请求:
private val locationPermissionRequest = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
when {
permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> {
getCurrentLocation()
}
permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> {
getCurrentLocation()
}
else -> {
// 权限被拒绝,使用默认城市
viewModel.fetchWeather("北京")
}
}
}
private fun checkLocationPermission() {
when {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED -> {
getCurrentLocation()
}
shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) -> {
showPermissionExplanationDialog()
}
else -> {
locationPermissionRequest.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
))
}
}
}
private fun getCurrentLocation() {
val locationClient = LocationServices.getFusedLocationProviderClient(this)
try {
locationClient.lastLocation
.addOnSuccessListener { location ->
location?.let {
viewModel.fetchWeather(it.latitude, it.longitude)
} ?: run {
viewModel.fetchWeather("北京")
}
}
} catch (e: SecurityException) {
Log.e("Location", "Error getting location", e)
}
}
implementation 'androidx.room:room-ktx:2.5.2'
kapt 'androidx.room:room-compiler:2.5.2'
@Entity(tableName = "weather_cache")
data class WeatherCache(
@PrimaryKey val city: String,
val timestamp: Long,
@ColumnInfo(name = "weather_data") val weatherData: String
)
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。