Чистая архитектура с 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.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Golnaz Torabi: Clean Architecture with MVVM

Предыдущая статьяДирижируйте горутинами с помощью каналов
Следующая статьяОдномерный клеточный автомат в JavaScript