Чистая архитектура с MVVM



Книга Чистая архитектура с MVVM

Для лучшего понимания чистой архитектуры давайте создадим примерный проект. Это приложение, на первой странице которого показывается список персонажей из мультсериала «Рик и Морти» с данными. Нажимая на каждого персонаж, на следующей странице можно увидеть серии, в которых эти персонажи появляются.


Поэтому у нас два типа сущностей: персонаж и серия.


Итак, прежде всего разберёмся, почему нам надо использовать чистую архитектуру?


  1. Разделение обязанностей  —  разделение кода на части или различные модули, которые имеют определённые обязанности, облегчает его сопровождение и дальнейшие изменения.
  2. Слабая связанность  —  в гибкий код можно легко вносить изменения, не меняя всей системы.
  3. Лёгкость тестирования.

В проекте у нас три слоя: приложение (представление), данные и предметная область.


Данные: в этом слое есть абстрактное определение различных источников данных и как их следует использовать. Мапперы выполняют отображение ответа сервера на модели баз данных. Модели представляют собой модель ответа сервера. Репозиторий существует для реализации вызовов API. Операции с базами данных  —  для реализации интерфейса «Dao», а пакет API  —  для определения методов API-вызовов с сервера. В обычном приложении мы, как правило, храним репозиторий и интерфейс репозитория в одном пакете. И можем делать это локально, чтобы везде иметь прямой доступ. Но в этом случае слой данных ни в коем разе не должен знать о других слоях.


Предметная область: этот слой известен как бизнес-логика. Это правила вашего бизнеса. Здесь находится пакет model, содержащий модели баз данных. А также репозиторий, являющийся лишь интерфейсом, и прецеденты. А что такое прецедент? Как известно, прецеденты выполняют единственную задачу. И в случае с персонажами, когда надо получить данные из базы данных, мы пишем прецедент с этой самой задачей получения данных из базы данных.


Приложение: этот слой взаимодействует только с UI (пользовательским интерфейсом) и содержит фрагменты, activity (т. е. визуальный интерфейс с отдельным экраном для одного действия пользователя), ViewModel и Di. Под Di подразумевается модуль, предназначенный для этого фрагмента или activity.


На этом рисунке показано, как слои взаимодействуют друг с другом:



Но закончим уже с текстом и перейдём скорее к коду.


Итак, в проекте мы используем:


RxJava


Hilt


databinding


Retrofit


Room


Kotlin


Зависимости:


```java
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'
implementation "com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0"
implementation group: 'com.squareup.retrofit2', name: 'converter-jackson', version: '2.4.0'
//implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.8.6"
//rx
implementation 'io.reactivex.rxjava2:rxjava:2.2.19'
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
//jackson
implementation 'com.fasterxml.jackson.core:jackson-core:2.10.1'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.10.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.1'
//Hilt
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
//Room
implementation 'androidx.room:room-runtime:2.2.5'
implementation "android.arch.persistence.room:rxjava2:2.2.4"
implementation "androidx.room:room-rxjava2:2.2.5"
kapt "androidx.room:room-compiler:2.2.5"
```

Для этого проекта в базе мы создаём три пакета: Episode («Серия»), Character («Персонаж») и utils.


Что такое utils? Это пакет, содержащий базовые и общие классы, которые используются более чем в двух классах.


Episode («Серия») и Character («Персонаж») содержат данные, предметную область (бизнес-логику) и представление:



Эти пакеты будут выглядеть так:



Пакет API содержит интерфейс «CharacterApi», который является лишь методом взаимодействия с сервером, и «CharacterApiImpl» для реализации этого взаимодействия.


```java
interface CharacterApi {
@GET("api/character")
fun getCharacters(): Single<ResponseCharacter>
}
```

Пакет базы данных содержит интерфейс «Dao»:


```java
@Dao
interface CharacterDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCharacter(characters: CharactersData): Maybe<Long>

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCharacters(characters: List<CharactersData>): Maybe<List<Long>>
}
```

В «CharacterRepositoryImpl» вызываем только те методы, которые нам нужны. Здесь нет бизнес-логики.


```java
class CharactersRepositoryImpl @Inject constructor(
private val charactersApi: CharactersApiImpl,
private val characterDao: CharacterDao
) : CharactersRepository {
override fun getCharacters(): Single<ResponseCharacter> {
return charactersApi.getCharacters()
}
}
```

В предметной области мы определяем модель, которую надо сохранить в базе данных:


```java
@Entity(tableName = "Characters")
@TypeConverters(StringConverter::class)
data class CharactersData(
@PrimaryKey(autoGenerate = true)
val characterId: Int? = 0,
var image: String? = null,
val gender: String? = null,
val url: String? = null,
@SuppressWarnings(RoomWarnings.DEFAULT_CONSTRUCTOR)
@Embedded(prefix = "org")
var origin: Origin? = null,
var name: String? = null,
@SuppressWarnings(RoomWarnings.DEFAULT_CONSTRUCTOR)
@Embedded(prefix = "loc")
var location: Location? = null,
var status: String? = null,
val episodes : List<String?>? = null
)
```

Репозиторий  —  это интерфейс, реализуемый в приведённом выше классе.


```java
interface CharactersRepository {
fun getCharacters(): Single<ResponseCharacter>
}
```

Прецедент, подобный упомянутому ранее, выполняет только одну задачу. Например, получение данных с сервера с сохранением их в базе данных.


```java
class GetCharactersUseCase @Inject constructor(private val charactersRepository: CharactersRepository) {
sealed class Result {
object Loading : Result()
data class Success(val responseCharacter: List<CharactersData>) : Result()
data class Failure(val throwable: Throwable) : Result()
}
fun getCharacters(hasNetwork: Boolean): Observable<Result> {
return if (!hasNetwork) {
return charactersRepository.getCharactersFromDb()
.toObservable()
.map {
Success(it) as Result
}
.onErrorReturn { Failure(it) }
.startWith(Result.Loading)
} else {
charactersRepository.deleteAllCharacters()
charactersRepository.getCharacters().toObservable().map {
val data = CharacterToDbMapper().reverseMap(it.results)
Success(data) as Result
}
.onErrorReturn { Failure(it) }
.startWith(Result.Loading)
}
}
}
```

Мы используем запечатанный класс для передачи данных и наблюдения за ними во ViewModel. И передаём прецедент конструктору для взаимодействия между этими двумя классами.


Для внедрения зависимостей используем hilt, поэтому пакета di нет в Episode («Серии») и Character («Персонаже»), но в util мы определяем «NetworkModule»:


```java
@InstallIn(ApplicationComponent::class)
@Module
class NetworkModule {
@Provides
@Singleton
fun provideCharacterApiService(retrofit: Retrofit): CharacterApi =
retrofit.create(CharacterApi::class.java)

@Provides
@Singleton
fun provideEpisodeApiService(retrofit: Retrofit): EpisodeApi =
retrofit.create(EpisodeApi::class.java)

@Provides
@Singleton
fun provideGsonRetrofit(
httpClient: OkHttpClient.Builder,
convertFactory: GsonConverterFactory
): Retrofit =
Retrofit.Builder()
.baseUrl("https://rickandmortyapi.com")
.client(httpClient.build())
.addConverterFactory(convertFactory)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()

@Provides
@Singleton
fun provideOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient.Builder {
val httpClient = OkHttpClient.Builder()
if (BuildConfig.DEBUG) {
httpClient.addInterceptor(httpLoggingInterceptor)
}
httpClient.retryOnConnectionFailure(true)
return httpClient
}

@Provides
@Singleton
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor=
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)

@Provides
@Singleton
fun provideJacksonConverterFactory(): JacksonConverterFactory =
JacksonConverterFactory.create()

@Provides
@Singleton
fun provideGsonConverterFactory(): GsonConverterFactory =
GsonConverterFactory.create()
}
```

И определяем «AppModule» в utils следующим образом:


```java
@InstallIn(ApplicationComponent::class)
@Module
class DatabaseModule {
@Singleton
@Provides
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"CHARACTERS-DATA.db"
).allowMainThreadQueries()
.build()
}

@Provides
fun provideCharacterDao(database: AppDatabase): CharacterDao {
return database.charactersDao()
}

@Provides
fun provideEpisodeDao(database: AppDatabase): EpisodeDao {
return database.episodesDao()
}

@Provides
fun provideCharacterEpisodeDao(database: AppDatabase): CharacterEpisodeDao {
return database.characterEpisodesDao()
}
```

А в пакете репозитория в utils определяем модуль репозитория:


```java
@InstallIn(ApplicationComponent::class)
@Module
class RepositoryModule {

@Provides
fun provideEpisodeRepository(repo: EpisodeRepositoryImpl): EpisodeRepository = repo

@Provides
fun provideCharacterEpisodeRepository(repo: CharacterEpisodeRepositoryImpl): CharacterEpisodeRepository =
repo
@Provides
fun provideCharactersRepository(repo: CharactersRepositoryImpl): CharactersRepository = repo

}
```

Заключение


Итак, мы узнали, как это здорово  —  разрабатывать свой проект с чистой архитектурой для обеспечения лучшей тестопригодности с возможностью повторного использования. Кроме того, мы теперь можем изменить код, не беспокоясь о том, что станет с остальной частью проекта.


Надеюсь, статья была полезной. Проект с чистой архитектурой загружен на GitHub.


662   0  

Comments

    Ничего не найдено.