初始化项目
原文地址:https://proandroiddev.com/mvvm-with-kotlin-android-architecture-components-dagger-2-retrofit-and-rxandroid-1a4ebb38c699
项目地址:https://github.com/gahfy/MVVMPosts
Bases
添加一个base package到项目中
Lifecycle library
在项目的build.gradle文件中添加依赖
buildscript {
ext.kotlin_version = '1.2.30'
ext.lifecycle_version = '1.1.1'
}
接着在app/build.gradle文件中添加lifecycle依赖
dependencies {
implementation "android.arch.lifecycle:extensions:$lifecycle_version"
}
添加BaseViewModel类
BaseViewModel
在base包中添加BaseViewModel类。
import android.arch.lifecycle.ViewModel
abstract class BaseViewModel: ViewModel(){
}
Model
定义Post对象,添加model包,然后在该包下创建Post类
/**
* Class which provides a model for post
* @constructor Sets all properties of the post
* @property userId the unique identifier of the author of the post
* @property id the unique identifier of the post
* @property title the title of the post
* @property body the content of the post
*/
data class Post(val userId: Int, val id: Int, val title: String, val body: String)
Retrofit
我们从JSONPlaceholder API
中获取Post列表,首先添加Retrofit的依赖到build.gradle文件中
buildscript {
ext.kotlin_version='1.2.30'
ext.lifecycle_version = '1.1.1'
ext.retrofit_version = '2.4.0'
}
在app/build.gradle中添加如下依赖
dependencies {
// ...
// Retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
}
创建network包,然后创建PostApi接口用来获取Post列表。
/**
* The interface which provides methods to get result of webservices
*/
interface PostApi {
/**
* Get the list of the pots from the API
*/
@GET("/posts")
fun getPosts(): Observable<List<Post>>
}
接下来在utils包中创建一个Kotlin文件,命名为Constants.kt用来定义一些常量,这里用来定义base url:
/** The base URL of the API */
const val BASE_URL: String = "https://jsonplaceholder.typicode.com"
Dagger2
在项目中的build.gradle文件中添加Dagger的版本号:
buildscript {
ext.kotlin_version = '1.2.30'
ext.lifecycle_version = '1.1.1'
ext.retrofit_version = '2.4.0'
ext.dagger2_version = '2.16'
//...
}
接着在app/build.gradle文件中添加Dagger2依赖
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
// ...
dependencies {
// ...
// Dagger 2
implementation "com.google.dagger:dagger:$dagger2_version"
kapt "com.google.dagger:dagger-compiler:$dagger2_version"
compileOnly "org.glassfish:javax.annotation:3.1.1"
}
注入Retrofit
首先创建一个module在ViewModel中注入Retrofit实例。我们将此Module命名为NetworkModule,并将其放在module包下面,module包必须在inject包下面,该包必须添加到应用程序的根包下。
我们将module和provider方法都设置成单例的以防每次使用都得初始化
/**
* Module which provides all required dependencies about network
*/
@Module
// Safe here as we are dealing with a Dagger 2 module
@Suppress("unused")
object NetworkModule {
/**
* Provides the Post service implementation.
* @param retrofit the Retrofit object used to instantiate the service
* @return the Post service implementation.
*/
@Provides
@Reusable
@JvmStatic
internal fun providePostApi(retrofit: Retrofit): PostApi {
return retrofit.create(PostApi::class.java)
}
/**
* Provides the Retrofit object.
* @return the Retrofit object
*/
@Provides
@Reusable
@JvmStatic
internal fun provideRetrofitInterface(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.build()
}
}
现在我们进一步开发应用程序。
Post MVVM
添加一个ui包,并且在ui包下面添加一个post包用来添加与Post相关的Views和ViewModel。
ViewModel组件和注入
创建一个PostListViewModel类用来从API获取数据并且显示在view中。
首先需要通过Dagger注入一个PostApi实例
class PostListViewModel:BaseViewModel(){
@Inject
lateinit var postApi: PostApi
}
然后在injection包中创建一个component包,然后创建一个ViewModelInjector:
/**
*Component providing inject() methods for presenters.
*/
@Singleton
@Component(modules = [(NetworkModule::class)])
interface ViewModelInjector {
/**
* Injects required dependencies into the specified PostListViewModel.
* @param postListViewModel PostListViewModel in which to inject the dependencies
*/
fun inject(postListViewModel: PostListViewModel)
@Component.Builder
interface Builder {
fun build(): ViewModelInjector
fun networkModule(networkModule: NetworkModule): Builder
}
}
我们现在需要做的是在BaseViewModel类中注入所需的依赖项
abstract class BaseViewModel:ViewModel(){
private val injector: ViewModelInjector = DaggerViewModelInjector
.builder()
.networkModule(NetworkModule)
.build()
init {
inject()
}
/**
* Injects the required dependencies
*/
private fun inject() {
when (this) {
is PostListViewModel -> injector.inject(this)
}
}
}
在ViewModel中获取数据
现在我们已经注入了PostApi,然后从Api中获取数据,我们需要在后台线程中执行call方法,然后在主线程执行actions方法,为了实现这个功能,我们使用RxAndroid库,在build.gradle文件中添加依赖:
dependencies {
//...
//Rx
implementation "io.reactivex.rxjava2:rxjava:2.1.15"
implementation "io.reactivex.rxjava2:rxandroid:2.0.2"
}
现在到PostListViewModel类中编写获取结果的方法:
class PostListViewModel:BaseViewModel(){
@Inject
lateinit var postApi: PostApi
private lateinit var subscription: Disposable
init{
loadPosts()
}
private fun loadPosts(){
subscription = postApi.getPosts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { onRetrievePostListStart() }
.doOnTerminate { onRetrievePostListFinish() }
.subscribe(
{ onRetrievePostListSuccess() },
{ onRetrievePostListError() }
)
}
private fun onRetrievePostListStart(){
}
private fun onRetrievePostListFinish(){
}
private fun onRetrievePostListSuccess(){
}
private fun onRetrievePostListError(){
}
}
ViewModel onCleared()
在ViewModel的onCleared方法中销毁订阅
class PostListViewModel:BaseViewModel(){
// ...
private lateinit var subscription: Disposable
// ...
override fun onCleared() {
super.onCleared()
subscription.dispose()
}
// ...
}
LiveData
添加一个MutableLiveData用来观察数据更新时设置ProgressBar的可见性。
class PostListViewModel:BaseViewModel(){
// ...
val loadingVisibility: MutableLiveData<Int> = MutableLiveData()
// ...
private fun onRetrievePostListStart(){
loadingVisibility.value = View.VISIBLE
}
private fun onRetrievePostListFinish(){
loadingVisibility.value = View.GONE
}
private fun onRetrievePostListSuccess(){
}
private fun onRetrievePostListError(){
}
}
Layout,DataBinding和BindingAdapters
这一部分使用DataBinding和DataBinders,首先需要添加依赖,然后还需要用到ConstraintLayout和RecyclerView。
buildscript {
ext.kotlin_version = '1.2.30'
ext.lifecycle_version = '1.1.1'
ext.retrofit_version = '2.4.0'
ext.dagger2_version = '2.16'
ext.android_support_version = '28.0.0-alpha3'
// ...
}
android {
compileSdkVersion 28
dataBinding {
enabled = true
}
// ...
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation "com.android.support:appcompat-v7:$android_support_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
// RecyclerView
implementation "com.android.support:recyclerview-v7:$android_support_version"
// Constraint Layout
implementation "com.android.support.constraint:constraint-layout:1.1.2"
// LiveData & ViewModel
implementation"android.arch.lifecycle:extensions:$lifecycle_version"
// Data binding
kapt "com.android.databinding:compiler:3.1.3"
// Retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
// Dagger 2
implementation "com.google.dagger:dagger:$dagger2_version"
kapt "com.google.dagger:dagger-compiler:$dagger2_version"
compileOnly "org.glassfish:javax.annotation:3.1.1"
//Rx
implementation "io.reactivex.rxjava2:rxjava:2.1.15"
implementation "io.reactivex.rxjava2:rxandroid:2.0.2"
}
创建一个layout文件activity_post_list.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="net.gahfy.mvvmposts.ui.post.PostListViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mutableVisibility="@{viewModel.getLoadingVisibility()}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<android.support.v7.widget.RecyclerView
android:id="@+id/post_list"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>
正如你看到的,我们添加了属性app:mutableVisibility="@{viewModel.getLoadingVisibility()}"到ProgressBar中,这个属性不存在,所以我们需要通过BindingAdapter定义它。
在utils包中创建一个文件命名为BindingAdapters.kt,在这个文件中,我们为mutableVisibility属性定义DataBinder。
@BindingAdapter("mutableVisibility")
fun setMutableVisibility(view: View, visibility: MutableLiveData<Int>?) {
val parentActivity: AppCompatActivity? = view.getParentActivity()
if(parentActivity != null && visibility != null) {
visibility.observe(parentActivity, Observer { value -> view.visibility = value?:View.VISIBLE})
}
}
其中View.getParentActivity()方法并不存在,所以我们需要添加一个扩展方法给View类,让我们在utils包中创建一个extension包,然后创建一个ViewExtension.kt文件:
fun View.getParentActivity(): AppcompatActivity?{
var context = this.context
while(context is ContextWrapper){
if (context is AppcompatActivity) {
return context
}
context = context.baseContext
}
return null
}
PostListActivity
现在创建PostListActivity类
class PostListActivity: AppCompatActivity() {
private lateinit var binding: ActivityPostListBinding
private lateinit var viewModel: PostListViewModel
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_post_list)
binding.postList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
viewModel = ViewModelProviders.of(this).get(PostListViewModel::class.java)
binding.viewModel = viewModel
}
}
Manifest
现在在清单文件中添加Internet权限并且添加Activity。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.gahfy.mvvmposts">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".ui.post.PostListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
现在可以运行该项目,可以看到ProgressBar出现然后消失。
错误处理
当发生错误的时候用SnackBar来显示错误信息,并且允许用户重新获取posts,首先需要添加SnackBar的依赖:
// ...
dependencies {
// ...
// Support Design
implementation "com.android.support:design:$android_support_version"
}
现在添加显示错误时将使用的字符串资源:
<resources>
<!-- ... -->
<string name="retry">Retry</string>
<string name="post_error">An error occurred while loading the posts</string>
</resources>
现在添加一个MutableLiveData和一个OnClickListener属性到ViewModel中。
class PostListViewModel:BaseViewModel(){
// ...
val errorMessage:MutableLiveData<Int> = MutableLiveData()
val errorClickListener = View.OnClickListener { loadPosts() }
// ...
private fun onRetrievePostListStart(){
loadingVisibility.value = View.VISIBLE
errorMessage.value = null
}
private fun onRetrievePostListFinish(){
loadingVisibility.value = View.GONE
}
private fun onRetrievePostListSuccess(){
}
private fun onRetrievePostListError(){
errorMessage.value = R.string.post_error
}
}
然后在PostListActivity中观察errorMessage并且当不为null的时候将错误信息显示在SnackBar中,当为null时隐藏SnackBar
class PostListActivity: AppCompatActivity() {
// ...
private var errorSnackbar: Snackbar? = null
override fun onCreate(savedInstanceState: Bundle?){
// ...
viewModel = ViewModelProviders.of(this).get(PostListViewModel::class.java)
viewModel.errorMessage.observe(this, Observer {
errorMessage -> if(errorMessage != null) showError(errorMessage) else hideError()
})
binding.viewModel = viewModel
}
private fun showError(@StringRes errorMessage:Int){
errorSnackbar = Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_INDEFINITE)
errorSnackbar?.setAction(R.string.retry, viewModel.errorClickListener)
errorSnackbar?.show()
}
private fun hideError(){
errorSnackbar?.dismiss()
}
}
当你在手机飞行模式下运行应用程序,就会先看到ProgressBar然后是显示错误信息的SnackBar。
显示Posts列表
接下来显示Posts列表,首先为列表中的每一个条目创建一个ViewModel,在ui.post包下创建一个PostViewModel。
class PostViewModel:BaseViewModel() {
private val postTitle = MutableLiveData<String>()
private val postBody = MutableLiveData<String>()
fun bind(post: Post){
postTitle.value = post.title
postBody.value = post.body
}
fun getPostTitle():MutableLiveData<String>{
return postTitle
}
fun getPostBody():MutableLiveData<String>{
return postBody
}
}
现在创建item的布局,命名为item_post.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="net.gahfy.mvvmposts.ui.post.PostViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="@+id/post_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textStyle="bold"
app:mutableText="@{viewModel.getPostTitle()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/post_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:mutableText="@{viewModel.getPostBody()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/post_title" />
</android.support.constraint.ConstraintLayout>
</layout>
现在需要编辑BindingAdapters.kt用来添加app:mutableText属性。
Adapter和ViewModel
在ui.post包中创建PostListAdapter:
class PostListAdapter: RecyclerView.Adapter<PostListAdapter.ViewHolder>() {
private lateinit var postList:List<Post>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostListAdapter.ViewHolder {
val binding: ItemPostBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_post, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: PostListAdapter.ViewHolder, position: Int) {
holder.bind(postList[position])
}
override fun getItemCount(): Int {
return if(::postList.isInitialized) postList.size else 0
}
fun updatePostList(postList:List<Post>){
this.postList = postList
notifyDataSetChanged()
}
class ViewHolder(private val binding: ItemPostBinding):RecyclerView.ViewHolder(binding.root){
fun bind(post:Post){
// ...
}
}
}
现在让我们来关注ViewHolder类,我们知道如何将ViewModel和Activity关联,因为我们添加了View.getParentActivity()扩展方法,通过ViewModelProviders.of(binding.root.getParentActivity()).get(PostViewModel::class.java)可以特别方便的获取PostViewModel的实例,问题是与Activity关联的ViewModel是单例的,所以会让列表中每行都显示相同的数据。
因为我们使用了MutableLiveData,所以我们不必为每一行实例化一个新的PostViewModel,而只用为每个ViewHolder实例化一个就行了。
class PostListAdapter: RecyclerView.Adapter<PostListAdapter.ViewHolder>() {
// ...
class ViewHolder(private val binding: ItemPostBinding):RecyclerView.ViewHolder(binding.root){
private val viewModel = PostViewModel()
fun bind(post:Post){
viewModel.bind(post)
binding.viewModel = viewModel
}
}
}
为RecyclerView设置adapter
现在我们只需要为RecyclerView设置PostListAdapter就行了,首先编辑PostListViewModel:
class PostListViewModel:BaseViewModel(){
//...
val postListAdapter: PostListAdapter = PostListAdapter()
// ...
private fun loadPosts(){
subscription = postApi.getPosts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { onRetrievePostListStart() }
.doOnTerminate { onRetrievePostListFinish() }
.subscribe(
// Add result
{ result -> onRetrievePostListSuccess(result) },
{ onRetrievePostListError() }
)
}
private fun onRetrievePostListStart(){
loadingVisibility.value = View.VISIBLE
errorMessage.value = null
}
private fun onRetrievePostListFinish(){
loadingVisibility.value = View.GONE
}
private fun onRetrievePostListSuccess(postList:List<Post>){
postListAdapter.updatePostList(postList)
}
private fun onRetrievePostListError(){
errorMessage.value = R.string.post_error
}
}
接下来在activity_list_post布局文件中设置adapter:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="net.gahfy.mvvmposts.ui.post.PostListViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mutableVisibility="@{viewModel.getLoadingVisibility()}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<android.support.v7.widget.RecyclerView
android:id="@+id/post_list"
android:layout_width="0dp"
android:layout_height="0dp"
app:adapter="@{viewModel.getPostListAdapter()}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>
然后添加一个BindingAdapter用来直接设置adapter到RecyclerView:
@BindingAdapter("adapter")
fun setAdapter(view: RecyclerView, adapter: RecyclerView.Adapter<*>) {
view.adapter = adapter
}
现在运行程序就可以看到post列表了
Room
添加依赖
buildscript {
ext.kotlin_version = '1.2.30'
ext.lifecycle_version = '1.1.1'
ext.retrofit_version = '2.4.0'
ext.dagger2_version = '2.16'
ext.android_support_version = '28.0.0-alpha3'
ext.room_version = '1.1.1'
// ...
}
// ...
dependencies {
// ...
// Room
implementation "android.arch.persistence.room:runtime:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"
}
Entity
现在更新Post类,用@Entity注解,可以被保存在数据库中。
@Entity
data class Post(
val userId: Int,
@field:PrimaryKey
val id: Int,
val title: String,
val body: String
)
Dao
添加一个DAO类用来从数据库中插入和获取Posts,在model中添加一个接口命名为PostDao。
@Dao
interface PostDao {
@get:Query("SELECT * FROM post")
val all: List<Post>
@Insert
fun insertAll(vararg posts: Post)
}
Database
最后添加AppDatabase类:
@Database(entities = arrayOf(Post::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun postDao(): PostDao
}
在ViewModel中使用Context Dependent实例
现在我们需要添加一个PostDao参数到PostListViewModel构造函数中,我们需要做的就是调用insert()和all()方法。
class PostListViewModel(private val postDao: PostDao):BaseViewModel(){
// ...
private fun loadPosts(){
subscription = Observable.fromCallable { postDao.all }
.concatMap {
dbPostList ->
if(dbPostList.isEmpty())
postApi.getPosts().concatMap {
apiPostList -> postDao.insertAll(*apiPostList.toTypedArray())
Observable.just(apiPostList)
}
else
Observable.just(dbPostList)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { onRetrievePostListStart() }
.doOnTerminate { onRetrievePostListFinish() }
.subscribe(
{ result -> onRetrievePostListSuccess(result) },
{ onRetrievePostListError() }
)
}
// ...
}
ViewModelProvider.Factory
现在我们需要在injection包中创建一个ViewModelFactory类,让ViewModelProvider知道如何初始化ViewModel。
class ViewModelFactory(private val activity: AppCompatActivity): ViewModelProvider.Factory{
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(PostListViewModel::class.java)) {
val db = Room.databaseBuilder(activity.applicationContext, AppDatabase::class.java, "posts").build()
@Suppress("UNCHECKED_CAST")
return PostListViewModel(db.postDao()) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
接着告诉provider使用factory来初始化PostViewModel类,接下来更新PostListActivity中的onCreate方法。
class PostListActivity: AppCompatActivity() {
// ...
override fun onCreate(savedInstanceState: Bundle?){
// ...
viewModel = ViewModelProviders.of(this, ViewModelFactory(this)).get(PostListViewModel::class.java)
// ...
}
// ...
}
现在运行应用程序,一旦你进入一次之后就可以将手机切换到飞行模式,然后post数据在之后就会自动显示出来。