Основы Kotlin Multiplatform. Бесплатный учебник на русском языке (KA0700)
Основы Kotlin Multiplatform. Бесплатный учебник на русском языке (KA0700)

KA0703 — Разработка пользовательского интерфейса: Android Jetpack Compose

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

В предыдущей главе вы узнали о системе сборки KMP. В этой главе вы узнаете о новом инструментарии пользовательского интерфейса, который можно использовать на Android. Этот инструментарий пользовательского интерфейса — Jetpack Compose . Это не будет подробным обсуждением Jetpack Compose, но оно научит вас основам. Откройте начальный проект из этой главы, потому что в нем есть некоторый начальный код.

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

KMP не предоставляет фреймворк для разработки пользовательского интерфейса, поэтому вам нужно будет использовать разные фреймворки для каждой платформы. В этой главе вы узнаете о написании пользовательского интерфейса для Android с помощью Jetpack Compose, который также работает на рабочем столе. В следующей главе вы узнаете о создании пользовательского интерфейса для iOS с помощью SwiftUI, который также работает на macOS.

Текущая система пользовательского интерфейса

На Android вы обычно используете систему компоновки XML для создания своих пользовательских интерфейсов. Хотя Android Studio предоставляет редактор макетов пользовательского интерфейса, он по-прежнему использует XML под ним. Это означает, что Android придется анализировать XML-файлы для создания своих классов представления, а затем создавать пользовательский интерфейс. Что, если бы вы могли просто создать свой пользовательский интерфейс в коде?

Составьте реактивный ранец

Это идея, лежащая в основе Jetpack Compose (JC). JC — это декларативная система пользовательского интерфейса, которая использует функции для создания всего или части вашего пользовательского интерфейса. Разработчики из Google поняли, что система Android View устаревает и имеет много недостатков. Итак, они решили создать совершенно новый фреймворк, который будет использовать библиотеку вместо встроенного фреймворка, что позволит разработчикам приложений продолжать предоставлять самую последнюю версию фреймворка независимо от версии Android.

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

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

Знакомство с Jetpack Compose

Единственный компонент Android, который все еще необходим в Jetpack Compose, — это Activityкласс. Должна быть отправная точка, и обычно Activityона является основной точкой входа. Одна из приятных особенностей JC заключается в том, что вам не нужно больше одного Activity(вы можете иметь больше, если хотите). Кроме того — и это еще более важно — вам больше не нужно использовать фрагменты. Если вы знакомы с упражнениями, вы знаете, что начальный метод таковonCreate. Вам больше не нужно вызыватьsetContentView, потому что вы не будете использовать файлы XML. Вместо этого вы используете setContent.

setContent ( сетКонтент )

Чтобы начать преобразование вашего приложения для использования Compose, откройте MainActivity . Удалите строку, содержащую setContentViewи добавьте следующее:

setContent {
  Text("Test")
}

Вам нужно будет импортировать:

import androidx.activity.compose.setContent
import androidx.compose.material.Text

Запустите приложение, и вы увидите небольшой “Тест” в верхнем левом углу.

Рис. 3.1 - Начальный экран в Jetpack Compose
Рис. 3.1 — Начальный экран в Jetpack Compose

Если вы посмотрите на источник setContent, вы увидите, что это метод расширения on ComponentActivity. Последним параметром в этом методе является ваш пользовательский интерфейс. Этот метод имеет тип @Composable, который представляет собой специальную аннотацию, которую вам нужно будет использовать во всех ваших функциях создания. Функция создания будет выглядеть примерно так:

@Composable
fun showName(text: String) {
  Text(text)
}

Самая важная часть — это @Composableаннотация. Это говорит JC, что это функция, которую можно отобразить на экране. Ни Composableодна функция не возвращает значение. Важно отметить, что вы хотите, чтобы большинство ваших функций были без состояния. Это означает, что вы передаете данные, которые хотите отобразить, и функция не сохраняет эти данные. Это делает функцию очень быстрой для рисования. См. Раздел «Что делать дальше» в конце этой главы, чтобы узнать больше о том, как работает Compose.

Искатель времени

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

Рис. 3.2 - Список выбранных часовых поясов
Рис. 3.2 — Список выбранных часовых поясов

Здесь вы видите местный часовой пояс, время и дату. Под ним находятся два разных часовых пояса: Нью-Йорк и Лондон. Ваш пользователь пытается найти время встречи во всех трех местах.

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

Когда пользователь захочет добавить часовой пояс, он нажмет плавающую кнопку действия (FAB), и появится диалоговое окно, позволяющее ему выбрать все нужные часовые пояса:

Рис. 3.3 - Диалоговое окно для поиска часовых поясов
Рис. 3.3 — Диалоговое окно для поиска часовых поясов

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

Рис. 3.4 - Выбор диапазонов времени начала и окончания собрания
Рис. 3.4 — Выбор диапазонов времени начала и окончания собрания

Нажатие кнопки поиска открывает диалоговое окно результатов:

Рис. 3.5 - Список возможного времени для встречи
Рис. 3.5 — Список возможного времени для встречи

Примечание: Хотя в этой главе подробно рассказывается о Jetpack Compose, она не предназначена для тщательного изучения того, как им пользоваться. Для более глубокого понимания Jetpack Compose ознакомьтесь с книгами по адресу

Темы

Одна из первых функций создания, о которой вам нужно узнать, — это тема. Это цветовая схема, которую вы будете использовать для своего приложения. В Android у вас обычно есть style.xml или theme.xml файл со спецификациями для цветов, шрифтов и других областей оформления пользовательского интерфейса. В Compose вы используете функцию темы. Поскольку вы включили библиотеку Material Compose, вы можете использовать MaterialThemeкласс в качестве отправной точки для настройки цветов, шрифтов и фигур. Compose также может сообщить вам, использует ли система темную тему. Начните с создания нового пакета в приложении AndroidApp модуль на том же уровне, что и MainActivity, и назовите его темой.

Рис. 3.6 - Создание нового пакета в модуле AndroidApp
Рис. 3.6 — Создание нового пакета в модуле AndroidApp

Затем создайте новый файл в этом пакете с именем Colors.kt. Добавьте следующее:

import androidx.compose.ui.graphics.Color

val primaryColor = Color(0xFF1e88e5)
val primaryLightColor = Color(0xFF6ab7ff)
val primaryDarkColor = Color(0xFF005cb2)
val secondaryColor = Color(0xFF26a69a)
val secondaryLightColor = Color(0xFF64d8cb)
val secondaryDarkColor = Color(0xFF00766c)
val primaryTextColor = Color(0xFF000000)
val secondaryTextColor = Color(0xFF000000)
val lightGrey = Color(0xFFA2B4B5)

Это определяет некоторые основные и дополнительные цвета. Вы можете видеть цвета на левом поле. Измените их, если вам нужна другая цветовая схема. Затем щелкните правой кнопкой мыши на каталоге темы и создайте новый файл Kotlin с именем Typography.kt. Добавьте следующее:

import androidx.compose.material.Typography
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

// 1
val typography = Typography(
    // 2
    h1 = TextStyle(
        // 3
        fontFamily = FontFamily.SansSerif,
        // 4
        fontSize = 24.sp,
        // 5
        fontWeight = FontWeight.Bold,
        // 6
        color = Color.White
    ),

    h2 = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontSize = 20.sp,
        color = Color.White
    ),

    h3 = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontSize = 12.sp,
        color = Color.White
    ),

    h4 = TextStyle(
        fontFamily = FontFamily.SansSerif,
        fontSize = 10.sp,
        color = Color.White
    )
)

В приведенном выше коде вы:

  1. Создайте переменную с именемtypography, являющимся экземпляром Typographyкласса Compose.
  2. Переопределение предопределенного h1типа.
  3. Определите семейство шрифтов для использования. Ты будешь использовать семью Сансериф.
  4. Установите размер шрифта.
  5. Установите вес шрифта.
  6. Установите цвет шрифта.

Вы также можете установить интервал между буквами и многие другие значения, определенные в TextStyle. Здесь вы определяете стили h1-h4. Существуют и другие стили, такие как тело, кнопки, подписи и субтитры.

Затем создайте новый файл в этом пакете с именем AppTheme.kt. Создайте темную и светлую палитры, добавив следующий код:

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color

private val DarkColorPalette = darkColors(
    primary = primaryDarkColor,
    primaryVariant = primaryLightColor,
    secondary = secondaryDarkColor,
    secondaryVariant = secondaryLightColor,
    onPrimary = Color.White,
    background = lightGrey,
    onSurface = lightGrey
)

private val LightColorPalette = lightColors(
    primary = primaryColor,
    primaryVariant = primaryLightColor,
    secondary = secondaryColor,
    secondaryVariant = secondaryLightColor,
    onPrimary = Color.Black,
    background = Color.White
)

Создайте новую функцию с именемAppTheme:


@Composable
fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
  // TODO: Add Colors
}

Эта функция примет необязательный параметр для установки темной темы. Если ничего не передано, он проверит, каковы системные настройки. Последний параметр — это отображаемая составная функция. Далее, возьмите палитру. // TODO: Add ColorsЗамените на следующий:

val colors = if (darkTheme) {
  DarkColorPalette
} else {
  LightColorPalette
}
// TODO: Add Theme

Это позволяет задать для переменной colors светлые или темные цвета. Эти две функции DarkColorPaletteи LightColorPaletteвозвращают определенный Colorsкласс, который вы можете скопировать и изменить несколько цветов. Изучите Colorsкласс, чтобы узнать, какие цвета вы можете изменить. Затем // TODO: Add Themeзамените его следующим:

MaterialTheme(
    colors = colors,
    typography = typography,
    content = content
)

Это применяется MaterialThemeк вашим цветам и типографике и передается в заданном контенте.

Типы

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

import androidx.compose.runtime.Composable

// 1
typealias OnAddType =  (List<String>) -> Unit
// 2
typealias onDismissType =  () -> Unit
// 3
typealias composeFun =  @Composable () -> Unit
// 4
typealias topBarFun =  @Composable (Int) -> Unit

// 5
@Composable
fun EmptyComposable() {
}

  1. Определите псевдоним с именемOnAddType, который принимает список строк и ничего не возвращает.
  2. Определите псевдоним, используемый при закрытии диалогового окна.
  3. Определите составную функцию.
  4. Определите функцию, которая принимает целое число.
  5. Определите пустую составную функцию (в качестве переменной по умолчанию для верхней панели).

Теперь, когда у вас настроены цвета и стили текста, пришло время создать свой первый экран.

Главный экран

В модуле AndroidApp в папке пользовательского интерфейса создайте новый файл Kotlin с именем MainView.kt. Вы начнете с создания нескольких вспомогательных классов и переменных. Сначала добавьте импорт, который вам понадобится (это сэкономит некоторое время на импорте).:

import androidx.compose.foundation.layout.padding
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Place
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.raywenderlich.findtime.android.theme.AppTheme

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

Чтобы отслеживать ваши два экрана, создайте новый закрытый класс с именем Screen:

sealed class Screen(val title: String) {
  object TimeZonesScreen : Screen("Timezones")
  object FindTimeScreen : Screen("Find Time")
}

Это просто определяет два экрана: TimeZonesScreenи FindTimeScreen, вместе с их названиями. Затем определите класс для обработки нижнего элемента навигации:

data class BottomNavigationItem(
  val route: String,
  val icon: ImageVector,
  val iconContentDescription: String
)

Это определяет маршрут, значок для этого маршрута и описание содержимого. Затем создайте переменную с двумя элементами:

val bottomNavigationItems = listOf(
  BottomNavigationItem(
    Screen.TimeZonesScreen.title,
    Icons.Filled.Language,
    "Timezones"
  ),
  BottomNavigationItem(
    Screen.FindTimeScreen.title,
    Icons.Filled.Place,
    "Find Time"
  )
)

При этом используются значки материалов и заголовки из класса screen. Теперь создайте MainViewсоставной:

// 1
@Composable
// 2
fun MainView(actionBarFun: topBarFun = { EmptyComposable() }) {
  // 3
  val showAddDialog = remember { mutableStateOf(false) }
  // 4
  val currentTimezoneStrings = remember { SnapshotStateList<String>() }
  // 5
  val selectedIndex = remember { mutableStateOf(0)}
  // 6
  AppTheme {
    // TODO: Add Scaffold
  }
}

  1. Определите эту функцию как составную.
  2. Эта функция принимает функцию, которая может предоставить верхнюю панель (панель инструментов на Android), и по умолчанию имеет значение пустой составной элемент.
  3. Удерживайте это состояние для отображения диалогового окна добавления.
  4. Удерживайте состояние, содержащее список строк текущего часового пояса.
  5. Используйте compose rememberи mutableStateOfфункции, чтобы запомнить состояние текущего выбранного индекса.
  6. Используйте тему, определенную ранее.

Государство

Состояние — это любое значение, которое может меняться со временем. Compose использует несколько функций для обработки состояния. Самый важный из rememberних . Это сохраняет переменную, чтобы она запоминалась между перерисовками экрана. Когда пользователь выбирает между двумя нижними кнопками, вы хотите сохранить, какой экран отображается. A MutableState— это держатель значения, который сообщает механизму компоновки перерисовывать при каждом изменении состояния.

Вот некоторые ключевые функции:

  1. remember: Запоминает переменную и сохраняет ее значение между перерисовками.
  2. mutableStateOf: Создает MutableStateэкземпляр, состояние которого отслеживается Compose .
  3. SnapshotStateList: Создает MutableListобъект, состояние которого отслеживается с помощью Compose.
  4. collectAsState: Собирает значения из сопрограммы Kotlin StateFlowи отслеживается Compose.

Эшафот

Compose использует функцию с именемscaffold, которая использует структуру макета Material Design с панелью приложений (панелью инструментов) и дополнительной плавающей кнопкой действия. С помощью этой функции ваш экран будет размещен должным образом. Начните с замены // TODO: Add Scaffoldна:

Scaffold(
  topBar = {
    // TODO: Add Toolbar
  },
  floatingActionButton = {
    // TODO: Add Floating action button
  },
  bottomBar = {
    // TODO: Add bottom bar
  }
  ) {
    // TODO: Replace with Dialog
    // TODO: Replace with screens
  }

Как вы можете видеть, есть места для добавления составных функций внутри параметров topBarFloatingActionButton и bottomBar.

Панель верхнего приложения

Верхняя панель приложений — это функция Compose для панели инструментов. Поскольку каждая платформа обрабатывает панель инструментов по—разному — macOS отображает пункты меню на системной панели инструментов, тогда как Windows использует отдельную панель инструментов — этот раздел является необязательным. Если платформа передает функцию, которая создает ее, она будет использовать это. Заменить // TODO: Add Toolbarна:

actionBarFun(selectedIndex.value)

Это вызывает переданную функцию с текущим выбранным индексом нижней панели, значение которого сохраняется в переменной selectedIndexсостояния. Поскольку actionBarFunпо умолчанию устанавливается значение пустой функции, ничего не произойдет, если не будет передана функция. Вы сделаете это позже для приложения для Android. Теперь добавьте код, чтобы показать плавающую кнопку действия, если вы находитесь на первом экране, но не на втором экране. Заменить // TODO: Add Floating action buttonна:

if (selectedIndex.value == 0) {
  // 1
  FloatingActionButton(
    // 2
    modifier = Modifier
      .padding(16.dp),
    // 3
    onClick = {
      showAddDialog.value = true
    }
  ) {
    // 4
    Icon(
      imageVector = Icons.Default.Add,
      contentDescription = null
    )
   }
}
  1. Для первой страницы создайте FloatingActionButton.
  2. Используйте Modifierфункцию Compose для добавления отступов.
  3. Установите прослушиватель щелчков. Установите переменную для отображения диалогового окна добавления. Изменение этого значения приведет к перерисовке экрана.
  4. Используйте Addзначок для фабрики.

Навигация по дну

У Compose есть BottomNavigationфункция, которая создает нижнюю панель со значками. Под ним Rowнаходится класс создания, который вы заполняете своим контентом. Заменить // TODO: Add bottom barна:

// 1
  BottomNavigation(
      backgroundColor = MaterialTheme.colors.primary
  ) {
  // 2
  bottomNavigationItems.forEachIndexed { i, bottomNavigationItem ->
    // 3                                        
    BottomNavigationItem(
        selectedContentColor = Color.White,
        unselectedContentColor = Color.Black,
        label = {
            Text(bottomNavigationitem.route, style = MaterialTheme.typography.h4)
        },
      // 4
      icon = {
        Icon(
          bottomNavigationItem.icon,
          contentDescription = bottomNavigationItem.iconContentDescription
        )
      },
      // 5
      selected = selectedIndex.value == i,
      // 6
      onClick = {
          selectedIndex.value = i
      }
    )
  }
}
  1. Создайте BottomNavigationкомпозитный объект.
  2. Используется forEachIndexedдля перехода по каждому элементу в вашем списке элементов навигации.
  3. Создайте новый BottomNavigationItem.
  4. Установите в поле значок значение значка в вашем списке.
  5. Выбран ли этот экран? Только в том случае, если selectedIndexзначение является текущим индексом.
  6. Установите прослушиватель щелчков. Измените selectedIndexзначение, и экран будет перерисован заново.

Затем вернитесь в MainActivity.kt и добавьте следующий импорт:

import androidx.compose.material.TopAppBar
import androidx.compose.ui.res.stringResource
import com.raywenderlich.findtime.android.ui.MainView
import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier

Затем замените setContentна:

// 1
Napier.base(DebugAntilog())
setContent {
    // 2
    MainView {
        // 3
        TopAppBar(title = {
            // 4
            when (it) {
                0 -> Text(text = stringResource(R.string.world_clocks))
                else -> Text(text = stringResource(R.string.findmeeting))
            }
        })
    }
}
  1. Инициализируйте библиотеку протоколирования Napier. (Обязательно укажите необходимые импортные данные.)
  2. Установите для вашего основного содержимого формат MainViewcomposable.
  3. Для Android вам нужна верхняя панель приложений.
  4. Когда появится первый экран, пусть заголовок будет «Мировые часы«. В противном случае, покажите Найти собрание.

Создайте и запустите приложение на устройстве или эмуляторе. Вот что вы увидите:

Рис. 3.7 - Базовая структура экрана мировых часов
Рис. 3.7 — Базовая структура экрана мировых часов

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

Карта местного времени

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

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

Рис. 3.8 - Карточка для отображения местного часового пояса
Рис. 3.8 — Карточка для отображения местного часового пояса

В папке пользовательского интерфейса создайте новый файл Kotlin с именем LocalTimeCard.kt. Добавьте следующий код:

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.raywenderlich.findtime.android.theme.primaryColor
import com.raywenderlich.findtime.android.theme.primaryDarkColor
import com.raywenderlich.findtime.android.theme.typography

@Composable
// 1
fun LocalTimeCard(city: String, time: String, date: String) {
    // 2
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(140.dp)
            .background(Color.White)
            .padding(8.dp)
    ) {
        // 3
        Card(
            shape = RoundedCornerShape(8.dp),
            border = BorderStroke(1.dp, Color.Black),
            elevation = 4.dp,
            modifier = Modifier
                .fillMaxWidth()
        )
        {
          // TODO: Add body
        }
    }
}
  1. Создайте функцию с именемLocalTimeCard, которая принимает citytimeи datestring .
  2. Используйте Boxфункцию, которая заполняет текущую ширину и имеет высоту 140 dp и белый фон. Boxэто контейнер, который рисует элементы друг над другом.
  3. Используйте Cardс закругленными углами и рамкой. Он также заполняет ширину.

Для корпуса замените // TODO: Add bodyна:

// 1
Box(
    modifier = Modifier
        .background(
            brush = Brush.horizontalGradient(
                colors = listOf(
                    primaryColor,
                    primaryDarkColor,
                )
            )
        )
        .padding(8.dp)
) {
    // 2
    Row(
        modifier = Modifier
            .fillMaxWidth()
    ) {
        // 3
        Column(
            horizontalAlignment = Alignment.Start

        ) {
            // 4
            Spacer(modifier = Modifier.weight(1.0f))
            Text(
                "Your Location", style = typography.h4
            )
            Spacer(Modifier.height(8.dp))
            // 5
            Text(
                city, style = typography.h2
            )
            Spacer(Modifier.height(8.dp))
        }
        // 6
        Spacer(modifier = Modifier.weight(1.0f))
        // 7
        Column(
            horizontalAlignment = Alignment.End
        ) {
            Spacer(modifier = Modifier.weight(1.0f))
            // 8
            Text(
                time, style = typography.h1
            )
            Spacer(Modifier.height(8.dp))
            // 9
            Text(
                date, style = typography.h3
            )
            Spacer(Modifier.height(8.dp))
        }
    }
}
  1. Используйте прямоугольник для отображения градиентного фона.
  2. Создайте строку, которая заполнит всю ширину.
  3. Создайте столбец для левой части карточки.
  4. Используйте разделитель с модификатором веса, чтобы переместить текст в самый низ.
  5. Отобразите текст города с заданной типографикой.
  6. Переверните правую колонку.
  7. Создайте правильный столбец.
  8. Покажите время.
  9. Покажите дату.

Экран часового пояса

Теперь, когда у вас готовы карты, пришло время собрать их все вместе на одном экране. В каталоге пользовательского интерфейса создайте новый файл с именем TimeZoneScreen.kt. Добавьте импорт и константу:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.raywenderlich.findtime.TimeZoneHelper
import com.raywenderlich.findtime.TimeZoneHelperImpl
import kotlinx.coroutines.delay

const val timeMillis = 1000 * 60L // 1 second

Затем создайте составной:

@Composable
fun TimeZoneScreen(
    currentTimezoneStrings: SnapshotStateList<String>
) {
    // 1
    val timezoneHelper: TimeZoneHelper = TimeZoneHelperImpl()
    // 2
    val listState = rememberLazyListState()
    // 3
    Column(
        modifier = Modifier
            .fillMaxSize()
    ) {
       // TODO: Add Content
    }
}

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

  1. Создайте экземпляр вашего класса TimeZoneHelper.
  2. Запомните состояние списка, которое будет определено позже.
  3. Создайте вертикальный столбец, занимающий всю ширину и высоту.

Заменить // TODO: Add Contentна:

// 1
var time by remember { mutableStateOf(timezoneHelper.currentTime()) }
// 2
LaunchedEffect(Unit) {
    while (true) {
        time = timezoneHelper.currentTime()
        delay(timeMillis) // Every minute
    }
}
// 3
LocalTimeCard(
    city = timezoneHelper.currentTimeZone(),
    time = time, date = timezoneHelper.getDate(timezoneHelper.currentTimeZone())
)
Spacer(modifier = Modifier.size(16.dp))

// TODO: Add Timezone items
  1. Помните о текущем времени.
  2. Используйте Compose LaunchedEffect. Он будет запущен один раз, но продолжит выполняться. Метод будет получать обновленное время каждую минуту. Вы передаете Unitв качестве параметраLaunchedEffect, чтобы он не отменялся и не запускался повторно при LaunchedEffectповторной компоновке.
  3. Используйте LocalTimeCardфункцию, которую вы создали ранее. Используйте TimeZoneHelperметоды, чтобы получить текущий часовой пояс и текущую дату.

Вернитесь в MainView// TODO: Replace with screensЗаменить следующим:

when (selectedIndex.value) {
  0 -> TimeZoneScreen(currentTimezoneStrings)
  // 1 -> FindMeetingScreen(currentTimezoneStrings)
}

Если индекс равен 0, отобразите экран часового пояса, в противном случае отобразите экран поиска собрания. Экран «Найти собрание» закомментирован до тех пор, пока вы не введете его.

Создайте и запустите приложение. Это будет выглядеть так:

Рис. 3.9 - Экран мировых часов с указанием местного часового пояса
Рис. 3.9 — Экран мировых часов с указанием местного часового пояса

Отлично сделано! Ваше приложение действительно начинает обретать форму.

Временная карта

Временная карточка будет выглядеть следующим образом:

Рис. 3.10 - Карточка для отображения часового пояса
Рис. 3.10 — Карточка для отображения часового пояса

В папке пользовательского интерфейса создайте новый файл Kotlin с именем TimeCard.kt. Добавить:

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
// 1
fun TimeCard(timezone: String, hours: Double, time: String, date: String) {
    // 2
    Box(
        modifier = Modifier
            .fillMaxSize()
            .height(120.dp)
            .background(Color.White)
            .padding(8.dp)
    ) {
        // 3
        Card(
            shape = RoundedCornerShape(8.dp),
            border = BorderStroke(1.dp, Color.Gray),
            elevation = 4.dp,
            modifier = Modifier
                .fillMaxWidth()
        )
        {
          // TODO: Add Content
        }
    }
}
  1. Эта функция принимает часовой пояс, часы, время и дату.
  2. Используйте рамку, чтобы занять всю ширину и придать ей белый фон.
  3. Создайте красивую открытку.

Теперь, когда у вас есть карточка, добавьте несколько строк и столбцов. Заменить // TODO: Add Contentна:

// 1
Box(
    modifier = Modifier
        .background(
            color = Color.White
        )
        .padding(16.dp)
) {
    // 2
    Row(
        modifier = Modifier
            .fillMaxWidth()
    ) {
        // 3
        Column(
            horizontalAlignment = Alignment.Start

        ) {
            // 4
            Text(
                timezone, style = TextStyle(
                    color = Color.Black,
                    fontWeight = FontWeight.Bold,
                    fontSize = 20.sp
                )
            )
            Spacer(modifier = Modifier.weight(1.0f))
            // 5
            Row {
                // 6
                Text(
                    hours.toString(), style = TextStyle(
                        color = Color.Black,
                        fontWeight = FontWeight.Bold,
                        fontSize = 14.sp
                    )
                )
                // 7
                Text(
                    " hours from local", style = TextStyle(
                        color = Color.Black,
                        fontSize = 14.sp
                    )
                )
            }
        }
        Spacer(modifier = Modifier.weight(1.0f))
        // 8
        Column(
            horizontalAlignment = Alignment.End
        ) {
            // 9
            Text(
                time, style = TextStyle(
                    color = Color.Black,
                    fontWeight = FontWeight.Bold,
                    fontSize = 24.sp
                )
            )
            Spacer(modifier = Modifier.weight(1.0f))
            // 10
            Text(
                date, style = TextStyle(
                    color = Color.Black,
                    fontSize = 12.sp
                )
            )
        }
    }
}
  1. Используйте рамку, чтобы сделать фон белым.
  2. Создайте строку, которая заполняет ширину.
  3. Создайте столбец с левой стороны.
  4. Показать часовой пояс.
  5. Создайте строку под предыдущей.
  6. Часы выделены жирным шрифтом.
  7. Покажите текст “часы от местного”.
  8. Создайте столбец с правой стороны.
  9. Покажите время.
  10. Покажите дату.

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

Вы можете добавить код для использования новой временной карты. Следующий код проведет по списку строк текущего часового пояса, обернет элемент в AnimatedSwipeDismissсимвол, чтобы позволить пользователю провести пальцем и удалить карточку, а затем использовать новую временную карточку. Вернитесь к экрану часового пояса и замените // TODO: Add Timezone itemsна:

// 1
LazyColumn(
    state = listState,
) {
    // 2
    items(currentTimezoneStrings,
        // 3
        key = { timezone ->
            timezone
        }) { timezoneString ->
        // 4
        AnimatedSwipeDismiss(
            item = timezoneString,
            // 5
            background = { _ ->
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .height(50.dp)
                        .background(Color.Red)
                        .padding(
                            start = 20.dp,
                            end = 20.dp
                        )
                ) {
                    val alpha = 1f
                    Icon(
                        Icons.Filled.Delete,
                        contentDescription = "Delete",
                        modifier = Modifier
                            .align(Alignment.CenterEnd),
                        tint = Color.White.copy(alpha = alpha)
                    )
                }
            },
            content = {
                // 6
                TimeCard(
                    timezone = timezoneString,
                    hours = timezoneHelper.hoursFromTimeZone(timezoneString),
                    time = timezoneHelper.getTime(timezoneString),
                    date = timezoneHelper.getDate(timezoneString)
                )
            },
            // 7
            onDismiss = { zone ->
                if (currentTimezoneStrings.contains(zone)) {
                    currentTimezoneStrings.remove(zone)
                }
            }
        )
    }
}
  1. Используйте LazyColumnфункцию Compose, которая похожа на RecyclerView в Android или UITableView в iOS.
  2. Используйте LazyColumnitemsметод ’s для просмотра списка часовых поясов.
  3. Используйте keyполе, чтобы задать уникальный ключ для каждой строки. Это важно, если вам нужно удалить элементы.
  4. Используйте включенный AnimatedSwipeDismissкласс для обработки удаления строки.
  5. Установите фон, который будет отображаться при пролистывании.
  6. Установите содержимое, которое будет отображаться поверх фона.
  7. Когда строка будет удалена, удалите строку часового пояса из своего списка.

Вернитесь в MainView. Теперь вы хотите показать диалоговое окно добавления часового пояса, когда showAddDialogлогическое значение равно true. Когда это значение равно true, передайте лямбды для добавления и закрытия диалогового окна. Заменить // TODO: Replace with Dialogна:

// 1
if (showAddDialog.value) {
  AddTimeZoneDialog(
    // 2
    onAdd = { newTimezones ->
      showAddDialog.value = false
      for (zone in newTimezones) {
        // 3
        if (!currentTimezoneStrings.contains(zone)) {
          currentTimezoneStrings.add(zone)
        }
      }
    },
    onDismiss = {
      // 4
      showAddDialog.value = false
    },
  )
}
  1. Если ваша переменная для отображения диалогового окна имеет значение true, вызовите AddTimeZoneDialogcomposable .
  2. Ваш onAddлямбда-код получит список новых часовых поясов.
  3. Если ваш текущий список еще не содержит часовой пояс, добавьте его в свой список.
  4. Установите для переменной show значение false.

Создайте и запустите приложение снова. Нажмите на ЗАВОД. Вы увидите следующее диалоговое окно:

Рис. 3.11 - Поиск по часовому поясу работает
Рис. 3.11 — Поиск по часовому поясу работает

Найдите часовой пояс и выберите его. Нажмите кнопку очистить, найдите другой часовой пояс и выберите его. Наконец, нажмите кнопку добавить. Если бы вы выбрали Лос-Анджелес и Нью-Йорк, вы бы увидели что-то вроде:

Рис. 3.12 - Список выбранных часовых поясов
Рис. 3.12 — Список выбранных часовых поясов

Экран Поиска времени встречи

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

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

Рис. 3.13 - Компонент пользовательского интерфейса для выбора времени
Рис. 3.13 — Компонент пользовательского интерфейса для выбора времени

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

Номер временной карточки

В папке пользовательского интерфейса создайте новый файл с именем NumberTimeCard.kt. При этом будет отображена карточка с меткой и средством выбора номера. Добавить:

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

// 1
@Composable
fun NumberTimeCard(label: String, hour: MutableState<Int>) {
    // 2
    Card(
        shape = RoundedCornerShape(8.dp),
        border = BorderStroke(1.dp, Color.White),
        elevation = 4.dp,
    ) {
        // 3
        Row(
            modifier = Modifier
                .padding(16.dp)
        ) {
            // 4
            Text(
                modifier = Modifier
                    .align(Alignment.CenterVertically),
                text = label,
                style = MaterialTheme.typography.body1
            )
            Spacer(modifier = Modifier.size(16.dp))
            // 5
            NumberPicker(hour = hour, range = IntRange(0, 23),
                onStateChanged = {
                    hour.value = it
                })
        }
    }
}
  1. Создайте композитный файл, который займет метку и час.
  2. Заверните его в открытку.
  3. Используйте ряд, чтобы разложить предметы горизонтально.
  4. Расположите этикетку по центру.
  5. Используется NumberPickerдля отображения времени с помощью стрелок вверх/вниз.

Это создаст карточку с текстовым полем слева и средством выбора номера справа.

Создание экрана «Найти время встречи»

Теперь вы можете создать экран Поиска времени встречи. В папке пользовательского интерфейса создайте новый файл с именем FindMeetingScreen.kt. Добавить:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Checkbox
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.raywenderlich.findtime.TimeZoneHelper
import com.raywenderlich.findtime.TimeZoneHelperImpl

// 1
@Composable
fun FindMeetingScreen(
    timezoneStrings: List<String>
) {
    val listState = rememberLazyListState()
    // 2
    // 8am
    val startTime = remember {
        mutableStateOf(8)
    }
    // 5pm
    val endTime = remember {
        mutableStateOf(17)
    }
    // 3 
    val selectedTimeZones = remember {
        val selected = SnapshotStateMap<Int, Boolean>()
        for (i in 0..timezoneStrings.size-1) selected[i] = true
        selected
    }
    // 4
    val timezoneHelper: TimeZoneHelper = TimeZoneHelperImpl()
    val showMeetingDialog = remember { mutableStateOf(false) }
    val meetingHours = remember { SnapshotStateList<Int>() }

    // 5
    if (showMeetingDialog.value) {
        MeetingDialog(
            hours = meetingHours,
            onDismiss = {
                showMeetingDialog.value = false
            }
        )
    }
    // TODO: Add Content
}

// TODO: Add getSelectedTimeZones
  1. Создайте составной файл, который принимает список строк часовых поясов.
  2. Создайте несколько переменных для хранения начальных и конечных часов. По умолчанию в 8 утра и 5 вечера.
  3. Запомните выбранные часовые пояса.
  4. Создайте свой помощник по часовому поясу и запомните некоторые переменные.
  5. Если логическое значение для этого равно true, покажите MeetingDialogрезультаты.

Здесь вы настроили все свои переменные и вставили небольшой фрагмент кода, чтобы показать диалоговое окно добавления собрания, когда переменная имеет значение true. Теперь замените // TODO: Add getSelectedTimeZonesна:

fun getSelectedTimeZones(
    timezoneStrings: List<String>,
    selectedStates: Map<Int, Boolean>
): List<String> {
    val selectedTimezones = mutableListOf<String>()
    selectedStates.keys.map {
        val timezone = timezoneStrings[it]
        if (isSelected(selectedStates, it) && !selectedTimezones.contains(timezone)) {
            selectedTimezones.add(timezone)
        }
    }
    return selectedTimezones
}

Это вспомогательная функция, которая вернет список выбранных часовых поясов на основе выбранной карты состояния. Теперь добавьте содержимое. Заменить // TODO: Add Contentна:

// 1
Column(
modifier = Modifier
    .fillMaxSize()
) {
  Spacer(modifier = Modifier.size(16.dp))
  // 2
  Text(
      modifier = Modifier
          .fillMaxWidth()
          .wrapContentWidth(Alignment.CenterHorizontally),
      text = "Time Range",
      style = MaterialTheme.typography.h6
  )
  Spacer(modifier = Modifier.size(16.dp))
  // 3
  Row(
      modifier = Modifier
          .fillMaxWidth()
          .padding(start = 4.dp, end = 4.dp)
          .wrapContentWidth(Alignment.CenterHorizontally),

  ) {
      // 4
      Spacer(modifier = Modifier.size(16.dp))
      NumberTimeCard("Start", startTime)
      Spacer(modifier = Modifier.size(32.dp))
      NumberTimeCard("End", endTime)
  }
  Spacer(modifier = Modifier.size(16.dp))
  // 5
  Row(
      modifier = Modifier
          .fillMaxWidth()
          .padding(start = 4.dp, end = 4.dp)

  ) {
      Text(
          modifier = Modifier
              .fillMaxWidth()
              .wrapContentWidth(Alignment.CenterHorizontally),
          text = "Time Zones",
          style = MaterialTheme.typography.h6
      )
  }
  Spacer(modifier = Modifier.size(16.dp))
  // TODO: Add LazyColumn
}
  1. Создайте столбец, который занимает всю ширину.
  2. Добавьте заголовок временного диапазона.
  3. Добавьте строку, центрированную по горизонтали.
  4. Добавьте два NumberTimeCards с их метками и часами.
  5. Добавьте строку, которая занимает всю ширину и имеет заголовок “Часовые пояса”.

Это создаст столбец с текстовым полем, средством выбора времени начала и окончания и другим текстовым полем. Затем замените // TODO: Add LazyColumnна:

// 1
LazyColumn(
    modifier = Modifier
        .weight(0.6F)
        .fillMaxWidth(),
    contentPadding = PaddingValues(16.dp),
    state = listState,
    ) {
    // 2
    itemsIndexed(timezoneStrings) { i, timezone ->
        Surface(
            modifier = Modifier
                .padding(8.dp)
                .fillMaxWidth(),

            ) {
            Row(
                modifier = Modifier
                    .fillMaxWidth(),
            ) {
                // 3
                Checkbox(checked = isSelected(selectedTimeZones, i),
                    onCheckedChange = {
                        selectedTimeZones[i] = it
                    })
                Text(timezone, modifier = Modifier.align(Alignment.CenterVertically))
            }
        }
    }
}
Spacer(Modifier.weight(0.1f))
Row(
    modifier = Modifier
        .fillMaxWidth()
        .weight(0.2F)
        .wrapContentWidth(Alignment.CenterHorizontally)
        .padding(start = 4.dp, end = 4.dp)

) {
    // 4
    OutlinedButton(onClick = {
        meetingHours.clear()
        meetingHours.addAll(
            timezoneHelper.search(
                startTime.value,
                endTime.value,
                getSelectedTimeZones(timezoneStrings, selectedTimeZones)
            )
        )
        showMeetingDialog.value = true
    }) {
        Text("Search")
    }
}
Spacer(Modifier.size(16.dp))
  1. Добавьте a LazyColumnдля списка выбранных часовых поясов. Придайте ему вес и наполнитель.
  2. Для каждого выбранного часового пояса создайте поверхность и строку.
  3. Создайте флажок, который устанавливает выбранную карту при нажатии.
  4. Создайте кнопку для запуска процесса поиска и отображения диалогового окна собрания.

Помните, что LazyColumnэто используется для списков. Функции itemsили используются itemsIndexedдля отображения элемента в списке. Каждая строка будет иметь флажок и текст с названием часового пояса. Внизу будет кнопка, которая запустит процесс поиска, получит все часы собрания, а затем покажет диалоговое окно собрания.

Вернитесь в MainView и раскомментируйте FindMeetingScreenвызов. Создайте и запустите приложение. Переключайтесь между мировыми часами и представлениями «Найти время встречи«. Добавьте несколько часовых поясов и нажмите кнопку поиска. Если часы не отображаются, попробуйте увеличить время окончания.

Вау, это была большая работа, но теперь у вас есть рабочее приложение для поиска встреч в Android с помощью Jetpack Compose!

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

  • В Android вы можете создавать свой пользовательский интерфейс как в традиционных XML-макетах, так и в новой Jetpack Compose framework.
  • Jetpack Compose состоит из составных функций.
  • Разбейте свой пользовательский интерфейс на более мелкие составные элементы.
  • Вы можете создать тему для своего приложения, которая включает цвета и типографику.
  • Jetpack Compose использует такие концепции, как ScaffoldTopAppBar и BottomNavigation, для упрощения создания экранов.

Куда идти дальше?

Чтобы узнать больше о Jetpack Compose, ознакомьтесь с этими ресурсами:

Поздравляю! Вы написали приложение Jetpack Compose, которое использует общую библиотеку для бизнес-логики. В следующей главе будет показано, как создать приложение для iOS.