介绍
这篇文章,我们将了解如何使用Koin库来搭建易于扩展/编辑的Android
App
。
基础知识
Coroutines(协程)
Kotlin
选择Kotlin
的主要原因是因为Kotlin
使Android
开发更快、更好、更简洁。
Koin:轻量级依赖注入框架。
至于设计模式,Android
开发目前基本上有两种主要设计模式:MVP
和MVVM
。我们将使用MVVM因为谷歌推荐用新LiveData和ViewModel库(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注解,它将告诉Retrofit
,JSON
响应的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<List<Cat>>
}
class CatRepositoryImpl(private val catApi: CatApi) : CatRepository {
override suspend fun getCatList(): UseCaseResult<List<Cat>> {
/*
我们试图从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<out T : Any> {
class Success<out T : Any>(val data: T) : UseCaseResult<T>()
class Error(val exception: Throwable) : UseCaseResult<Nothing>()
}
另外,请注意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<List<Cat>>
}
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<Boolean>()
val catsList = MutableLiveData<List<Cat>>()
val showError = SingleLiveEvent<String>()
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接口、CatRepository
和viewModel
。
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<CatApi>(
okHttpClient = createHttpClient(),
factory = RxJava2CallAdapterFactory.create(),
baseUrl = CAT_API_BASE_URL
)
}
// Tells Koin how to create an instance of CatRepository
factory<CatRepository> { 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 <reified T> 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
在这里使用Koin
的get()
来满足构造函数的参数。无论何时我们需要它,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
以覆盖默认的应用程序类。另外,别忘了添加互联网权限。
使用以下行更新清单:
///...
<uses-permission android:name="android.permission.INTERNET" />
<application>
android:name=".application.DemoMeowApplication"
///...
</application>
View层
现在我们已经准备好了所有的逻辑,我们需要向用户展示它们。
这就是观点的来源。它的简单目的是显示数据。
我们首先准备适配器,它将处理要显示的列表:
class CatAdapter : RecyclerView.Adapter<CatAdapter.CatViewHolder>() {
// Our data list is going to be notified when we assign a new list of data to it
private var catsList: List<Cat> 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<Cat>) {
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)文件 :
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="120dp"
android:padding="1dp">
<ImageView
android:id="@+id/itemCatImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true" />
</android.support.constraint.ConstraintLayout>
最后,让我们实现MainActivity
(View
层)逻辑:
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):
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presentation.MainActivity">
<ProgressBar
android:id="@+id/mainProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<android.support.v7.widget.RecyclerView
android:id="@+id/catsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>
现在,你可以运行APP,享受小猫列表了😺
这个实现仍然是基本的,但您已经了解了如何使用Koin
实现依赖注入,使用协程路由切换线程(MAIN/IO)来进行API调用,检索数据并使用MVVM
模式显示它们。
请随意添加一些实现来扩展此应用程序并加深您的理解。以下是一些想法:一个onImageClick
事件,加载更多数据等等。
传送门:Github
请用git checkout 430211e 拉去对应的版本。