Android开发(kotlin) 开发一个简单天气应用

目录

编写一个简单的天气预报app。

功能需求及技术可行性

搜索国内各个城市的的数据

查看国内大多数城市的天气信息

获取天气数据的api

kotlin 复制代码
https://pan.baidu.com/s/1wMjVpsiigYZ46ErDJm-W6A 提取码: 17a4

城市数据选用adcode.csv文件中的数据

接口使用彩云天气,token是注册后得到的,接口地址的经纬度,从adcode.csv红获取。

天气实况

https://api.caiyunapp.com/v2.6/{token}/101.6656,39.2072/realtime

kotlin 复制代码
{
  "status": "ok",             // 返回状态  
  "api_version": "v2.6",    // API 版本
  "api_status": "active",     // API 服务状态
  "lang": "zh_CN",          // 返回语言
  "unit": "metric",        // 单位
  "tzshift": 28800,       // 时区偏移
  "timezone": "Asia/Shanghai",  // 时区
  "server_time": 1640745758,
  "location": [39.2072, 101.6656],
  "result": {
    "realtime": {
      "status": "ok",
      "temperature": -7,  // 地表 2 米气温
      "humidity": 0.58,  // 地表 2 米湿度相对湿度(%)
      "cloudrate": 0,  // 总云量(0.0-1.0)
      "skycon": "CLEAR_DAY",  // 天气现象
      "visibility": 7.8,  // 地表水平能见度
      "dswrf": 47.7,  // 向下短波辐射通量(W/M2)
      "wind": {
        "speed": 1.8,  // 地表 10 米风速
        "direction": 22  // 地表 10 米风向
      },
      "pressure": 85583.47,  // 地面气压
      "apparent_temperature": -9.9,  // 体感温度
      "precipitation": {
        "local": {
          "status": "ok",
          "datasource": "radar",
          "intensity": 0  // 本地降水强度
        },
        "nearest": {
          "status": "ok",
          "distance": 10000,  // 最近降水带与本地的距离
          "intensity": 0  // 最近降水处的降水强度
        }
      },
      "air_quality": {
        "pm25": 45,  // PM25 浓度(μg/m3)
        "pm10": 49,  // PM10 浓度(μg/m3)
        "o3": 6,  // 臭氧浓度(μg/m3)
        "so2": 8,  // 二氧化硫浓度(μg/m3)
        "no2": 42,  // 二氧化氮浓度(μg/m3)
        "co": 1.1,  // 一氧化碳浓度(mg/m3)
        "aqi": {
          "chn": 63,  // 国标 AQI
          "usa": 124
        },
        "description": {
          "chn": "良",
          "usa": "轻度污染"
        }
      },
      "life_index": {
        "ultraviolet": {
          "index": 3,
          "desc": "弱"  // 参见 [生活指数](tables/lifeindex)
        },
        "comfort": {
          "index": 12,
          "desc": "湿冷"  // 参见 [生活指数](tables/lifeindex)
        }
      }
    },
    "primary": 0
  }
}

未来三天天气预报

https://api.caiyunapp.com/v2.6/{token}/101.6656,39.2072/daily?dailysteps=3

kotlin 复制代码
{
  "status": "ok", // 返回状态
  "api_version": "v2.6", // API 版本
  "api_status": "alpha", // API 状态
  "lang": "zh_CN", // 语言
  "unit": "metric", // 单位
  "tzshift": 28800, // 时区偏移
  "timezone": "Asia/Shanghai", // 时区
  "server_time": 1653552787, // 服务器时间
  "location": [
    39.2072, // 纬度
    101.6656 // 经度
  ],
  "result": {
    "daily": {
      "status": "ok",
      "astro": [ // 日出日落时间
        {
          "date": "2022-05-26T00:00+08:00",
          "sunrise": {
            "time": "05:51" // 日出时间
          },
          "sunset": {
            "time": "20:28" // 日落时间
          }
        }
      ],
      "precipitation_08h_20h": [ // 白天降水数据
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 0, // 白天最大降水量
          "min": 0, // 白天最小降水量
          "avg": 0, // 白天平均降水量
          "probability": 0 // 白天降水概率
        }
      ],
      "precipitation_20h_32h": [ // 夜晚降水数据
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 0, // 夜晚最大降水量
          "min": 0, // 夜晚最小降水量
          "avg": 0, // 夜晚平均降水量
          "probability": 0 // 夜晚降水概率
        }
      ],
      "precipitation": [ // 降水数据
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 0, // 全天最大降水量
          "min": 0, // 全天最小降水量
          "avg": 0, // 全天平均降水量
          "probability": 0 // 全天降水概率
        }
      ],
      "temperature": [ // 全天地表 2 米气温
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 27, // 全天最高气温
          "min": 18, // 全天最低气温
          "avg": 23.75 // 全天平均气温
        }
      ],
      "temperature_08h_20h": [ // 白天地表 2 米气温
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 27, // 白天最高气温
          "min": 18, // 白天最低气温
          "avg": 24.57 // 白天平均气温
        }
      ],
      "temperature_20h_32h": [ // 夜晚地表 2 米气温
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 24.8, // 夜晚最高气温
          "min": 18, // 夜晚最低气温
          "avg": 20.02 // 夜晚平均气温
        }
      ],
      "wind": [ // 全天地表 10 米风速
        {
          "date": "2022-05-26T00:00+08:00",
          "max": {
            "speed": 28.24,
            "direction": 122.62
          },
          "min": {
            "speed": 9,
            "direction": 104
          },
          "avg": {
            "speed": 21.61,
            "direction": 118.02
          }
        }
      ],
      "wind_08h_20h": [ // 白天地表 10 米风速
        {
          "date": "2022-05-26T00:00+08:00",
          "max": {
            "speed": 28.24,
            "direction": 122.62
          },
          "min": {
            "speed": 9,
            "direction": 104
          },
          "avg": {
            "speed": 22.74,
            "direction": 115.78
          }
        }
      ],
      "wind_20h_32h": [ // 夜晚地表 10 米风速
        {
          "date": "2022-05-26T00:00+08:00",
          "max": {
            "speed": 22.39,
            "direction": 97.46
          },
          "min": {
            "speed": 9.73,
            "direction": 125.93
          },
          "avg": {
            "speed": 16,
            "direction": 121.62
          }
        }
      ],
      "humidity": [ // 地表 2 米相对湿度(%)
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 0.18,
          "min": 0.08,
          "avg": 0.09
        }
      ],
      "cloudrate": [ // 云量(0.0-1.0)
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 1,
          "min": 0,
          "avg": 0.75
        }
      ],
      "pressure": [ // 地面气压
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 84500.84,
          "min": 83940.84,
          "avg": 83991.97
        }
      ],
      "visibility": [ // 地表水平能见度
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 25, // 最大能见度
          "min": 24.13, // 最小能见度
          "avg": 25 // 平均能见度
        }
      ],
      "dswrf": [ // 向下短波辐射通量(W/M2)
        {
          "date": "2022-05-26T00:00+08:00",
          "max": 741.9, // 最大辐射通量
          "min": 0, // 最小辐射通量
          "avg": 368.6 // 平均辐射通量
        }
      ],
      "air_quality": {
        "aqi": [
          {
            "date": "2022-05-26T00:00+08:00",
            "max": {
              "chn": 183, // 中国国标 AQI 最大值
              "usa": 160 // 美国国标 AQI 最大值
            },
            "avg": {
              "chn": 29, // 中国国标 AQI 平均值
              "usa": 57 // 美国国标 AQI 平均值
            },
            "min": {
              "chn": 20, // 中国国标 AQI 最小值
              "usa": 42 // 美国国标 AQI 最小值
            }
          }
        ],
        "pm25": [
          {
            "date": "2022-05-26T00:00+08:00",
            "max": 74, // PM2.5 浓度最大值
            "avg": 15, // PM2.5 浓度平均值
            "min": 10 // PM2.5 浓度最小值
          }
        ]
      },
      "skycon": [
        {
          "date": "2022-05-26T00:00+08:00",
          "value": "PARTLY_CLOUDY_DAY" // 全天主要天气现象
        }
      ],
      "skycon_08h_20h": [
        {
          "date": "2022-05-26T00:00+08:00",
          "value": "PARTLY_CLOUDY_DAY" // 白天主要天气现象
        }
      ],
      "skycon_20h_32h": [
        {
          "date": "2022-05-26T00:00+08:00",
          "value": "CLOUDY" // 夜晚主要天气现象
        }
      ],
      "life_index": {
        "ultraviolet": [
          {
            "date": "2022-05-26T00:00+08:00",
            "index": "1",
            "desc": "最弱" // 紫外线指数自然语言
          }
        ],
        "carWashing": [
          {
            "date": "2022-05-26T00:00+08:00",
            "index": "1",
            "desc": "适宜" // 洗车指数自然语言
          }
        ],
        "dressing": [
          {
            "date": "2022-05-26T00:00+08:00",
            "index": "4",
            "desc": "温暖" // 穿衣指数自然语言
          }
        ],
        "comfort": [
          {
            "date": "2022-05-26T00:00+08:00",
            "index": "4",
            "desc": "温暖" // 舒适度指数自然语言
          }
        ],
        "coldRisk": [
          {
            "date": "2022-05-26T00:00+08:00",
            "index": "4",
            "desc": "极易发" // 感冒指数自然语言
          }
        ]
      }
    },
    "primary": 0
  }
}

mvvm项目架构

依赖库

kotlin 复制代码
dependencies {
	...........
    //数据库
    val room_version = "2.8.4"

    implementation("androidx.room:room-runtime:$room_version")
    ksp("androidx.room:room-compiler:$room_version")
    //协程
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")


    implementation("com.squareup.retrofit2:retrofit:3.0.0")
    implementation("com.squareup.retrofit2:converter-gson:3.0.0")

    //livedata
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3")

    //fragment
    val fragment_version = "1.8.9"
    implementation("androidx.fragment:fragment-ktx:${fragment_version}")

    implementation("androidx.recyclerview:recyclerview:1.4.0")

    implementation("androidx.cardview:cardview:1.0.0")
}

开启viewBinding

kotlin 复制代码
android {
    ............
    buildFeatures {
        compose = true
        viewBinding = true
    }
}

图片素材自行下载

https://www.alipan.com/s/qHdnmYXkxCX

提取码: 2cqi

获取全国城市数据

创建一个全局获取Context的方式,创建WeatherApplication

kotlin 复制代码
class WeatherApplication : Application() {

    companion object {

        @SuppressLint("StaticFieldLeak")
        lateinit var context: Context
		//申请的token 
        const val token = "***************"
    }

    override fun onCreate() {
        super.onCreate()
        context = applicationContext
    }

}

在mainfests中指定WeatherApplication

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:name=".WeatherApplication"
        android:allowBackup="false"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:networkSecurityConfig="@xml/network_security_config"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Lichengweather">
    </application>

</manifest>

使用

kotlin 复制代码
WeatherApplication.context

将csv的数据导入包数据库中,从本地数据库获取相关数据。

先将csv中的数据导入到sqlite数据库中,导出lc.db文件,准备预存数据。

定义城市数据模型,添加@Entity注解,成为数据库的实体类

kotlin 复制代码
@Entity
data class Place(
    @PrimaryKey
    val adcode: Int,
    val address: String,
    val lng: String,
    val lat: String
)

定义Dao

kotlin 复制代码
@Dao
interface PlaceDao {

    @Insert
    fun insertPlace(place: Place)


    /**
     * 通过adcode来获取Place
     */
    @Query("select * from place where adcode = :adcode")
    fun findPlaceByCode(adcode: Int): Place


    /**
     * 通过关键字进行模糊查询
     */
    @Query("select * from place where address like '%'||:name||'%'")
    fun findPlaceByAddress(name: String): List<Place>

}

定义数据库

将上面的导出db文件复制到assets/db,通过createFromAsset()方法就可以预存数据了。

kotlin 复制代码
@Database(version = 1, entities = [Place::class])
abstract class AppDataBase : RoomDatabase() {

    abstract fun placeDao(): PlaceDao

    companion object {

        private var instant: AppDataBase? = null

        fun getDataBase(context: Context): AppDataBase {

            instant?.let {
                return it
            }
            return Room.databaseBuilder(
                context.applicationContext,
                AppDataBase::class.java,
                "licheng.db"
            ) .createFromAsset("db/lc.db").build().apply {
                instant = this
            }
        }
    }

}

仓库层,创建Repository类

kotlin 复制代码
object Repository {
    /**
     * 获取地址
     */
    fun getPlace(content: String) = liveData(Dispatchers.IO) {

        val result = try {

            val appDataBase = AppDataBase.getDataBase(WeatherApplication.context)
            val placeDao = appDataBase.placeDao()

            val placeList = placeDao.findPlaceByAddress(content)
            Result.success(placeList)

        } catch (e: Exception) {

            Result.failure(e)
        }
        emit(result)
    }
}

上面liveData()可以自动构建并返回一个LiveData对象,并在它的代码块中提供一个挂起函数的上下文。如果获取到城市列表数据, 用Result.success()来包装获取的数据;如果出现异常则用 Result.failure(e)来包装一个异常信息。最后用 emit(result)将结果发射出去。

定义viewmodel

kotlin 复制代码
class PlaceVM : ViewModel() {

    private val contentData = MutableLiveData<String>()

    val placeList = ArrayList<Place>()

    val getPlaceData = contentData.switchMap { content ->

        Repository.getPlace(content)
    }


    fun getContent( content: String) {

        contentData.value = content
    }

}

这里没有直接调用仓库中的getPlace(),将值赋值给了contentData,使用switchMap()方法来观察这个对象。

ui层

fragment_place.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">


    <EditText
        android:id="@+id/place_et_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="10dp"
        android:hint="请输入文字"
        android:paddingTop="8dp"
        android:paddingBottom="8dp" />


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/place_rv_address"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

这个布局有了两个部分,EditTexty用于给用户提供一个搜索框来进行搜索,RecyclerView则用于对搜索出来的结构进行展示。

RecyclerView的item_place.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="10dp"
    android:layout_marginTop="8dp"
    android:layout_marginEnd="10dp"
    android:layout_marginBottom="8dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingStart="10dp"
        android:paddingTop="8dp"
        android:paddingBottom="8dp">

        <TextView
            android:id="@+id/place_tv_adcode"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="123456" />

        <TextView
            android:id="@+id/place_tv_address"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="济南" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/place_tv_lng"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="lng:127.0" />

            <TextView
                android:id="@+id/place_tv_lat"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="lat:56.2" />
        </LinearLayout>

    </LinearLayout>

</androidx.cardview.widget.CardView>

创建城市适配器PlaceAdapter

kotlin 复制代码
class PlaceAdapter(private val fragment: Fragment, private val placeList: List<Place>) :
    RecyclerView.Adapter<PlaceAdapter.PlaceViewHolder>() {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceViewHolder {

        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_place, parent, false)
        return PlaceViewHolder(view)
    }

    override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) {

        val place = placeList[position]
        holder.setData(place)
    }

    override fun getItemCount(): Int {

        return placeList.size
    }


    inner class PlaceViewHolder(view: View) : RecyclerView.ViewHolder(view) {

        val place_tv_adcode = view.findViewById<TextView>(R.id.place_tv_adcode)
        val place_tv_address = view.findViewById<TextView>(R.id.place_tv_address)
        val place_tv_lng = view.findViewById<TextView>(R.id.place_tv_lng)
        val place_tv_lat = view.findViewById<TextView>(R.id.place_tv_lat)

        lateinit var place: Place

        fun setData(place: Place) {

            this.place = place

            place_tv_adcode.text = place.adcode.toString()
            place_tv_address.text = place.address
            place_tv_lng.text = "lng:" + place.lng.toString()
            place_tv_lat.text = "lat:" + place.lat.toString()
            itemView.setOnClickListener {

                val intent = Intent(itemView.context, MainActivity::class.java).apply {

                    putExtra("location_lng", place.lng)
                    putExtra("location_lat", place.lat)
                    putExtra("place_name", place.address)
                }
                fragment.startActivity(intent)
                fragment.activity?.finish()
            }
        }
    }
}

创建城市fragment

kotlin 复制代码
class PlaceFragment : Fragment() {

    val viewModel by lazy {

        ViewModelProvider(this)[PlaceVM::class.java]
    }

    private lateinit var placeAdapter: PlaceAdapter

    private lateinit var fragmentPlaceBinding: FragmentPlaceBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        fragmentPlaceBinding = FragmentPlaceBinding.inflate(inflater, container, false)
        return fragmentPlaceBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val layoutManager = LinearLayoutManager(view.context)
        fragmentPlaceBinding.placeRvAddress.layoutManager = layoutManager
        placeAdapter = PlaceAdapter(this, viewModel.placeList)
        fragmentPlaceBinding.placeRvAddress.adapter = placeAdapter
        fragmentPlaceBinding.placeEtContent.addTextChangedListener { editable ->

            val content = editable.toString()
            if (content.isNotEmpty()) {

                //获取城市
                viewModel.getContent(content)
            } else {

                fragmentPlaceBinding.placeRvAddress.visibility = View.GONE
                viewModel.placeList.clear()
                placeAdapter.notifyDataSetChanged()
            }
        }

        viewModel.getPlaceData.observe(viewLifecycleOwner) { result ->

            val places = result.getOrNull()
            if (places != null) {

                fragmentPlaceBinding.placeRvAddress.visibility = View.VISIBLE
                viewModel.placeList.clear()
                viewModel.placeList.addAll(places)
                placeAdapter.notifyDataSetChanged()
            } else {

                Toast.makeText(WeatherApplication.context, "没有数据", Toast.LENGTH_SHORT).show()
                result.exceptionOrNull()?.printStackTrace()
            }
        }

    }
}

lazy ()函数懒加载获取PlaceVMd额实例。onCreateView加载布局。onViewCreated给RecyclerViews设置layoutmanager和adapter,监听输入框的内容变化来从数据库中获取数据,订阅getPlaceDatal来监听数据变化进行显示。

将fragment添加到activity

kotlin 复制代码
class PlaceActivity : AppCompatActivity() {


    private lateinit var activityPlaceBinding: ActivityPlaceBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        activityPlaceBinding = ActivityPlaceBinding.inflate(layoutInflater)
        setContentView(activityPlaceBinding.getRoot())

        supportFragmentManager
            .beginTransaction()
            .add(R.id.place_ll_f, PlaceFragment())
            .commit()
    }
}
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/place_ll_f"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

</LinearLayout>

效果图

显示天气信息

实时天气数据模型

Realtime.kt

kotlin 复制代码
package com.bz.yh.xd.lichengweather.model

/**
 * 天气实况类
 */
data class Realtime(
    val api_status: String,
    val api_version: String,
    val lang: String,
    val location: List<Double>,
    val result: RealResult,
    val server_time: Int,
    val status: String,
    val timezone: String,
    val tzshift: Int,
    val unit: String
)

data class RealResult(
    val primary: Int,
    val realtime: RealtimeX,
)

data class RealtimeX(
    val air_quality: AirQuality,
    val apparent_temperature: Double,
    val cloudrate: Float,
    val dswrf: Double,
    val humidity: Double,
    val life_index: LifeIndex,
    val precipitation: Precipitation,
    val pressure: Double,
    val skycon: String,
    val status: String,
    val temperature: Float,
    val visibility: Double,
    val wind: Wind
)

data class AirQuality(
    val aqi: Aqi,
    val co: Double,
    val description: Description,
    val no2: Int,
    val o3: Int,
    val pm10: Int,
    val pm25: Int,
    val so2: Int
)

data class LifeIndex(
    val comfort: Comfort,
    val ultraviolet: Ultraviolet
)

data class Precipitation(
    val local: Local,
    val nearest: Nearest
)

data class Wind(
    val direction: Float,
    val speed: Float
)

data class Aqi(
    val chn: Int,
    val usa: Int
)

data class Description(
    val chn: String,
    val usa: String
)

data class Comfort(
    val desc: String,
    val index: Int
)

data class Ultraviolet(
    val desc: String,
    val index: Int
)

data class Local(
    val datasource: String,
    val intensity: Int,
    val status: String
)

data class Nearest(
    val distance: Int,
    val intensity: Int,
    val status: String
)

未来几天天气信息数据格式

DailyWeather.kt

kotlin 复制代码
package com.bz.yh.xd.lichengweather.model

data class DailyWeather(
    val api_status: String,
    val api_version: String,
    val lang: String,
    val location: List<Double>,
    val result: DailyResult,
    val server_time: Int,
    val status: String,
    val timezone: String,
    val tzshift: Int,
    val unit: String
)

data class DailyResult(
    val daily: Daily,
    val primary: Int
)

data class Daily(
    val air_quality: DailyAirQuality,
    val astro: List<Astro>,
    val cloudrate: List<Cloudrate>,
    val dswrf: List<Dswrf>,
    val humidity: List<Humidity>,
    val life_index: DailyLifeIndex,
    val precipitation: List<DailyPrecipitation>,
    val precipitation_08h_20h: List<Precipitation08h20h>,
    val precipitation_20h_32h: List<Precipitation08h20h>,
    val pressure: List<Pressure>,
    val skycon: List<Skycon>,
    val skycon_08h_20h: List<Skycon08h20h>,
    val skycon_20h_32h: List<Skycon08h20h>,
    val status: String,
    val temperature: List<Temperature>,
    val temperature_08h_20h: List<Temperature08h20h>,
    val temperature_20h_32h: List<Temperature08h20h>,
    val visibility: List<Visibility>,
    val wind: List<DailyWind>,
    val wind_08h_20h: List<Wind08h20h>,
    val wind_20h_32h: List<Wind08h20h>
)

data class DailyAirQuality(
    val aqi: List<DailyAqi>,
    val pm25: List<Pm25>
)

data class Astro(
    val date: String,
    val sunrise: Sunrise,
    val sunset: Sunset
)

data class Cloudrate(
    val avg: Double,
    val date: String,
    val max: Double,
    val min: Double
)

data class Dswrf(
    val avg: Double,
    val date: String,
    val max: Double,
    val min: Double
)

data class Humidity(
    val avg: Double,
    val date: String,
    val max: Double,
    val min: Double
)

data class DailyLifeIndex(
    val carWashing: List<CarWashing>,
    val coldRisk: List<ColdRisk>,
    val comfort: List<DailyComfort>,
    val dressing: List<Dressing>,
    val ultraviolet: List<DailyUltraviolet>
)

data class DailyPrecipitation(
    val avg: Double,
    val date: String,
    val max: Double,
    val min: Double,
    val probability: Int
)

data class Precipitation08h20h(
    val avg: Double,
    val date: String,
    val max: Double,
    val min: Double,
    val probability: Int
)

data class Pressure(
    val avg: Double,
    val date: String,
    val max: Double,
    val min: Double
)

data class Skycon(
    val date: String,
    val value: String
)

data class Skycon08h20h(
    val date: String,
    val value: String
)

data class Temperature(
    val avg: Double,
    val date: String,
    val max: Double,
    val min: Double
)

data class Temperature08h20h(
    val avg: Double,
    val date: String,
    val max: Double,
    val min: Double
)

data class Visibility(
    val avg: Double,
    val date: String,
    val max: Double,
    val min: Double
)

data class DailyWind(
    val avg: AvgX,
    val date: String,
    val max: MaxX,
    val min: MinX
)

data class Wind08h20h(
    val avg: AvgX,
    val date: String,
    val max: MaxX,
    val min: MinX
)

data class DailyAqi(
    val avg: Avg,
    val date: String,
    val max: Max,
    val min: Min
)

data class Pm25(
    val avg: Int,
    val date: String,
    val max: Int,
    val min: Int
)

data class Avg(
    val chn: Int,
    val usa: Int
)

data class Max(
    val chn: Int,
    val usa: Int
)

data class Min(
    val chn: Int,
    val usa: Int
)

data class Sunrise(
    val time: String
)

data class Sunset(
    val time: String
)

data class CarWashing(
    val date: String,
    val desc: String,
    val index: String
)

data class ColdRisk(
    val date: String,
    val desc: String,
    val index: String
)

data class DailyComfort(
    val date: String,
    val desc: String,
    val index: String
)

data class Dressing(
    val date: String,
    val desc: String,
    val index: String
)

data class DailyUltraviolet(
    val date: String,
    val desc: String,
    val index: String
)

data class AvgX(
    val direction: Double,
    val speed: Double
)

data class MaxX(
    val direction: Double,
    val speed: Double
)

data class MinX(
    val direction: Double,
    val speed: Double
)

创建Weatherl类,合并数据

kotlin 复制代码
data class Weather(val realtime: RealtimeX, val daily: Daily)

网路层,

创建retrofit

kotlin 复制代码
object ServiceCreator {


    private const val BASE_URL = "http://api.caiyunapp.com"

    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    fun <T> create(serviceClass: Class<T>): T = retrofit.create<T>(serviceClass)

    inline fun <reified T> create(): T = create(T::class.java)
}

添加网络接口,新建WeatherService接口

kotlin 复制代码
interface WeatherService {

    /**
     * 根据经纬度获取天气实况
     */
    @GET("/v2.6/{token}/{lng},{lat}/realtime")
    fun realtime(
        @Path("token") token: String,
        @Path("lng") lng: Float,
        @Path("lat") lat: Float
    ): Call<Realtime>


    //获取未来三天的天气
    @GET("/v2.6/{token}/{lng},{lat}/daily?dailysteps=3")
    fun daily(
        @Path("token") token: String,
        @Path("lng") lng: Float,
        @Path("lat") lat: Float
    ): Call<DailyWeather>
}

realtime()方法用来获取实时天气信息,daily()获取未来的天气信息。

在网络数据源WeatherNetwork中对接口进行封装

kotlin 复制代码
object WeatherNetwork {

    private val weatherService = ServiceCreator.create<WeatherService>()

    suspend fun realtime(token: String, lng: Float, lat: Float) =
        weatherService.realtime(token, lng, lat).await()

    suspend fun daily(token: String, lng: Float, lat: Float) =
        weatherService.daily(token, lng, lat).await()

    private suspend fun <T> Call<T>.await(): T {

        return suspendCoroutine { continuation ->

            enqueue(object : Callback<T> {
                override fun onResponse(call: Call<T?>, response: Response<T?>) {

                    val body = response.body()
                    if (body != null) {

                        continuation.resume(body)
                    } else {

                        continuation.resumeWithException(RuntimeException("response body is null"))
                    }
                }

                override fun onFailure(call: Call<T?>, t: Throwable) {

                    continuation.resumeWithException(t)
                }

            })
        }
    }
}

WeatherNetwork创建了一个WeatherServicej接口的动态代理对象。await()使用协程简化retrofit的回调,将realtime()和daily()声明成挂起函数。

仓库层,添加获取天气数据的代码

kotlin 复制代码
object Repository {


    fun refreshWeather(lng: Float, lat: Float) = liveData(Dispatchers.IO) {

        val result = try {

            coroutineScope {

                val realtime = async {

                    WeatherNetwork.realtime(WeatherApplication.token, lng, lat)
                }
                val daily3 = async {
                  	delay(1500)//防止请求太频繁,获取数据失败
                    WeatherNetwork.daily(WeatherApplication.token, lng, lat)
                }
                val realtimeResponse = realtime.await()
                val daily3Response = daily3.await()
                if (realtimeResponse.status == "OK" && daily3Response.status == "OK") {

                    val weather =
                        Weather(realtimeResponse.result.realtime, daily3Response.result.daily)
                    Result.success(weather)
                } else {

                    Result.failure(
                        RuntimeException(
                            "realtimeException response status is ${realtimeResponse.status}" +
                                    "daily response status is ${daily3Response.status}"
                        )
                    )
                }

            }

        } catch (e: Exception) {

            Result.failure(e)
        }

        emit(result)
    }

    /**
     * 获取地址
     */
    fun getPlace(content: String) = liveData(Dispatchers.IO) {

      .....
    }

}

refreshWeather()使用async()、await()函数,保证两个请求都成功响应后再执行下一步。两个请求成功获取数据后,拼装成一个Weather对象,通过emit()将结果发送出去。

简化仓库层try catch,在某个统一的入口函数中进行封装,使得只要一次try catch处理就行了。

kotlin 复制代码
object Repository {


    fun refreshWeather(lng: Float, lat: Float) = liveData(Dispatchers.IO) {
        .......
        emit(result)
    }


    /**
     * 处理try-catch
     */
    fun refreshWeather2(lng: Float, lat: Float) = fire(Dispatchers.IO) {

        coroutineScope {

            val realtime = async {

                WeatherNetwork.realtime(WeatherApplication.token, lng, lat)
            }

            val daily3 = async {
                delay(1500)
                WeatherNetwork.daily(WeatherApplication.token, lng, lat)
            }
            val realtimeResponse = realtime.await()
            val daily3Response = daily3.await()
            if (realtimeResponse.status.equals("ok",true) && daily3Response.status.equals("ok",true)) {

                val weather =
                    Weather(realtimeResponse.result.realtime, daily3Response.result.daily)
                Result.success(weather)
            } else {

                Result.failure(
                    RuntimeException(
                        "realtimeException response status is ${realtimeResponse.status}" +
                                "daily response status is ${daily3Response.status}"
                    )
                )
            }

        }
    }

    private fun <T> fire(context: CoroutineContext, block: suspend () -> Result<T>) =
        liveData<Result<T>>(context) {

            val result = try {

                block()
            } catch (e: Exception) {

                Result.failure<T>(e)
            }
            emit(result)
        }
}

新增fire()函数,在函数的内部会先调用一下liveData()函数,在liveData()函数的代码块中统一进行了try catch处理。

在liveData()函数的代码块中,是拥有挂起函数上下文的,lambda表达式中的代码是在挂起函数中运行的。我们需要再函数类型前声明一个suspend关键字,表示所有传入的lambda表达式中的代码也是拥有挂起函数上下文的。

定义viewmodel

kotlin 复制代码
class WeatherViewModel : ViewModel() {

    private val locatioinLiveData = MutableLiveData<Location>()

    var locationLng = ""

    var locationLat = ""

    var placeName = ""

    val weatherLiveData = locatioinLiveData.switchMap { location ->

        Repository.refreshWeather2(location.lng, location.lat)
    }

    fun refreshWeather(lng: Float, lat: Float) {

        locatioinLiveData.value = Location(lng, lat)
    }
}

这里定义了refreshWeather()方法来获取天气信息,将传入的lng和lat参数封装成一个Location对象,赋值给locatioinLiveData对象,使用locatioinLiveData的switchMap方法来观察这个对象,当值有变化的时候,调用仓库层中的refreshWeather2()方法。
ui实现

创建MainActivity来显示天气信息

将页面分成多个xml布局,然后再拼接起来。

天气实况

now.xml 当前天气信息的布局

kotlin 复制代码
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/nowLayout"
    android:layout_width="match_parent"
    android:fitsSystemWindows="true"
    android:layout_height="530dp">

    <FrameLayout
        android:id="@+id/titleLayout"
        android:layout_width="match_parent"
        android:layout_height="70dp">

        <TextView
            android:id="@+id/placeName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:ellipsize="middle"
            android:maxLines="1"
            android:textColor="#fff"
            android:textSize="22sp" />
    </FrameLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:orientation="vertical">

        <TextView
            android:id="@+id/currentTemp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:textColor="#fff"
            android:textSize="70sp" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="20dp">

            <TextView
                android:id="@+id/currentSky"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="#fff"
                android:textSize="18sp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="13dp"
                android:text="|"
                android:textColor="#fff"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/currentAQI"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="13dp"
                android:textColor="#fff"
                android:textSize="18sp" />

        </LinearLayout>

    </LinearLayout>


</RelativeLayout>

未来几天天气预报

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="15dp"
    android:layout_marginTop="15dp"
    android:layout_marginRight="15dp"
    app:cardCornerRadius="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="15dp"
            android:layout_marginTop="20dp"
            android:layout_marginBottom="20dp"
            android:text="预报"
            android:textColor="?android:textColorPrimary"
            android:textSize="20sp" />

        <LinearLayout
            android:id="@+id/forecastLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">


        </LinearLayout>
    </LinearLayout>


</androidx.cardview.widget.CardView>

上面的代码并没有未来天气信息的子项布局,创建forecast_item.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="15dp"
    android:layout_marginTop="15dp"
    android:layout_marginRight="15dp"
    app:cardCornerRadius="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="15dp"
            android:layout_marginTop="20dp"
            android:layout_marginBottom="20dp"
            android:text="预报"
            android:textColor="?android:textColorPrimary"
            android:textSize="20sp" />

        <LinearLayout
            android:id="@+id/forecastLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">


        </LinearLayout>
    </LinearLayout>


</androidx.cardview.widget.CardView>

生活指数 life_index.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="15dp"
    app:cardCornerRadius="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="15dp"
            android:layout_marginTop="20dp"
            android:text="生活指数"
            android:textColor="?android:textColorPrimary"
            android:textSize="20sp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp">

            <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="60dp"
                android:layout_weight="1">

                <ImageView
                    android:id="@+id/coldRiskImg"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="20dp"
                    android:src="@mipmap/ic_coldrisk" />

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="20dp"
                    android:layout_toEndOf="@id/coldRiskImg"
                    android:orientation="vertical">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="感冒"
                        android:textSize="12sp" />

                    <TextView
                        android:id="@+id/coldRiskText"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="4dp"
                        android:textColor="?android:textColorPrimary"
                        android:textSize="16sp" />
                </LinearLayout>

            </RelativeLayout>

            <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="60dp"
                android:layout_weight="1">

                <ImageView
                    android:id="@+id/dressingImg"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="20dp"
                    android:src="@mipmap/ic_dressing" />

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="20dp"
                    android:layout_toEndOf="@id/dressingImg"
                    android:orientation="vertical">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="穿衣"
                        android:textSize="12sp" />

                    <TextView
                        android:id="@+id/dressingText"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="4dp"
                        android:textColor="?android:textColorPrimary"
                        android:textSize="16sp" />
                </LinearLayout>
            </RelativeLayout>

        </LinearLayout>
        <!--第二行-->

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp">

            <!--实时紫外线-->
            <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="60dp"
                android:layout_weight="1">

                <ImageView
                    android:id="@+id/ultravioletImg"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="20dp"
                    android:src="@mipmap/ic_ultraviolet" />

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="20dp"
                    android:layout_toEndOf="@id/ultravioletImg"
                    android:orientation="vertical">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="实时紫外线"
                        android:textSize="12sp" />

                    <TextView
                        android:id="@+id/ultravioletText"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="4dp"
                        android:textColor="?android:textColorPrimary"
                        android:textSize="16sp" />
                </LinearLayout>

            </RelativeLayout>

            <!--洗车-->
            <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="60dp"
                android:layout_weight="1">

                <ImageView
                    android:id="@+id/carWashingImg"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="20dp"
                    android:src="@mipmap/ic_dressing" />

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="20dp"
                    android:layout_toEndOf="@id/carWashingImg"
                    android:orientation="vertical">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="洗车"
                        android:textSize="12sp" />

                    <TextView
                        android:id="@+id/carWashingText"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="4dp"
                        android:textColor="?android:textColorPrimary"
                        android:textSize="16sp" />
                </LinearLayout>
            </RelativeLayout>

        </LinearLayout>


    </LinearLayout>

</androidx.cardview.widget.CardView>

将每个部分的布局引入activity_main.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/weatherLayout"
    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
            android:id="@+id/now"
            layout="@layout/now" />

        <include
            android:id="@+id/forecast"
            layout="@layout/forecast" />

        <include
            android:id="@+id/life_index"
            layout="@layout/life_index" />

    </LinearLayout>
</ScrollView>

布局完成。编写一个将天气代码转换成Sky对象的函数。

kotlin 复制代码
class Sky(val info: String, val icon: Int, val bg: Int)


private val sky = mapOf(
    "CLEAR_DAY" to Sky("晴", R.mipmap.ic_clear_day, R.mipmap.bg_clear_day),
    "CLEAR_NIGHT" to Sky("晴", R.mipmap.ic_clear_night, R.mipmap.bg_clear_night),
    "PARTLY_CLOUDY_DAY" to Sky("多云", R.mipmap.ic_partly_cloud_day, R.mipmap.bg_partly_cloudy_day),
    "PARTLY_CLOUDY_NIGHT" to Sky(
        "多云",
        R.mipmap.ic_partly_cloud_night,
        R.mipmap.bg_partly_cloudy_night
    ),
    "CLOUDY" to Sky("阴", R.mipmap.ic_cloudy, R.mipmap.bg_cloudy),
    "LIGHT_HAZE" to Sky("轻度雾霾", R.mipmap.ic_light_haze, R.mipmap.bg_fog),
    "MODERATE_HAZE" to Sky("中度雾霾", R.mipmap.ic_moderate_haze, R.mipmap.bg_fog),
    "HEAVY_HAZE" to Sky("重度雾霾", R.mipmap.ic_heavy_haze, R.mipmap.bg_fog),
    "LIGHT_RAIN" to Sky("小雨", R.mipmap.ic_light_rain, R.mipmap.bg_rain),
    "MODERATE_RAIN" to Sky("中雨", R.mipmap.ic_moderate_rain, R.mipmap.bg_rain),
    "HEAVY_RAIN" to Sky("大雨", R.mipmap.ic_heavy_rain, R.mipmap.bg_rain),
    "STORM_RAIN" to Sky("暴雨", R.mipmap.ic_storm_rain, R.mipmap.bg_rain),
    "FOG" to Sky("雾", R.mipmap.ic_fog, R.mipmap.bg_fog),
    "LIGHT_SNOW" to Sky("小雪", R.mipmap.ic_light_snow, R.mipmap.bg_snow),
    "MODERATE_SNOW" to Sky("中雪", R.mipmap.ic_moderate_snow, R.mipmap.bg_snow),
    "HEAVY_SNOW" to Sky("大雪", R.mipmap.ic_heavy_snow, R.mipmap.bg_snow),
    "STORM_SNOW" to Sky("暴雪", R.mipmap.ic_heavy_snow, R.mipmap.bg_snow),
    "DUST" to Sky("浮尘", R.mipmap.ic_fog, R.mipmap.bg_fog),
    "SAND" to Sky("沙尘", R.mipmap.ic_fog, R.mipmap.bg_fog),
    "WIND" to Sky("大风", R.mipmap.ic_cloudy, R.mipmap.bg_wind)
)

fun getSky(skycon: String): Sky {

    return sky[skycon] ?: sky["CLEAR_DAY"]!!
}

Sky包含info,icon,bg。对应文字、图标和背景。getSky()根据天气代码获取对应的Sky对象。

下面在MainActivity中请求天气数据,并将数据展示到界面上。

kotlin 复制代码
class MainActivity : FragmentActivity() {

    val viewModel by lazy {

        ViewModelProvider(this).get(WeatherViewModel::class)
    }

    private lateinit var activityMainBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        if (viewModel.locationLng.isEmpty()) {

            viewModel.locationLng = intent.getStringExtra("location_lng") ?: ""
        }

        if (viewModel.locationLat.isEmpty()) {

            viewModel.locationLat = intent.getStringExtra("location_lat") ?: ""
        }

        if (viewModel.placeName.isEmpty()) {

            viewModel.placeName = intent.getStringExtra("place_name") ?: ""
        }
        viewModel.weatherLiveData.observe(this) { result ->

            val weather = result.getOrNull()
            if (weather != null) {

                showWeatherInfo(weather)
            } else {

                Toast.makeText(this, "无法成功获取天气情况", Toast.LENGTH_SHORT).show()
                result.exceptionOrNull()?.printStackTrace()
            }
        }

        viewModel.refreshWeather(viewModel.locationLng.toFloat(), viewModel.locationLat.toFloat())
    }


    private fun showWeatherInfo(weather: Weather) {

        //城市的名字
        activityMainBinding.now.placeName.text = viewModel.placeName
        val realtime = weather.realtime
        val daily = weather.daily

        //now
        val currentTempText = "${realtime.temperature.toInt()} ℃"
        activityMainBinding.now.currentTemp.text = currentTempText
        activityMainBinding.now.currentSky.text = getSky(realtime.skycon).info
        val currentPM25Text = "空气指数 ${realtime.air_quality.aqi.chn.toInt()}"
        activityMainBinding.now.currentAQI.text = currentPM25Text
        activityMainBinding.now.nowLayout.setBackgroundResource(getSky(realtime.skycon).bg)
        //forecast
        activityMainBinding.forecast.forecastLayout.removeAllViews()
        val days = daily.skycon.size
        for (i in 0 until days) {

            val skycon = daily.skycon[i]
            val temperature = daily.temperature[i]
            val view = LayoutInflater.from(this)
                .inflate(R.layout.forecast_item, activityMainBinding.forecast.forecastLayout, false)
            val dateInfo = view.findViewById<TextView>(R.id.dateInfo)
            val skyIcon = view.findViewById<ImageView>(R.id.skyIcon)
            val skyInfo = view.findViewById<TextView>(R.id.skyInfo)
            val temperatureInfo = view.findViewById<TextView>(R.id.temperatureInfo)
            val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault())


            val date = simpleDateFormat.parse(skycon.date)
            date?.let {
                dateInfo.text = simpleDateFormat.format(date)
            }
            val sky = getSky(skycon.value)
            skyIcon.setImageResource(sky.icon)
            skyInfo.text = sky.info
            val tempText = "${temperature.min.toInt()} ~ ${temperature.max.toInt()}"
            temperatureInfo.text = tempText
            activityMainBinding.forecast.forecastLayout.addView(view)
        }
        //life_inex
        val lifeIndex = daily.life_index
        activityMainBinding.lifeIndex.coldRiskText.text = lifeIndex.coldRisk[0].desc
        activityMainBinding.lifeIndex.dressingText.text = lifeIndex.dressing[0].desc
        activityMainBinding.lifeIndex.ultravioletText.text = lifeIndex.ultraviolet[0].desc
        activityMainBinding.lifeIndex.carWashingText.text = lifeIndex.carWashing[0].desc
        activityMainBinding.weatherLayout.visibility = View.VISIBLE
    }

}

上面代码简要说明,从intent获取经纬度和城市名称,赋值到viewmodel的变量中,对weatherLiveData对象进行观察,当获取到数据,调用showWeatherInfo()来展示数据。viewModel.refreshWeather()来获取天气数据。

接下来,将搜索城市页面与天气页面关联起来。

在PlaceViewHolder中添加点击事件的处理。

kotlin 复制代码
    inner class PlaceViewHolder(view: View) : RecyclerView.ViewHolder(view) {

        val place_tv_adcode = view.findViewById<TextView>(R.id.place_tv_adcode)
        val place_tv_address = view.findViewById<TextView>(R.id.place_tv_address)
        val place_tv_lng = view.findViewById<TextView>(R.id.place_tv_lng)
        val place_tv_lat = view.findViewById<TextView>(R.id.place_tv_lat)

        lateinit var place: Place

        fun setData(place: Place) {

            this.place = place

            place_tv_adcode.text = place.adcode.toString()
            place_tv_address.text = place.address
            place_tv_lng.text = "lng:" + place.lng.toString()
            place_tv_lat.text = "lat:" + place.lat.toString()
            itemView.setOnClickListener {

                val intent = Intent(itemView.context, MainActivity::class.java).apply {

                    putExtra("location_lng", place.lng)
                    putExtra("location_lat", place.lat)
                    putExtra("place_name", place.address)
                }
                fragment.startActivity(intent)
                fragment.activity?.finish()
            }
        }
    }

效果图

观察效果图,背景图没有和状态栏融合在一起,添加代码进行处理。在mainactivity的onCreate()中添加代码。

kotlin 复制代码
  val windowInsetsController =
            WindowCompat.getInsetsController(window, window.decorView)

        windowInsetsController.systemBarsBehavior =
            WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

        windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
        windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())

        WindowCompat.setDecorFitsSystemWindows(window, false)//页面布局是否在状态栏下方,false:侵入状态栏

效果图

相关推荐
SweetCode3 小时前
汉诺塔问题
android·java·数据库
音视频牛哥4 小时前
Android 端构建高性能 RTSP 转 RTMP|轻量级RTSP服务 网关:透传与二次编码深度实践
android·音视频·大牛直播sdk·rtsp转rtmp推送·rtsp转发到rtsp服务器·rtsp转rtmp二次编码推送·rtsp二次编码加水印
方白羽4 小时前
Android 16 (Target 36) 应用适配指南
android·app·客户端
微爱帮监所写信寄信5 小时前
微爱帮监狱寄信写信系统后台PHP框架优化实战手册
android·开发语言·人工智能·网络协议·微信·https·php
zhangphil5 小时前
Kotlin新式管道Channel融合flow流,协程实现Android废弃的AsyncTaskLoader(B)
kotlin
私人珍藏库5 小时前
[Android] 无印2.2视频解析去水印工具,支持多个平台 2025.12.29更新
android·app·安卓·工具·软件·音乐·music
雨声不在5 小时前
多进程的多语言切换
android
Yang-Never6 小时前
Android 内存泄漏 -> ViewModel持有Activity/Fragment导致的内存泄漏
android·java·开发语言·kotlin·android studio
Android_xiong_st6 小时前
(原创)Android遍历文件方法walk函数介绍
android