dagger kotlin_Android教程第2部分:将Room与RxJava 2,Dagger 2,Kotlin和MVVM结合使用

dagger kotlin

This is the second part of the tutorial: Using Room with RxJava 2, Dagger 2, Kotlin and MVVM.

这是本教程的第二部分:将Room与RxJava 2,Dagger 2,Kotlin和MVVM一起使用。

In Part 1 we set up Gradle dependencies, created the models, created an interface for the API endpoint, created the layout for a giphy and created the ViewHolder and Adapter.

第1部分中,我们设置了Gradle依赖关系,创建了模型,为API端点创建了接口,为giphy创建了布局以及创建了ViewHolder和Adapter。

现在是时候使用RecyclerView了 (It is time for the RecyclerView)

Since most Android Developers immediately associate with ViewHolder and Adapter the RecyclerView, I suggest let us continue with that. Create inside the view package a new package and name it ui. Move MainActivity inside the ui package. All information will be displayed on the MainActivity so open the activity_main.xml. The user should be able to see a progress circle while the data is loading. If something went wrong he should see a message which says the list is empty. In conclusion, the following Views and ViewGroups are needed: ConstraintLayout, RecyclerView, TextView, and ProgressBar.

由于大多数Android开发人员会立即与ViewHolder和Adapter the RecyclerView关联,因此建议让我们继续进行。 在视图包中创建一个新包,并将其命名为ui 。 向内移动MainActivity ui 所有信息都将显示在MainActivity上,因此请打开activity_main.xml 。 加载数据时,用户应该能够看到进度圈。 如果出现问题,他应该会看到一条消息,指出该列表为空。 总之,需要以下视图和视图组:ConstraintLayout,RecyclerView,TextView和ProgressBar。

Here is the complete implemented activity_main.xml.

这是完整的实现的activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    android:background="@android:color/white"
    tools:context=".view.ui.MainActivity">


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="8dp"
        android:clipToPadding="false"
        android:paddingBottom="8dp"
        android:scrollbars="vertical"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <ProgressBar
        android:id="@+id/fetch_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <TextView
        android:id="@+id/empty_text"
        style="@style/Base.TextAppearance.AppCompat.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingStart="16dp"
        android:paddingEnd="16dp"
        android:text="Zero giphies found!"
        android:visibility="gone"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/fetch_progress"
        tools:visibility="visible" />


</androidx.constraintlayout.widget.ConstraintLayout>

完成API GET请求 (Complete the API GET request)

The API call to https://api.giphy.com/v1/gifs/trending is still missing. Head to the network package and create an object class called GiphyApiService.kt.

仍然缺少对https://api.giphy.com/v1/gifs/trending的API调用。 转到网络包并创建一个名为GiphyApiService.kt的对象类。

Next, you will need a method that adds an interceptor to log the HTTP request, sets a connection timeout, and tries to request again on failure.

接下来,您将需要一种方法,该方法添加一个拦截器来记录HTTP请求,设置连接超时并尝试在失败时再次请求。

private fun createOkHttpClient(): OkHttpClient {
        val logger = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
        return OkHttpClient.Builder()
            .addInterceptor(logger)
            .connectTimeout(15, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build()
}

Before you can continue with the last method, you have to define a constant for the base URL. Go to the package internal and create the file Constant.kt. Inside the file create the constant BASE_URL. Because this is a file and not a class the constants inside this file can be used everywhere in the project. Otherwise we would have something like “FileName.ConstantName”

您必须先为基本URL定义一个常量,然后才能继续使用最后一种方法。 转到内部软件包,并创建文件Constant.kt 。 在文件内部创建常量BASE_URL 因为这是文件而不是类,所以该文件中的常量可以在项目中的任何地方使用。 否则我们会有类似“ FileName.ConstantName”的内容

const val BASE_URL = "https://api.giphy.com/"

Then create an API key on https://developers.giphy.com/ when you have your API key go to the file “.gitgignore”. Add at the end of the file following line:

然后,当您拥有API密钥时,请在https://developers.giphy.com/上创建一个API密钥,然后转到文件“ .gitgignore”。 在文件末尾添加以下行:

# Files in the project where version control should not be used
Config.kt

Commit that change in “.gitgignore”

在“ .gitgignore”中提交更改

The reason for that was an API key should never be tracked with Git or another version control system! So we can create the file Config.kt and the file will only be locally accessable and will not get pushed to GitHub.

原因是永远不要用Git或其他版本控制系统跟踪API密钥! 因此,我们可以创建文件Config.kt,并且该文件只能在本地访问,而不会推送到GitHub。

Now head to the package internal and create the file Config.kt. Inside Config.kt create a constant to save the API key from https://developers.giphy.com/

现在转到内部包,并创建文件Config.kt 。 在Config.kt内部创建一个常量以保存来自https://developers.giphy.com/的API密钥

const val KEY = "YOUR API KEY FROM developers.giphy.com"

Time to implement the last method in GiphyApiService.kt. The third method now has to create the Retrofit instance and should use the method createOkHttpClient().

是时候在GiphyApiService.kt中实现最后一个方法了。 现在,第三个方法必须创建Retrofit实例,并且应使用方法createOkHttpClient()

fun getClient(): GiphyApi {
        return Retrofit.Builder()
            .client(createOkHttpClient())
            .baseUrl(BASE_URL)
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
            .create(GiphyApi::class.java)
}

仓库模式 (The Repository Pattern)

Create a new package and name it repository. Then create a class and name it TrendingRepository.kt. Add an injected field of GiphyApi.

创建一个新程序包并将其命名为存储库 。 然后创建一个类,并将其命名为TrendingRepository.kt 。 添加GiphyApi的注入字段。

class TrendingRepository {
@Inject
lateinit var giphyApiService: GiphyApi
}

The implementation of the repository will continue after the Dagger 2 @Module and @Component is done.

Dagger 2 @Module@Component完成后,存储库的实现将继续。

依赖注入 (Dependency injection)

Dependency injection allows you to easily separate logic for Unit Testing. The dependency injection library Dagger 2 enables the instantiation of fields and injects them into the right class. All the instantiation (in @Modules) and injecting (in @Component) can be collected in specific classes.

依赖注入使您可以轻松地分离单元测试的逻辑。 依赖项注入库Dagger 2可以实例化字段并将其注入正确的类中。 所有实例化(在@Modules中 )和注入(在@Component中 )都可以收集在特定的类中。

Before continuing with the repository, where GiphyApiService gets used we should use dependency injection for this.

在继续使用GiphyApiService的存储库之前,我们应该为此使用依赖项注入。

需要@Module (@Module is needed)

Create a new package and name it di. Then create a new class called AppModule.kt. Above the class AppModule {} annotate it with @Module.

创建一个新包,并将其命名为di 。 然后创建一个名为AppModule.kt的新类。 AppModule {}上方,使用@Module对其进行注释

@Module
class AppModule {

}

Methods in @Module provide something, so start with @Provides then write your method. It is standard that those method names start with provideAnyName(). The return type of the method is very important. Whenever another provideMethod() in AppModule needs an argument to instantiate something. Dagger will look at the return type of the other methods in AppModule. If a correct return type is there, Dagger will automatically link those methods.

@Module中的方法提供了一些东西,因此从@Provides开始,然后编写您的方法。 这些方法的名称以提供 AnyName()开头是标准的。 退货类型 该方法非常重要。 每当另一个provideMethod()的AppModule需要一个自变量来实例化的东西。 Dagger将查看AppModule中其他方法的返回类型 如果存在正确的返回类型,则Dagger将自动链接这些方法。

Let’s start with an instance of GiphyApiService it would look like this:

让我们从一个看起来像GiphyApiService的实例开始

@Singleton
@Provides
fun provideApi(): GiphyApi = GiphyApiService.getClient()

Note that the return type is the interface and the actual instance is the service.

请注意,返回类型是接口,实际实例是服务。

Here how AppModule.kt looks right now:

下面是AppModule.kt的外观:

@Module
class AppModule {


    @Singleton
    @Provides
    fun provideApi(): GiphyApi = GiphyApiService.getClient()
}

下一步@Component (Next step @Component)

In the package di create a new Interface called AppComponent.kt. Annotate it with @Singleton and @Component then add parentheses and inside those write:

在包di中,创建一个名为AppComponent.kt的新接口 。 用@Singleton@Component对其进行注释,然后添加括号并在其中写入:

@Singleton
@Component(modules = [AppModule::class])

The keyword modules will link the AppModule with AppComponent.

关键字模块将链接AppModuleAppComponent。

Dagger needs to know now, which classes to inject with fields annotated with @Inject. That’s we you have to tell Dagger it should inject the TrendingRepository.kt.

Dagger现在需要知道,哪些类要注入带有@Inject注释的字段。 那就是我们您必须告诉Dagger它应该注入TrendingRepository.kt。

@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {


    fun inject(trendingRepository: TrendingRepository)
}

Now you can go back to the TrendingRepository.kt.

现在,您可以返回TrendingRepository.kt

返回资料库 (Back to the Repository)

Before you can use dependency injection in TrendingRepository.kt you have to do these steps:

必须先执行以下步骤,然后才能在TrendingRepository.kt中使用依赖项注入

  1. Click on Build -> Clean Project

    单击构建 -> 清洁项目

  2. Click on Build -> Build Project

    点击Build- > Build Project

For classes that are not Activities or Fragments, just create an init {} block to enable injecting the class. Inside init {} goes following line:

对于不是Activity或Fragments的类,只需创建一个init {}块即可注入该类。 内部初始化{}中的内容如下:

DaggerAppComponent.create().inject(this)

This class DaggerAppComponent was created by Dagger. Notice the word Dagger is always added before the Component interface name.

此类DaggerAppComponent由Dagger创建。 请注意,始终在组件接口名称之前添加单词Dagger。

Next, we need three private MutableLiveData objects and three public LiveData objects which will be needed in the MainActivity.kt. One LiveData object will hold the list of data. One LiveData object will show the progress bar when data is loading. One LiveData object will show the list is empty message when there is no Internet connection for fetching data.

接下来,我们需要在MainActivity.kt中需要三个私有MutableLiveData对象和三个公共LiveData对象。 一个LiveData对象将保存数据列表。 数据加载时,一个LiveData对象将显示进度栏。 当没有用于获取数据的Internet连接时,一个LiveData对象将显示列表为空消息。

class TrendingRepository {


    @Inject
    lateinit var giphyApiService: GiphyApi


    private val _data by lazy { MutableLiveData<List<Data>>() }
    val data: LiveData<List<Data>>
        get() = _data


    private val _isInProgress by lazy { MutableLiveData<Boolean>() }
    val isInProgress: LiveData<Boolean>
        get() = _isInProgress


    private val _isError by lazy { MutableLiveData<Boolean>() }
    val isError: LiveData<Boolean>
        get() = _isError




    init {
        DaggerAppComponent.create().inject(this)
    }
}

Okay, implementation of TrendingRepository has to be again paused for now, because the database is missing.

好的,由于缺少数据库,因此必须暂时再次暂停TrendingRepository的实现。

本地数据库 (The local database)

First things first, so we need a table for the database. Create inside the package data new package and name it database. Inside database create the data class DataEntity.kt. Annotate the class with @Entity so Room knows this is a database table. You also have to give the table a unique name, I would name it “data”.

首先,我们需要一个用于数据库的表。 在数据包内部创建新数据包,并将其命名为数据库 。 在数据库内部,创建数据类DataEntity.kt 。 用@Entity注释该类,以便Room知道这是一个数据库表。 您还必须给表一个唯一的名称,我将其命名为“数据”。

@Entity(tableName = "data")
data class DataEntity (

)

For not violating any rule for database tables we need a primary key. Room enables us to get an autogenerated primary key this will be the id for each row. If you want to know why I set id = 0, click here. The other fields we need are the same as in Data.kt.

为了不违反数据库表的任何规则,我们需要一个主键。 Room使我们能够获取自动生成的主键,该键将成为每一行的ID。 如果您想知道为什么我将id设置为0, 请单击此处 。 我们需要的其他字段与Data.kt中的相同。

@Entity(tableName = "data")
data class DataEntity(


    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    val id: Int = 0,
    @ColumnInfo(name = "images")
    val images: String,
    @ColumnInfo(name = "title")
    val title: String,
    @ColumnInfo(name = "type")
    val type: String,
    @ColumnInfo(name = "username")
    val username: String
)

Note that the images column is a String and not an Images class. Because you can’t store objects in a table. That's the reason why we need a Mapper right now.

请注意,images列是String而不是Images类。 因为您不能在表中存储对象。 这就是为什么我们现在需要Mapper的原因。

映射器 (The Mapper)

Inside the database package create a new file and name it DataMapper.kt. This file will transform a Data.kt object to a DataEntity.kt and vice versa.

数据库包内部创建一个新文件,并将其命名为DataMapper.kt 。 该文件将变换Data.kt对象到DataEntity.kt,反之亦然。

fun DataEntity.toData() = Data(
    Images(FixedHeightSmallStill(this.images, "320", "420")),
    this.title,
    this.type,
    this.username
)


fun List<DataEntity>.toDataList() = this.map { it.toData() }


fun Data.toDataEntity() = DataEntity(
    images = this.images.fixedHeightSmallStill.url,
    title = this.title,
    type = this.type,
    username = this.username
)


fun List<Data>.toDataEntityList() = this.map { it.toDataEntity() }

These are basically extension functions for Data.kt and DataEntity.kt

这些基本上都是针对Data.ktDataEntity.kt扩展功能

数据访问对象(DAO) (The data access object (DAO))

Inside the database package create a new interface and name it DataDao.kt. Annotate it with @Dao. The DAO class for our purpose inserts and queries the data from the database.

数据库包内部创建一个新接口,并将其命名为DataDao.kt 。 用@Dao注释它。 用于我们目的的DAO类插入并查询数据库中的数据。

@Dao
interface DataDao {


    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertData(data: List<DataEntity>)


    @Query("SELECT * from data")
    fun queryData(): Single<List<DataEntity>>
}

RoomDatabase (RoomDatabase)

Finally, we come to the actual database. Inside the database package create a new abstract class and name it TrendingDatabase.kt. This abstract class needs the @Database annotation and inside parenthesis entities as tables as well as the version of the database. Whenever you add a new entity or modify an existing entity you have to increase the version number. In our case, it is the first version. Or you delete the application on the device, where you want to run the app.

最后,我们进入实际的数据库。 在数据库包内创建一个新的抽象类,并将其命名为TrendingDatabase.kt 。 这个抽象类需要@Database批注和内部括号实体(如表)以及数据库的版本。 每当您添加新实体或修改现有实体时,都必须增加版本号。 在我们的情况下,它是第一个版本。 或者,您在要运行该应用程序的设备上删除该应用程序。

@Database(entities = [DataEntity::class], version = 1)
abstract class TrendingDatabase: RoomDatabase() {
}

Next, you always need a method that is abstract to use the DAO interface.

接下来,您总是需要一种抽象的方法来使用DAO接口。

abstract fun dataDao(): DataDao

After that create a companion object {} which will provide logic to instantiate the database. Inside companion object {} we need @Volatile instance of the database which all threads have immediate access.

之后,创建一个伴随对象{} ,该对象将提供实例化数据库的逻辑。 在伴随对象{}中,我们需要数据库的@Volatile实例,所有线程都可以立即访问该实例。

@Volatile // All threads have immediate access to this property
private var instance: TrendingDatabase? = null

For safety reasons, we need another instance makes sure no threads making the same thing at the same time.

出于安全原因,我们需要另一个实例来确保没有线程同时做同一件事。

private val LOCK = Any() // Makes sure no threads making the same thing at the same time

Before continuing jump to the package internal and open again Constant.kt. Create here the constant for the database name.

在继续之前,请跳到内部软件包并再次打开Constant.kt 。 在此处创建数据库名称的常量。

const val DATABASE_NAME = "trending.db"

Now go back to TrendingDatabase and create the builder method for the database.

现在返回TrendingDatabase并为数据库创建构建器方法。

private fun buildDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
TrendingDatabase::class.java,
DATABASE_NAME
).fallbackToDestructiveMigration().build()

The last method which is needed is for creating an instance of the database.

最后需要的方法是创建数据库实例。

operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: buildDatabase(context).also { instance = it }
}

The abstract class TrendingDatabase should look like this:

抽象类TrendingDatabase应该看起来像这样:

@Database(entities = [DataEntity::class], version = 1)
abstract class TrendingDatabase: RoomDatabase() {


    abstract fun dataDao(): DataDao


    companion object {


        @Volatile // All threads have immediate access to this property
        private var instance: TrendingDatabase? = null


        private val LOCK = Any() // Makes sure no threads making the same thing at the same time


        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(
                context.applicationContext,
                TrendingDatabase::class.java,
                DATABASE_NAME
            ).fallbackToDestructiveMigration().build()


        operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
            instance ?: buildDatabase(context).also { instance = it }
        }
    }


}

Okay, that’s it for the second part, here you find the third part: Part 3

好吧,这是它的第二部分,在这里你会发现第三部分: 第3部分

Here is the completed project, check out branch part2:

这是已完成的项目,请查看分支part2

翻译自: https://medium.com/@fahri.c93/android-tutorial-part-2-using-room-with-rxjava-2-dagger-2-kotlin-and-mvvm-65b8c067d3af

dagger kotlin

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值