NoArchitecture Kotlin Compose

от автора

Начало

Все начинается в setContent. ComposeGenAppTheme необязательна. Surface кстати внутри себя содержит простой Box. Не привычно формировать все элементы без XML. Хотя интеграция во Fragmets как View возможна Using Compose in Views.

        setContent {             ComposeGenAppTheme {                 Surface(                     modifier = Modifier.fillMaxSize(),                     color = MaterialTheme.colorScheme.background                 ) { 

Про TabRow расказывать не буду. Кому надо быстро скопипастит из проекта @Preview. Просмотреть что примерно выйдет можно с помощью метода помеченного этой аннотацией. Придется подготовить набор данных вручную.

Preview

Preview
@Preview @Composable fun PreviewConversation() {     ComposeGenAppTheme {         FalconInfoListView(SampleData.getRockets())     } } 

Чтобы посмотреть как будет выводится список, генерируем данные в список из FalconInfo.

class SampleData {     companion object {         private const val mesageSize = 10;         private val conversationSample: List<FalconInfo> = buildList<FalconInfo>(mesageSize) {             for (i in 0 until mesageSize) {                 add(FalconInfo(name = "Name $i",                     rocket = "Rocket $i",                     details = "Detail can be long, Detail can be long, Detail can be long, Detail can be long, Detail can be long, Detail can be long, "))             }         }          fun getRockets(): List<FalconInfo> {             return conversationSample         }     } } 

Наблюдатели

Remember — запоминает состояние State, который создается при помощи mutableIntStateOf. Затем при изменении selectedTabIndex который используется в качестве ключа LaunchedEffect(key) вызывается анимация. Compose функция живет как бы в воздухе. Она может перезапускать сама себя и чтобы каждый раз не создавался selectedTabIndex используется remember.

                    var selectedTabIndex by remember {                         mutableIntStateOf(0)                     }                       LaunchedEffect(selectedTabIndex) {                         pagerState.animateScrollToPage(selectedTabIndex)                     } 

Получение данных из сети. Вывод списка

Для получения результата FalconInfo делается http запрос. Это код из примера. Чтобы как то распределить ответственность ввел mutableStateOf на прогресс и результат. Что не особо помогло. Потому что все стало бесконтрольно триггерится и появились ошибки.

                    var status by remember { mutableStateOf("Loading") }                     ...                     LaunchedEffect(true) {                         scope.launch {                             status = try {                                 rockets = Greeting().greeting()                                 "Ok"                             } catch (e: Exception) {                                 e.localizedMessage ?: "error"                             }                         }                     }                     GreetingView(text)                      

Http запрос и json парсер Ktor. А как же Retrofit?

  class SpaceX {      private val httpClient = HttpClient {         install(HttpCache)         install(ContentNegotiation) {             json(Json {                 prettyPrint = true                 isLenient = true                 ignoreUnknownKeys = true             })         }     }      @Throws(Exception::class)     suspend fun getRockets(): List<FalconInfo> {         val rockets: Array<FalconInfo> =             httpClient.get("https://api.spacexdata.com/v4/launches").body()         return rockets.asList()     } } 

Формирование списка элементов. Preview с моками использует его же. Магия отображения списков происходит в LazyColumn и надо его копнуть на предмет оптимизации. При первом старте есть притормаживания на старом флагмане. После первого прокручивания все становится плавным. Надо отдать должное авторам. С минимальным количеством кода, без холдеров и пэйлоадеров получается быстрый список. Надо посмотреть как его «прогреть» при старте если это возможно.

@Composable fun FalconInfoListView(falconInfos: List<FalconInfo>) {     LazyColumn {         items(falconInfos) { falconInfos ->             FalconInfoCard(falconInfos)         }     } } 

Дальше карточки. Ниже основной код где отображается список. PreviewMessageCard это предпросмотр одной строки списка. Из примера от Гугла взял реализацию выпадающего текста. Если текст длиннее одной строки. Кстати состояние не сохраняется, если проскролить вверх или вниз, когда элемент становится невидимым. recyclerview под капотом?

AsyncImage — из библиотекиcoil

Карточки
@Composable fun FalconInfoCard(falconInfo: FalconInfo) {     Row(modifier = Modifier.padding(all = 8.dp)) {          AsyncImage(             placeholder = rememberVectorPainter(Icons.Filled.Rocket),             model = falconInfo.links?.patch?.small,             contentDescription = null,             contentScale = ContentScale.FillWidth,             modifier = Modifier                 .size(40.dp)                 .clip(CircleShape)             //.aspectRatio(0.8F)         )          Spacer(modifier = Modifier.width(8.dp))          var isExpanded by remember { mutableStateOf(false) }          val surfaceColor by animateColorAsState(             if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,             label = "surf_color",         )          Column(modifier = Modifier.clickable { isExpanded = !isExpanded })         {             falconInfo.name?.let {                 Text(                     text = it,                     color = MaterialTheme.colorScheme.secondary                 )             }              Spacer(modifier = Modifier.height(4.dp))              Surface(                 shape = MaterialTheme.shapes.medium,                 shadowElevation = 1.dp,                 // surfaceColor color will be changing gradually from primary to surface                 color = surfaceColor,                 // animateContentSize will change the Surface size gradually                 modifier = Modifier                     .animateContentSize()                     .padding(1.dp)             ) {                 falconInfo.details?.let {                     Text(                         text = it,                         maxLines = if (isExpanded) Int.MAX_VALUE else 1,                         style = MaterialTheme.typography.bodyMedium                     )                 }             }         }     } }  @Preview @Composable fun PreviewMessageCard() {     FalconInfoCard(         FalconInfo(             staticFireDateUtc = "234234234",             staticFireDateUnix = 1,             name = "Rocket", rocket = "N1",             details = "Detail can be long, Detail can be long, Detail can be long, "         )     )  }

Карточка

Карточка

Проблемы

Появились проблемы с отображением переменных в Runtime. Пришлось покурить мануалы

The coroutine scope left the composition

https://developer.android.com/jetpack/compose/side-effects

produceState
launches a coroutine scoped to the Composition that can push values into a
returned State. Use it to
convert non-Compose state into Compose state, for example bringing external
subscription-driven state such as Flow, LiveData, or RxJava into the
Composition

Следующая версия с шаблонной оберткой. Теперь уже все приходило предсказуемо. НО при переходе между табами, проскакивает загрузка. в версии с LaunchedEffect из примера она не происходила. initialValue = ResponseResult.Loading — стоит ли кэшировать это состояние в переменной?

    val resLoad =         produceState<ResponseResult<List<FalconInfo>>>(initialValue = ResponseResult.Loading) {             value = try {                 ResponseResult.Success(SpaceX().getRockets())             } catch (e: Exception) {                 ResponseResult.Error(e.localizedMessage ?: "error")             }         } 

Кэширование можно перенести на сторону репозитария. produceState позволяет его передать. install(HttpCache) в пакете io.ktor.client.plugins.cache так же имеется.

return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { 

Обновление

Следующая версия с использованием KtorFit+Room и Clean Architecture описана в статье https://habr.com/ru/articles/765416/

Исходный код в ветке на GitHub https://github.com/app-z/NoArchitecture-Kotlin-Compose/tree/architecture

Заключение

Хотелось бы сделать действительно кросс-платформенное решение. Compose это не позволяет делать в части UI. Может быть пока не позволяет?

Запустить xcode-kotlin не удалось. После установки плагина XCode перестало запускаться. Удалось запустить от рута sudo ./Xcode хотя kdoctor не нашел проблем.

brew install xcode-kotlin

xcode-kotlin install

MacBook MacOS % kdoctor                                                                                                     Environment diagnose (to see all details, use -v option): [✓] Operation System [✓] Java [✓] Android Studio [✓] Xcode [✓] Cocoapods  Conclusion:   ✓ Your system is ready for Kotlin Multiplatform Mobile Development!

Мнение опытных Композиторов строго приветствуется.

Ссылки

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

За Compose будущее

85.19% Да23
14.81% Нет. Поиграют и бросят4

Проголосовали 27 пользователей. Воздержались 11 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/763980/