KA0707 — Архитектура приложений

  • Запись изменена:08.12.2022
  • Post category:Публикации
  • Reading time:22 минут чтения

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

Любой, кто когда-либо играл с кубиками LEGO, пытался построить максимально высокую башню, ставя все кубики друг на друга. Хотя это может сработать в определенных сценариях, ваша башня упадет даже при малейшем дуновении ветерка.

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

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

Хотя термин « архитектура программного обеспечения » является относительно новым в отрасли, разработчики программного обеспечения применяют его фундаментальные принципы с середины 1980-х годов.

Шаблоны проектирования

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

В зависимости от того, как долго вы занимаетесь программированием, вы, возможно, слышали или использовали несколько таких шаблонов, таких как чистая архитектура , модель-представление-представление-модель (MVVM) , модель-представление-контроллер (MVC) и модель -представление-контроллер. Ведущий (MVP) .

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

Если вы имеете опыт работы с iOS и вам в основном удобно работать с MVC, KMP вас примет. Если вы в основном являетесь разработчиком Android и следуете рекомендациям Google по использованию MVVM, вы также будете чувствовать себя как дома.

Нет лучшего или худшего способа сделать это.

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

Модель-представление-контроллер

История шаблона MVC восходит к 1970-м годам. Разработчики обычно используют MVC для создания графических пользовательских интерфейсов в настольных и веб-приложениях.

В мобильном мире Apple сделала MVC мейнстримом, когда представила iPhone SDK в 2008 году. Если вы занимались разработкой для iOS до SwiftUI, вы могли заметить, что одним из базовых компонентов был файл UIViewController. Это говорит само за себя, как много Apple вложила в этот шаблон.

За долгие годы до того, как Google занял свое мнение о шаблонах и архитектурах разработки Android, разработчики использовали Model-View-Presenter или MVP , что является близким отклонением от MVC.

В MVC вы разделяете свой код на три отдельных лагеря:

  • Модель : Центральный компонент узора. Он полностью независим от пользовательского интерфейса и обрабатывает логику и правила приложения.
  • Представление : любое представление информации, такое как списки, сетки и т. д. Этот раздел обычно зависит от платформы и фреймворка. Вы можете использовать UIKit и SwiftUI на iOS и Views или Jetpack Compose на Android.
  • Контроллер : принимает ввод и преобразует его в команды для модели или представления. Он также получает обратную связь от модели и отражает изменения в представлении. Это как-то всезнайка шаблона.

На приведенной ниже диаграмме показаны отношения между различными разделами:

updateuser actionupdatenotifycontrollermodelview

Рис. 7.1 – Схема MVC
![[Снимок экрана 2022-03-17 в 20.16.16.png]]

Model-View-ViewModel

Как следует из названия, MVVM отлично подходит для приложений с представлениями или пользовательскими интерфейсами. Поскольку в этом шаблоне заметна концепция привязок, некоторые люди также называют его Model-View-Binder .

Это намного новее, чем MVC. Джон Госсман, один из инженеров Microsoft, анонсировал MVVM в своем блоге в 2005 году. Microsoft внедрила MVVM в фреймворки .NET и сделала этот шаблон очень популярным.

Google представила компоненты архитектуры на Google I/O 2017. Это был первый раз, когда Google решил порекомендовать шаблон проектирования для разработки приложений для Android. На протяжении многих лет Google также представлял различные инструменты и компоненты, связанные с концепцией MVVM. В настоящее время MVVM является первым выбором для большинства разработчиков Android при разработке приложения.

Компоненты MVVM следующие:

  • Модель : это очень похоже на уровень модели MVC. Он представляет данные и правила приложения.
  • Вид : очень похож на одноименный компонент в MVC. Он представляет модель, получает ввод от пользователя и перенаправляет обработку ввода в ViewModel через связь между View и ViewModel. Люди обычно называют эту ссылку Binding .
  • ViewModel : ViewModel — это в основном состояние данных в модели. Он предоставляет некоторые общедоступные методы и свойства, на которые View подписывается и автоматически получает изменения. Люди называют этот механизм Data Binding или просто Binding .

Android-разработчикам не привыкать использовать LiveData , а в последнее время — Kotlin Flow или StateFlow , в качестве Binder внутри ViewModel.

После того, как Apple представила платформу Combine и SwiftUI в качестве собственного решения для реактивного программирования и декларативного пользовательского интерфейса, разработчики iOS начали все больше и больше использовать шаблон проектирования MVVM и механизм привязки.

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

В 2012 году Роберт С. Мартин, также известный как дядя Боб, опубликовал в своем блоге сообщение, объясняющее детали нового шаблона проектирования, который он придумал на основе гексагональной архитектуры, луковой архитектуры и многих других.

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

DevicesGatewaysExternal interfacesPresentersControllersUse CasesWebDBUIEntities
![[Снимок экрана 2022-03-17 в 20.17.31.png]]
Рис. 7.2 — График чистой архитектуры

Кружки выше представляют разные уровни программного обеспечения в приложении. При работе с этим графиком следует помнить два принципа:

  1. Центральный круг — самый абстрактный, а внешний круг — самый конкретный. Это называется принципом абстракции . Принцип абстракции указывает, что внутренние круги должны содержать бизнес-логику, а внешние круги — детали реализации. Другими словами, чем ближе вы к центру, тем меньше у вас зависимости от конкретной платформы.
  2. Еще один принцип Чистой Архитектуры — Правило Зависимости . Это правило указывает, что каждый круг может зависеть исключительно от ближайшего внутреннего круга — это то, что заставляет архитектуру работать. Это делает код, основанный на чистой архитектуре, довольно несвязанным и, следовательно, тестируемым.

Ниже перечислены основные компоненты чистой архитектуры, которые объясняются от внешних кругов внутрь:

  • Представление и структура : самый внешний уровень обычно содержит структуры и инструменты, специфичные для платформы. Используете SwiftUI для создания интерфейсов? Вот это место. Используете комнату для базы данных? Он также принадлежит здесь. Обычно вы не можете обмениваться кодом на этом уровне между платформами.
  • Контроллеры или презентеры : это уровень, который вы использовали в MVC в качестве контроллера или ViewModel в MVVM. Они получают входные данные от внешнего слоя и передают их следующему слою. Вы можете комбинировать MVVM и MVC с чистой архитектурой. Это также хорошо, так как обязанности ваших контроллеров или ViewModels уменьшатся.
  • Варианты использования или интеракторы : этот слой определяет действия, которые может инициировать пользователь. Объекты на предыдущем уровне имеют доступ к вариантам использования и могут вызывать только определенные взаимодействия. В исходном определении чистой архитектуры это слой, на который вы помещаете свою бизнес-логику. Поскольку вы можете добавлять свои слои, вы также можете делегировать эту ответственность внутренним слоям.
  • Сущности : Абстрактные определения всех источников данных. Он может содержать некоторую бизнес-логику.

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

Делимся бизнес-логикой

KMP сияет, когда вы пытаетесь свести к минимуму дублированный код, который вы пишете. В предыдущей главе вы дважды написали логику для страницы « Об устройстве ». Этот код легко может быть внутри общего модуля, и все платформы смогут воспользоваться им.

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

Откройте стартовый проект в Android Studio. В основном это финальный проект предыдущей главы.

Внутри каталога презентации папки commonMain в общем модуле создайте новый файл и назовите его BaseViewModel.kt .

Заполните файл этой строкой:

expect abstract class BaseViewModel()

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

Поместите курсор в середину имени класса и нажмите Alt+Enter . Android Studio поможет вам создать все необходимые файлы. Повторяйте процесс до тех пор, пока красная линия под именем класса, показывающая ошибку отсутствия файлов, не исчезнет.

Рис. 7.3 — Alt+Enter при ожидаемом имени класса

Рис. 7.3 — Alt+Enter при ожидаемом имени класса

Примечание . Если Android Studio не удалось автоматически создать для вас необходимые фактические файлы, не беспокойтесь. Создайте файл в том же пакете с тем же именем в папке отсутствующей платформы.

Откройте версию BaseViewModel.kt для Android и замените содержимое этой строкой:

actual abstract class BaseViewModel : ViewModel()

Не забудьте импортировать нужный пакет:

import androidx.lifecycle.ViewModel

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

Если это сбивает с толку разработчиков iOS, вот небольшое пояснение:

В Android, когда происходит изменение конфигурации — например, устройство поворачивается или пользователь меняет общесистемную тему или языковой стандарт — система воссоздает все компоненты представления. Однако система сохранит в памяти тот же экземпляр ViewModel, который расширяется из пакета Lifecycle AndroidX. Следовательно, вы можете хранить данные представления внутри ViewModel и применять их к вновь созданным компонентам представления, и пользователь ничего не заметит.

Для iOS и рабочего стола вам не нужно ничего расширять. То, что Android Studio сделала для реальных файлов на этих платформах, более чем достаточно. Они выглядят так:

actual abstract class BaseViewModel actual constructor()

Создание AboutViewModel

Теперь, когда у вас есть базовая модель представления, пришло время создать конкретные версии. Начните с создания файла с именем AboutViewModel.kt в папке commonMain внутри каталога презентации .

Определите класс и подкласс из созданной ранее модели BaseViewModel .

class AboutViewModel: BaseViewModel() { }

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

private val platform = Platform()

Определите класс данных внутри AboutViewModelкласса для хранения данных, которые вы показываете в каждой строке страницы «О программе»:

data class RowItem( val title: String, val subtitle: String, )

Затем создайте функцию, которая генерирует элементы для страницы «О программе». Вы написали одну и ту же логику три раза — по одному разу для каждой платформы — в предыдущей главе. Вы удалите их все позже.

private fun makeRowItems(platform: Platform): List<RowItem> { val rowItems = mutableListOf( RowItem("Operating System", "${platform.osName} ${platform.osVersion}"), RowItem("Device", platform.deviceModel), RowItem("CPU", platform.cpuType), ) platform.screen?.let { rowItems.add( RowItem( "Display", "${ max(it.width, it.height) }×${ min(it.width, it.height) } @${it.density}x" ), ) } return rowItems }

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

val items: List<RowItem> = makeRowItems(platform)

Это будет общедоступный API ViewModel.

Использование AboutViewModel в слое View

Андроид

Откройте AboutView.kt внутри модуля androidApp .

Удалите makeItems()метод, так как аналогичная реализация теперь существует внутри AboutViewModel .

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

@Composable fun AboutView( viewModel: AboutViewModel = AboutViewModel(), onUpButtonClick: () -> Unit )

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

После этого обновите ContentViewметод следующим образом:

@Composable private fun ContentView(items: List<AboutViewModel.RowItem>) { LazyColumn( modifier = Modifier.fillMaxSize(), ) { items(items) { row -> RowView(title = row.title, subtitle = row.subtitle) } } }

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

Теперь, когда ContentViewметоду нужен параметр, вернитесь к реализации AboutViewдля передачи элементов. Замените ContentView()на это:

ContentView(items = viewModel.items)

Создайте и запустите приложение для Android. Он работает так же, как и раньше, но на этот раз использует модель представления.

Рис. 7.4. Страница «Об устройстве» в Organize на Android, созданная с использованием ViewModel.

Рис. 7.4. Страница «Об устройстве» в Organize на Android, созданная с использованием ViewModel.

iOS

Откройте проект Xcode и переключитесь на AboutView.swift .

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

import shared

Затем внутри AboutViewструктуры добавьте свойство для модели представления:

@StateObject private var viewModel = AboutViewModel()

Вы аннотируете свойство @StateObjectдирективой, чтобы SwiftUI создавал и хранил экземпляр на AboutViewModelпротяжении всего времени существования AboutView.

Вы получите сообщение об ошибке компилятора, говорящее вам, что это AboutViewModelдолжно соответствовать ObservableObjectпротоколу, чтобы вы могли аннотировать его с помощью @StateObject. Не волнуйтесь — это довольно легко исправить. Добавьте соответствие этому протоколу, добавив этот блок в конец файла:

extension AboutViewModel: ObservableObject { }

Примечание . В Swift вы можете соответствовать протоколам где угодно. В отличие от Kotlin, вам не нужно делать это при определении типа.

Затем откройте AboutListView.swift . Удалите RowItemструктуру, а также itemsсвойство. Затем добавьте свойство для хранения элементов, отображаемых в этом представлении, следующим образом:

let items: [AboutViewModel.RowItem]

Не забудьте импортировать sharedмодуль.

Внутри AboutListView_Previewsструктуры измените AboutListView()вызов на следующее:

AboutListView(items: [AboutViewModel.RowItem(title: "Title", subtitle: "Subtitle")])

В приведенном выше коде вы используете жестко заданный элемент строки для исправления предварительного просмотра пользовательского интерфейса внутри Xcode.

Вернитесь к AboutView.swift и передайте нужный параметр AboutListView:

AboutListView(items: viewModel.items)

Соберите и запустите, чтобы увидеть результат рефакторинга, который вы только что сделали.

Рис. 7.5. Страница «Об устройстве» в Organize на iOS, созданная с использованием ViewModel.

Рис. 7.5. Страница «Об устройстве» в Organize на iOS, созданная с использованием ViewModel.

Рабочий стол

Теперь вы знакомы с процессом. Поскольку вы создали настольное приложение с помощью Jetpack Compose, даже имена функций, которые вам нужно изменить, такие же или очень похожие на версию для Android. Удалите ненужную функцию для генерации данных и замените ContentViewметод в AboutView.kt в модуле desktopApp .

Результат будет выглядеть так:

@Composable fun AboutView(viewModel: AboutViewModel = AboutViewModel()) { ContentView(items = viewModel.items) } @Composable private fun ContentView(items: List<AboutViewModel.RowItem>) { LazyColumn( modifier = Modifier.fillMaxSize(), ) { items(items) { row -> RowView(title = row.title, subtitle = row.subtitle) } } }

Создайте и запустите настольное приложение и посмотрите на изменения… или их отсутствие!

Рис. 7.6. Страница «Об устройстве» приложения «Организовать на рабочем столе», созданного с использованием ViewModel.

Рис. 7.6. Страница «Об устройстве» приложения «Организовать на рабочем столе», созданного с использованием ViewModel.

Раздел «Создание напоминаний»

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

Шаблон репозитория

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

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

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

Создайте новый каталог как одноуровневый для презентации глубоко внутри папки commonMain общего модуля и назовите его data . Затем создайте новый файл с именем RemindersRepository.kt .

Во-первых, добавьте свойство для хранения Remindersобъектов внутри:

private val _reminders: MutableList<Reminder> = mutableListOf()

Вы получите ошибку компилятора о том, что Reminderтип не разрешен. Reminderбудет моделью данных для нашего приложения. Чтобы все было более организованно, вы собираетесь создать Reminderкласс внутри каталога с именем domain , который является еще одним братом представления и данных . Если вы обратите пристальное внимание, то заметите, что здесь есть некоторые намеки на чистую архитектуру. Но не беспокойтесь — вы будете использовать только некоторые соглашения об именах и не будете копать глубже.

Примечание . Если Android Studio не добавляет объявление пакета автоматически, добавьте его вручную в качестве первой строки в RemindersRepository.kt : package com.raywenderlich.organize.data.

Создайте файл Reminder.kt и добавьте этот блок кода:

data class Reminder( val id: String, val title: String, val isCompleted: Boolean = false, )

Каждое напоминание будет иметь идентификатор, заголовок и значение того, завершено оно или нет.

Далее в RemindersRepository, добавьте эту функцию для создания нового напоминания:

fun createReminder(title: String) { val newReminder = Reminder( id = UUID().toString(), title = title, isCompleted = false ) _reminders.add(newReminder) }

UUID— это класс, используемый для создания случайных идентификаторов с механизмом ожидания/действия. Это уже есть в стартовом проекте. Взгляните на его реализацию, если вам интересно.

Затем добавьте функцию для обновления isCompletedстатуса напоминания:

fun markReminder(id: String, isCompleted: Boolean) { val index = _reminders.indexOfFirst { it.id == id } if (index != -1) { _reminders[index] = _reminders[index].copy(isCompleted = isCompleted) } }

Сначала он проверяет, существует ли элемент с idатрибутом. Если ответ да, он обновляет isCompletedзначение.

В конце создайте общедоступное свойство получения для всех напоминаний. Позже вы измените это на Kotlin Flow , чтобы иметь возможность распространять живые изменения в модели представления и представлении. Поскольку использовать Flows на iOS немного сложно, пока вы будете придерживаться простых свойств.

val reminders: List<Reminder> get() = _reminders

Вы создали красивый API для репозитория. Молодец!

Создание RemindersViewModel

Внутри каталога презентации модуля commonMain создайте новый файл и назовите его RemindersViewModel.kt . Обновите его следующим образом:

class RemindersViewModel : BaseViewModel() { //1 private val repository = RemindersRepository() //2 private val reminders: List<Reminder> get() = repository.reminders //3 var onRemindersUpdated: ((List<Reminder>) -> Unit)? = null set(value) { field = value onRemindersUpdated?.invoke(reminders) } //4 fun createReminder(title: String) { val trimmed = title.trim() if (trimmed.isNotEmpty()) { repository.createReminder(title = trimmed) onRemindersUpdated?.invoke(reminders) } } //5 fun markReminder(id: String, isCompleted: Boolean) { repository.markReminder(id = id, isCompleted = isCompleted) onRemindersUpdated?.invoke(reminders) } }

Вот что включает в себя этот класс:

  1. Свойство для хранения строгой ссылки на репозиторий.
  2. Свойство, которое обращается к напоминаниям из репозитория.
  3. Представления могут подключаться к этому свойству, чтобы узнавать об изменениях в напоминаниях. На данный момент это ссылка или компонент привязки MVVM. Вы обязательно вызываете лямбду с текущим состоянием remindersв ее блоке установки.
  4. Способ создания напоминания после защиты от напоминаний с пустыми заголовками. Когда viewModelрепозиторий просит создать новое напоминание, он распространяет изменения через файлы onRemindersUpdated.
  5. Метод изменения isCompletedсвойства конкретного напоминания.

Обновление представления на Android

Откройте RemindersView.kt внутри модуля androidApp . В начале добавьте параметр со значением по умолчанию для функции RemindersViewModelto RemindersView. Затем перейдите viewModelв ContentViewметод.

@Composable fun RemindersView( viewModel: RemindersViewModel = RemindersViewModel(), onAboutButtonClick: () -> Unit, ) { Column { Toolbar(onAboutButtonClick = onAboutButtonClick) ContentView(viewModel = viewModel) } }

Вы дадите ContentViewфункции массивное обновление. Во-первых, удалите существующий код в функции. Затем измените сигнатуру функции, чтобы она принимала параметр типа RemindersViewModel.

@Composable private fun ContentView(viewModel: RemindersViewModel) { }

Добавьте переменную для запоминания состояния напоминаний. Jetpack Compose перерисовывает или перекомпоновывает функцию всякий раз, когда это состояние изменяется.

var reminders by remember { mutableStateOf(listOf<Reminder>(), policy = neverEqualPolicy()) }

В приведенном выше коде byиспользуется синтаксис делегата. При этом методы get()и делегируются методу. Добавьте следующие импорты, чтобы устранить предупреждение IDE:set()``remember

import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue

Чтобы связать базовые изменения напоминаний viewModelс этой функцией, добавьте эту строку после remindersинициализации:

viewModel.onRemindersUpdated = { reminders = it }

Всякий раз, когда модель представления вызывает onRemindersUpdatedлямбду, вы устанавливаете новое значение списка напоминаний в remindersпеременную состояния. Это заставляет компонент реагировать на изменения. Установка policy, которую вы установили на предыдущем шаге, гарантирует, что рекомпозиция будет происходить всегда, независимо от статуса равенства новых и старых значений.

Затем создайте LazyColumnдля отображения напоминаний в виде списка:

LazyColumn(modifier = Modifier.fillMaxSize()) { //1 items(items = reminders) { item -> //2 val onItemClick = { viewModel.markReminder(id = item.id, isCompleted = !item.isCompleted) } //3 ReminderItem( title = item.title, isCompleted = item.isCompleted, modifier = Modifier .fillMaxWidth() .clickable(enabled = true, onClick = onItemClick) .padding(horizontal = 16.dp, vertical = 4.dp) ) } }

  1. Используя itemsкомпонуемую функцию, вы предоставляете ей remindersпеременную состояния LazyColumn. LazyColumn — это эффективная версия List, которая отображает только подмножество элементов, которые могут отображаться на экране.
  2. Сохраните лямбду, которая вызывается viewModelдля обновления isCompletedстатуса конкретного напоминания.
  3. Используйте уже предоставленную ReminderItemфункцию для каждой строки списка. Вы можете ознакомиться с его реализацией.

После itemsблока вы добавляете элемент item, который будет содержать текстовое поле для добавления новых напоминаний.

item { //1 val onSubmit = { viewModel.createReminder(title = textFieldValue) textFieldValue = "" } //2 NewReminderTextField( value = textFieldValue, onValueChange = { textFieldValue = it }, onSubmit = onSubmit, modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp, horizontal = 16.dp) ) }

Индивидуальный NewReminderTextFieldнаходится внутри стартового проекта. Вы привязываете valueи onValueChangeк textFieldValueпеременной состояния.

Когда пользователь нажимает клавишу « Возврат» или « Готово » на клавиатуре своего телефона, система вызывает onSubmitлямбду, которая создает новое напоминание и очищает текстовое поле.

Наконец, добавьте textFieldValueпеременную состояния в начало функции следующим образом:

var textFieldValue by remember { mutableStateOf("") }

Создайте и запустите приложение. Добавьте пару напоминаний и отметьте некоторые из них как выполненные.

Рис. 7.7 — Первая страница «Напоминания» на Android

Рис. 7.7 — Первая страница «Напоминания» на Android

Обновление представления на iOS

Чтобы реактивный характер привязки данных работал, SwiftUI в значительной степени зависит от структуры Combine . Возможно, вы использовали @Stateдля аннотирования типов данных значения. Если вы подключаетесь к данным внешней эталонной модели с помощью @ObservableObjectили @StateObject, что вы сделали в AboutViewModel, SwiftUI может затем использовать опубликованные свойства для автоматического обновления представлений.

Однако в использовании RemindersViewModel есть одна загвоздка . Поскольку вы определили модель представления внутри общего модуля KMP , вы не смогли использовать Combine там, так как он предназначен только для Swift.

Один из способов решить эту проблему — создать оболочку вокруг модели представления и предоставить опубликованное свойство для использования SwiftUI.

Откройте iosApp.xcodeproj и создайте новый файл Swift, нажав Command-N . Назовите его RemindersViewModelWrapper.swift и поместите в каталог Reminders .

Добавьте в файл следующий код:

//1 import Combine import shared //2 final class RemindersViewModelWrapper: ObservableObject { //3 let viewModel = RemindersViewModel() //4 @Published private(set) var reminders: [Reminder] = [] init() { //5 viewModel.onRemindersUpdated = { [weak self] items in self?.reminders = items } } }

  1. Вы должны импортировать Combine, а также sharedframework.
  2. Сделайте обертку соответствующей ObservableObject. Вы сделали то же самое для AboutViewModel.
  3. Здесь вы держите сильную ссылку на реальную модель представления.
  4. Вы предоставляете @Publishedсвойство из этого класса. SwiftUI будет повторно отображать тело представления при изменении этого свойства. @Publishedобертки свойств являются частью структуры Combine.
  5. При инициализации вы подписываетесь на onRemindersUpdatedзакрытие viewModelи соответствующим образом обновляете опубликованное свойство. Используя [weak self], вы прерываете потенциальный цикл памяти.

Откройте RemindersView.swift и замените содержимое структуры следующим кодом. Он довольно длинный, но выглядит и ведет себя очень похоже на то, что вы делали с Jetpack Compose:

struct RemindersView: View { //1 @StateObject private var viewModelWrapper = RemindersViewModelWrapper() //2 @State private var textFieldValue = "" var body: some View { //3 List { //4 if !viewModelWrapper.reminders.isEmpty { Section { ForEach(viewModelWrapper.reminders, id: \.id) { item in //5 ReminderItem(title: item.title, isCompleted: item.isCompleted) .onTapGesture { //6 withAnimation { viewModelWrapper.viewModel.markReminder( id: item.id, isCompleted: !item.isCompleted ) } } } } } //7 Section { NewReminderTextField(text: $textFieldValue) { withAnimation { viewModelWrapper.viewModel.createReminder(title: textFieldValue) textFieldValue = "" } } } } .navigationTitle("Reminders") } }

  1. Используя @StateObjectаннотацию, вы создаете экземпляр оболочки, созданной на предыдущем шаге.
  2. Используя @Stateаннотацию, вы создаете свойство для хранения текстового значения текстового поля. Вы даже использовали то же имя переменной в Jetpack Compose.
  3. Listв SwiftUI это эквивалент LazyColumnJetpack Compose.
  4. Если remindersсвойство содержит значения, вы создаете раздел с элементами напоминания.
  5. Для каждой строки списка в первом разделе вы используете экземпляр ReminderItemвнутри начального проекта.
  6. Когда пользователь нажимает на каждую строку, вы вызываете, viewModelчтобы пометить напоминание как выполненное или наоборот. Функция withAnimatonделает переход плавным.
  7. Этот раздел предназначен для создания текстового поля для добавления новых элементов. Вы привязываете его к textFieldValueсвойству.

Создавайте и запускайте, и посмотрите на страницу напоминаний во всей красе!

Рис. 7.8 — Первая страница «Напоминания» на iOS

Рис. 7.8 — Первая страница «Напоминания» на iOS

Обновление вида на рабочем столе

Поскольку настольное приложение использует Jetpack Compose, вы можете буквально скопировать и вставить код из RemindersView.kt в модуль androidApp в тот же файл в модуле desktopApp .

Примечание . В коде могут появиться ошибки, говорящие о том, что Android Studio не может получить доступ к androidx.lifecycle.ViewModelфайлам com.raywenderlich.organize.presentation.RemindersViewModel. Вы можете спокойно игнорировать эту ошибку, так как приложение будет успешно создано и запущено. Это похоже на ошибку в плагине KMM на Android Studio.

После этого создайте и запустите настольное приложение.

Рис. 7.9 — Первая страница «Напоминания» на iOS

Рис. 7.9 — Первая страница «Напоминания» на iOS

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

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

Совместное использование тестов и пользовательского интерфейса

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

В следующей главе вы добавите в проект тесты. Поскольку вся бизнес-логика теперь находится в одном месте, вы собираетесь написать единый набор тестов. Следовательно, вы будете писать меньше тестовых кодов — а значит, вы втайне радуетесь!

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

Следует отметить, что совместное использование кода пользовательского интерфейса не всегда может быть хорошим решением по нескольким причинам:

  • Возможно, вы заметили, что настольное приложение выглядит немного странно эстетически. Он соответствует рекомендациям по материальному дизайну, что не является типичным подходом для настольных компьютеров. Он также не похож на другие родные приложения для Windows или macOS. Многие предпочли бы придерживаться собственного набора инструментов пользовательского интерфейса для каждой платформы вместо использования Jetpack Compose, который использует Java Swing под капотом. Если уж на то пошло, многие разработчики не стали бы создавать свои настольные приложения, используя подход, описанный в этой книге. Если вы этого не сделаете, у вас не будет Jetpack Compose для рабочего стола и, следовательно, не будет кода для совместного использования между Android и рабочим столом.
  • Каждая платформа имеет свои отличия. Для настольного приложения обычно требуется другой дизайн, чем для приложения для Android. Например, ему не нужны большие сенсорные мишени, у него нет мультитач, он в основном использует мышь и клавиатуру вместо сенсорного ввода и т. д. Если вы хотите создать отличное приложение для каждой платформы, вам нужно принять это во внимание.

Все эти объяснения применимы и к тестированию пользовательского интерфейса. Если вы каким-то образом делитесь кодом пользовательского интерфейса, у вас могут быть общие тесты пользовательского интерфейса. Если вы этого не сделаете, вам нужно будет создавать тесты пользовательского интерфейса для каждой платформы отдельно.

Испытание

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

Задача: перенос заголовков страниц в модели просмотра

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

Ключевые моменты

  • Вы можете использовать любой шаблон проектирования, который вы считаете подходящим, с Kotlin Multiplatform.
  • Вы познакомились с основными концепциями MVC, MVVM и Clean Architecture.
  • Совместное использование моделей данных, моделей представления и репозиториев между платформами с помощью Kotlin Multiplatform очень просто.
  • Вы можете делиться тестами бизнес-логики, используя Kotlin Multiplatform.
  • Хотя возможно, это не всегда лучшее решение для совместного использования пользовательского интерфейса между платформами.