Как стать автором
Обновить

NoArchitecture Kotlin Compose

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров2.2K

Начало

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

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

Про TabRow расказывать не буду. Кому надо быстро скопипастит из проекта NoArchitecture‑Kotlin‑Compose.git а кто захочет вникнуть, на Youtybe есть видео. На английском весьма понятно. Ссылка внизу статьи.

                        TabRow(selectedTabIndex = selectedTabIndex) {
                            tabItems.forEachIndexed { index, item ->
                                Tab(selected = index == selectedTabIndex,
                                    onClick = {
                                        selectedTabIndex = index
                                    },
                                    text = {
                                        Text(item.title)
                                    },
                                    icon = {
                                        Icon(
                                            imageVector = if (index == selectedTabIndex) {
                                                item.selectedItem
                                            } else {
                                                item.unselectedItem
                                            },
                                            contentDescription = item.title
                                        )
                                    })
                            }
                        }

Предпросмотр

Для preview делается метод с аннотацией @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 пользователей.
Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+4
Комментарии4

Публикации

Истории

Работа

Ближайшие события