В создаваемом с нуля приложении Android с самого начала реализуются передовые практики, чем обеспечиваются долгосрочные масштабируемость, сопровождаемость и производительность. Расскажем обо всем, что при этом важно учитывать, дополнив фрагментами кода и пояснениями.

1. Заблаговременное внедрение зависимостей

Внедрением зависимостей упрощаются создание объектов и управление ими, ослабляется связанность между классами. Соответствующими инструментами вроде Hilt или Koin поддерживаются масштабируемость и тестопригодность приложения.

И вот почему:

  • Обеспечивается принцип единственной ответственности: создание объекта отделяется от его использования, поэтому код становится чище, модульнее.
  • Совершенствуется тестирование: упрощается предоставление имитированных зависимостей во время модульных тестов.
  • Повышается масштабируемость: по мере роста кодовой базы упрощается управление зависимостями.

Пример Hilt:

// Этап 1: в файл «build.gradle» добавляются зависимости Hilt
dependencies {
implementation "com.google.dagger:hilt-android:2.x"
kapt "com.google.dagger:hilt-android-compiler:2.x"
}

// Этап 2: аннотируется класс «Application»
@HiltAndroidApp
class MyApp : Application()

// Этап 3: для зависимостей используются «@Inject» и «@Provides»
// Простой пример «Repository» и «ViewModel»

// Repository
class MyRepository @Inject constructor(
private val apiService: ApiService
) {
fun fetchData(): List<Data> {
// Извлекается логика данных
}
}

// ViewModel
@HiltViewModel
class MyViewModel @Inject constructor(
private val repository: MyRepository
) : ViewModel() {
val data = liveData {
emit(repository.fetchData())
}
}

// Этап 4: в «Module» предоставляются зависимости
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideApiService(): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
.create(ApiService::class.java)
}
}

Пример Koin:

// Этап 1: в файл «build.gradle» добавляются зависимости Koin
dependencies {
implementation "io.insert-koin:koin-android:3.x"
implementation "io.insert-koin:koin-androidx-compose:3.x" // Для Jetpack Compose
}

// Этап 2: для зависимостей создается «Module» Koin
val appModule = module {
single { provideApiService() } // Предоставляется синглтон «ApiService»
single { MyRepository(get()) } // Предоставляется «MyRepository» с внедренным «ApiService»
factory { MyViewModel(get()) } // При необходимости предоставляется новый экземпляр «MyViewModel»
}

fun provideApiService(): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
.create(ApiService::class.java)
}

// Этап 3: в классе «Application» инициализируется Koin
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApp)
modules(appModule) // Загружается «appModule»
}
}
}

// Этап 4: в «Repository» и «ViewModel» используются зависимости
// Repository
class MyRepository(private val apiService: ApiService) {
fun fetchData(): List<Data> {
// Извлекается логика данных
}
}

// ViewModel
class MyViewModel(private val repository: MyRepository) : ViewModel() {
val data = liveData {
emit(repository.fetchData())
}
}

// Этап 5: в «Activity» или «Fragment» внедряется «ViewModel»
class MyActivity : AppCompatActivity() {
private val myViewModel: MyViewModel by viewModel() // Внедряется «ViewModel»

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

myViewModel.data.observe(this) { data ->
// Пользовательский интерфейс обновляется извлеченными данными
}
}
}

2. Структурирование проекта чистой архитектурой

Чистой архитектурой обеспечивается разделение обязанностей посредством организации кодовой базы на отдельные слои: предметную область, данные и представление. Хотя принципы чистой архитектуры универсальны, конкретная структура папок и детали реализации варьируются в зависимости от потребностей и размера проекта, предпочтений команды.

Преимущества чистой архитектуры:

  • Проще в тестировании и сопровождении: каждый слой сфокусирован на одной задаче, поэтому тестирование и рефакторинг упрощаются.
  • Бизнес-логика отделена от фреймворков: основная логика остается независимой, чем обеспечивается гибкость в применении или замене фреймворков.
  • Масштабируемость и модульность: с чистой архитектурой проще добавлять новый функционал, не трогая имеющуюся функциональность.

Гибкая структура папок

Вот привычная структура папок, она приспосабливается под требования проекта:

- domain/              // Основная бизнес-логика, независимая от фреймворков
- model/ // Основные структуры данных, например «User», «Product»
- repository/ // Интерфейсы для операций с данными
- usecase/ // Бизнес-логика, инкапсулированная в виде вариантов применения
- data/ // Обрабатываются источники данных: сеть, база данных и т. д.
- network/ // API-сервисы и удаленные операции с данными
- database/ // Локальное хранилище данных и объекты доступа к ним
- repository/ // Реализации репозиториев слоя предметной области
- presentation/ // Слой пользовательского интерфейса
- view/ // «Composable», «Fragment» или «Activity»
- viewmodel/ // Управление состоянием и логика пользовательского интерфейса

Внимание: в меньших проектах сочетаются слои domain и data или даже пропускается usecase, если бизнес-логика проста. Чистая архитектура не жесткая, адаптируйте ее к своему сценарию.

Пример реализации сценария

Слой предметной области

Бизнес-логика определяется как сценарий:

// Слой предметной области: сценарий
class GetUserUseCase(private val repository: UserRepository) {
suspend operator fun invoke(userId: String): User {
return repository.getUser(userId)
}
}

Слой данных

Реализуется интерфейс репозитория для слоя предметной области:

// Слой данных: реализация репозитория
class UserRepositoryImpl(private val apiService: ApiService) : UserRepository {
override suspend fun getUser(userId: String): User {
return apiService.getUser(userId)
}
}

Слой представления

Обрабатываются пользовательские взаимодействия с ViewModel и обновляется пользовательский интерфейс:

// Слой представления: ViewModel
class UserViewModel(private val getUserUseCase: GetUserUseCase) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user

fun fetchUser(userId: String) {
viewModelScope.launch {
_user.value = getUserUseCase(userId)
}
}
}

Ключевые соображения:

  1. Адаптирование архитектуры к потребностям: не переусердствуйте с инженерией в небольших проектах, используйте принципы гибко.
  2. Поддержание независимости предметной области: обеспечьте независимость слоя domain от внешних фреймворков или библиотек.
  3. Важна масштабируемость: начните с простого, но оставьте возможность масштабирования архитектуры по мере роста проекта.

Чистая архитектура  —  это руководящий принцип, не правило. Адаптируйте ее к сложности проекта и сильным сторонам команды, чтобы найти оптимальный баланс между структурой и гибкостью.

3. Jetpack Compose для разработки современного ПИ

Jetpack Compose  —  это будущее разработки пользовательских интерфейсов Android, создание которых упрощается применением декларативного подхода. Благодаря уходу от XML-макетов, в Compose ускоряется разработка и сокращается шаблонный код при создании реактивных, современных ПИ.

Преимущества Jetpack Compose

Разработка декларативного ПИ

  • Объявляется, как пользовательский интерфейс должен выглядеть для заданного состояния: никакого обязательного описания, как менять ПИ.
  • Когда изменяются базовые данные, ПИ обновляется в Compose автоматически.

Обновления реактивного ПИ

  • Compose легко взаимодействует с API-интерфейсами Kotlin State и Flow, ПИ обновляется в реальном времени без ручных манипуляций.

Усовершенствованный инструментарий

  • В Android Studio имеются инструменты специально для Compose: предпросмотры в реальном времени и интерактивная отладка.
  • С предпросмотром нет необходимости переразвертывать приложение для изменений ПИ.

Меньше шаблонного кода

  • Больше никаких findViewById или XML-макетов. Разработка в Compose упрощается определениями ПИ на основе Kotlin.

Постепенный переход

  • XML-макеты можно оставить и интегрировать Compose в имеющиеся проекты постепенно.

Начало работы с Jetpack Compose

Вот простой пример:

@Composable
fun Greeting(name: String) {
Text(
text = "Hello, $name!",
style = MaterialTheme.typography.h4,
modifier = Modifier.padding(16.dp)
)
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
Greeting(name = "Jetpack Compose")
}

Основной функционал:

  • @Composable: им определяются переиспользуемые компоненты ПИ.
  • MaterialTheme: для стилевого оформления используется Material3.
  • Modifier: в макет добавляются изменения, например внутренний отступ.
  • @Preview: выполняются предпросмотры компонентов ПИ в реальном времени.

Расширенный пример: отображение списка

@Composable
fun UserList(users: List<String>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp)
) {
items(users) { user ->
Text(
text = user,
modifier = Modifier.padding(8.dp),
style = MaterialTheme.typography.body1
)
}
}
}

@Preview(showBackground = true)
@Composable
fun UserListPreview() {
UserList(users = listOf("Alice", "Bob", "Charlie"))
}
  • LazyColumn: отрисовываются только видимые элементы списка.
  • items: на основе данных динамически генерируются элементы списка.

Основные рекомендации по Jetpack Compose

  • Использование Material3
    Это современные, динамичные компоненты ПИ: Button, Card, TextField. Поддерживаются адаптивные темы в соответствии с системным настройкам устройства.
  • Эффективное управление состоянием
    Для обработки изменений состояния воспользуйтесь remember и mutableStateOf.

Пример:

@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
  • Оптимизация производительности
    Чтобы избежать лишних рекомпозиций, переведите область состояния к наименьшему composable, для отладки производительности ПИ воспользуйтесь Compose Layout Inspector.
  • Поэтапный переход
    Добавляйте Compose в имеющиеся проекты постепенно, начиная с отдельных экранов или компонентов.
  • Чтобы избежать лишних рекомпозиций, переведите область состояния к наименьшему composable.

4. Мощь Kotlin

Kotlin  —  первоклассный язык программирования на Android для решения задач современной разработки. Благодаря лаконичному синтаксису, функционалу безопасности и мощным конструкциям в Kotlin упрощается разработка приложений Android, повышаются удобство восприятия и сопровождаемость.

Преимущества Kotlin

Защита от значений null

  • Явной обработкой null в системе типов Kotlin устраняется страшное NullPointerException  —  исключение нулевого указателя.
  • С типами nullable ? и non-nullable код безопаснее.

Меньше шаблонного кода

  • Типичные задачи  —  поиск представлений, определение моделей, написание прослушивателей  —  упрощаются выразительным синтаксисом Kotlin.
  • Пример: классами данных одной строкой сокращается шаблонный код для простых объектов языка Java.
data class User(val id: Int, val name: String)

Конструкции функционального программирования

  • Благодаря функционалу вроде map, filter и reduce в Kotlin упрощаются работа с коллекциями и преобразование данных.

Корутины для асинхронного программирования

  • Корутинами Kotlin  —  в противовес сложности обратных вызовов или RxJava  —  структурированно и эффективно выполняются асинхронные задачи.

Основной функционал Kotlin

1. Функции-расширения

  • Функциональность добавляется к имеющимся классам без изменения их кода.

Пример:

fun String.capitalizeWords(): String {
return split(" ").joinToString(" ") { it.capitalize() }
}

val text = "hello world"
println(text.capitalizeWords()) // Вывод: «Hello World»

2. Корутины для конкурентности

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

Пример:

viewModelScope.launch {
val data = withContext(Dispatchers.IO) { repository.getData() }
_uiState.value = UiState.Success(data)
}

3. Изолированные классы для управления состоянием

  • Изолированными классами представляются ограниченные иерархии вроде состояний или ответов ПИ.

Пример:

sealed class UiState {
object Loading : UiState()
data class Success(val data: List<String>) : UiState()
data class Error(val message: String) : UiState()
}

4. Делегированные свойства

  • Делегирование свойств упрощается встроенными делегатами вроде lazy или пользовательских.

Пример:

val lazyValue: String by lazy {
"This value is computed only once!"
}

Корутины в действии

Корутины  —  новое слово в асинхронном программировании на Kotlin. Вот как они используются в сетевом запросе:

viewModelScope.launch {
try {
_uiState.value = UiState.Loading
val data = withContext(Dispatchers.IO) { repository.getData() }
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Unknown Error")
}
}

Ключевые моменты в примере:

  • viewModelScope.launch: запускается корутина, привязанная к жизненному циклу ViewModel.
  • withContext(Dispatchers.IO): переключается на поток ввода-вывода для фоновой работы.
  • Благодаря управлению состоянием с UiState ПИ остается адаптивным к изменениям.

Основные рекомендации по Kotlin

  • Классы данных
    Используйте их для объектов вроде ответов или моделей, чтобы не писать вручную методы toString(), equals(), hashCode().
  • Используйте функции области видимости apply, let, run
    С ними меньше повторяющегося кода, выше удобство восприятия:
val person = Person().apply {
name = "John"
age = 30
}
  • Неизменяемость
    Создавайте неизменяемые объекты при помощи val, снижая риск нежелательных побочных эффектов.
  • Стандартная библиотека
    Используйте ее лаконичные способы выполнения задач: манипулирования строками, файлового ввода-вывода и т. д.

5. Конвейеры сборки

Непрерывная интеграция и непрерывное развертывание  —  важные практики современной разработки ПО, при которых автоматизируются процессы тестирования, сборки и развертывания. Применением конвейеров сборки упрощается рабочий процесс разработки, обеспечивается качество кода, ускоряются циклы выпуска.

Преимущества непрерывной интеграции и непрерывного развертывания

  1. Заблаговременное выявление проблем

С каждым коммитом кода выполняются автоматизированные тесты, где баги, регрессии или проблемы с интеграцией обнаруживаются до их попадания в продакшен.

2. Ускорение выпуска

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

3. Согласованность сборок

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

Ключевые компоненты конвейера сборки

1. Непрерывная интеграция

  • Каждый раз, когда код отправляется в репозиторий, автоматически активируются сборки и тесты.
  • Проверяется, что код благополучно интегрируется с имеющейся кодовой базой.

2. Непрерывное развертывание

  • Процесс развертывания сборок в промежуточной или производственной средах автоматизируется.
  • В него включаются этапы вроде подписания APK-файлов, загрузки в Firebase и развертывания в Google Play.

3. Артефакты и логи

  • Системами непрерывной интеграции и непрерывного развёртывания сохраняются  —  для будущего использования и отладки  —  артефакты сборки, например APK-файлы, и логи.

Вот простой пример конвейера сборки для приложения Android с GitHub Actions:

name: Android CI

on:
push:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest

steps:
# Этап 1: извлекается код
- name: Checkout code
uses: actions/checkout@v2

# Этап 2: настраивается среда Java
- name: Set up JDK
uses: actions/setup-java@v2
with:
java-version: '17'

# Этап 3: при помощи Gradle собирается приложение
- name: Build with Gradle
run: ./gradlew build

# Этап 4: выполняются тесты
- name: Run Unit Tests
run: ./gradlew test

# Этап 5: дополнительно архивируются артефакты сборки
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: app-release.apk
path: app/build/outputs/apk/release/app-release.apk

Принцип работы этого конвейера

1. Активация при отправке

  • Конвейер активируется при каждой отправке кода в ветку main.

2. Настройка среды

  • Для процесса сборки настраивается необходимая среда Java.

3. Сборка и тестирование

  • Командами Gradle компилируется приложение и выполняются модульные тесты.

4. Управление артефактами

  • Сгенерированный APK-файл сохраняется в виде загружаемого артефакта  —  для распространения или дальнейшего тестирования.

Другие инструменты для непрерывной интеграции и непрерывного развертывания

  1. Azure DevOps

Это комплексная платформа непрерывной интеграции и непрерывного развертывания, на которой при помощи гибких конвейеров собираются, тестируются и развертываются приложения Android.

trigger:
branches:
include:
- main

pool:
vmImage: 'ubuntu-latest'

steps:
- task: UseJava@1
inputs:
version: '17'
- script: ./gradlew build
displayName: 'Build with Gradle'

2. Firebase App Distribution

Бета-тестирование упрощается автоматической загрузкой APK-файлов тестировщикам Firebase. Упростите предваряющее выпуск тестирование, интегрировав этот инструмент в свой конвейер сборки.

3. Jenkins

Это сервер непрерывной интеграции и непрерывного развертывания с открытым исходным кодом для расширенной настройки и обеспечения гибкости. Идеален для сложных или корпоративных настроек.

Рекомендации по непрерывной интеграции и непрерывному развертыванию

  • Написание комплексных тестов
    В хорошем конвейере непрерывной интеграции проблемы заблаговременно выявляются надежными модульными и интеграционными тестами, а также тестами пользовательского интерфейса.
  • Автоматизация развертывания
    После успешных сборок и тестов настраиваются, например API-интерфейсами Play Console, автоматические развертывания в промежуточной или производственной средах.
  • Отслеживание сборок
    Для отслеживания сборки, тестирования производительности и оперативного устранения узких мест используются инструменты мониторинга.
  • От малого к большему
    Начинают с базовой автоматизации сборки и тестирования, добавляя затем статический анализ кода, анализ покрытия кода, а по прошествии времени  —  автоматизацию выпуска.

6. Защита приложения современными методами

Безопасность  —  основа любого приложения Android. Реализацией надежных мер безопасности с самого начала обеспечиваются защита конфиденциальных данных пользователя и устойчивость приложения к атакам. Применением современных методов обеспечения безопасностиизбегают уязвимостей, которые дорого обходятся, и придерживаются отраслевых стандартов, например тех, которые требуются для тестирования на проникновение.

Почему важна безопасность?

  • Защита конфиденциальных данных
    Без надлежащих мер защиты конфиденциальная информация вроде паролей, платежных и персональных данных может быть раскрыта, что чревато потерей репутации и доверия пользователей.
  • Предотвращение атак
    Приложения, которым недостает мер безопасности, уязвимы для атак «человека посередине», реверсинга и утечки данных.
  • Тесты на проникновение
    При оценивании безопасности требуется соблюдение строгих стандартов: шифрованный обмен данными, безопасное хранилище, корректные механизмы проверки подлинности.

Ключевые области безопасности приложения

1. Защита конфиденциальных данных шифрованными средствами хранения
Конфиденциальные данные, такие как токены аутентификации или учетные данные пользователя, надежно сохраняются средствами шифрования:

  • Используйте EncryptedSharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
"secure_prefs",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

sharedPreferences.edit().putString("token", "secure_value").apply()
  • Избегайте жесткого задания секретов
    Никогда не включайте API-ключи, учетные или конфиденциальные данные в исходный код. Пользуйтесь для хранения инструментами вроде Android Keystore или безопасными переменными окружения.

2. Безопасное сетевое взаимодействие

  • Всегда проверяйте серверные соединения и шифруйте сетевой трафик:

Везде используйте HTTPS

  • Обеспечьте применение HTTPS-соединений, указав это в network_security_config.xml приложения:
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">example.com</domain>
</domain-config>

Проверка сертификатов SSL/TLS

  • Во избежание атак «человека посередине» не принимайте самоподписанные сертификаты без проверки.
  • Для обязательного закрепления сертификата используйте доверенную библиотеку, такую как OkHttp.
val client = OkHttpClient.Builder()
.certificatePinner(
CertificatePinner.Builder()
.add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
)
.build()

3. Предотвращение реверсинга

Приложения Android декомпилируются, что чревато раскрытием исходного кода и конфиденциальных данных. Минимизируйте этот риск:

  • Использованием ProGuard или R8: запутайте код, включив ProGuard в build.gradle:
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

Обнаружение рутованных устройств

  • Несанкционированный доступ предотвращается определением, рутовано ли устройство:
val isRooted = RootBeer(context).isRooted
if (isRooted) {
Log.e("Security", "Rooted device detected!")
}

4. Безопасная аутентификация

Во избежание несанкционированного доступа усиливаются механизмы аутентификации приложения:

  • OAuth 2.0 для аутентификации
  • Для API-интерфейсов всегда используйте безопасные протоколы аутентификации. Не внедряйте собственную логику аутентификации.
  • Биометрическая аутентификация
  • Для легкого и безопасного входа используйте Biometric API от Android:
val biometricPrompt = BiometricPrompt(
this,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
// Проверка подлинности пройдена
}
}
)
biometricPrompt.authenticate(promptInfo)

Рекомендации по защите приложения

  • Шифрование всех конфиденциальных данных
    Для баз данных используйте EncryptedSharedPreferences и SQLCipher, для ключей  —  Android Keystore.
  • Следуйте рекомендациям OWASP
    В руководстве по тестированию безопасности мобильных приложений OWASP содержатся исчерпывающие рекомендации для обеспечения безопасности приложений.
  • Отслеживание уязвимостей
    Устраняйте известные уязвимости, регулярно обновляя зависимости инструментами вроде Snyk или Gradle Dependency Check.
  • Регулярное тестирование на проникновение
    Проводите для приложения тесты на проникновение, проверяя SSL/TLS, шифруя данные и следуя рекомендациям безопасности.

Заключение

Запуская кодовую базу Android с нуля, вы закладываете прочную основу для приложения. А с этими рекомендациями создадите масштабируемое, сопровождаемое, высокопроизводительное приложение, готовое к вызовам современной разработки.

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

Читайте нас в Telegram, VK и Дзен


Перевод статьи Dobri Kostadinov: Top 6 Tips for Starting a Modern Android Codebase From Scratch

Предыдущая статьяОпыт работы с Python в течение 2 лет: уроки и рекомендации
Следующая статьяСлоты: сделайте свой Angular API гибким