I'm trying to build a Chat UI with ListView. My first version was a ListView.build that works fine.
I want to upgrade my chat view for "replyTo message" option (similar to whatsapp). This requires the user to be able to click the "replyTo" message and scroll backwards to the original message. The ListView.build does not allow to scroll up to a message that is outside the screen view (as it is not even built).
body: Column(
children: [
Expanded(
child: EasyRefresh.custom(
...
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
ChatMessage _currMsg = vm.chat[index]!;
...
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
...
SwipeTo(
onRightSwipe: () {
setState(() {
isReplying = true;
replyMessage = _currMsg;
});
},
child: ShowMessage(
key: Key(_currMsg.id!),
msg: _currMsg,
myMessage: _currMsg.postById == vm.user!.uid,
onTapReply: (id) async {
final ctx = _itemKey.currentContext;
if (ctx != null)
await Scrollable.ensureVisible(ctx);
},
),
),
],
);
},
childCount: vm.chat.length,
),
),
],
),
),
It seems like the only way is to build all the list items at first build itself.
My question is, is there a way I can retain the already built items, and only add new incoming items rather than rebuilding all items every time.
Thanks.
Related
Question
I have an app that loads a list of habits as a Stream from Firestore, and I want the app to show a loading screen until habit loading has completed, and then show the list if there is one. Is there a way to keep a loading screen showing until we've either finished loading the first value in the stream or determined there won't be one?
The issue I'm having is that while my app is showing the loading screen, it briefly loads with a "no habits found" view before switching to show the list of habits.
Setup
This view uses three components:
I have a model-view-viewmodel architecture based on the Stacked Package.
(1) My view is a ViewModelBuilder widget with a StreamBuilder inside it. It looks at a (2) DailyViewModel, where the relevant components are an isBusy boolean property and a Stream<List<HabitCompletionViewModel> property that the view's StreamBuilder looks at to display the habits. The habits are loaded from Firestore via a FirestoreHabitService (this is an asynchronous call - will be described in a minute).
The View works as follows:
If DailyViewModel.isBusy is true, show a Loading... text.
If isBusy is false, it will show a Stream of Habits, or the text "No Habits Found" if the stream is not returning any habits (either snapshot.hasData is false, or data.length is less than 1).
#override
Widget build(BuildContext context) {
return ViewModelBuilder.reactive(
viewModelBuilder: () => vm,
disposeViewModel: false,
builder: (context, DailyViewModel vm, child) => vm.isBusy
? Center(child: Text('Loading...'))
: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'BaseDate of ${DateFormat.yMd().format(vm.week.baseDate)}'),
Text(
'Week ${DateFormat.yMd().format(vm.week.beginningOfWeek)} - ${DateFormat.yMd().format(vm.week.endOfWeek)}'),
SizedBox(height: 20),
Row(
children: [
Flexible(
child: Text('Today',
style: Theme.of(context).textTheme.headline6),
),
],
),
const Padding(padding: EdgeInsets.all(2)),
StreamBuilder<List<HabitCompletionViewModel>>(
stream: vm.todaysHabits,
builder: ((context, snapshot) {
if (snapshot.hasData == false ||
snapshot.data == null ||
snapshot.data!.length < 1) {
return Center(child: Text('No Habits Found'));
} else {
return Column(children: [
ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: snapshot.data!.length,
itemBuilder: (context, i) => HabitCompletionTile(
key: ValueKey(snapshot.data![i].habit.id),
vm: snapshot.data![i],
),
),
]);
}
})),
SizedBox(height: 40),
TextButton(
child: Text('Create a New Habit'),
onPressed: () => vm.navigateToCreateHabitPage(),
),
],
),
),
);
}
}
Process of Loading the Data
My loading happens as follows:
The ViewModel is initialized, and setBusy is set to true.
DailyViewModel({required WeekDates week}) {
setBusy(true);
_log.v('initializing the daily viewmodel');
pageSubtitle =
'Week of ${week.startWeekday.title} ${DateFormat.Md().format(week.beginningOfWeek)}';
_log.v('page subtitle is $pageSubtitle');
mainAsyncCode();
}
Then it starts this mainAsyncCode() method, which gets a stream of habits from the FirestoreHabitService (this returns a Future<Stream<List<Habit>>> because there is a potential modification function performed on the habits before returning them), and once that is completed, transforms that stream into a Stream<List<HabitCompletionViewModel>>, and then sets isBusy on the ViewModel to false.
void mainAsyncCode() async {
_myHabits = await _habitService.loadActiveHabitsByUserFuture(_loginAndUserService.loggedInUser!.id!);
todaysHabits = _myHabits!.transform(currentViewModelCompletion);
await Future.delayed(Duration(seconds: 5));
setBusy(false);
}
Issue
The problem is that there is a temporary delay where the screen goes from "Loading..." to "No Habits Found" before it shows the list of Habits. I want it to wait on "Loading" until the stream list has been published, but I can't think of a way to do that.
Are there any options for doing this that others are aware of?
I want to keep the current page, when i refersh the screen or navigating back in page view
here is the code , actually i want display the particular index screen programmatically with out clicking the button. Some times iam getting controller.hasClients getting empty. If page view has 3 screens , if iam on 2ndscreen when i refresh the second screen should display second screen, instead it always display 1st screen . Iam using page controller to display , but not able to instantiate page controller becacuse i want to access from top widgets of page view. Please help
Container(
// width: 370,
height: MediaQuery.of(context).size.height * 0.685,
child: Stack(children: <Widget>[
_pages(),
Stack(
children: <Widget>[
Align(
alignment: Alignment.bottomCenter,
child: Container(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (int i = 0; i < 3; i++)
(i == currentPage
? circleBar(true)
: circleBar(false))
],
),
),
),
],
),
]),
),
Widget _pages() {
List _pagesList = [
FirstScreen(
),
SecondScreen(
),
ThirdScreen(
),
];
return PageView.builder(
controller: _pageController,
itemCount: _pagesList.length,
// restorationId: currentPage.toString(),
itemBuilder: (BuildContext context, int index) {
// _pageController=PageController(initialPage: currentPage);
return _pagesList[index];
},
onPageChanged: (int page) {
setState(() {
currentPage = page;
selected = analyticsList[currentPage].anlyticName;
isIndexChanged = true;
page;
});
},
);
}
For a named route, you can use this code.
Navigator.pushNamed(context, '/screen2').then((_) {
// This block runs when you have returned back from screen 2.
setState(() {
// code here to refresh data
});
});
first saving the current page and instantiating page controller in pages() it worked for me
i just instantiated the page controller in pages() in my code as if declare on the top it is not getting read every refresh so i declared where the pageview get build .
Widget _pages() {
_pageController= PageController(initialPage: currentPage, keepPage: true);
List _pagesList = [
FirstScreen(
),
SecondScreen(
),
ThirdScreen(
),
];
return PageView.builder(
controller: _pageController,
itemCount: _pagesList.length,
// restorationId: currentPage.toString(),
itemBuilder: (BuildContext context, int index) {
return _pagesList[index];
},
onPageChanged: (int page) {
setState(() {
currentPage = page;
selected = analyticsList[currentPage].anlyticName;
isIndexChanged = true;
page;
});
},
);
}
I have a Flutter web application that displays multiple user profiles on a card within a Row. The cards can each flip over to reveal more information via this library:
https://pub.dev/packages/flip_card
The application uses WebSockets and receives a JSON list of user details which maps to a User dart class, and as soon as new list arrives on a socket, we create a widget and add it to a widgetList and wrap it in a setState():
webSocket.onMessage.listen((e) {
final List receivedJsonUserList = json.decode(e.data);
final List<User> userListFromSocket =
receivedJsonUserList.map((item) => User.fromJson(item)).toList();
userListFromSocket.forEach((newUser) {
setState(() {
widgets[newUser.user.id] = UserDetails(user: newUser);
widgetList = widgets.entries.map((entry) => entry.value).toList();
});
});
}
}
});
The widget is drawn like this:
#override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (context, sizingInformation) => Scaffold(
drawer: sizingInformation.deviceScreenType == DeviceScreenType.mobile
? NavigationDrawer()
: null,
backgroundColor: Colors.white,
body: Scrollbar(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widgetList),
],
),
),
),
),
);
}
The code works 90% of the time, but occasionally the wrong data is on the back of a card. So User 1 will have User 2's data on the back, etc.
Am I doing this correctly? Is there an obvious issue with this implementation? I tried to create a seperate widget for each user and it seems to resolve the issue but re-using widgets surely has to be possible.
I have this weird problem: I want to update a grid of items when I click on it. I use a BLoC pattern to manage the changement so the view just receive a list and have to display it. My problem is that the view doesn't fully update.
Before I go further in the explanation, here my code
body: BlocEventStateBuilder<ShopEvent, ShopState>(
bloc: bloc,
builder: (BuildContext context, ShopState state) {
staggeredTile.clear();
cards.clear();
staggeredTile.add(StaggeredTile.count(4, 0.1));
cards.add(Container());
if (state.products != null) {
state.products.forEach((item) {
staggeredTile.add(StaggeredTile.count(2, 2));
cards.add(
Card(
child: InkWell(
child: Column(
children: <Widget>[
Image.network(
item.picture,
height: 140,
),
Container(margin: EdgeInsets.only(top: 8.0)),
Text(item.title)
],
),
onTap: () {
bloc.emitEvent(ClickShopEvent(item.id));
},
),
),
);
});
}
return StaggeredGridView.count(
crossAxisCount: 4,
staggeredTiles: staggeredTile,
children: cards,
);
}),
So, I have two items. When I click on the first one, I'm suppose to have one item with a different name and picture. But when I click, I have one item as expected, but with the same text and image. When I print thoses values, it's correctly updated but the view doesn't show it.
Do you have any clues of my problem?
For a reason that I can't explain, when I replaced
staggeredTile.clear();
cards.clear();
by
staggeredTile = new List<StaggeredTile>();
cards = new List<Widget>();
It works fine.
If someone can explain me the reason, I'd be gratefull.
I'm trying to achieve a scrollable List with videos. I'm using the video_player widget and wrapping the player in a Card with a simple button.
Now i noticed that whenever I use ListView.builder with videos in the list, it is extremely lagging, especially when scrolling back up. i'm posting a GiF bellow if you would like to see the behaviour.
I have this problem ONLY when I have Videos in the list.
If I replace the videos with a simple Image widget, the scrolling is smooth and runs as intended. (Also provided a GiF below)
When I scroll through the list I get this message in my Console:
flutter: Another exception was thrown: setState() or markNeedsBuild() called during build.
And I think (but not sure) that this is the cause of the problem, maybe the way I implemented the video_player plugin (?)
class VideoPlayPause extends StatefulWidget {
VideoPlayPause(this.controller);
final VideoPlayerController controller;
#override
_VideoPlayPauseState createState() => _VideoPlayPauseState();
}
class _VideoPlayPauseState extends State<VideoPlayPause> {
//This Part here
_VideoPlayPauseState() {
listener = () {
setState(() {});
};
}
Maybe its setting state every time I scroll ?
I tried flutter run --release but saw no difference at all.
I'm running the app on a physical Iphone X.
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('MVP_Test1'),
),
body: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: videoUrl.length,
itemBuilder: (context, i) {
return Card(
child: Column(
children: <Widget>[
Column(
children: <Widget>[
const ListTile(
leading: Icon(Icons.live_tv),
title: Text("Nature"),
),
Stack(
alignment: FractionalOffset.bottomRight +
const FractionalOffset(-0.1, -0.1),
children: <Widget>[
// If I replace this with Image.asset("..."), the scrolling is very smooth.
AssetPlayerLifeCycle(
videoUrl[i],
(BuildContext context,
VideoPlayerController controller) =>
AspectRatioVideo(controller)),
]),
],
),
ButtonTheme.bar(
child: ButtonBar(
children: <Widget>[
FlatButton(
child: const Text('ADD VIDEO'),
onPressed: () {
/* ... */
},
),
],
),
),
],
),
);
},
),
);
}
Result when I run Flutter Analyze
flutter analyze
Analyzing mvp_1...
No issues found! (ran in 1.9s)
You can see how when scrolling back up it looks like the app is skipping frames or something. Here's a video:
https://giphy.com/gifs/u48BNQ13r15Zay5SnN
Here's a video with photos instead of videos:
https://giphy.com/gifs/236RKyA8y1pfecmR1d