前言
软件架构可以简单理解为代码的组织方式,如果把软件比作一个建筑物,那么软件架构就是建筑的主体框架。建筑一个矮小的土坯房不需要复杂的框架,同样一个简单的项目也不需要过多注重架构的设计。如果要建造一个摩天大厦,如果没有一个好的主体框架是很难建起来的,同样地要开发一个大型的软件项目,也需要进行架构的设计。
Clean Architecture为Android官方推荐的应用架构,该架构基于关注点分离和依赖反转的原则。它将应用程序分为多个层,每个层都有特定的职责。这些层通常包括表示层(用户界面)、域层(业务逻辑)和数据层(数据访问),如下图所示。
从上图可以看出,Clean Architecture将应用程序分为3层。
界面层(UI Layer):界面层的作用是在屏幕上显示应用数据,并充当主要的用户互动点。
网域层(Domain Layer):网域名位于界面层与数据层之间,是一个可选层,主要封装负责的业务逻辑。
数据层(Data Layer):数据层为应用提供应用数据,应用数据的来源可以是云端的服务器,也可以是本地数据库。
下面通过一个简单的示例加深理解Clean Architecture分层设计思想。
UI Layer
本示例介绍通过手机号登录的流程,UI Layer主要包含Screen,ViewModel和UiState,下面分别介绍它们的具体实现。
1)LoginScreen.kt
LoginScreen主要绘制登录界面,以及与用户交互的逻辑,代码如下所示。
@SuppressLint("UnrememberedMutableState")
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val loginUiState by viewModel.loginUiState.collectAsState()
Column {
Button(onClick = { viewModel.onLoginByPhoneSmsCodeTriggered("13811118888","1234")}) {
Text(text = "Login")
}
when(loginUiState){
LoginUiState.Login -> Text(text = "already Login")
LoginUiState.UnLogin -> Text(text = "not Login")
}
}
}
这里使用Jetpack Compose绘制界面,在下一讲开始介绍Jetpack Compose相关UI组件的用法。这里用到了3个UI组件,Bottun是一个按钮,Text用于显示文字,Column用于布局子UI控件。
这里会根据登录的状态显示already Login还是not Login,刚开始处于为登录状态,所以显示“not Login”,登录界面显示如下。
2)LoginViewModel.kt
用于点击登录界面的按钮时,调用LoginViewModel的方法onLoginByPhoneSmsCodeTriggered触发登录流程,该方法主要通知网域层进行处理,处理完成后根据结果更新登录状态,代码如下。
class LoginViewModel(
private val loginUseCase: LoginUseCase = LoginUseCase(),
) : ViewModel() {
private var _loginUiState = MutableStateFlow<LoginUiState>(LoginUiState.UnLogin)
val loginUiState: StateFlow<LoginUiState> = _loginUiState
fun onLoginByPhoneSmsCodeTriggered(phone:String, code :String){
viewModelScope.launch {
val result = loginUseCase(phone,code)
_loginUiState.emit(LoginUiState.Login)
}
}
}
从上面的代码可以看出,登录状态初始化为UnLogin,登录完成后把状态修改为Login。在登录界面会记录登录的状态,当状态发生变化后自动更新界面。
3)LoginUiState.kt
登录状态是一个密封接口,类Login和UnLogin都继承该接口,分别表示登录和未登录状态。
sealed interface LoginUiState{
data object Login : LoginUiState
data object UnLogin : LoginUiState
}
以上是UI Layer的简单实现。
Domain Layer
Domain Layer表示网域层,主要处理业务逻辑,当业务不是太复杂时,该层可以没有,相关的工作可以放到ViewModel处理。在本示例中,登录流程也不算太复杂,但是为了说明网域层的实现流程,还是在网域层实现一个简单的登录流程,代码如下。
class LoginUseCase(
private val loginRepository: LoginRepository = DefaultLoginRepository(),
) {
operator fun invoke(phone:String, code :String): String {
return loginRepository.login(phone,code);
}
}
网域层一般有固定的实现方法,通常是实现一个类,类名为XXX+UseCase,表示一种用例,用于实现某个目的,如本例要实现登录的流程,使用LoginUseCase。
在LoginUseCase的invoke方法中实现登录流程,这里调用数据层的接口进行处理并返回结果
Data Layer
数据层主要实现提供数据的仓库,登录对应的仓库为LoginRepository,对应的接口如下。
interface LoginRepository {
fun login(phone: String, code: String): String;
}
根据不同的业务情况,仓库可以有不同的实现,下面的实现只是简单地返回登录的结果。
class DefaultLoginRepository(): LoginRepository {
override fun login(phone: String, code: String): String = "user-token:11111111"
}
数据层除了定义仓库,还可以定义数据源DataSource,数据源可分本地数据源(LocalDataSource)和远程数据源(RemoteDataSource),前者从本地数据库获取数据,后者通过网络从服务器获取数据。如果有数据源,则调用数据源的接口进行处理,由于这些内容涉及到数据库组件和网络组件,这里只是简单返回结果表示登录成功。
数据层返回数据后,UI层即可更新登录状态,如下图所示。
本示例的工程已经上传到github,地址如下