生活中的Design.

对最近写的Android应用的总结(1)

字数统计: 2.6k阅读时长: 12 min
2022/06/06 Share

对最近写的Android应用的总结(1)

去年公司的Android应用,主要语言是Kotlin,基本架构如下(取自项目README.md):

img.png

ViewModel 负责从 UI 层接受事件,然后向 repository 请求更新的数据。它使用 LiveData 来存储当前排序的列表数据,以供 UI 进行展示

ViewModel 负责启动协程,并保证用户离开了相应界面时它们就会被取消。它本身并不会做一些耗时的操作,而是依赖别的层级来做。一旦有了结果,就使用 LiveData 将数据发送到 UI 层。因为 ViewModel 并不做一些耗时操作,所以它是在主线程启动协程的,以便能够更快地响应用户事件。

Repository 提供了挂起函数用来访问数据,它通常不会启动一些生命周期比较长的协程,因为它们一旦启动了便无法取消。无论何时 Repository 想要做一些耗时操作,比如对列表内容进行转换,都应该使用 withContext 来提供主线程安全的接口。

数据层 (网络或数据库) 总是会提供挂起函数,使用 Kotlin 协程的时候要保证这些挂起函数是主线程安全的,Room 和 Retrofit 都遵循了这一点。

UseCase 是 Clean 架构中的一个概念,其中主要用于 UI 和数据层的连接同时也会进行 IO 的切换,这里可以看到本项目抛弃了 Rxjava 因为他完全可以用 Kotlin 来替代。

img_1.png

  • View:一个网络请求的发送并订阅,处理 UI 数据。
  • ViewModel:为 View(Activity/Fragment) 提供数据,并处理业务逻辑。
  • LiveData:具有生命周期可观察的数据存储器类,LiveData 存储在 ViewModel 中
  • UseCases:用于连接 ViewModel 和 Model,并更新 LiveData。
  • Model:可以从网络、数据库或其他 API 获取数据

1.项目用到的依赖

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
dependencies {
implementation "androidx.core:core-ktx:$rootProject.ktxVersion"
implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
implementation "androidx.fragment:fragment-ktx:$rootProject.fragmentVersion"
implementation "com.google.android.material:material:$rootProject.materialVersion"
implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion"
implementation "androidx.recyclerview:recyclerview:$rootProject.recyclerViewVersion"
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.annotation:annotation:1.2.0'
implementation files('libs/pstore-sdk-pstore-2.5.8-release.jar')
implementation files('libs/libfriapkrecord-r1.0.1.jar')
testImplementation "junit:junit:$rootProject.junitVersion"
androidTestImplementation "androidx.test.ext:junit:$rootProject.testExtJunit"
androidTestImplementation "androidx.test.espresso:espresso-core:$rootProject.espressoVersion"

//hilt
implementation "com.google.dagger:hilt-android:$rootProject.hiltVersion"
kapt "com.google.dagger:hilt-android-compiler:$rootProject.hiltVersion"
//room
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
debugImplementation 'com.amitshekhar.android:debug-db:1.0.6'

//lifecycle扩展
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion"
// ViewModel utilities for Compose
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$rootProject.lifecycleVersion"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycleVersion"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$rootProject.lifecycleVersion"
// Saved state module for ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$rootProject.lifecycleVersion"
// Annotation processor
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.lifecycleVersion"


//retrofit-coroutine
implementation "com.google.code.gson:gson:$rootProject.gsonVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$rootProject.okhttpLoggingVersion"
implementation "com.squareup.retrofit2:converter-gson:$rootProject.retrofitVersion"
implementation "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutinesVersion"
//permission
implementation 'com.permissionx.guolindev:permissionx:1.4.0'

//amr库
implementation 'io.kvh:amr:1.1.1'

//agentWeb
implementation 'com.just.agentweb:agentweb-androidx:4.1.4' // (必选)
implementation 'com.just.agentweb:filechooser-androidx:4.1.4'// (可选)

//badge
implementation 'q.rorbin:badgeview:1.1.3'

//agentWeb
implementation 'com.just.agentweb:agentweb-androidx:4.1.4' // (必选)
implementation 'com.just.agentweb:filechooser-androidx:4.1.4'// (可选)
//implementation 'com.download.library:downloader-androidx:4.1.4'// (可选)


//glide https://github.com/bumptech/glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
//RecyclerView集成库
implementation("com.github.bumptech.glide:recyclerview-integration:4.12.0") {
// Excludes the support library because it's already included by Glide.
transitive = false
}
//coil
implementation("io.coil-kt:coil:1.4.0")
implementation("io.coil-kt:coil-gif:1.4.0")

//crash
implementation 'cat.ereza:customactivityoncrash:2.3.0'
//album
implementation 'com.yanzhenjie:album:2.1.3'

//datastore
implementation "androidx.datastore:datastore:$rootProject.datastoreVersion"
implementation "androidx.datastore:datastore-preferences:$rootProject.datastoreVersion"

//日志
implementation "com.jakewharton.timber:timber:$rootProject.timberVersion"
implementation 'com.orhanobut:logger:2.2.0'

implementation "androidx.startup:startup-runtime:$rootProject.startupVersion"

// okdownload核心库
implementation 'com.liulishuo.okdownload:okdownload:1.0.7'
//存储断点信息的数据库
implementation 'com.liulishuo.okdownload:sqlite:1.0.7'
//Utils if u use AndroidX, use the following
implementation 'com.blankj:utilcodex:1.31.0'
// 文件选择
implementation 'com.droidninja:filepicker:2.2.5'
//doraemon kit
implementation 'io.github.didi.dokit:dokitx:3.5.0'
}

项目结构

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
    @HiltViewModel
    class LoginViewModel @Inject 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
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
@AndroidEntryPoint
class LoginFragment : Fragment() {

private lateinit var binding: FragmentLoginBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}

@Inject
lateinit var snackbarMessageManager: SnackbarMessageManager
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel: LoginViewModel by viewModels()
setupSnackbarManager(snackbarMessageManager)
binding.executeAfter {
this.viewModel = viewModel
this.lifecycleOwner = viewLifecycleOwner
}
lifecycleScope.launchWhenStarted {
dismissKeyboard(view)
}
}
}

ViewModel的数据与View的绑定

在一些页面中需要展示一些数据,例如用户信息,需要在ViewModel中暴露一个方法,让ViewModel与View进行绑定,上游的Repository/Service暴露的是StateFlow,ViewModel获取Flow后使用ViewBinding进行XML的绑定.

TalkieUserRepository的上层接口

1
2
3
4
5
6
7
8
9
10
11
interface TalkieUserService {
fun userInfo(): Flow<AuthenticatedUserInfo?>
fun userBaned(): Flow<Boolean>
fun userPermission(): Flow<AuthenticatedUserInfo.UserType>

suspend fun retrieveUserAuthorization()

suspend fun resetUserPassword(userId:Int):Result<String>
suspend fun deleteUser(userId:Int):Result<String>
suspend fun updateNickname(nickName:String,userId: String):Result<String>
}

TalkieUserRepository的具体实现,这里cacheStorage.userInfo是一个StateFlow,是用Repository/Service模式封装的DataStore/Preferences

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class TalkieUserRepository @Inject constructor(
private val cacheStorage: CacheStorage,
private val userApi: UserApi
) : TalkieUserService {
private val _userInfo = cacheStorage.userInfo.distinctUntilChanged()

override fun userInfo(): Flow<AuthenticatedUserInfo?> = _userInfo

override fun userBaned(): Flow<Boolean> {
return _userInfo.map {
it == null || it.getBaned()
}
}
//....
}

在ViewModel中获取State,绑定到UI,例子里转成了LiveData

1
2
3
4
5
6
7
8
9
val userInfo = talkieUserService.userInfo()
.map {
it ?: EmptyAuthenticatedUserInfo()
}
.stateIn(
viewModelScope,
started = Eagerly,
initialValue = EmptyAuthenticatedUserInfo()
).asLiveData()

在XML中使用State/LiveData进行数据绑定

1
2
3
4
5
6
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.AppCompat.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.userInfo.displayName}"
tools:text="@string/app_name" />

Event/Result 事件处理

项目使用一个Event类来抽象所有的事件,使用ShareFlow来进行事件的分发,这样我们就可以在不同的地方接收事件,而不用依赖具体的ViewModel来处理事件.

  • 例如接收消息的事件,被抽象为IChatMessage,事件的发送者是一个Repository,每次SDK接收到消息就会向SharedFlow emit一个IChatMessage,所有的接收者都可以接收到消息.
  • 事件的接收,Repository上层接口暴露了一个获取消息Flow的接口,Repository的下游(例如ViewModel)调用后获取一个Flow,这个Flow就是接收到消息的流.

ToTalkRepository

1
2
3
4
5
6
7
8
9
//文件、语音消息
private val chatMessage = MutableSharedFlow<IChatMessage>(replay = 0, extraBufferCapacity = 1)

//暴露的Flow,一般会包装成Event,用于事件已处理未处理状态的判断
override fun chatMessage(): Flow<Event<IChatMessage>> {
return chatMessage.map {
Event(it)
}
}
CATALOG
  1. 1. 对最近写的Android应用的总结(1)
    1. 1.1. 1.项目用到的依赖
    2. 1.2. 项目结构
      1. 1.2.1. 1.Repository
      2. 1.2.2. 2.ViewModel
        1. 1.2.2.1. ViewModel的数据与View的绑定
      3. 1.2.3. Event/Result 事件处理