SunnyWeather项目总结
练手的第一个APP,总结了他人开发架构与方法,以搜索全球城市数据功能为例做的一个总结
我们可以将程序分为了若干层。
绿色部分表示的是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,向网络索取天气数据的方法
采用MVVM架构由于从ViewModel层就不再持有Activity的引用了,所以经常出现缺context的情况,所以要提供全局获取Context的方式。
class SunnyWeatherApplication : Application() {
companion object {
@SuppressWarnings("StaticFieldLeak")
lateinit var context: Context
const val TOKEN = "获取到的TOKEN"
}
override fun onCreate() {
super.onCreate()
context = applicationContext
}
}
编写完代码后要记得修改注册文件
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信息,定义数据模型。
用于访问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对象。
定义根路径,构建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)
}
统一的网络数据源访问入口,对所有网络请求的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()
}
判断是从本地获取数据还是从网络中获取
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.places
Result.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()
}
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()
}
这里为了复用搜索功能,将该布局定为了fragment。搜索结果用RecyclerView显示。
搜索结果的子项使用了卡片式布局方法显示
class PlaceFragment : Fragment() {
val viewModel by lazy { ViewModelProviders.of(this).get(PlaceViewModel::class.java) }
private lateinit var adapter: PlaceAdapter
override 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)
//判断是否已经保存,如果已保存在本地则直接使用本地数据并打开WeatherActivity
if (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 = layoutManager
adapter = PlaceAdapter(this, viewModel.placeList)
recyclerView.adapter = adapter
searchPlaceEdit.addTextChangedListener { editable ->
val content = editable.toString()
if (content.isNotEmpty()) viewModel.searchPlaces(content)
else{
recyclerView.visibility = View.GONE
bgImageView.visibility = View.VISIBLE
viewModel.placeList.clear()
adapter.notifyDataSetChanged()
}
}
//获取服务器响应的数据
viewModel.placeLiveData.observe(this, Observer { result ->
val places = result.getOrNull()
if (places!=null){
recyclerView.visibility = View.VISIBLE
bgImageView.visibility = View.GONE
viewModel.placeList.clear()
viewModel.placeList.addAll(places)
adapter.notifyDataSetChanged()
}else{
Toast.makeText(activity, "未能查询到任何地点",Toast.LENGTH_SHORT).show()
result.exceptionOrNull()?.printStackTrace()
}
})
}
}