SunnyWeather项目总结

练手的第一个APP,总结了他人开发架构与方法,以搜索全球城市数据功能为例做的一个总结

目录

  • 项目架构
    • 项目结构
    • 组件用途
  • 开发前准备:
  • 实现逻辑层
    • SunnyWeatherApplication
    • 数据层
      • PlaceResponse
    • 网络层
      • PlaceService
      • ServiceCreator
      • SunnyWeatherNetwork
    • 仓库层 Repository
    • ViewModel 层
      • PlaceViewModel
    • 实现UI层
      • fragment_place
      • place_item
      • 编写Adapter
      • PlaceFragment
    • 更改AppTheme,添加权限
  • 未来的优化方向
    • 可能采取的实现

项目架构

项目结构

我们可以将程序分为了若干层。

绿色部分表示的是UI控制层,这部分就是我们平时写的Activity和Fragment。

蓝色部分表示的是ViewModel层,ViewModel用于持有和UI元素相关的数据,以保证这些数据在屏幕旋转时不会丢失,以及负责和仓库之间进行通讯。

黄色部分表示的是仓库层,仓库层要做的工作是自主判断接口请求的数据应该是从数据库中读取还是从网络中获取,并将数据返回给调用方。如果是从网络中获取的话还要将这些数据存入到数据库当中,以避免下次重复从网络中获取。简而言之,仓库的工作就是在本地和网络数据之间做一个分配和调度的工作,调用方不管你的数据是从何而来的,我只是要从你仓库这里获取数据而已,而仓库则要自主分配如何更好更快地将数据提供给调用方。

接下来灰色部分表示是的本地数据层。

最后红色部分表示的是网络数据层,项目使用了Retrofit从web服务接口获取数据。

另外,图中所有的箭头都是单向的,比方说WeatherActivity指向了WeatherViewModel,表示WeatherActivity持有WeatherViewModel的引用,但是反过来WeatherViewModel不能持有WeatherActivity的引用。其他的几层也是一样的道理,一个箭头就表示持有一个引用。 (好莱坞原则)

还有,引用不能跨层持有,就比方说UI控制层不能持有仓库层的引用,每一层的组件都只能和它的相邻层交互。

组件用途

具体解释:
MainActivity:APP启动后打开的Activity,布局中只有一个fragment,加载时需要判断本地SP中是否已有place信息,如有则跳转WeatherActivity显示其天气信息,如果没有再加载该Fragment
WeatherActivity:用来显示具体天气信息的Activity
PlaceVIewModel,WeatherVIewModel:保存数据,提供接口给UI层调用,与仓库层Reposotory通信。前者保存位置数据,后者保存天气数据。
Reposotory:为ViewModel层的数据操作提供了一些方法
PlaceDao:封装了一些与本地SP交互的的方法
SunnyWeatherNetwork:封装了利用彩云API,向网络索取天气数据的方法

开发前准备:

  1. 分析彩云天气API传回来的json格式,获取彩云天气API的Token
  2. github控制代码版本
  3. 搭建MVVM项目架构,添加依赖

实现逻辑层

SunnyWeatherApplication

采用MVVM架构由于从ViewModel层就不再持有Activity的引用了,所以经常出现缺context的情况,所以要提供全局获取Context的方式。

class SunnyWeatherApplication : Application() {companion object {@SuppressWarnings("StaticFieldLeak")lateinit var context: Contextconst val TOKEN = "获取到的TOKEN"}override fun onCreate() {super.onCreate()context = applicationContext}
}

编写完代码后要记得修改注册文件

数据层

PlaceResponse

data class PlaceResponse(val status : String, val places : List<Place>)
data class Place(val name: String, val location : Location,@SerializedName("formatted_address") val address : String)
data class Location(val lng : String, val lat : String)

就搜索地点后返回的位置JSON信息,定义数据模型。

网络层

PlaceService

用于访问API的Retrofit接口。

interface PlaceService {@GET("v2/place?token=${SunnyWeatherApplication.TOKEN}&lang=zh_CN")fun searchPlaces(@Query("query") query: String) : Call<PlaceResponse>
}

这里将返回值声明为Call< PlacePesponse >,使Retrofit将服务器返回的JSON数据解析为PlaceResponse对象。

ServiceCreator

定义根路径,构建Retrofit

object ServiceCreator {private const val BASE_URL = "https://api.caiyunapp.com/"private val retrofit = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build()fun <T> create(serviceClass : Class<T>) : T = retrofit.create(serviceClass)inline fun <reified T> create() : T = create(T::class.java)
}

SunnyWeatherNetwork

统一的网络数据源访问入口,对所有网络请求的API进行封装

object SunnyWeatherNetwork {private val placeService = ServiceCreator.create<PlaceService>()suspend fun searchPlaces(query : String) = placeService.searchPlaces(query).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)}})}}private val weatherService = ServiceCreator.create<WeatherService>()suspend fun getDailyWeather(lng : String, lat : String) = weatherService.getDailyWeather(lng, lat).await()suspend fun getRealtimeWeather(lng : String, lat : String) = weatherService.getRealtimeWeather(lng, lat).await()
}

仓库层 Repository

判断是从本地获取数据还是从网络中获取

object Repository {//liveData函数可以自动构建并返回一个LiveData对象,然后再它的代码块中提供一个挂起函数的上下文,//这样我们就可以在liveData()函数的代码块中调用任意的挂起函数了fun searchPlaces(query : String) = fire(Dispatchers.IO) {//调用SunnyWeatherNetwork.searchPlaces(query)搜索城市数据val placeResponse = SunnyWeatherNetwork.searchPlaces(query)if (placeResponse.status=="ok"){val places = placeResponse.placesResult.success(places)}else{Result.failure(RuntimeException("response status is ${placeResponse.status}"))}}fun refreshWeather(lng : String, lat : String) = fire(Dispatchers.IO) {coroutineScope {val deferredRealtime = async { SunnyWeatherNetwork.getRealtimeWeather(lng,lat) }val deferredDaily = async { SunnyWeatherNetwork.getDailyWeather(lng,lat) }val realtimeResponse = deferredRealtime.await()val dailyResponse = deferredDaily.await()if (realtimeResponse.status=="ok" && dailyResponse.status=="ok"){val weather = Weather(realtimeResponse.result.realtime, dailyResponse.result.daily)Result.success(weather)}else{Result.failure(RuntimeException("realtime response status is ${realtimeResponse.status}" +"daily response status is ${dailyResponse.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)}fun savePlace(place: Place) = PlaceDao.savePlace(place)fun getSavedPlace() = PlaceDao.getSavedPlace()fun isPlaceSaved() = PlaceDao.isPlaceSaved()
}

ViewModel 层

PlaceViewModel

class PlaceViewModel : ViewModel() {private val searchLiveData = MutableLiveData<String>()val placeList = ArrayList<Place>()//缓存界面上显示的城市数据val placeLiveData = Transformations.switchMap(searchLiveData) { query ->Repository.searchPlaces(query)//将仓库返回的LiveData对象转换成一个可供Activity观察的对象}fun searchPlaces(query : String){searchLiveData.value = query}fun savePlace(place: Place) = Repository.savePlace(place)fun getSavedPlace() = Repository.getSavedPlace()fun isPlaceSaved() = Repository.isPlaceSaved()
}

实现UI层

fragment_place

这里为了复用搜索功能,将该布局定为了fragment。搜索结果用RecyclerView显示。

place_item

搜索结果的子项使用了卡片式布局方法显示

编写Adapter

PlaceFragment

class PlaceFragment  : Fragment() {val viewModel by lazy { ViewModelProviders.of(this).get(PlaceViewModel::class.java) }private lateinit var adapter: PlaceAdapteroverride fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {//加载布局return inflater.inflate(R.layout.fragment_place, container, false)}override fun onActivityCreated(savedInstanceState: Bundle?) {super.onActivityCreated(savedInstanceState)//判断是否已经保存,如果已保存在本地则直接使用本地数据并打开WeatherActivityif (activity is MainActivity && viewModel.isPlaceSaved()){val place = viewModel.getSavedPlace()val intent = Intent(context, WeatherActivity::class.java).apply {putExtra("location_lng", place.location.lng)putExtra("location_lat", place.location.lat)putExtra("place_name", place.name)}startActivity(intent)activity?.finish()return}//为RecyclerView设置了LayoutManager和适配器val layoutManager = LinearLayoutManager(activity)recyclerView.layoutManager = layoutManageradapter = PlaceAdapter(this, viewModel.placeList)recyclerView.adapter = adaptersearchPlaceEdit.addTextChangedListener { editable ->val content = editable.toString()if (content.isNotEmpty()) viewModel.searchPlaces(content)else{recyclerView.visibility = View.GONEbgImageView.visibility = View.VISIBLEviewModel.placeList.clear()adapter.notifyDataSetChanged()}}//获取服务器响应的数据viewModel.placeLiveData.observe(this, Observer { result ->val places = result.getOrNull()if (places!=null){recyclerView.visibility = View.VISIBLEbgImageView.visibility = View.GONEviewModel.placeList.clear()viewModel.placeList.addAll(places)adapter.notifyDataSetChanged()}else{Toast.makeText(activity, "未能查询到任何地点",Toast.LENGTH_SHORT).show()result.exceptionOrNull()?.printStackTrace()}})}
}

更改AppTheme,添加权限

未来的优化方向

  1. 允许选择多个城市,现在只能显示一个城市的数据,如果想获取另一城市数据,只能重新搜索,搜索完成后会替换本地数据,导致本地始终只能保存一个数据。
  2. 提供更加完整的天气信息,现在只获取了服务器所返回的一小部分数据
  3. 增加后台更新天气功能,并允许用户手动设定后台更新频率
  4. 适配深色主题

可能采取的实现

  1. 现在只能保存一个数据的原因是获取的数据键值对之键固定为place,每次获取时会替换数据,试试如何新增place而不是替换,并在滑动菜单页面添加一个RecyclerView显示。
  2. 根据服务器返回的天气信息,完善WeatherActivity界面,使其能够显示更多数据
  3. 在BaseActivity中添加一个menu,设置后台更新频率。后者可通过Service实现,至于如何实现还需探讨

SunnyWeather项目总结相关推荐

  1. 在k8s中使用gradle构建java web项目镜像Dockerfile

    在k8s中使用gradle构建java web项目镜像Dockerfile FROM gradle:6-jdk8 AS build COPY --chown=gradle:gradle . /home ...

  2. Dockerfile springboot项目拿走即用,将yml配置文件从外部挂入容器

    Dockerfile 将springboot项目jar包打成镜像,并将yml配置文件外挂. # 以一个镜像为基础,在其上进行定制.就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜 ...

  3. SpringBoot项目使用nacos,kotlin使用nacos,java项目使用nacos,gradle项目使用nacos,maven项目使用nacos

    SpringBoot项目使用nacos kotlin demo见Gitte 一.引入依赖 提示:这里推荐使用2.2.3版本,springboot与nacos的依赖需要版本相同,否则会报错. maven ...

  4. Gradle 将项目publish到Nexus,Kotlin将项目发布到nexus,springboot项目发布到maven仓库

    示例见:Gitte 公仓设置 在项目中添加maven-publish的插件 plugins {kotlin("jvm") version "1.3.72"kot ...

  5. springboot项目使用junit4进行单元测试,maven项目使用junit4进行单元测试

    首先,maven项目中引入依赖 <dependency><groupId>junit</groupId><artifactId>junit</ar ...

  6. IDEA设置单个文件、单个包、单个项目的编码格式

    IDEA设置单个文件.单个包.单个项目的编码格式 File-> Settings-> File Enclodings 选择编码格式,确定即可. 注意:此处的编码格式设定以后,该包已经存在的 ...

  7. spring boot项目 中止运行 最常用的几种方法

    spring boot项目 中止运行 最常用的几种方法: 1. 调用接口,停止应用上下文 @RestController public class ShutdownController impleme ...

  8. 两步完成项目定时启动,java项目定时启动

    两步完成项目定时设置: 在需要定时启动或运行的方法上面加上注解@Scheduled //当天只跑一次 @Scheduled(cron = "0 40 21 * * ?") 在启动类 ...

  9. Myeclipse中项目没有代码错误提示,jsp页面无编译迹象?如何解决

    在使用Myeclipse开发项目时,发现jsp页面中嵌入的java代码没有编译的迹象,错误的get方法没有报错,没有报错信息我们如何知道我们开发的内容是正确的呢? 接下来就演示一下如何解决

最新文章

  1. pandas使用isna函数和any函数计算返回dataframe中包含缺失值的数据行(rows with missing values in dataframe)
  2. Windows cmd命令反斜杠问题
  3. Luogu P3455 [POI2007]ZAP-Queries
  4. 右下角android sdk content loader 加载很慢的解决方法
  5. 【JS 逆向百例】cnki 学术翻译 AES 加密分析
  6. SLAM Cartographer(18)后端优化问题求解器
  7. 『嗨威说』数据结构 - 第七章学习内容小结
  8. 【Flink】Flink1.11.2 on YARN滚动日志配置
  9. MySQL学习笔记:三种组内排序方法
  10. 【图像处理】MATLAB:点、线、边缘检测
  11. mysql 5.7基本优化_mysql5.7优化
  12. 工作的思考十:思维的高度决定设计的好坏(小菜的思维)
  13. PyAudio库简介
  14. jdk10安装及环境变量配置
  15. linux 打开关闭CPU超线程和查看逻辑CPU的个数
  16. iOS永久不掉签名工具,TrollStore超详使用教程
  17. 特斯拉model3中控屏怎么关_特斯拉Model 3为什么取消仪表盘? - 全文
  18. HTTP3 RFC标准正式发布,QUIC会成为传输技术的新一代颠覆者吗?
  19. 《PyQT5软件开发 - 控件篇》第3章 单行文本框QLineEdit
  20. 设计模式之禅学习笔记

热门文章

  1. 实现Unity声音的立体空间感
  2. H264/AVC 参考图像管理
  3. 云教育公共服务平台(三通两平台)整体建设方案
  4. 北京交通大学计算机学院保研,北京交通大学计算机与信息技术学院网络空间安全保研条件...
  5. 关于使用simple-robot框架(Mirai组件)登录QQ解决验证问题
  6. 元素周期表读起来方便
  7. 清零复位:废墨盒已满墨水回收盒已满将满等故障 兄弟MFC-J3930DW J3530 J2730 J2330DW 喷墨机
  8. 搭建HTTPS从域名申请、SSL证书申请、Nginx配置等一步步玩起来。
  9. 51单片机学习----中断
  10. db2报01650_DB2数据库访问表报SQL0668 reason code 3“