Я начинаю изучать Jetpack Compose. Я собрал это приложение, где я исследую различные повседневные варианты использования, каждый из функциональных модулей в рамках этого проекта должен решать разные сценарии.
Один из этих функциональных модулей — функциональный модуль chatexample
, пытается реализовать простую ViewPager
, где каждая из страниц представляет собой Fragment
, а первая страница «Сообщения» должна отображать разбитый на страницы RecyclerView
, обернутый вокруг SwipeRefreshLayout
. Теперь цель — реализовать все это с помощью Jetpack Compose. Это проблема, которая у меня сейчас:
PullRefreshIndicator, который я использую для реализации действия Pull-To-Refresh, работает, как и ожидалось, и пока все кажется довольно простым, но я не могу понять, почему ProgresBar остается сверху.
До сих пор я пытался; Продолжая Modifier
от родителя Scaffold
до конца. Убедившись, что я явно установил размеры, соответствующие максимальной высоте и ширине. Добавьте пустой Box
в оператор , когда - но пока ничего не сработало, я предполагаю, что могу просто удалить PullRefreshIndicator
, если увижу, что ViewModel не должен обновляться, но я не думаю, что это правильный поступок.
Чтобы быстро объяснить Composables, которые я использую здесь, у меня есть:
<Surface>
<Scaffold> // Set with a topBar
<Column>
<ScrollableTabRow>
<Tab/> // Set for the first "Messages" tab
<Tab/> // Set for the second "Dashboard" tab
</ScrollableTabRow>
<HorizontalPager>
// ChatExampleScreen
<Box> // A Box set with the pullRefresh modifier
// Depending on the ChatExamleViewModel we might pull different composables here
</PullRefreshIndicator>
</Box>
// Another ChatExampleScreen for the second tab
</HorizontalPager>
</Column>
<Scaffold>
</Surface>
Честно говоря, я не понимаю, как PullRefreshIndicator
, который находится в совершенно другом Composable (ChatExampleScreen
), может перекрываться с ScrollableTabRow
, который снаружи.
Надеюсь, это немного облегчит восприятие пользовательского интерфейса. Любой совет, совет или рекомендация приветствуются. Спасибо! 🙇
Редактировать: Чтобы быть полностью ясным, я пытаюсь добиться того, чтобы на каждой странице был PullRefreshIndicator. Что-то вроде этого:
На каждой странице вы опускаете вниз, видите, как появляется ProgressBar, и когда это делается, он исчезает на той же странице. Не пересекается с вкладками выше.
добавьте также свой код, чтобы мы его изменили и протестировали
@SyedIbrahim - Там есть ссылка на репозиторий GH. Позвольте мне поделиться им здесь: github.com/4gus71n/TheOneApp
@ 4gus71n, просто уточнение, вы уверены («уверен» может быть неправильным словом) или вы решили в своей архитектуре? что вашим ViewModel
делятся Tabs
?
@z.y Эй, нет, моя идея заключалась в том, чтобы ViewPager с TabLayout и каждая страница была полностью независимым экраном. Я повторно использовал ChatExampleScreen
, потому что мне было лень создавать еще один Composable, ха-ха.
хммм, я думала, что все уже позади, хотя и не ожидала, что будет больнее ^_^, jk! , дайте мне попробовать, но поскольку мы уже установили хорошую отправную точку, вы даже можете решить ее самостоятельно!
@z.y — Не заморачивайтесь, ха-ха — единственное, что я пытаюсь понять, — это как заставить PullRefreshIndicator работать должным образом на каждой странице. Использование одних и тех же или разных ViewModel — это одно и то же. Дайте мне знать, если снимок экрана, который я там разместил, не имеет смысла.
@ 4gus71n ага! это имеет смысл, позвольте мне попытаться сжать немного больше до последней капли, ^_^, худший сценарий, последнее, что я могу утверждать, это иметь общую ViewModel, и просто иметь эти разные вкладки, наблюдающие определенное состояние, используя структуру в мой ответ и вернуть флаг/перечисление в область PullRefresh, какая вкладка в данный момент выбрана/активна
О, да! прежде чем я попытаюсь стать сухим, вы были бы удовлетворены, если бы мы могли сохранить структуру, как в моем ответе, и сообщить PullRefresh
прицелу, который Tab
на самом деле выполняет притяжение? поскольку вы сказали: «Единственное, что я пытаюсь понять, это как заставить PullRefreshIndicator работать должным образом на каждой странице». Я думаю о том, чтобы иметь перечисление, которое будет представлять каждую вкладку, и предоставить его для обновления обновления и просто сделать оператор when, чтобы вызвать соответствующую функцию viewModel.
@z.y Да, полностью. О, так вы говорите, что из-за того, что эти две ChatExampleScreen
страницы используют одну и ту же ViewModel
, поэтому PullRefreshIndicator не исчезает и не отображается должным образом? Мм! Не знал об этом!
Я думаю, что нет ничего плохого в том, что PullRefresh
API и Compose/Accompanist Tab/Pager
API используются вместе, кажется, что PullRefresh
просто соблюдает структуру размещения макета/контейнера, в который он помещен.
Рассмотрим этот код, никаких вкладок, никакого пейджера, просто простая настройка виджетов, идентичная вашей настройке.
Column(
modifier = Modifier.padding(it)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.background(Color.Blue)
)
val pullRefreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = { viewModel.fetchMessages() }
)
Box(
modifier = Modifier.pullRefresh(pullRefreshState)
) {
PullRefreshIndicator(
modifier = Modifier.align(Alignment.TopCenter),
refreshing = false,
state = pullRefreshState,
)
}
}
На что это похоже.
PullRefresh
размещается внутри компонента (Box
), который размещается под другим компонентом в Column
вертикальном расположении, и, поскольку он находится под другим виджетом, его исходное положение не будет скрыто, как в образце изображения.
С вашей настройкой, так как я заметил, что ViewModel
используется вкладками, а также причина, по которой я подтверждал, что вы решили с вашей архитектурой, заключается в том, что единственное исправление, о котором я могу думать, это перемещение PullRefresh
вверх в последовательности составных виджетов.
Первые изменения, которые я внес в ваш ChatExampleScreen
компонуемый, в итоге все PullRefresh
компоненты удалены.
@Composable
fun ChatExampleScreen(
chatexampleViewModel: ChatExampleViewModel,
modifier: Modifier = Modifier
) {
val chatexampleViewModelState by chatexampleViewModel.state.observeAsState()
Box(
modifier = modifier
.fillMaxSize()
) {
when (val result = chatexampleViewModelState) {
is ChatExampleViewModel.State.SuccessfullyLoadedMessages -> {
ChatExampleScreenSuccessfullyLoadedMessages(
chatexampleMessages = result.list,
modifier = modifier,
)
}
is ChatExampleViewModel.State.NoMessagesFetched -> {
ChatExampleScreenEmptyState(
modifier = modifier
)
}
is ChatExampleViewModel.State.NoInternetConnectivity -> {
NoInternetConnectivityScreen(
modifier = modifier
)
}
else -> {
// Agus - Do nothing???
Box(modifier = modifier.fillMaxSize())
}
}
}
}
а в вашем Activity
я переместил всю область setContent{…}
в другую функцию с именем ChatTabsContent
и поместил в нее все, включая компоненты PullRefresh
.
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ChatTabsContent(
modifier : Modifier = Modifier,
viewModel : ChatExampleViewModel
) {
val chatexampleViewModelIsLoadingState by viewModel.isLoading.observeAsState()
val pullRefreshState = rememberPullRefreshState(
refreshing = chatexampleViewModelIsLoadingState == true,
onRefresh = { viewModel.fetchMessages() }
)
Box(
modifier = modifier
.pullRefresh(pullRefreshState)
) {
Column(
Modifier
.fillMaxSize()
) {
val pagerState = rememberPagerState()
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier.tabIndicatorOffset(
currentTabPosition = tabPositions[pagerState.currentPage],
)
)
}
) {
Tab(
selected = pagerState.currentPage == 0,
onClick = { },
text = {
Text(
text = "Messages"
)
}
)
Tab(
selected = pagerState.currentPage == 1,
onClick = { },
text = {
Text(
text = "Dashboard"
)
}
)
}
HorizontalPager(
count = 2,
state = pagerState,
modifier = Modifier.fillMaxWidth(),
) { page ->
when (page) {
0 -> {
ChatExampleScreen(
chatexampleViewModel = viewModel,
modifier = Modifier.fillMaxSize()
)
}
1 -> {
ChatExampleScreen(
chatexampleViewModel = viewModel,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
PullRefreshIndicator(
modifier = Modifier.align(Alignment.TopCenter),
refreshing = chatexampleViewModelIsLoadingState == true,
state = pullRefreshState,
)
}
}
что закончилось вот так
setContent {
TheOneAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = { TopAppBarSample() }
) {
ChatTabsContent(
modifier = Modifier.padding(it),
viewModel = viewModel
)
}
}
}
}
Результат:
Структурные изменения.
<Surface>
<Scaffold> // Set with a topBar
<Box>
<Column>
<ScrollableTabRow>
<Tab/> // Set for the first "Messages" tab
<Tab/> // Set for the second "Dashboard" tab
</ScrollableTabRow>
<HorizontalPager>
<Box/>
</HorizontalPager>
</Column>
// pull refresh is now at the most "z" index of the
// box, overlapping the content (tabs/pager)
<PullRefreshIndicator/>
</Box>
<Scaffold>
</Surface>
Я еще не изучал этот API, но похоже, что его следует использовать непосредственно в z-ориентированном родительском макете/контейнере, таком как Box
, в качестве последнего дочернего элемента.
- Большое спасибо за очень подробный ответ. Вопрос; Сначала я пытался добиться того, чтобы на каждой странице ChatExampleScreen был индикатор PullRefreshIndicator. Я вижу, что теперь ProgressBar отображается поверх ScrollableTabRow. Есть ли способ разместить PullRefreshIndicator на каждой из страниц? Извините, я должен был прояснить это раньше. Дайте мне знать, если это не имеет смысла.
Я собираюсь обновить вопрос и добавить захват, чтобы быть полностью ясным.
Сделанный. Дайте мне знать, если это не имеет смысла.
Я хочу оставить свой первый ответ, так как я чувствую, что он все еще будет полезен будущим читателям, так что вот еще один, который вы могли бы рассмотреть.
Одна из Box
на вкладках имеет модификатор прокрутки, потому что согласно Документам аккомпаниатора и фактической функциональности.
… Контент должен быть «прокручиваемым по вертикали» для SwipeRefresh() чтобы иметь возможность реагировать на жесты смахивания. Такие макеты, как LazyColumn, автоматически прокручивается по вертикали, но другие, такие как Column или LazyRow нет. В таких случаях вы можете предоставить Модификатор Modifier.verticalScroll…
Это из документации аккомпаниатора о переносе API, но по-прежнему применимо к текущему фреймворку компоновки.
Насколько я понимаю, должно присутствовать событие прокрутки, чтобы PullRefresh
активировался вручную (например, макет/контейнер с модификатором вертикальной прокрутки или LazyColumn), что-то, что будет потреблять событие перетаскивания/перелистывания на экране.
Вот краткий рабочий пример. Все это можно скопировать и вставить.
Активность:
class PullRefreshActivity: ComponentActivity() {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = { TopAppBarSample() }
) {
MyScreen(
modifier = Modifier.padding(it),
viewModel = viewModel
)
}
}
}
}
}
}
Некоторые классы данных:
data class MessageItems(
val message: String = "",
val author: String = ""
)
data class DashboardBanner(
val bannerMessage: String = "",
val content: String = ""
)
ViewModel:
class MyViewModel: ViewModel() {
var isLoading by mutableStateOf(false)
private val _messageState = MutableStateFlow(mutableStateListOf<MessageItems>())
val messageState = _messageState.asStateFlow()
private val _dashboardState = MutableStateFlow(DashboardBanner())
val dashboardState = _dashboardState.asStateFlow()
fun fetchMessages() {
viewModelScope.launch {
isLoading = true
delay(2000L)
_messageState.update {
it.add(
MessageItems(
message = "Hello First Message",
author = "Author 1"
),
)
it.add(
MessageItems(
message = "Hello Second Message",
author = "Author 2"
)
)
it
}
isLoading = false
}
}
fun fetchDashboard() {
viewModelScope.launch {
isLoading = true
delay(2000L)
_dashboardState.update {
it.copy(
bannerMessage = "Hello World!!",
content = "Welcome to Pull Refresh Content!"
)
}
isLoading = false
}
}
}
Компоненты экрана вкладок:
@Composable
fun MessageTab(
myViewModel : MyViewModel
) {
val messages by myViewModel.messageState.collectAsState()
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(messages) { item ->
Column(
modifier = Modifier
.fillMaxWidth()
.border(BorderStroke(Dp.Hairline, Color.DarkGray)),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = item.message)
Text(text = item.author)
}
}
}
}
@Composable
fun DashboardTab(
myViewModel: MyViewModel
) {
val banner by myViewModel.dashboardState.collectAsState()
Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
contentAlignment = Alignment.Center
) {
Column {
Text(
text = banner.bannerMessage,
fontSize = 52.sp
)
Text(
text = banner.content,
fontSize = 16.sp
)
}
}
}
Наконец, компонуемый, который содержит компоненты PullRefresh
и Pager/Tab
, и все они являются прямыми потомками ConstraintLayout
. Таким образом, чтобы получить PullRefresh
позади Tabs
, но все же поверх HorizontalPager
, сначала мне пришлось поместить HorizontalPager
в качестве первого потомка, PullRefresh
в качестве второго и Tabs
в качестве последнего, ограничивая их соответствующим образом, чтобы сохранить визуальное расположение пейджер с вкладками.
@OptIn(ExperimentalMaterialApi::class, ExperimentalPagerApi::class)
@Composable
fun MyScreen(
modifier : Modifier = Modifier,
viewModel: MyViewModel
) {
val refreshing = viewModel.isLoading
val pagerState = rememberPagerState()
val pullRefreshState = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = {
when (pagerState.currentPage) {
0 -> {
viewModel.fetchMessages()
}
1 -> {
viewModel.fetchDashboard()
}
}
},
refreshingOffset = 100.dp // just an arbitrary offset where the refresh will animate
)
ConstraintLayout(
modifier = modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
val (pager, pullRefresh, tabs) = createRefs()
HorizontalPager(
count = 2,
state = pagerState,
modifier = Modifier.constrainAs(pager) {
top.linkTo(tabs.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
height = Dimension.fillToConstraints
}
) { page ->
when (page) {
0 -> {
MessageTab(
myViewModel = viewModel
)
}
1 -> {
DashboardTab(
myViewModel = viewModel
)
}
}
}
PullRefreshIndicator(
modifier = Modifier.constrainAs(pullRefresh) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
refreshing = refreshing,
state = pullRefreshState,
)
ScrollableTabRow(
modifier = Modifier.constrainAs(tabs) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier.tabIndicatorOffset(
currentTabPosition = tabPositions[pagerState.currentPage],
)
)
},
) {
Tab(
selected = pagerState.currentPage == 0,
onClick = {},
text = {
Text(
text = "Messages"
)
}
)
Tab(
selected = pagerState.currentPage == 1,
onClick = {},
text = {
Text(
text = "Dashboard"
)
}
)
}
}
}
выход:
<Surface>
<Scaffold>
<ConstraintLayout>
// top to ScrollableTabRow's bottom
// start, end, bottom to parent's start, end and bottom
// 0.dp (view), fillToConstraints (compose)
<HorizontalPager>
<PagerScreens/>
</HorizontalPager>
// top, start, end of parent
<PullRefreshIndicator/>
// top, start and end of parent
<ScrollableTabRow>
<Tab/> // Set for the first "Messages" tab
<Tab/> // Set for the second "Dashboard" tab
</ScrollableTabRow>
</ConstraintLayout>
<Scaffold>
</Surface>
Итак, нет никакого способа фактически переместить PullRefreshIndicator на каждую страницу, как на скриншотах, которые я разместил?
Я имею в виду, что я вижу на этой гифке, что ProgressBar все еще перекрывается с TabLayouts.
@ 4gus71n, пожалуйста, посмотрите мой обновленный ответ, используя ConstraintLayout
Спасибо за помощь в решении этой проблемы — и последнее, из любопытства, что вы думаете о Jetpack Compose? Я имею в виду, что все это было бы намного проще с XML. Я не вижу, что Jetpack Compose дает нам решение, которое требует меньше кода или является более понятным/
TBH, даже с моим ограниченным набором навыков в составлении, я уже сталкиваюсь с множеством его нюансов и проблем, вы можете посмотреть мои недавние встречи, анимацию первого элемента LazyColumn, Toast, вызывающий нежелательные рекомпозиции, просто чтобы назвать два, но учитывая, что JC был рядом для всего пару лет по сравнению с XML, который является древним, JC пройдёт долгий путь.., IMO, это просто сначала сложно, но как только вы освоитесь, как только вы получите реактивный ранец, вы никогда не вернетесь назад
что касается количества кода, мой взгляд на это не только с композицией, но и с декларативностью в целом, я думаю, что даже с реакцией или флаттером ожидается то же самое, поскольку все вещи декалируются/кодируются в одном месте.
Я просто хочу поделиться более подробной информацией о проблеме здесь и о том, каково ее решение. Я очень ценю решения, представленные выше, и они определенно были ключом к выяснению проблемы.
Минимальное решение здесь — заменить Box
на ConstraintLayout
в составном ChatScreenExample
:
Почему? Поскольку, как @z.y поделился выше, PullRefreshIndicator
должен содержаться в компоновке с «вертикальной прокруткой», и хотя компоновка Box
может быть установлена с помощью модификатора vericalScroll()
, нам нужно убедиться, что мы ограничиваем высоту содержимого, поэтому нам пришлось изменить на ConstraintLayout
.
Не стесняйтесь исправлять меня, если я что-то упустил.
Сравнительно более простым решением в моем случае было просто присвоить Box, содержащему мой Composable с вертикальной прокруткой, и мой PullRefreshIndicator zIndex, равный -1f:
Box(Modifier.fillMaxSize().zIndex(-1f)) {
LazyColumn(...)
PullRefreshIndicator(...)
}
И это уже помогло мне. У меня очень похожая установка на OP, Scaffold, содержащий ScrollableTabRow и HorizontalPager с обновляемыми списками на отдельных вкладках.
Определенно, это самое аккуратное решение на данный момент. У вас есть причина использовать zIndex? Как вы поняли это решение?
Z-индексы по умолчанию определяются порядком, в котором объявляются Composables (объявленный последним имеет самый высокий zIndex и рисуется сверху). Принятое решение, предложенное здесь, направлено на то, чтобы сделать zIndex PullRefreshIndicator ниже, чем ScrollableTabRow, объявив его первым, но используя ConstraintLayout вместо Column, чтобы по-прежнему правильно позиционировать два. Поэтому я решил, что установка zIndex вручную может привести к тому же результату без реструктуризации моего макета, и это сработало.
@ 4gus71n отличное решение и объяснение, код работает весело, но когда я опускаю и отпускаю, индикатор остается и не исчезает, как ожидалось. Есть зацепки?
Есть еще одно решение этой проблемы — использование модификатора .clipToBounds()
над контейнером содержимого вкладки.
Я отмечаю это как принятый ответ, предыдущее решение привело к ошибке Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed
.
Нет необходимости в limitedLayout , просто используйте Scaffold()
Ваш ответ может быть улучшен с помощью дополнительной вспомогательной информации. Пожалуйста, отредактируйте , чтобы добавить дополнительные сведения, такие как цитаты или документация, чтобы другие могли подтвердить правильность вашего ответа. Вы можете найти больше информации о том, как писать хорошие ответы в справочном центре.
я столкнулся с той же проблемой пару дней назад. Используя Zindex, я столкнулся с этим одним установленным tabindicator Zindex как положительное целое число, а Zindex of RefreshIndicator - с любым отрицательным целым числом. Надеюсь это поможет