一. 简介

本项目为了解决家庭成员在生活中帐目信息不能同步,家庭备忘录不同步的问题,并且有一定的互动功能

1.1. HomeHelper 家庭帮手 特点

  • :chart_with_upwards_trend:家庭记账,和分析近期情况,并且能家庭同步
  • :golf:同步分享动态
  • :ledger:同步家庭备忘录
  • :closed_lock_with_key: 二维码分享加入家庭,且token校验

1.2. 界面展示

  1. 主界面
2. 个人中心

1.3. 使用的技术栈

  • Android jetpack
  • Room 数据库的ORM框架
  • LiveData 动态刷新页面
  • ViewModel 保存页面数据,用其维护数据
  • kotlinx.coroutines 协程, io操作在协程中运行, 防止阻塞ui线程
  • retrofit2OkHttp2 和后台发送网络请求
  • 后台使用 spring boot spring security 开发的api, 以及安全验证功能

二. 主要功能介绍

2.1. 添加帐单并同步

  • 记账
  • 同步家庭帐单
  • 通过折线图统计近期家庭帐单信息

2.2. 扫描二维码加入家庭

  • 二维码分享
  • 扫描二维码加入家庭

2.3. 备忘录添加并同步

  • 同步备忘录,
  • 家庭成员都能看到备忘录

2.4. 分享功能

  • 分享近期的动态(表情,图片,回复)

三. 主要功能逻辑实现

3.1. 登陆功能 api-android.ghovos.top/user/login

1
2
3
4
5
@startuml
安卓端 -> 后台 : 帐号密码
后台 -> 安卓端: 校验, 并返回结果, 通过则返回token
安卓端 -> 安卓端: 存储返回的token
@enduml

3.2. 注册功能 api-android.ghovos.top/user/regist

  • 大致流程与登陆相同, 但是要判断用户名是否重复

3.3. 二维码分享加入家庭 api-android.ghovos.top/user/join_family

1
2
3
4
5
6
7
@startuml
安卓端_邀请人 -> 安卓端_邀请人 : 通过本地存储的token,\n 生成二维码
安卓端_邀请人 -> 安卓端_被邀请人 : 扫描二维码
安卓端_被邀请人 -> 后台: 将token传给服务器
后台 -> 后台 : 通过token识别<b>邀请人</b>的家庭id,\n并设置<b>被邀请人</b>的id
后台 -> 安卓端_被邀请人 : 返回家庭成员列表
@enduml

3.4. 同步功能(以帐单为例)api-android.ghovos.top/user/syn_family

1
2
3
4
5
6
@startuml
安卓端 -> 安卓端 : 更新数据后, 本地数据版本号加一
安卓端 -> 后台 : 同步数据给后台</font>
后台 -> 后台 : 比对数据版本\n将旧数据更新
后台 -> 安卓端 : 传回较新的数据给安卓端
@enduml

3.5. 头像上传功能

  • 本地读取图片, 通过api上传端后台
  • 后台将图片存入oss, 加速图片访问

四. 页面部分

4.1 RecycleView 和 LiveData配合使用

1
2
3
4
<!-- example -->
<androidx.recyclerview.widget.RecyclerView
<!-- 一些属性 -->
tools:listitem="@layout/memorandum_item" />
  • 通过自定义adapter ,设置view的数据, 以及 因为有livedata,数据可以实时自动刷新
1
2
3
4
5
6
7
8
9
10
viewModel.getLiveData()
?.observe(viewLifecycleOwner, { memorandumList: List<Memorandum> ->
memorandum_recycle_view.adapter =
MemorandumAdapter(
ctx,
memorandumList,
viewModel,
)
memorandum_recycle_view.layoutManager = LinearLayoutManager(activity)
})

4.2 Fragment

  • 在MainActivity中使用Fragment
  • 多个页面在Activity中嵌套切换
1
2
3
4
5
6
7
 //底部导航栏绑定
bottomNavigationView = findViewById(R.id.bottomNavigationView)
navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment
navController = navHostFragment.navController
configuration = AppBarConfiguration.Builder(bottomNavigationView.menu).build()
NavigationUI.setupActionBarWithNavController(this, navController, configuration)
NavigationUI.setupWithNavController(bottomNavigationView, navController)

五. 功能主要代码实现

5.1. 网络部分

  1. 添加网络权限
1
<uses-permission android:name="android.permission.INTERNET" />
  1. 使用retrofit 封装不同的请求
1
2
3
4
5
/**
* example
*/
@POST("account/syn")
suspend fun syn(@Body accountList: List<AccountTdo>): BaseResponse<List<AccountTdo>>
  1. 创建retrofit对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* example
* 通过 retrofit 网络发送请求
*/
object UserApi {
private val gsonFormat = GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create()

private val api by lazy {
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gsonFormat))
.client(
OkHttpClient
.Builder()
.addInterceptor(MyIntercept())
.build()
)
.build()
retrofit.create(UserService::class.java)
}

fun get(): UserService {
return api
}
}
  1. retrofit异常处理
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
46
47
suspend inline fun <T> apiCall(crossinline call: suspend CoroutineScope.() -> BaseResponse<T>): BaseResponse<T> {
return withContext(Dispatchers.IO) {
val res: BaseResponse<T>
try {
res = call()
} catch (e: Throwable) {
Log.e("ApiCaller", "request error", e)
// 请求出错,将状态码和消息封装为 ResponseResult
return@withContext ApiException.build(e).toResponse<T>()
}
if (res.code == ApiException.CODE_AUTH_INVALID) {
Log.e("ApiCaller", "request auth invalid")
// 登录过期,取消协程,跳转登录界面
// 省略部分代码
cancel()
}
return@withContext res
}
}

// 网络、数据解析错误处理
class ApiException(
private val code: Int,
override val message: String?,
override val cause: Throwable? = null
) : RuntimeException(message, cause) {
companion object {
// 网络状态码
const val CODE_NET_ERROR = 4000
const val CODE_TIMEOUT = 4080
const val CODE_JSON_PARSE_ERROR = 4010
const val CODE_SERVER_ERROR = 5000

// 业务状态码
const val CODE_AUTH_INVALID = 401

fun build(e: Throwable): ApiException {
/**
* 各种异常 不同处理
*/
}
}

fun <T> toResponse(): BaseResponse<T> {
return BaseResponse(code, message)
}
}
  1. 后端和安卓端之间传递数据用json

5.2. 数据库部分

  1. 创建实体 Account User Memorandum
  2. 编写数据库查询语句 dao
1
2
3
4
5
6
/**
* 获取所有数据的列表
* @return LiveData 实时数据 不用在新的线程执行
*/
@Query("SELECT * FROM account_table WHERE isDeleted=0")
fun getAll(): LiveData<List<Account>>
  1. 用单例模式创建数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 单例模式
*/
companion object {
@Volatile
private var INSTANCE: FamilyShareDatabase? = null
private val applicationScope = CoroutineScope(SupervisorJob())


fun getInstance(context: Context): FamilyShareDatabase = INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}

private fun buildDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
FamilyShareDatabase::class.java,
"family_share_db"
).addCallback(UserDatabaseCallback(applicationScope)) // 加入callback
.build()
}
  1. 调用数据库例子
1
2
3
4
5
6
7
8
9
10
suspend fun getUserById(id: Long) = withContext(viewModelScope.coroutineContext) {
userDao?.getUserById(id)
}

/**
* 协程中调用
*/
GlobalScope.launch{
getUserById(1)
}

为了性能,数据库的增删改查都在新的协程执行