SunnyWeather项目总结

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,向网络索取天气数据的方法

开发前准备:

  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: Context

        const 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.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()
}

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: 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()
            }
        })
    }
}

更改AppTheme,添加权限

未来的优化方向

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

可能采取的实现

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值