Android结构设计系列[1]--初识工程

声明:

本文是对Mark Allison系列博客的翻译和学习笔记,感谢作者提供的demo和这么好的博客。

前言

从标题上想必大家已经猜出来了,从本篇开始将是一个系列的Android工程结构设计博文,我们将从一个简单的获取天气并展示的APP开始,一步步的重构并研究如何更好的设计整个工程的结构。本系列中一部分知识点在以前的博文中可以找到相关知识,另一部则是新的框架和特性,如果遇到会详细说明。

说明:在本系列中我会认为大家能熟练使用Kotlin开发Android项目,并且会使用Retrofit、RxJava等主流框架,这些技术会从一开始就会出现在源码中。

显示天气

本案例中的天气数据来自 OpenWeatherApp API,在该页面注册后会发送一份邮件包含一个API Key。

开始类

src目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
src
│ Converter.kt
│ WeatherStationApplication.kt

├─model
│ Common.kt
│ Current.kt

├─net
│ OpenWeatherMap.kt

└─ui
CurrentWeatherFragment.kt
MainActivity.kt
NoPermissionFragment.kt
PreferencesFragment.kt

首先我们看看MainActivity的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MainActivity : AppCompatActivity() {

//所需的权限集合
private val permissions: Array<out String> = arrayOf(
ACCESS_FINE_LOCATION,
ACCESS_COARSE_LOCATION
)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

//权限检查
if (permissions.any { checkSelfPermission(it) == PERMISSION_DENIED }) {
requestPermissions(permissions, 0)
} else {
supportFragmentManager.transaction(allowStateLoss = true) {
replace(R.id.activity_main, CurrentWeatherFragment())
addToBackStack(CurrentWeatherFragment::class.java.simpleName)
}
}

//设置actionbar(布局中的toolbar)
//注意这里的toolbar使用了kotlin的扩展库,可以方便的使用id来获取对象
setSupportActionBar(toolbar)
}

override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
//处理权限请求结果
if (requestCode == 0) {
supportFragmentManager.transaction(allowStateLoss = true) {
if (this@MainActivity.permissions.any {
checkSelfPermission(it) == PERMISSION_DENIED }) {
//加载无权限Fragment
replace(R.id.activity_main, NoPermissionFragment())
} else {
//加载天气显示Fragment
replace(R.id.activity_main, CurrentWeatherFragment())
addToBackStack(CurrentWeatherFragment::class.java.simpleName)
}
}
}
}
}

ConstraintLayout

MainActivity里面的实现很简单,布局是一个fragment,申请了定位权限来获取当前位置用于获取本地天气情况。根据定位权限来分别跳转到NoPermissionFragmentCurrentWeatherFragment两个Fragment,其中NoPermissionFragment的内容很简单,就是一个提示而已。

any操作符

在kotlin中集合有很多操作符,any就是其中的一个,用来判断集合中是否有满足条件的元素。常见的简单操作符如下:

集合操作符说明
any如果至少有一个元素符合判断条件,则返回true,否则false
all如果集合中所有的元素都符合判断条件,则返回true否则false
count返回集合中符合判断条件的元素总数
max返回集合中最大的一项,如果没有则返回null
min返回集合中最小的一项,如果没有则返回null
forEach遍历所有元素,并执行给定的操作(类似于Java 中的for循环)

权限申请

这里使用了LOCATION权限组的权限,Android M 之后的权限管理机制来申请权限。PackageManager.PERMISSION_DENIED表示应用不具有此权限,PackageManager.PERMISSION_GRANTED表示应用具有此权限。

1
2
3
4
5
6
7
8
//用来检测应用是否已经具有权限
int checkSelfPermission(String permission)

//进行请求单个或多个权限
void requestPermissions(String[] permissions, int requestCode)

//请求权限结果回调
void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)

有关更多权限问题请参考我的另一篇博文《Android 6.0/7.0权限问题》

Kotlin的扩展库

在使用Kotlin开发Android中我们可以使用安卓扩展库来方便Android开发,可以很方便的访问在xml中定义的控件,不再需要使用findViewById(),请参考我的另一篇博文《Android开发中Kotlin的扩展库及实现Parcelable》

ConstraintLayout

ConstraintLayout使用可视化的方式来编写界面,可以有效地解决布局嵌套过多的问题。使用约束的方式来指定各个控件的位置和关系的,它有点类似于RelativeLayout,但远比RelativeLayout要更强大。

有关ConstraintLayout的使用请参考 guolin 的 《Android新特性介绍,ConstraintLayout完全解析》

核心类

通过上面分析,我们知道天气显示的真正逻辑和界面在CurrentWeatherFragment,首先定义Retrofit的ApiService。

1
2
3
4
5
6
7
8
9
10
interface OpenWeatherMap {

@GET("/data/2.5/weather")
@Headers("Cache-Control: private, max-age=600, max-stale=600")
fun currentWeather(
@Query("lat") latitude: Double, //经度
@Query("lon") longitude: Double, //纬度
@Query("appid") appId: String //api key
): Call<Current>
}

上面代码使用了Okhttp和Retrofit的网络缓存,需要配合Okhttp的setCache()方法使用。

1
2
3
4
5
6
7
8
9
10
11
12
private val cacheSize: Long = 10 * 1024 * 1024

private val okHttpClient: OkHttpClient by lazy {
context?.let {
OkHttpClient.Builder()
.cache(Cache(it.cacheDir, cacheSize)) //设置缓存目录和大小
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
} ?: throw IllegalStateException("Context is not valid")
}

Cache-Control用于指定所有缓存机制在整个请求/响应链中必须服从的指令,常用的Cache-Control值。

Cache-Control说明
public所有内容都将被缓存(客户端和代理服务器都可缓存)
private内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
no-cache必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。
no-store所有内容都不会被缓存到缓存或 Internet 临时文件中
must-revalidation/proxy-revalidation如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证
max-age=xxx (xxx is numeric)缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级较高

max-age=600 表示当访问此地址后的600秒内再次访问不会去服务器。
max-stale=600 指示客户机可以接收超出超时期间的响应消息。这里客户机可以接收超出600秒的响应消息。

接下来就是在CurrentWeatherFragmentonResume()中请求位置信息,然后根据经纬度来请求天气数据(这意味着每次我们开启屏幕或者返回界面都会执行一次)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
override fun onResume() {
super.onResume()
locationProvider.requestUpdates(::retrieveForecast) //请求位置信息
}

//根据经纬度来请求位置信息
private fun retrieveForecast(latitude: Double, longitude: Double) {
Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.openweathermap.org/")
.addConverterFactory(moshiConverterFactory)
.build().apply {
call = create(OpenWeatherMap::class.java)
.currentWeather(latitude, longitude, BuildConfig.API_KEY)
}
call?.enqueue(object : Callback<Current> {
override fun onFailure(call: Call<Current>?, t: Throwable?) {
println("Request Error: $t")
}

override fun onResponse(call: Call<Current>?, response: Response<Current>?) {
println("Got current: ${response?.body()}")
response?.body()?.also { current ->
currentWeather = current
bind(current) //数据和界面绑定
}
}
})
}

设置类

值得注意的是在CurrentWeatherFragment中不仅仅有请求和展示当前天气的功能,还有一个设置入口来更改当前fragment为PreferencesFragment

设置界面

1
2
3
4
5
6
7
8
9
10
11
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.action_settings -> { //点击设置按钮
fragmentManager?.transaction(allowStateLoss = true) {
replace(R.id.activity_main, PreferencesFragment())
addToBackStack(PreferencesFragment::class.java.simpleName)
}
true
}
else -> super.onOptionsItemSelected(item)
}

PreferencesFragment是一个继承自PreferenceFragmentCompat的Fragment,可以很方便的通过配置xml中的ListPreference来进行菜单的设置,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class PreferencesFragment : PreferenceFragmentCompat() {

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
//绑定设置界面的UI
addPreferencesFromResource(R.xml.preferences)
}

override fun onAttach(context: Context?) {
super.onAttach(context)

if (context is AppCompatActivity) {
//更改ActionBar显示
context.supportActionBar?.apply {
title = getString(R.string.units)
setDisplayHomeAsUpEnabled(true)
setHasOptionsMenu(true)
}
}
}

override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
android.R.id.home -> { //返回按钮事件
fragmentManager?.popBackStack()
true
}
else -> super.onOptionsItemSelected(item)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/units">
<ListPreference
android:defaultValue="mph"
android:entries="@array/speed_entries" <!-- 定义显示的entries集合 -->
android:entryValues="@array/speed_values" <!-- 定义values集合 -->
android:key="@string/speed_units"
android:summary="%s"
android:title="@string/speed_units" />
<ListPreference
android:defaultValue="celsius"
android:entries="@array/temperature_entries"
android:entryValues="@array/temperature_values"
android:key="@string/temperature_units"
android:summary="%s"
android:title="@string/temperature_units" />
</PreferenceScreen>

然后在显示的时候,在我们的Converter.kt中读取SharePreference中的单位设置,对温度单位进行了转换。

1
2
3
4
5
6
7
8
9
10
11
12
private val sharedPreferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context),

fun speed(value: Float): String =
sharedPreferences.getString("Speed", "mph").let { units ->
when (units) {
"mph" -> msToMph(value)
else -> value
}
}.let { newValue ->
context.getString(R.string.wind_speed, newValue)
}

上面的代码实现了一个现实当前天气的APP,但是代码结构是很糟糕的,如果我们的最终目的仅仅是为了实现这么小的APP,那么这么做确实无可厚非,但是如果我们要实现比较大的项目,特别是需要长期迭代的项目这个工程显然不合格。

创建一个可维护,灵活的代码结构其实并不是很容易,但是这些是促成软件工程的基础,接下来的几篇博文中我们将结合这个小案例来分析一下如何去实现一个更加合理的结构。

源码请参考:https://github.com/StylingAndroid/WeatherStation/tree/introduction

评论

Ajax Android AndroidStudio Animation Anroid Studio AppBarLayout Babel Banner Buffer Bulma ByteBuffer C++ C11 C89 C99 CDN CMYK COM1 COM2 CSS Camera Raw, 直方图 Chrome Class ContentProvider CoordinatorLayout C语言 DML DOM Dagger Dagger2 Darktable Demo Document DownloadManage ES2015 ESLint Element Error Exception Extensions File FileProvider Flow Fresco GCC Git GitHub GitLab Gradle Groovy HTML5 Handler HandlerThread Hexo Hybrid I/O IDEA IO ImageMagick IntelliJ Intellij Interpolator JCenter JNI JS Java JavaScript JsBridge Kotlin Lab Lambda Lifecycle Lint Linux Looper MQTT MVC MVP Maven MessageQueue Modbus Momentum MySQL NDK NIO NexT Next Nodejs ObjectAnimator Oracle VM Permission PhotoShop Physics Python RGB RS-232 RTU Remote-SSH Retrofit Runnable RxAndroid RxJava SE0 SSH Spring SpringBoot Statubar Style Task Theme Thread Tkinter UI UIKit UML VM virtualBox VS Code VUE ValueAnimator ViewPropertyAnimator Vue Vue.js Web Web前端 Workbench api apk bookmark by关键字 cli compileOnly component computed css c语言 databases demo hexo hotfix html iOS icarus implementation init jQuery javascript launchModel logo merge methods mvp offset photos pug query rxjava2 scss servlet shell svg tkinter tomcat transition unicode utf-8 vector virtual box vscode watch webpack 七牛 下载 中介者模式 串口 临潼石榴 主题 书签 事件 享元模式 仓库 代理模式 位运算 依赖注入 修改,tables 光和色 内存 内核 内部分享 函数 函数式编程 分支 分析 创建 删除 动画 单例模式 压缩图片 发布 可空性 合并 同向性 后期 启动模式 命令 命令模式 响应式 响应式编程 图层 图床 图片压缩 图片处理 图片轮播 地球 域名 基础 增加 备忘录模式 外观模式 多线程 大爆炸 天气APP 太白山 头文件 奇点 字符串 字符集 存储引擎 宇宙 宏定义 实践 属性 属性动画 岐山擀面皮 岐山肉臊子 岐山香醋 工具 工厂模式 年终总结 开发技巧 异常 弱引用 恒星 打包 技巧 指令 指针 插件 插值 摄影 操作系统 攻略 故事 数据库 数据类型 数组 文件 新功能 旅行 旋转木马 时序图 时空 时间简史 曲线 杂谈 权限 枚举 架构 查询 标准库 标签选择器 样式 核心 框架 案例 桥接模式 检测工具 模块化 模板 模板引擎 模板方法模式 油泼辣子 泛型 洛川苹果 浅色状态栏 渲染 源码 源码分析 瀑布流 热修复 版本 版本控制 状态栏 状态模式 生活 留言板 相册 相对论 眉县猕猴桃 知识点 码云 磁盘 科学 笔记 策略模式 类图 系统,发行版, GNU 索引 组件 组合模式 绑定 结构 结构体 编码 网易云信 网格布局 网站广播 网站通知 网络 美化 联合 脚手架 膨胀的宇宙 自定义 自定义View 自定义插件 蒙版 虚拟 虚拟机 补码 补齐 表单 表达式 装饰模式 西安 观察者模式 规范 视图 视频 解耦器模式 设计 设计原则 设计模式 访问者模式 语法 责任链模式 贪吃蛇 转换 软件工程 软引用 运算符 迭代子模式 适配器模式 选择器 通信 通道 配置 链表 锐化 错误 键盘 闭包 降噪 陕西地方特产 面向对象 项目优化 项目构建 黑洞
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×