一. 简介
本项目为了解决家庭成员在生活中帐目信息不能同步,家庭备忘录不同步的问题,并且有一定的互动功能
1.1. HomeHelper 家庭帮手 特点
- :chart_with_upwards_trend:家庭记账,和分析近期情况,并且能家庭同步
- :golf:同步分享动态
- :ledger:同步家庭备忘录
- :closed_lock_with_key: 二维码分享加入家庭,且token校验
1.2. 界面展示
- 主界面
2. 个人中心
1.3. 使用的技术栈
Android jetpack
Room 数据库的ORM框架
LiveData 动态刷新页面
ViewModel 保存页面数据,用其维护数据
kotlinx.coroutines 协程, io操作在协程中运行, 防止阻塞ui线程
retrofit2 和 OkHttp2 和后台发送网络请求
- 后台使用
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
| <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
| <uses-permission android:name="android.permission.INTERNET" />
|
- 使用retrofit 封装不同的请求
1 2 3 4 5
|
@POST("account/syn") suspend fun syn(@Body accountList: List<AccountTdo>): BaseResponse<List<AccountTdo>>
|
- 创建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
|
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 } }
|
- 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) 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) } }
|
- 后端和安卓端之间传递数据用json
5.2. 数据库部分
- 创建实体
Account User Memorandum
- 编写数据库查询语句 dao
1 2 3 4 5 6
|
@Query("SELECT * FROM account_table WHERE isDeleted=0") fun getAll(): LiveData<List<Account>>
|
- 用单例模式创建数据库
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)) .build() }
|
- 调用数据库例子
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) }
|
为了性能,数据库的增删改查都在新的协程执行