使用Koin和Kotlin搭建简单的MVVM框架(上)

介绍

这篇文章,我们将了解如何使用Koin库来搭建易于扩展/编辑的Android App

基础知识

Coroutines(协程)
Kotlin
选择Kotlin的主要原因是因为Kotlin使Android开发更快、更好、更简洁。
Koin:轻量级依赖注入框架。

至于设计模式,Android开发目前基本上有两种主要设计模式:MVPMVVM。我们将使用MVVM因为谷歌推荐用新LiveDataViewModel库(Android架构组件)来搭建APP框架。

本APP将使用theCatApi。它将包含一个简单的MainActivity,该活动将显示猫图像列表 。为了展示如何扩展App,如果需要,我可能会在下一个篇上添加一些额外的功能。

咱们开始吧

创建一个带有空活动的新项目。将应用命名为“DemoMeow”,并选择Kotlin作为语言。设置最小API 为21并单击finish完成。

我们将在Kotlin中开发,所以不要忘记检查Kotlin的插件,并在Gradle文件中实现其依赖项(如果需要):

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
    ///...
dependencies { 
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.20'
    ///...
}

对于这个项目,我们将在Gradle dependencies中添加以下库:

dependencies { 
    ///...
// Glide for loading and caching cat images
implementation 'com.github.bumptech.glide:glide:4.9.0'
kapt 'com.github.bumptech.glide:compiler:4.9.0'
// Retrofit as our REST service
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
// Koin for the dependencies injections
implementation 'org.koin:koin-android-viewmodel:2.0.0-rc-2'
// Coroutines for asynchronous calls (and Deferred’s adapter)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0'
// Coroutines - Deferred adapter
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
}

Model 层

package com.gunaya.demo.demomeow.data.entities

import com.google.gson.annotations.SerializedName

data class Cat(
    val id: String,
    @SerializedName("url")
    val imageUrl: String
)
/* Cat response example
  {
    "id": "89f",
    "url": "https://25.media.tumblr.com/tumblr_lznbbvPuZy1r63pb5o1_250.gif",
    "breeds": [],
    "categories": []
  }
 */

请注意@SerializedName注解,它将告诉RetrofitJSON响应的url字段必须与我们模型的imageUrl字段相关联。
我们现在需要一个类,它允许我们使用这个模型进行操作。

为了良好实践(以及更简单的测试)本App从Interface访问存储库。

为了拥有一个干净的代码,我们将创建一个名为CatRepository的接口,并在CatRepositoryImpl类中实现它。该接口将提供与cat相关的所有操作函数(目前只有一个方法——getCatList(),但是,例如,如果以后我们想要检索一个cat,我们只需要在接口中添加getCat(s:Name)方法并在实现类中添加其逻辑,就可以更新该存储库)。

CatRepository及其实现将编写如下:

package com.gunaya.demo.demomeow.data.repositories

import com.gunaya.demo.demomeow.UseCaseResult
import com.gunaya.demo.demomeow.data.entities.Cat
import com.gunaya.demo.demomeow.data.remote.CatApi
import java.lang.Exception

// 咱们需要的数量
const val NUMBER_OF_CATS = 30

interface CatRepository {
    // Suspend挂起函数
    suspend fun getCatList(): UseCaseResult>
}

class CatRepositoryImpl(private val catApi: CatApi) : CatRepository {
    override suspend fun getCatList(): UseCaseResult> {
      /*
    我们试图从API中返回猫的列表
    等待web服务的结果,然后返回它,捕获API中的任何错误
*/
        return try {
            val result = catApi.getCats(limit = NUMBER_OF_CATS).await()
            UseCaseResult.Success(result)
        } catch (ex: Exception) {
            UseCaseResult.Error(ex)
        }
    }
}

如果请求成功,UseCaseResult类将有助于获取数据,如果请求失败,将有助于获取异常。


sealed class UseCaseResult {
    class Success(val data: T) : UseCaseResult()
    class Error(val exception: Throwable) : UseCaseResult()
}

另外,请注意CatRepositoryImpl构造函数中的CatApi参数,它是我们的REST接口。按如下方式创建:

import com.gunaya.demo.demomeow.data.entities.Cat
import kotlinx.coroutines.Deferred
import retrofit2.http.GET
import retrofit2.http.Query

interface CatApi {
    /* 获取用于检索cat图像的路径,limit是获取cat的数量*/
    @GET("images/search")
    fun getCats(@Query("limit") limit: Int)
            : Deferred>
}

ViewModel 层


ViewModel类旨在以生命周期意识的方式存储和管理UI相关的数据

让我们创建一个Kotlin类,将其命名为MainViewModel并从Google中实现ViewModel。它将是与我们的MainActivity相关联的viewModel。

class MainViewModel(private val catRepository: CatRepository) : ViewModel(), CoroutineScope {
    // Coroutine's background job
    private val job = Job()
    // Define default thread for Coroutine as Main and add job
    override val coroutineContext: CoroutineContext = Dispatchers.Main + job

    val showLoading = MutableLiveData()
    val catsList = MutableLiveData>()
    val showError = SingleLiveEvent()

    fun loadCats() {
        // Show progressBar during the operation on the MAIN (default) thread
        showLoading.value = true
        // launch the Coroutine
        launch {
            // Switching from MAIN to IO thread for API operation
            // Update our data list with the new one from API
            val result = withContext(Dispatchers.IO) { catRepository.getCatList() }
            // Hide progressBar once the operation is done on the MAIN (default) thread
            showLoading.value = false
            when (result) {
                is UseCaseResult.Success -> catsList.value = result.data
                is UseCaseResult.Error -> showError.value = result.exception.message
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        // Clear our job when the linked activity is destroyed to avoid memory leaks
        job.cancel()
    }
}

-CoroutineScope必须在将要切换线程的类中实现。随之而来的是我们要重写CoroutineContext。
-正如名字所说,MutableLiveData类型是一个可变的LiveData(我们可以设置值)。
-简而言之,SingleLiveEvent类型是只触发一个事件的LiveData。更多信息此处(将这个类从Google添加到项目中。)→ SingleLiveEvent.kt)
-showLoading是一个布尔值,我们的视图将根据其值观察并更新progressBar。
-catsList是我们的视图将观察和显示的数据列表。
下一步是填充我们的猫对象。为了实现它,我们必须用Koin设置我们新创建的API接口、CatRepositoryviewModel

Koin 模型的实现

现在,让我们创建模块文件,在其中设置Koin的模块。我们必须在这个文件中编写如何构建类的逻辑,所以当我们需要其中一个类时,我们只需告诉Koin需要哪一个,他就会依赖注入给我们。

const val CAT_API_BASE_URL = "https://api.thecatapi.com/v1/"

val appModules = module {
    // The Retrofit service using our custom HTTP client instance as a singleton
    single {
        createWebService(
            okHttpClient = createHttpClient(),
            factory = RxJava2CallAdapterFactory.create(),
            baseUrl = CAT_API_BASE_URL
        )
    }
    // Tells Koin how to create an instance of CatRepository
    factory { CatRepositoryImpl(catApi = get()) }
    // Specific viewModel pattern to tell Koin how to build MainViewModel
    viewModel { MainViewModel(catRepository = get()) }
}

/* Returns a custom OkHttpClient instance with interceptor. Used for building Retrofit service */
fun createHttpClient(): OkHttpClient {
    val client = OkHttpClient.Builder()
    client.readTimeout(5 * 60, TimeUnit.SECONDS)
    return client.addInterceptor {
        val original = it.request()
        val requestBuilder = original.newBuilder()
        requestBuilder.header("Content-Type", "application/json")
        val request = requestBuilder.method(original.method(), original.body()).build()
        return@addInterceptor it.proceed(request)
    }.build()
}
/* function to build our Retrofit service */
inline fun  createWebService(
    okHttpClient: OkHttpClient,
    factory: CallAdapter.Factory, baseUrl: String
): T {
    val retrofit = Retrofit.Builder()
        .baseUrl(baseUrl)
        .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .addCallAdapterFactory(factory)
        .client(okHttpClient)
        .build()
    return retrofit.create(T::class.java)
}

这里需要注意三点:
-模块包含将在·appModule·范围内组装的类(在本例中是我们的改装服务和CatRepository)。
-为了构建改造服务,我们将使用createWebService()作为单例,并使用我们的自定义OkHttpClient作为参数,使用函数createWebService()
-我们的CatRepository在这里使用Koinget()来满足构造函数的参数。无论何时我们需要它,Koin都会在我们使用的时候注入给我们提供使用。
我们现在需要在项目中添加这个appModules,以便在应用程序中访问它。让我们创建DemoMeowApplication类,它将继承Application,并使用我们的AppModule启动Koin

class DemoMeowApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        // Adding Koin modules to our application
        startKoin {
            androidContext(this@DemoMeowApplication)
            modules(appModules)
        }
    }
}

我们需要更新AndroidManifest以覆盖默认的应用程序类。另外,别忘了添加互联网权限。
使用以下行更新清单:

   ///...


    android:name=".application.DemoMeowApplication"
    ///...

View层

现在我们已经准备好了所有的逻辑,我们需要向用户展示它们。
这就是观点的来源。它的简单目的是显示数据。
我们首先准备适配器,它将处理要显示的列表:

class CatAdapter : RecyclerView.Adapter() {

    // Our data list is going to be notified when we assign a new list of data to it
    private var catsList: List by Delegates.observable(emptyList()) { _, _, _ ->
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CatViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val view = inflater.inflate(R.layout.item_cat, parent, false)
        return CatViewHolder(view)
    }

    override fun getItemCount(): Int = catsList.size

    override fun onBindViewHolder(holder: CatViewHolder, position: Int) {
        // Verify if position exists in list
        if (position != RecyclerView.NO_POSITION) {
            val cat: Cat = catsList[position]
            holder.bind(cat)
        }
    }

    // Update recyclerView's data
    fun updateData(newCatsList: List) {
        catsList = newCatsList
    }

    class CatViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(cat: Cat) {
            // Load images using Glide library
            Glide.with(itemView.context)
                .load(cat.imageUrl)
                .centerCrop()
                .thumbnail()
                .into(itemView.itemCatImageView)
        }
    }
}

(item_cat.xml)文件 :




    


最后,让我们实现MainActivityView层)逻辑:

const val NUMBER_OF_COLUMN = 3

class MainActivity : AppCompatActivity() {

    // Instantiate viewModel with Koin
    private val viewModel: MainViewModel by viewModel()
    private lateinit var catAdapter: CatAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // Instantiate our custom Adapter
        catAdapter = CatAdapter()
        catsRecyclerView.apply {
            // Displaying data in a Grid design
            layoutManager = GridLayoutManager(this@MainActivity, NUMBER_OF_COLUMN)
            adapter = catAdapter
        }
        // Initiate the observers on viewModel fields and then starts the API request
        initViewModel()
    }
    
    private fun initViewModel() {
        // Observe catsList and update our adapter if we get new one from API
        viewModel.catsList.observe(this, Observer { newCatsList ->
            catAdapter.updateData(newCatsList!!)
        })
        // Observe showLoading value and display or hide our activity's progressBar
        viewModel.showLoading.observe(this, Observer { showLoading ->
            mainProgressBar.visibility = if (showLoading!!) View.VISIBLE else View.GONE
        })
        // Observe showError value and display the error message as a Toast
        viewModel.showError.observe(this, Observer { showError ->
            Toast.makeText(this, showError, Toast.LENGTH_SHORT).show()
        })
        // The observers are set, we can now ask API to load a data list
        viewModel.loadCats()
    }
}

简单说明:首先,我们使用Koin实例化viewModel,然后,我们用观察者模式去观察viewModel的数据。最后,我们用viewModel启动API请求loadCats(),然后在viewModel中加载数据,这会在第39行触发观察者,并通过调用updateData()更新视图的适配器。

MainActivity的布局文件 (activity_main.xml):




    

    


现在,你可以运行APP,享受小猫列表了
这个实现仍然是基本的,但您已经了解了如何使用Koin实现依赖注入,使用协程路由切换线程(MAIN/IO)来进行API调用,检索数据并使用MVVM模式显示它们。

请随意添加一些实现来扩展此应用程序并加深您的理解。以下是一些想法:一个onImageClick事件,加载更多数据等等。

传送门:Github
请用git checkout 430211e 拉去对应的版本。

你可能感兴趣的:(使用Koin和Kotlin搭建简单的MVVM框架(上))