Flutter state rebuild when keyboard appears - flutter

Hello Everyone!
I am using Modal BottomSheet for view comments. When I click TextField (Comment Page) all widgets rebuilding also parents! Why all Flutter rebuilding all widgets. And why also parents widgets rebuilding? I know when keyboard appears or rotation changed eg. both StatefulWidget and StateLessWidget rebuilded. But I can’t do something in this situation. Please help me
Here CommentPage.
class CommentPage extends StatelessWidget {final String activityname;
final String postid;
final String usernameuid;
const CommentPage(
{Key key,
#required this.activityname,
#required this.postid,
#required this.usernameuid,
}): super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
height: MediaQuery.of(context).size.height * 0.8,
child: Scaffold(
resizeToAvoidBottomInset: false,
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text('Coments'),
centerTitle: true,
),
body: Container(
height:MediaQuery.of(context).size.height * 0.8,
width: MediaQuery.of(context).size.width,
child: ChangeNotifierProvider(
create: (context) => CommentLikeController(),
builder: (context, child) => FutureBuilder<List<Comment>>(
future: Provider.of<CommentLikeController>(context,
listen: false)
.initial(),
builder: (context, snapshot) {
if (snapshot.hasData)
return Stack(children: [
Positioned(
bottom: 50,
child: Container(
height:
MediaQuery.of(context).size.height * 0.8 -
105,
width: MediaQuery.of(context).size.width,
child: AnimatedList(
shrinkWrap: true,
reverse: true,
key: Provider.of<CommentLikeController>(
context)
.listkey,
initialItemCount: snapshot.data.length,
itemBuilder: (context, index, animation) {
return ListItem(
index: index,
postid: postid,
activityname: activityname,
usernameuid: usernameuid,
);
},
),
),
),
Positioned(
bottom: 0,
right: 0,
child: IconButton(
icon: Icon(Icons.add),
onPressed: () {
Provider.of<CommentLikeController>(
context,
listen: false)
.add();
})),
Positioned(
bottom: 0,
left: 0,
child: Container(
height: 50,
width:
MediaQuery.of(context).size.width - 50,
child: TextField()))
]);
else
return LinearProgressIndicator();
})),
)),
),
);
}
}
Parents CommentPage
class PageviewItem extends StatelessWidget {
final DBcontroller value;
final int index;
final String activityname;
final String username;
const PageviewItem(
{Key key,
#required this.value,
#required this.index,
#required this.activityname,
#required this.username})
: super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Stack(
children: [
Container(
child: value.posts[index].urln.contains('.mp4')
? VideoItem(value: value, index: index)
: PhotoItem(value: value, index: index),
),
UserInfo(value: value, index: index),
Positioned(
bottom: 5,
left: 5,
child: GestureDetector(
onTap: () {
showpopup(context);
}, //show pop up
child: Container(
decoration: BoxDecoration(
color: Colors.blue[400].withOpacity(0.3),
borderRadius: BorderRadius.all(Radius.circular(5))),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: RichText(text: TextSpan(text: 'Comment')),
),
),
)),
Header(activityname: activityname),
],
),
);
}
showpopup(context) {
return showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10), topRight: Radius.circular(10))),
isScrollControlled: true,
context: context,
builder: (context1) {
return CommentPage(
activityname: activityname,
postid: value.posts[index].from_uid,
usernameuid: username,
);
},
);
}
}
Note I am also have PageviewItem parents cLass. And each one rebuilded when click TextField(keyboard appears )

I'm not good at explaining but maybe I can give an example
class _CommentPageState extends State<CommentPage> {
late Future<QuerySnapshoot> commentStream; //late initialization for future comment
#ovveride
void initState(){
commentStream = FirebaseFirestore.instance.collection('forumchat').snapshot //add this on initstate
} //first initial when commentpage loaded
Widget build(BuildContext context) {
FutureBuilder(
future: commentStream, //add the initialization here
....//
}
}
so future comments must be initialized when the page is first opened, then when the keyboard appears, the build will not rebuild because it has already been initiated.
i hope this helping you, im very bad at explanation lol ;D

Related

How can I send data from bottom sheet to parent widget?

I have list of products in bottom sheet, when I choose any product I want to parent's widget to add it, unfortunately my product adds only after hot reload, or when I create a new route from bottom sheet to parent's widget, how can I solve this problem, any ideas? Here is the part of the bottom sheet code
class IceBottomSheet extends StatefulWidget {
const IceBottomSheet({Key? key}) : super(key: key);
#override
_IceBottomSheetState createState() => _IceBottomSheetState();
}
class _IceBottomSheetState extends State<IceBottomSheet> {
final _model = ProductWidgetsModel();
#override
Widget build(BuildContext context) {
List<Widget> productWidgetList = [];
products.forEach((product) =>
productWidgetList.add(SingleProductWidget(product: product)));
return Provider(
model: _model,
child: Expanded(
child: GridView.count(
crossAxisSpacing: 10,
mainAxisSpacing: 16,
shrinkWrap: true,
crossAxisCount: 2, children: productWidgetList),
),
);
}
}
class SingleProductWidget extends StatefulWidget {
final Product product;
const SingleProductWidget({Key? key, required this.product})
: super(key: key);
#override
State<SingleProductWidget> createState() => _SingleProductWidgetState();
}
class _SingleProductWidgetState extends State<SingleProductWidget> {
#override
Widget build(BuildContext context) {
final model = Provider.of(context)?.model;
return Padding(
padding: const EdgeInsets.all(5.0),
child:
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: double.infinity,
height: 100,
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: widget.product.image,
),
shape: BoxShape.circle,
border: Border.all(
color: model?.idSelected == widget.product.id
? Colors.yellow
: Colors.grey,
width: 5.0,
style: BorderStyle.solid,
),
),
child: GestureDetector(
onTap: () {
model?.idSelected = widget.product.id;
// Route route =
// MaterialPageRoute(builder: (context) => BerryPage(context,));
// Navigator.push(context, route);
if(model?.idSelected == 1){
menuRow.removeAt(2);
Navigator.pop(context);
choice.insert(2, Adds(id: 102, name: 'Холодок', img: 'https://autogear.ru/misc/i/gallery/73434/2759438.jpg'));
}
}),
),
),
And here is the part of parent's widget code, it is inside GestureDetector
else if (index == 2){
setState(() {
});
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext builder) {
return Container(
height: 250,
child: Column(
children: [
SizedBox(
height: 10,
),Row(
children: [
Padding(
padding: EdgeInsets.only(left: MediaQuery.of(context).size.width*0.25 + MediaQuery.of(context).size.width *0.12),
child: Text(
'Холодок',
style: TextStyle(
fontFamily: 'Newfont',
fontSize: 22,
),
),
),
SizedBox(width: MediaQuery.of(context).size.width*0.25,),
IconButton(icon: Icon(Icons.close),onPressed: (){Navigator.pop(context);},)
],
),
Divider(),
IceBottomSheet(),
],
));
},
);
So when you open the BottomSheet you have to add await before it, so when you call Navigator.pop(context, data_you_want_to_pass_to_parent) it will wait for some data to be returned.
final data = await openBottomSheet();
inside the bottomSheet when you want to close, just pass the the desired data as so
Navigator.pop(context, data_you_want_to_pass_to_parent);

Move an item from one list to another with animation in Flutter

I have two vertical lists, one on the left side and the other one on the right, let's call them "Selected List" and "Unselected List".
I want the items in Unselected List to Animate from left side to the right side of the screen and add to Selected List.
the other items should fill the empty space in Unselected List and items in Selected List should free up the space for new item.
Here's the Ui
My Code:
class AddToFave extends StatefulWidget {
const AddToFave({Key? key}) : super(key: key);
#override
_AddToFaveState createState() => _AddToFaveState();
}
class _AddToFaveState extends State<AddToFave> {
List<String> unselected = [ '1','2','3','4','5','6','7','8','9','10'];
List<String> selected = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: MediaQuery.of(context).size.width / 5,
height: MediaQuery.of(context).size.height,
child: ListView.builder(
itemCount: selected.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
unselected.add(selected[index]);
selected.removeAt(index);
setState(() {});
},
child: Container(
width: MediaQuery.of(context).size.width / 5,
height: MediaQuery.of(context).size.width / 5,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(
MediaQuery.of(context).size.width / 5)),
child: Center(
child: Text(
selected[index],
style: TextStyle(color: Colors.white),
)),
),
);
}),
),
Container(
width: MediaQuery.of(context).size.width / 5,
height: MediaQuery.of(context).size.height,
child: ListView.builder(
itemCount: unselected.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
selected.add(unselected[index]);
unselected.removeAt(index);
setState(() {});
},
child: Container(
width: MediaQuery.of(context).size.width / 5,
height: MediaQuery.of(context).size.width / 5,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(
MediaQuery.of(context).size.width / 5)),
child: Center(
child: Text(
unselected[index],
style: TextStyle(color: Colors.white),
)),
),
);
}),
),
],
),
),
);
}
}
Thank you in advance.
This task can be broken into 2 parts.
First, use an AnimatedList instead of a regular ListView, so that when an item is removed, you can control its "exit animation" and shrink its size, thus making other items slowly move upwards to fill in its spot.
Secondly, while the item is being removed from the first list, make an OverlayEntry and animate its position, to create an illusion of the item flying. Once the flying is finished, we can remove the overlay and insert the item in the actual destination list.
Full source code for you to use, as a starting point:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TwoAnimatedListDemo(),
);
}
}
class TwoAnimatedListDemo extends StatefulWidget {
const TwoAnimatedListDemo({Key? key}) : super(key: key);
#override
_TwoAnimatedListDemoState createState() => _TwoAnimatedListDemoState();
}
class _TwoAnimatedListDemoState extends State<TwoAnimatedListDemo> {
final List<String> _unselected = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
final List<String> _selected = [];
final _unselectedListKey = GlobalKey<AnimatedListState>();
final _selectedListKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Two Animated List Demo'),
),
body: Row(
children: [
SizedBox(
width: 56,
child: AnimatedList(
key: _unselectedListKey,
initialItemCount: _unselected.length,
itemBuilder: (context, index, animation) {
return InkWell(
onTap: () => _moveItem(
fromIndex: index,
fromList: _unselected,
fromKey: _unselectedListKey,
toList: _selected,
toKey: _selectedListKey,
),
child: Item(text: _unselected[index]),
);
},
),
),
Spacer(),
SizedBox(
width: 56,
child: AnimatedList(
key: _selectedListKey,
initialItemCount: _selected.length,
itemBuilder: (context, index, animation) {
return InkWell(
onTap: () => _moveItem(
fromIndex: index,
fromList: _selected,
fromKey: _selectedListKey,
toList: _unselected,
toKey: _unselectedListKey,
),
child: Item(text: _selected[index]),
);
},
),
),
],
),
);
}
int _flyingCount = 0;
_moveItem({
required int fromIndex,
required List fromList,
required GlobalKey<AnimatedListState> fromKey,
required List toList,
required GlobalKey<AnimatedListState> toKey,
Duration duration = const Duration(milliseconds: 300),
}) {
final globalKey = GlobalKey();
final item = fromList.removeAt(fromIndex);
fromKey.currentState!.removeItem(
fromIndex,
(context, animation) {
return SizeTransition(
sizeFactor: animation,
child: Opacity(
key: globalKey,
opacity: 0.0,
child: Item(text: item),
),
);
},
duration: duration,
);
_flyingCount++;
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) async {
// Find the starting position of the moving item, which is exactly the
// gap its leaving behind, in the original list.
final box1 = globalKey.currentContext!.findRenderObject() as RenderBox;
final pos1 = box1.localToGlobal(Offset.zero);
// Find the destination position of the moving item, which is at the
// end of the destination list.
final box2 = toKey.currentContext!.findRenderObject() as RenderBox;
final box2height = box1.size.height * (toList.length + _flyingCount - 1);
final pos2 = box2.localToGlobal(Offset(0, box2height));
// Insert an overlay to "fly over" the item between two lists.
final entry = OverlayEntry(builder: (BuildContext context) {
return TweenAnimationBuilder(
tween: Tween<Offset>(begin: pos1, end: pos2),
duration: duration,
builder: (_, Offset value, child) {
return Positioned(
left: value.dx,
top: value.dy,
child: Item(text: item),
);
},
);
});
Overlay.of(context)!.insert(entry);
await Future.delayed(duration);
entry.remove();
toList.add(item);
toKey.currentState!.insertItem(toList.length - 1);
_flyingCount--;
});
}
}
class Item extends StatelessWidget {
final String text;
const Item({Key? key, required this.text}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: CircleAvatar(
child: Text(text),
radius: 24,
),
);
}
}

Flutter prevent widget rebuild

When i select TextField()(means keyboard open) my widget tree rebuilt.
I know the reason for this is because when the keyboard is opened, the screen sizes change and the whole class is rebuilt (both StateFull and Stateless widgets). And this is normal.
And so the AnimatedList() in my class is rebuilt. I do not want it to be rebuilt (because list indexes and list items in it change). How can I prevent this AnimatedList() Widget rebuilt? Or is it possible to prevent the class from being rebuilt when the keyboard is opened in general?
Please Help Me
Here my code:
class CommentPage extends StatelessWidget {
final String activityname;
final String postid;
final String usernameuid;
const CommentPage({
Key key,
#required this.activityname,
#required this.postid,
#required this.usernameuid,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
height: 400,
child: Scaffold(
resizeToAvoidBottomInset: false,
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text('Coments'),
centerTitle: true,
),
body: Container(
height: 350,
width: 300,
child: ChangeNotifierProvider(
create: (context) => CommentLikeController(),
builder: (context, child) => FutureBuilder<List<Comment>>(
future:
Provider.of<CommentLikeController>(context, listen: false)
.initial(),
builder: (context, snapshot) {
if (snapshot.hasData)
return Stack(children: [
Positioned(
bottom: 50,
child: Container(
height: 350,
width: 300,
child: AnimatedList(
shrinkWrap: true,
reverse: true,
key: Provider.of<CommentLikeController>(context)
.listkey,
initialItemCount: snapshot.data.length,
itemBuilder: (context, index, animation) {
return ListItem(
index: index,
postid: postid,
activityname: activityname,
usernameuid: usernameuid,
);
},
),
),
),
Positioned(
bottom: 0,
left: 0,
child: Container(
height: 50,
width: 350,
child: TextField(),
),
),
]);
else
return LinearProgressIndicator();
}),
),
),
),
),
);
}
}
Create another StatefulWidget and place your TextField widget inside it. Create the text field controller in your CommentPage and pass it as parameter to your new created StatefulWidget and use it as the the TextField's controller. The keyboard effect will only impact the new created StatefulWidget

Keyboard automatically disappears from TextField in ListView.Builder

I'm trying to implement a solution where a row (containing both a TextField and a Text) in ListView.Builder is automatically for every record retrieved from a webserver.
However when I want to start typing in such a TextField the keyboard appears and immediatly disappears again.
This is the code of my screen.
class GameScreen extends StatelessWidget {
static const RouteName = "/GameScreen";
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
const horizontalMargin = 20.0;
return Scaffold(
appBar: getAppBar(),
backgroundColor: Colors.transparent,
body: Stack(
children: <Widget>[
Background(),
Column(
children: <Widget>[
Header("Starting letter: B"),
Expanded(
child: ListBlocProvider(
listWidget: GameCategoriesList(),
itemsService: CategoriesService(),
margin: EdgeInsets.only(
left: horizontalMargin,
bottom: 10,
right: horizontalMargin,
),
),
),
SizedBox(
height: 20,
),
SizedBox(
width: size.width - 40,
height: 60,
child: Container(
height: 60,
child: TextButtonWidget(
() {
// Navigator.of(context).pushNamed(GameScreen.RouteName);
},
"Stop game",
),
),
),
SizedBox(
height: 20,
)
],
),
],
),
);
}
}
This is the code of my ListBlocProvider:
class ListBlocProvider extends StatelessWidget {
final ListWidget listWidget;
final ItemsService itemsService;
final bool useColor;
final bool usePaddingTop;
final double height;
final EdgeInsets margin;
const ListBlocProvider({
#required this.listWidget,
#required this.itemsService,
this.useColor = true,
this.usePaddingTop = true,
this.height = 200,
this.margin,
});
#override
Widget build(BuildContext context) {
const horizontalMargin = 20.0;
return BlocProvider(
create: (context) => ItemsBloc(itemsService: itemsService)..add(ItemsFetched()),
child: Container(
padding: usePaddingTop ? EdgeInsets.only(top: 10) : null,
decoration: BoxDecoration(
color: this.useColor ? Color.fromRGBO(10, 50, 75, 0.9) : null,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
margin: this.margin,
height: this.height,
child: this.listWidget,
),
);
}
}
This is the code of my List:
class GameCategoriesList extends ListWidget {
#override
_GameCategoriesListState createState() => _GameCategoriesListState();
}
class _GameCategoriesListState extends State<GameCategoriesList> {
#override
Widget build(BuildContext context) {
return BlocBuilder<ItemsBloc, ItemsState>(
builder: (context, state) {
if (state is ItemsFailure) {
return Center(
child: Text('failed to fetch categories'),
);
}
if (state is ItemsSuccess) {
if (state.items.isEmpty) {
return Center(
child: Text('no categories found.'),
);
}
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
var textEditingController = TextEditingController();
return GameCategoryItemWidget(
key: UniqueKey(),
categoryModel: state.items[index],
textEditingController: textEditingController,
);
},
itemCount: state.items.length,
);
}
return Center(
child: LoadingIndicator(),
);
},
);
}
}
And this is the code where the both the TextField and the Text are build:
class GameCategoryItemWidget extends StatefulWidget {
final CategoryModel categoryModel;
final TextEditingController textEditingController;
const GameCategoryItemWidget({Key key, this.categoryModel, this.textEditingController}) :
super(key: key);
#override
_GameCategoryItemWidgetState createState() => _GameCategoryItemWidgetState();
}
class _GameCategoryItemWidgetState extends State<GameCategoryItemWidget> {
var formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Container(
child: Form(
key: this.formKey,
child: Column(
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 10, top: 20, bottom: 10),
child: Text(
this.widget.categoryModel.name,
style: TextStyle(
color: Colors.white,
fontSize: 18,
),
),
),
Container(
color: Colors.white,
child: InputField(
InputDecoration(labelText: this.widget.categoryModel.name),
this.widget.textEditingController,
false,
),
),
],
),
),
);
}
#override
void dispose() {
this.widget.textEditingController.dispose();
super.dispose();
}
}
The InputField is a custom widget to hide the switch between a Material and a Cupertino version of the TextField.
I've already tried to remove the Key from the custom TextField widget. The funny part is that the input is actually working, however it can't determine for which of the TextFields in the ListView the input is determined so it adds the input to all of them. I've also tried to swap things around with making Stateless widgets Statefull, but that didn't help either.
The entire build is based upon: https://bloclibrary.dev/#/flutterinfinitelisttutorial.
Hoping you guys can help me.

Draw outside listview bounds in Flutter

I want to transform my item that it is bigger than the listview itself. (intention for focused navigation)
My List:
Container(
height: 100,
child: ListView.builder(
itemBuilder: (context, index) => HomeItem(title: '$index'),
scrollDirection: Axis.horizontal,
),
),
My Item:
class HomeItem extends StatelessWidget {
final String title;
final bool expand;
const HomeItem({
#required this.title,
this.expand = false,
});
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: ThemeDimens.padding8),
child: Transform.scale(
scale: expand ? 1.5 : 1,
child: AnimatedContainer(
width: 50,
height: 100,
color: expand ? ThemeColors.accent : ThemeColors.primary,
duration: ThemeDurations.shortAnimationDuration(),
child: Center(
child: Text(title),
),
),
),
);
}
}
Current behaviour
Expected behaviour
If you try to use OverflowBox or Transform, content of an item will still clip and won't be drawn outside of its bounding box. But it's possible to use Overlay to draw an element on top of list and position it on a specific list item, though it's a bit complicated.
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
final elements = List.generate(12, (i) => i);
int selectedIndex;
OverlayEntry overlayEntry;
List<LayerLink> layerLinks;
#override
void initState() {
super.initState();
// Creating a layer link for each list cell
layerLinks = List.generate(elements.length, (i) => LayerLink());
}
void createOverlayEntry(int i, BuildContext context) {
// Removing an overlay entry, if there was one
overlayEntry?.remove();
final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
// Creating a new overlay entry linked to specific list element
overlayEntry = OverlayEntry(
builder: (context) => Positioned(
left: 0,
top: 0,
child: CompositedTransformFollower(
link: layerLinks[i],
showWhenUnlinked: false,
offset: Offset(-20, 0),
child: Material(
color: Colors.yellow,
child: InkWell(
onTap: () {
setState(() {
selectedIndex = null;
});
overlayEntry?.remove();
overlayEntry = null;
},
child: Container(
alignment: Alignment.center,
width: 70,
height: elementHeight,
child: Text('$i')
),
)
),
)
)
);
// Inserting an entry
Overlay.of(context).insert(overlayEntry);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
height: elementHeight,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: elements.length,
itemBuilder: (c, i) {
return CompositedTransformTarget(
link: layerLinks[i],
child: Material(
color: Colors.red,
child: InkWell(
onTap: () {
setState(() {
selectedIndex = i;
});
createOverlayEntry(i, context);
},
child: Container(
alignment: Alignment.center,
width: 30,
child: Text('${elements[i]}'),
),
),
),
);
},
separatorBuilder: (c, i) {
return Container(width: 10, height: 10);
},
),
),
);
}
}