对最近写的Android应用的总结(1)
去年公司的Android应用,主要语言是Kotlin,基本架构如下(取自项目README.md):
ViewModel 负责从 UI 层接受事件,然后向 repository 请求更新的数据。它使用 LiveData 来存储当前排序的列表数据,以供 UI 进行展示
ViewModel 负责启动协程,并保证用户离开了相应界面时它们就会被取消。它本身并不会做一些耗时的操作,而是依赖别的层级来做。一旦有了结果,就使用 LiveData 将数据发送到 UI 层。因为 ViewModel 并不做一些耗时操作,所以它是在主线程启动协程的,以便能够更快地响应用户事件。
Repository 提供了挂起函数用来访问数据,它通常不会启动一些生命周期比较长的协程,因为它们一旦启动了便无法取消。无论何时 Repository 想要做一些耗时操作,比如对列表内容进行转换,都应该使用 withContext 来提供主线程安全的接口。
数据层 (网络或数据库) 总是会提供挂起函数,使用 Kotlin 协程的时候要保证这些挂起函数是主线程安全的,Room 和 Retrofit 都遵循了这一点。
UseCase 是 Clean 架构中的一个概念,其中主要用于 UI 和数据层的连接同时也会进行 IO 的切换,这里可以看到本项目抛弃了 Rxjava 因为他完全可以用 Kotlin 来替代。
- View:一个网络请求的发送并订阅,处理 UI 数据。
- ViewModel:为 View(Activity/Fragment) 提供数据,并处理业务逻辑。
- LiveData:具有生命周期可观察的数据存储器类,LiveData 存储在 ViewModel 中
- UseCases:用于连接 ViewModel 和 Model,并更新 LiveData。
- Model:可以从网络、数据库或其他 API 获取数据
1.项目用到的依赖
1 | dependencies { |
项目结构
1.Repository
因为项目依赖于一个第三方的SDK,该SDK提供了一个Service,并且大部分的SDK API都通过Service来调用,所以我们的UI层需要依赖于Service,并且服务的绑定和解绑需要在Activity的生命周期中进行,这样我们的Activity就需要继承自AppCompatActivity,并且在onCreate中调用bindService,在onDestroy中调用unbindService. 一开始的思路是通过LifecycleObserver来管理服务的绑定,但是后来考虑到项目结构其实只用到一两个Activity,其他都是Fragment,所以只在主页面Activity进行绑定,将SDK Service的功能抽象到一个kotlin
Object
中,只通过MainActivity注入Object
的实现.
在MainActivity获取服务实例,注入到Object Repo中
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 class MainActivity : AppCompatActivity() {
private val mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val localBinder = service as InterpttService.LocalBinder
mService = localBinder.service
ToTalkerRepository.bindService(mService)
}
override fun onServiceDisconnected(name: ComponentName?) {
ToTalkerRepository.unbindService(mService)
}
}
override fun onStart() {
super.onStart()
bindService(
Intent(this, InterpttService::class.java),
mServiceConnection,
Service.BIND_AUTO_CREATE and Service.BIND_IMPORTANT
)
}
override fun onDestroy() {
super.onDestroy()
unbindService(mServiceConnection)
unregister()
}
}
在Repo中,ToTalkerRepository实现了ToTalkService,ToTalkService抽象了SDK服务的功能,这样我们的ViewModel调用oTalkerRepository就完成了对SDK的调用.并且隐藏了对SDK的依赖.
1
2
3
4
5
6
7
8
9 object ToTalkerRepository : ToTalkService {
private var mService: InterpttService? = null
fun bindService(service: InterpttService) {
mService = service
//1.发送初始化事件
//2.设置SDK服务的状态
//3.注册SDK服务的状态监听
}
}
2.ViewModel
ViewModel
在项目中的主要作用是
负责从 UI 层接受事件,然后向 repository 请求更新的数据
负责启动协程,并保证用户离开了相应界面时它们就会被取消。它本身并不会做一些耗时的操作,而是依赖别的层级来做。
负责一些数据与View的绑定
- 以登录页ViewModel为例,ViewModel中的Repository和其他工具依赖通过hilt注入,注入Repository的上层接口,这样我们的ViewModel就可以调用Repository的方法,而不用依赖具体的Repository实现.
- loginViewModel的ViewBinding使用ObservableField来进行ui的绑定,暴露public方法用于点击事件处理
- 使用ViewBinding来把ViewModel与View进行绑定,ViewModel中的方法可以直接调用View的方法,这样我们就可以在ViewModel中暴露一些方法,例如跳转到下一个界面,或者提示用户等等.
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class LoginViewModel constructor(
private val authService: AuthService,
private val talkieService: TalkieService,
private val toTalkService: ToTalkService,
private val snackbarMessageManager: SnackbarMessageManager,
private val mainActivityNavigator: MainActivityNavigator
) : ViewModel() {
var username = ObservableField<String>()
var password = ObservableField<String>()
var loading = ObservableField<Boolean>()
val autoLoginChecked: LiveData<Boolean> = authService.getAutoLogin().stateIn(
viewModelScope,
started = SharingStarted.Eagerly,
initialValue = false
).asLiveData()
init {
viewModelScope.launch {
username.set(authService.getPrevLoginAccount().first())
}
viewModelScope.launch {
password.set(authService.getPrevLoginPassword().first())
}
viewModelScope.launch {
tryRelogin()
}
}
fun handleLogin(view: View) {
dismissKeyboard(view)
realLogin()
}
private fun realLogin() {
viewModelScope.launch {
loading.set(true)
try {
val username = username.get()
val password = password.get()
require(!username.isNullOrBlank()) { "用户名不能为空" }
require(!password.isNullOrBlank()) { "请输入密码" }
authService.login(username, password)
} catch (e: CancellationException) {
e.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
snackbarMessageManager.addMessage(
SnackbarMessage(
0, e.localizedMessage
)
)
} finally {
//延迟后
delay(800)
loading.set(false)
}
}
}
fun handleChangeAutoLogin(checked: Boolean) {
viewModelScope.launch {
authService.setAutoLogin(checked)
}
}
fun handleLoginClick(view: View): Boolean {
DoKit.showToolPanel()
return true
}
private suspend fun tryRelogin() {
val autoLogin = authService.getAutoLogin().first()
val isAuthorized = talkieService.isAuthorized().first()
val serviceOnceLogin = toTalkService.serviceOnceLogin().first()
val serviceOnline = toTalkService.serviceOnline().first()
if (isAuthorized && serviceOnline && serviceOnceLogin) {
mainActivityNavigator.navigate(MainActivityNavigator.LaunchNavigationAction.ToDashboardNavigation)
} else if (autoLogin) {
realLogin()
}
}
}
LoginFragment中,项目中Fragment的作用比较薄弱,基本只有把ViewModel注入到对应的XML文件的功能逻辑
1 |
|
ViewModel的数据与View的绑定
在一些页面中需要展示一些数据,例如用户信息,需要在ViewModel中暴露一个方法,让ViewModel与View进行绑定,上游的Repository/Service暴露的是StateFlow,ViewModel获取Flow后使用ViewBinding进行XML的绑定.
TalkieUserRepository的上层接口
1 | interface TalkieUserService { |
TalkieUserRepository的具体实现,这里
cacheStorage.userInfo
是一个StateFlow,是用Repository/Service
模式封装的DataStore/Preferences
1 | class TalkieUserRepository constructor( |
在ViewModel中获取State,绑定到UI,例子里转成了LiveData
1 | val userInfo = talkieUserService.userInfo() |
在XML中使用State/LiveData进行数据绑定
1 | <com.google.android.material.textview.MaterialTextView |
Event/Result 事件处理
项目使用一个Event类来抽象所有的事件,使用ShareFlow来进行事件的分发,这样我们就可以在不同的地方接收事件,而不用依赖具体的ViewModel来处理事件.
- 例如接收消息的事件,被抽象为
IChatMessage
,事件的发送者是一个Repository,每次SDK接收到消息就会向SharedFlow
emit一个IChatMessage
,所有的接收者都可以接收到消息.- 事件的接收,Repository上层接口暴露了一个获取消息Flow的接口,Repository的下游(例如ViewModel)调用后获取一个Flow,这个Flow就是接收到消息的流.
ToTalkRepository
1 | //文件、语音消息 |