Android项目搭建教程(Viewmodel、DataBinding)

新项目准备工作

新建项目建议使用 kotlinandroix.* 组件进行开发,毕竟属于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() {
}
}

© 著作权归作者所有
这个作品真棒,我要支持一下!
Android 开发路上的点滴记录
2条评论

您好,abstract class BaseViewmodel(val frag: FragmentManager)这个设计方法会不会导致ViewModel持有了View的引用FragmentManager

Goorwl
#2

#1楼 @大肌霸 baseviewmodel 是抽象类,因此子类会实例化, 持有view。但是viewmodel是绑定了activity的生命周期,所以当activity销毁时,viewmodel也是会被销毁的,销毁后会释放持有的fragmentmanager的实例,所以不存在内存泄露的问题。

top Created with Sketch.