新项目准备工作
新建项目建议使用 kotlin
和 androix.*
组件进行开发,毕竟属于Google官方推荐方式。
初始化建议实现方式
配置使用 Startup 组件进行开发,配置依赖:
implementation "androidx.startup:startup-runtime:1.0.0"
新建类实现 Initializer<Unit>
接口:
class Init : Initializer<Unit> {
override fun create(context: Context) {
// 进行初始化逻辑
Utils.mContext = context
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
清单文件配置启动模式:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.xxx.xxx.Init" // 修改为自己Init类的全路径
android:value="androidx.startup" />
</provider>
更多关于 Startup
使用请参考 官方文档
工具类
建议实现方式:
object Utils {
// 已在Init类初始化
lateinit var mContext: Context
fun showToast(content: String) {
Toast.makeText(mContext, content, Toast.LENGTH_LONG).show()
}
}
键值对存取
这里推荐使用腾讯的 MMKV
开源库进行实现。当然也可以使用官方推荐的 SharedPreferences
。
新建 SPUtils
类,封装如下:
object SPUtils {
private val mk by lazy {
MMKV.defaultMMKV()
}
fun put(key: String, value: Any) {
when (value) {
is String -> {
mk.encode(key, value)
}
is Boolean -> {
mk.encode(key, value)
}
is Int -> {
mk.encode(key, value)
}
is Long -> {
mk.encode(key, value)
}
is Parcelable -> {
mk.encode(key, value)
}
is Float -> {
mk.encode(key, value)
}
is Double -> {
mk.encode(key, value)
}
}
}
fun removeFromKey(key: String) {
mk.removeValueForKey(key)
}
fun getInt(key: String): Int {
return mk.decodeInt(key, 0)
}
fun getBoolean(key: String): Boolean {
return mk.decodeBool(key, false)
}
fun getString(key: String): String {
return mk.decodeString(key, "")
}
fun getFloat(key: String): Float {
return mk.decodeFloat(key, 0.0f)
}
fun getDouble(key: String): Double {
return mk.decodeDouble(key, 0.0)
}
fun <T : Parcelable> getParceable(key: String, tClass: Class<T>): T {
return mk.decodeParcelable(key, tClass)
}
}
可以扩展自己实现方式,也可以直接使用此处封装好的。
更多使用方式及原理请参考 MMKV官方文档
BaseViewModel
目前的趋势,推荐使用 ViewModel
进行开发,降低业务逻辑耦合关联。
新建 ViewModel
工厂类创建带参数的 ViewModel
,如果不需要带参数的 ViewModel
,可以不用创建这个类:
class ViewmodelFactory(private val frag: FragmentManager) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(FragmentManager::class.java).newInstance(frag)
}
}
创建 BaseViewModel
,实现简单的页面展示及常用异常捕获:
abstract class BaseViewmodel(val frag: FragmentManager) : ViewModel(), LifecycleObserver {
fun showLoading() {
}
fun hideLoading() {
}
fun getException(ex: Exception) {
when (ex) {
is NetworkErrorException, is ConnectException ->
Utils.showToast("网络连接异常~")
is SocketTimeoutException, is TimeoutException ->
Utils.showToast("网络连接超时~")
else ->
ex.message?.let {
Utils.showToast("未知异常~")
Logger.e(it)
}
}
}
}
此处创建参数 FragmentManger
参数,是考虑到在 ViewModel
需要进行等待对话框的展示,当然也可以不用传入参数,直接使用在 Utils
中创建的 context
来进行对话框创建。对话框逻辑在这里实现是因为 ViewModel
一般会处理耗时操作,包括请求网络以及数据库读取。
BaseActivity
创建基类:
abstract class BaseActivity<VM : ViewModel> : AppCompatActivity() {
}
使用泛型进行约束,确保子类实现Viewmodel。如果有需要可以在泛型内添加 databing
约束:
abstract class BaseActivity<T : ViewDataBinding, VM: ViewModel> : AppCompatActivity() {
}
首先创建抽象函数,确保子类都需要实现的功能:
abstract fun initView()
abstract fun initData()
abstract fun setBarTitle(): String // 赋值标题栏
abstract fun getModel(): Class<VM> // 赋值子类的viewmodel类
abstract fun getLayoutId(): Int // 赋值布局文件
一般布局都会隐藏 Toolbar
并且把通知栏透明处理:
fun initConetnt() {
val option: Int = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.decorView.systemUiVisibility = option
window.statusBarColor = Color.TRANSPARENT
supportActionBar?.hide()
}
声明布局容器:
private val contentLayout by lazy { // 创建布局根容器为LinearLayout
LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
}
}
有统一的标题栏:
fun initToolbar() {
findViewById<ViewGroup>(android.R.id.content).apply { // 找到Layout文件的根布局
removeAllViews() // 移除所有布局
addView(contentLayout) // 添加声明的根布局
}
toolBar = layoutInflater.inflate(R.layout.layout_toolbar, null) // 引入自定义的标题栏
contentLayout.addView(toolBar) // 根布局添加标题栏
toolbar_back.setOnClickListener { // 标题栏左边功能为返回
finish()
}
toolbar_title.text = setBarTitle() // 根据子类实现的函数设置标题栏题目
}
重写添加布局函数
override fun setContentView(layoutResID: Int) {
layoutInflater.inflate(layoutResID, contentLayout) // 重写添加函数,把对应的布局文件添加到声明的根布局内
}
最后还需要根据泛型约束创建带参数的 ViewModel
实例:
protected val viewModel: VM by lazy {
ViewModelProvider(this, ViewmodelFactory(supportFragmentManager))[getModel()]
}
如果实际逻辑中用不到带参数的 ViewModel
,可以使用官方的实例方式:
protected val viewModel: VM by lazy {
ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[getModel()]
}
整合以上逻辑,onCreate
函数最终内容:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initContent()
initToolbar()
setContentView(getLayoutId()) // 根据子类实现填充布局
initView()
initData()
}
还需要提供隐藏 toolbar
的函数:
fun hideToolbar() {
contentLayout.removeViewAt(0)
}
使用方式:
class SplashActivity : BaseActivity<SplashViewmodel>() {
override fun initView() {
hideToolbar() // 隐藏标题栏
}
override fun initData() {
viewModel.spLD.observe(this, {
// 数据观察
})
viewModel.getData(2) // 因为BaseActivity已经声明了viewModel,所以可以直接使用
}
override fun getModel(): Class<SplashViewmodel> = SplashViewmodel::class.java
override fun getLayoutId(): Int = R.layout.activity_splash
override fun setBarTitle(): String = "我是标题"
}
如此操作,一个基本的Android项目框架便是搭建完成。
如果有更好的建议,欢迎大家提出来一起讨论进步。
小建议
在viewmodel中使用协程可以使用viewmodel域内的协程,自动绑定viewmodel的生命周期。
添加依赖:
//viewmodel 协程
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha07'
使用方式(在viewmodel中):
viewModelScope.launch {
// dosomething
}
不用再单独声明协程作用域。
2020年12月4日更新
基类推荐实现ViewBinding
当初写这篇文章的时候,控件查找使用的kt的 kotlin-android-extensions
插件,但是随着kt版本1.4.20
的发布,已经标明不推荐使用当前插件,因为谷歌官方推出的 viewbinding
效果更好,安全性更高。关于kt更新说明查看 Kotlin 1.4.20 Released 。
鉴于此,更新基类:
abstract class BaseActivity<T : ViewBinding, VM : ViewModel> : AppCompatActivity() {
abstract fun getViewBinding(): T
abstract fun getModel(): Class<VM> // 赋值子类的viewmodel类
abstract fun setBarTitle(): String // 赋值标题栏
abstract fun initView()
abstract fun initData()
lateinit var binding: T
private val contentLayout by lazy { // 创建布局根容器为LinearLayout
LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
}
}
protected val viewModel: VM by lazy {
ViewModelProvider(this, ViewModelFactory(supportFragmentManager))[getModel()]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = getViewBinding()
initContent()
initToolbar()
setContentView(binding.root) // 根据子类实现填充布局
initView()
initData()
}
fun initContent() {
val option: Int = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.decorView.systemUiVisibility = option
window.statusBarColor = Color.TRANSPARENT
supportActionBar?.hide()
}
fun hideToolbar() {
contentLayout.removeViewAt(0)
}
fun initToolbar() {
findViewById<ViewGroup>(android.R.id.content).apply { // 找到Layout文件的根布局
removeAllViews() // 移除所有布局
addView(contentLayout) // 添加声明的根布局
}
val toolBar = layoutInflater.inflate(R.layout.layout_toolbar, null) // 引入自定义的标题栏
contentLayout.addView(toolBar) // 根布局添加标题栏
toolBar.findViewById<TextView>(R.id.toolbar_back).setOnClickListener { // 标题栏左边功能为返回
finish()
}
toolBar.findViewById<TextView>(R.id.toolbar_title).setOnClickListener { // 设置标题栏标题
setBarTitle()
}
}
override fun setContentView(layoutResID: Int) {
layoutInflater.inflate(layoutResID, contentLayout) // 重写添加函数,把对应的布局文件添加到声明的根布局内
}
}
其中 viewmodel
工厂类:
class ViewModelFactory(private val supportManager: FragmentManager) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(FragmentManager::class.java).newInstance(supportManager)
}
}
使用方式效果:
class MainActivity : BaseActivity<ActivityMainBinding, EmptyVM>() {
override fun getModel(): Class<EmptyVM> = EmptyVM::class.java
override fun getViewBinding(): ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
override fun setBarTitle(): String = "测试标题"
override fun initView() {
binding.button.setOnClickListener {
binding.textView.text = "测试"
}
}
override fun initData() {
}
}
新项目准备工作
新建项目建议使用 kotlin
和 androix.*
组件进行开发,毕竟属于Google官方推荐方式。
初始化建议实现方式
配置使用 Startup 组件进行开发,配置依赖:
implementation "androidx.startup:startup-runtime:1.0.0"
新建类实现 Initializer<Unit>
接口:
class Init : Initializer<Unit> {
override fun create(context: Context) {
// 进行初始化逻辑
Utils.mContext = context
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
清单文件配置启动模式:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.xxx.xxx.Init" // 修改为自己Init类的全路径
android:value="androidx.startup" />
</provider>
更多关于 Startup
使用请参考 官方文档
工具类
建议实现方式:
object Utils {
// 已在Init类初始化
lateinit var mContext: Context
fun showToast(content: String) {
Toast.makeText(mContext, content, Toast.LENGTH_LONG).show()
}
}
键值对存取
这里推荐使用腾讯的 MMKV
开源库进行实现。当然也可以使用官方推荐的 SharedPreferences
。
新建 SPUtils
类,封装如下:
object SPUtils {
private val mk by lazy {
MMKV.defaultMMKV()
}
fun put(key: String, value: Any) {
when (value) {
is String -> {
mk.encode(key, value)
}
is Boolean -> {
mk.encode(key, value)
}
is Int -> {
mk.encode(key, value)
}
is Long -> {
mk.encode(key, value)
}
is Parcelable -> {
mk.encode(key, value)
}
is Float -> {
mk.encode(key, value)
}
is Double -> {
mk.encode(key, value)
}
}
}
fun removeFromKey(key: String) {
mk.removeValueForKey(key)
}
fun getInt(key: String): Int {
return mk.decodeInt(key, 0)
}
fun getBoolean(key: String): Boolean {
return mk.decodeBool(key, false)
}
fun getString(key: String): String {
return mk.decodeString(key, "")
}
fun getFloat(key: String): Float {
return mk.decodeFloat(key, 0.0f)
}
fun getDouble(key: String): Double {
return mk.decodeDouble(key, 0.0)
}
fun <T : Parcelable> getParceable(key: String, tClass: Class<T>): T {
return mk.decodeParcelable(key, tClass)
}
}
可以扩展自己实现方式,也可以直接使用此处封装好的。
更多使用方式及原理请参考 MMKV官方文档
BaseViewModel
目前的趋势,推荐使用 ViewModel
进行开发,降低业务逻辑耦合关联。
新建 ViewModel
工厂类创建带参数的 ViewModel
,如果不需要带参数的 ViewModel
,可以不用创建这个类:
class ViewmodelFactory(private val frag: FragmentManager) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(FragmentManager::class.java).newInstance(frag)
}
}
创建 BaseViewModel
,实现简单的页面展示及常用异常捕获:
abstract class BaseViewmodel(val frag: FragmentManager) : ViewModel(), LifecycleObserver {
fun showLoading() {
}
fun hideLoading() {
}
fun getException(ex: Exception) {
when (ex) {
is NetworkErrorException, is ConnectException ->
Utils.showToast("网络连接异常~")
is SocketTimeoutException, is TimeoutException ->
Utils.showToast("网络连接超时~")
else ->
ex.message?.let {
Utils.showToast("未知异常~")
Logger.e(it)
}
}
}
}
此处创建参数 FragmentManger
参数,是考虑到在 ViewModel
需要进行等待对话框的展示,当然也可以不用传入参数,直接使用在 Utils
中创建的 context
来进行对话框创建。对话框逻辑在这里实现是因为 ViewModel
一般会处理耗时操作,包括请求网络以及数据库读取。
BaseActivity
创建基类:
abstract class BaseActivity<VM : ViewModel> : AppCompatActivity() {
}
使用泛型进行约束,确保子类实现Viewmodel。如果有需要可以在泛型内添加 databing
约束:
abstract class BaseActivity<T : ViewDataBinding, VM: ViewModel> : AppCompatActivity() {
}
首先创建抽象函数,确保子类都需要实现的功能:
abstract fun initView()
abstract fun initData()
abstract fun setBarTitle(): String // 赋值标题栏
abstract fun getModel(): Class<VM> // 赋值子类的viewmodel类
abstract fun getLayoutId(): Int // 赋值布局文件
一般布局都会隐藏 Toolbar
并且把通知栏透明处理:
fun initConetnt() {
val option: Int = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.decorView.systemUiVisibility = option
window.statusBarColor = Color.TRANSPARENT
supportActionBar?.hide()
}
声明布局容器:
private val contentLayout by lazy { // 创建布局根容器为LinearLayout
LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
}
}
有统一的标题栏:
fun initToolbar() {
findViewById<ViewGroup>(android.R.id.content).apply { // 找到Layout文件的根布局
removeAllViews() // 移除所有布局
addView(contentLayout) // 添加声明的根布局
}
toolBar = layoutInflater.inflate(R.layout.layout_toolbar, null) // 引入自定义的标题栏
contentLayout.addView(toolBar) // 根布局添加标题栏
toolbar_back.setOnClickListener { // 标题栏左边功能为返回
finish()
}
toolbar_title.text = setBarTitle() // 根据子类实现的函数设置标题栏题目
}
重写添加布局函数
override fun setContentView(layoutResID: Int) {
layoutInflater.inflate(layoutResID, contentLayout) // 重写添加函数,把对应的布局文件添加到声明的根布局内
}
最后还需要根据泛型约束创建带参数的 ViewModel
实例:
protected val viewModel: VM by lazy {
ViewModelProvider(this, ViewmodelFactory(supportFragmentManager))[getModel()]
}
如果实际逻辑中用不到带参数的 ViewModel
,可以使用官方的实例方式:
protected val viewModel: VM by lazy {
ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[getModel()]
}
整合以上逻辑,onCreate
函数最终内容:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initContent()
initToolbar()
setContentView(getLayoutId()) // 根据子类实现填充布局
initView()
initData()
}
还需要提供隐藏 toolbar
的函数:
fun hideToolbar() {
contentLayout.removeViewAt(0)
}
使用方式:
class SplashActivity : BaseActivity<SplashViewmodel>() {
override fun initView() {
hideToolbar() // 隐藏标题栏
}
override fun initData() {
viewModel.spLD.observe(this, {
// 数据观察
})
viewModel.getData(2) // 因为BaseActivity已经声明了viewModel,所以可以直接使用
}
override fun getModel(): Class<SplashViewmodel> = SplashViewmodel::class.java
override fun getLayoutId(): Int = R.layout.activity_splash
override fun setBarTitle(): String = "我是标题"
}
如此操作,一个基本的Android项目框架便是搭建完成。
如果有更好的建议,欢迎大家提出来一起讨论进步。
小建议
在viewmodel中使用协程可以使用viewmodel域内的协程,自动绑定viewmodel的生命周期。
添加依赖:
//viewmodel 协程
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha07'
使用方式(在viewmodel中):
viewModelScope.launch {
// dosomething
}
不用再单独声明协程作用域。
2020年12月4日更新
基类推荐实现ViewBinding
当初写这篇文章的时候,控件查找使用的kt的 kotlin-android-extensions
插件,但是随着kt版本1.4.20
的发布,已经标明不推荐使用当前插件,因为谷歌官方推出的 viewbinding
效果更好,安全性更高。关于kt更新说明查看 Kotlin 1.4.20 Released 。
鉴于此,更新基类:
abstract class BaseActivity<T : ViewBinding, VM : ViewModel> : AppCompatActivity() {
abstract fun getViewBinding(): T
abstract fun getModel(): Class<VM> // 赋值子类的viewmodel类
abstract fun setBarTitle(): String // 赋值标题栏
abstract fun initView()
abstract fun initData()
lateinit var binding: T
private val contentLayout by lazy { // 创建布局根容器为LinearLayout
LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
}
}
protected val viewModel: VM by lazy {
ViewModelProvider(this, ViewModelFactory(supportFragmentManager))[getModel()]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = getViewBinding()
initContent()
initToolbar()
setContentView(binding.root) // 根据子类实现填充布局
initView()
initData()
}
fun initContent() {
val option: Int = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.decorView.systemUiVisibility = option
window.statusBarColor = Color.TRANSPARENT
supportActionBar?.hide()
}
fun hideToolbar() {
contentLayout.removeViewAt(0)
}
fun initToolbar() {
findViewById<ViewGroup>(android.R.id.content).apply { // 找到Layout文件的根布局
removeAllViews() // 移除所有布局
addView(contentLayout) // 添加声明的根布局
}
val toolBar = layoutInflater.inflate(R.layout.layout_toolbar, null) // 引入自定义的标题栏
contentLayout.addView(toolBar) // 根布局添加标题栏
toolBar.findViewById<TextView>(R.id.toolbar_back).setOnClickListener { // 标题栏左边功能为返回
finish()
}
toolBar.findViewById<TextView>(R.id.toolbar_title).setOnClickListener { // 设置标题栏标题
setBarTitle()
}
}
override fun setContentView(layoutResID: Int) {
layoutInflater.inflate(layoutResID, contentLayout) // 重写添加函数,把对应的布局文件添加到声明的根布局内
}
}
其中 viewmodel
工厂类:
class ViewModelFactory(private val supportManager: FragmentManager) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(FragmentManager::class.java).newInstance(supportManager)
}
}
使用方式效果:
class MainActivity : BaseActivity<ActivityMainBinding, EmptyVM>() {
override fun getModel(): Class<EmptyVM> = EmptyVM::class.java
override fun getViewBinding(): ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
override fun setBarTitle(): String = "测试标题"
override fun initView() {
binding.button.setOnClickListener {
binding.textView.text = "测试"
}
}
override fun initData() {
}
}
新项目准备工作
新建项目建议使用 kotlin
和 androix.*
组件进行开发,毕竟属于Google官方推荐方式。
初始化建议实现方式
配置使用 Startup 组件进行开发,配置依赖:
implementation "androidx.startup:startup-runtime:1.0.0"
新建类实现 Initializer<Unit>
接口:
class Init : Initializer<Unit> {
override fun create(context: Context) {
// 进行初始化逻辑
Utils.mContext = context
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
清单文件配置启动模式:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.xxx.xxx.Init" // 修改为自己Init类的全路径
android:value="androidx.startup" />
</provider>
更多关于 Startup
使用请参考 官方文档
工具类
建议实现方式:
object Utils {
// 已在Init类初始化
lateinit var mContext: Context
fun showToast(content: String) {
Toast.makeText(mContext, content, Toast.LENGTH_LONG).show()
}
}
键值对存取
这里推荐使用腾讯的 MMKV
开源库进行实现。当然也可以使用官方推荐的 SharedPreferences
。
新建 SPUtils
类,封装如下:
object SPUtils {
private val mk by lazy {
MMKV.defaultMMKV()
}
fun put(key: String, value: Any) {
when (value) {
is String -> {
mk.encode(key, value)
}
is Boolean -> {
mk.encode(key, value)
}
is Int -> {
mk.encode(key, value)
}
is Long -> {
mk.encode(key, value)
}
is Parcelable -> {
mk.encode(key, value)
}
is Float -> {
mk.encode(key, value)
}
is Double -> {
mk.encode(key, value)
}
}
}
fun removeFromKey(key: String) {
mk.removeValueForKey(key)
}
fun getInt(key: String): Int {
return mk.decodeInt(key, 0)
}
fun getBoolean(key: String): Boolean {
return mk.decodeBool(key, false)
}
fun getString(key: String): String {
return mk.decodeString(key, "")
}
fun getFloat(key: String): Float {
return mk.decodeFloat(key, 0.0f)
}
fun getDouble(key: String): Double {
return mk.decodeDouble(key, 0.0)
}
fun <T : Parcelable> getParceable(key: String, tClass: Class<T>): T {
return mk.decodeParcelable(key, tClass)
}
}
可以扩展自己实现方式,也可以直接使用此处封装好的。
更多使用方式及原理请参考 MMKV官方文档
BaseViewModel
目前的趋势,推荐使用 ViewModel
进行开发,降低业务逻辑耦合关联。
新建 ViewModel
工厂类创建带参数的 ViewModel
,如果不需要带参数的 ViewModel
,可以不用创建这个类:
class ViewmodelFactory(private val frag: FragmentManager) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(FragmentManager::class.java).newInstance(frag)
}
}
创建 BaseViewModel
,实现简单的页面展示及常用异常捕获:
abstract class BaseViewmodel(val frag: FragmentManager) : ViewModel(), LifecycleObserver {
fun showLoading() {
}
fun hideLoading() {
}
fun getException(ex: Exception) {
when (ex) {
is NetworkErrorException, is ConnectException ->
Utils.showToast("网络连接异常~")
is SocketTimeoutException, is TimeoutException ->
Utils.showToast("网络连接超时~")
else ->
ex.message?.let {
Utils.showToast("未知异常~")
Logger.e(it)
}
}
}
}
此处创建参数 FragmentManger
参数,是考虑到在 ViewModel
需要进行等待对话框的展示,当然也可以不用传入参数,直接使用在 Utils
中创建的 context
来进行对话框创建。对话框逻辑在这里实现是因为 ViewModel
一般会处理耗时操作,包括请求网络以及数据库读取。
BaseActivity
创建基类:
abstract class BaseActivity<VM : ViewModel> : AppCompatActivity() {
}
使用泛型进行约束,确保子类实现Viewmodel。如果有需要可以在泛型内添加 databing
约束:
abstract class BaseActivity<T : ViewDataBinding, VM: ViewModel> : AppCompatActivity() {
}
首先创建抽象函数,确保子类都需要实现的功能:
abstract fun initView()
abstract fun initData()
abstract fun setBarTitle(): String // 赋值标题栏
abstract fun getModel(): Class<VM> // 赋值子类的viewmodel类
abstract fun getLayoutId(): Int // 赋值布局文件
一般布局都会隐藏 Toolbar
并且把通知栏透明处理:
fun initConetnt() {
val option: Int = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.decorView.systemUiVisibility = option
window.statusBarColor = Color.TRANSPARENT
supportActionBar?.hide()
}
声明布局容器:
private val contentLayout by lazy { // 创建布局根容器为LinearLayout
LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
}
}
有统一的标题栏:
fun initToolbar() {
findViewById<ViewGroup>(android.R.id.content).apply { // 找到Layout文件的根布局
removeAllViews() // 移除所有布局
addView(contentLayout) // 添加声明的根布局
}
toolBar = layoutInflater.inflate(R.layout.layout_toolbar, null) // 引入自定义的标题栏
contentLayout.addView(toolBar) // 根布局添加标题栏
toolbar_back.setOnClickListener { // 标题栏左边功能为返回
finish()
}
toolbar_title.text = setBarTitle() // 根据子类实现的函数设置标题栏题目
}
重写添加布局函数
override fun setContentView(layoutResID: Int) {
layoutInflater.inflate(layoutResID, contentLayout) // 重写添加函数,把对应的布局文件添加到声明的根布局内
}
最后还需要根据泛型约束创建带参数的 ViewModel
实例:
protected val viewModel: VM by lazy {
ViewModelProvider(this, ViewmodelFactory(supportFragmentManager))[getModel()]
}
如果实际逻辑中用不到带参数的 ViewModel
,可以使用官方的实例方式:
protected val viewModel: VM by lazy {
ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[getModel()]
}
整合以上逻辑,onCreate
函数最终内容:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initContent()
initToolbar()
setContentView(getLayoutId()) // 根据子类实现填充布局
initView()
initData()
}
还需要提供隐藏 toolbar
的函数:
fun hideToolbar() {
contentLayout.removeViewAt(0)
}
使用方式:
class SplashActivity : BaseActivity<SplashViewmodel>() {
override fun initView() {
hideToolbar() // 隐藏标题栏
}
override fun initData() {
viewModel.spLD.observe(this, {
// 数据观察
})
viewModel.getData(2) // 因为BaseActivity已经声明了viewModel,所以可以直接使用
}
override fun getModel(): Class<SplashViewmodel> = SplashViewmodel::class.java
override fun getLayoutId(): Int = R.layout.activity_splash
override fun setBarTitle(): String = "我是标题"
}
如此操作,一个基本的Android项目框架便是搭建完成。
如果有更好的建议,欢迎大家提出来一起讨论进步。
小建议
在viewmodel中使用协程可以使用viewmodel域内的协程,自动绑定viewmodel的生命周期。
添加依赖:
//viewmodel 协程
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha07'
使用方式(在viewmodel中):
viewModelScope.launch {
// dosomething
}
不用再单独声明协程作用域。
2020年12月4日更新
基类推荐实现ViewBinding
当初写这篇文章的时候,控件查找使用的kt的 kotlin-android-extensions
插件,但是随着kt版本1.4.20
的发布,已经标明不推荐使用当前插件,因为谷歌官方推出的 viewbinding
效果更好,安全性更高。关于kt更新说明查看 Kotlin 1.4.20 Released 。
鉴于此,更新基类:
abstract class BaseActivity<T : ViewBinding, VM : ViewModel> : AppCompatActivity() {
abstract fun getViewBinding(): T
abstract fun getModel(): Class<VM> // 赋值子类的viewmodel类
abstract fun setBarTitle(): String // 赋值标题栏
abstract fun initView()
abstract fun initData()
lateinit var binding: T
private val contentLayout by lazy { // 创建布局根容器为LinearLayout
LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
}
}
protected val viewModel: VM by lazy {
ViewModelProvider(this, ViewModelFactory(supportFragmentManager))[getModel()]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = getViewBinding()
initContent()
initToolbar()
setContentView(binding.root) // 根据子类实现填充布局
initView()
initData()
}
fun initContent() {
val option: Int = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.decorView.systemUiVisibility = option
window.statusBarColor = Color.TRANSPARENT
supportActionBar?.hide()
}
fun hideToolbar() {
contentLayout.removeViewAt(0)
}
fun initToolbar() {
findViewById<ViewGroup>(android.R.id.content).apply { // 找到Layout文件的根布局
removeAllViews() // 移除所有布局
addView(contentLayout) // 添加声明的根布局
}
val toolBar = layoutInflater.inflate(R.layout.layout_toolbar, null) // 引入自定义的标题栏
contentLayout.addView(toolBar) // 根布局添加标题栏
toolBar.findViewById<TextView>(R.id.toolbar_back).setOnClickListener { // 标题栏左边功能为返回
finish()
}
toolBar.findViewById<TextView>(R.id.toolbar_title).setOnClickListener { // 设置标题栏标题
setBarTitle()
}
}
override fun setContentView(layoutResID: Int) {
layoutInflater.inflate(layoutResID, contentLayout) // 重写添加函数,把对应的布局文件添加到声明的根布局内
}
}
其中 viewmodel
工厂类:
class ViewModelFactory(private val supportManager: FragmentManager) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(FragmentManager::class.java).newInstance(supportManager)
}
}
使用方式效果:
class MainActivity : BaseActivity<ActivityMainBinding, EmptyVM>() {
override fun getModel(): Class<EmptyVM> = EmptyVM::class.java
override fun getViewBinding(): ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
override fun setBarTitle(): String = "测试标题"
override fun initView() {
binding.button.setOnClickListener {
binding.textView.text = "测试"
}
}
override fun initData() {
}
}