I created a list view based on a Future Provider. It works as expected.
Now I want to add a ScrollController in order to create a animated FloatingActionButton like Gmail "Compose" button.
I put controller attribute on listView.builder.
And here I have weird behaviour : I can't scroll. As soon as I scroll down or up listview is rebuilding and I can't perform any scroll.
Here my code :
ScrollController _scrollController = ScrollController();
bool isFAB = false;
#override
void initState() {
_scrollController.addListener(() {
if (_scrollController.offset > 50) {
setState(() {
isFAB = true;
});
} else {
setState(() {
isFAB = false;
});
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.toolListTitle),
),
body: FutureBuilder(
future:
Provider.of<MyTools>(context, listen: false).fetchAndSetTools(),
builder: (ctx, snapshot) => snapshot.connectionState ==
ConnectionState.waiting
? const Center(
child: CircularProgressIndicator(),
)
: Consumer<MyTools>(
child: Center(
child: Text(AppLocalizations.of(context)!.noToolYet),
),
builder: (ctx, myTools, ch) => myTools.items.isEmpty
? Center(
child: Text(AppLocalizations.of(context)!.noToolYet),
)
: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.vertical,
itemCount: myTools.items.length,
itemBuilder: (ctx, i) => ToolWidget(
id: myTools.items[i].id,
name: myTools.items[i].name,
createdAt: myTools.items[i].createdAt,
description: myTools.items[i].description,
),
),
),
),
floatingActionButton: isFAB
? FloatingActionButton(
onPressed: () =>
Navigator.of(context).pushNamed(AddToolScreen.routeName),
child: Icon(
Icons.add_sharp,
color: Theme.of(context).primaryColor,
),
backgroundColor: Colors.black,
)
: FloatingActionButton.extended(
onPressed: () =>
Navigator.of(context).pushNamed(AddToolScreen.routeName),
icon: Icon(
Icons.add_sharp,
color: Theme.of(context).primaryColor,
),
backgroundColor: Colors.black,
label: Text(
"Add Tool",
style: TextStyle(
color: Theme.of(context).primaryColor,
),
),
));
}
}
Can you help me ?
Thanks
I think the
setState(() {
isFAB = true;
});
in your _scrollController.addListener function is resetting the scroll position to the top again.
Related
I have a variable to which i assign a value inside gridView.Builder and there is a button, when clicked on which my variable should change, I use setState for this, but it does not change, what could be the reason?
class _CatalogItemsState extends State<CatalogItems> {
Set<int> _isFavLoading = {};
bool isFavorite = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(name!),
),
body: Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Column(
children: [
Expanded(
child: FutureBuilder<List<Product>>(
future: productFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return buildGridShimmer();
} else if (snapshot.hasData) {
final catalog = snapshot.data;
if (catalog!.isEmpty) {
return const Center(
child: Text(
'Нет товаров',
style: TextStyle(
fontSize: 25, fontWeight: FontWeight.bold),
),
);
}
return buildCatalog(catalog);
} else {
print(snapshot.error);
return const Text("No widget to build");
}
}),
),
],
),
),
);
}
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(),
delegate: SliverChildBuilderDelegate(childCount: product.length,
(BuildContext context, int index) {
final media =
product[index].media?.map((e) => e.toJson()).toList();
final photo = media?[0]['links']['local']['thumbnails']['350'];
final productItem = product[index];
isFavorite = productItem.is_favorite; // this is the new value of the variable, work good
IconButton(
icon: Icon(_isFavLoading.contains(index) || isFavorite ? Icons.favorite : Icons.favorite_border, color: Colors.red,),
onPressed: () {
setState(() {
isFavorite = !isFavorite;
});
print('t: ${isFavorite}'); // the value of the variable does not change
},
)
This is because you're not really updating your product object. You must change its value when icon is pressed
onPressed: () {
setState(() {
productItem.is_favorite = !isFavorite;
});
print('t: ${productItem.is_favorite}'); // the value of the variable does not change
}
I am trying to toggle between Login Screen and HomeScreen based on the user status. The logic seems to be working as long as I don't put HomeScreen.
I replaced HomeScreen with a different screen to check and the app works as it should. It displays different screens on hot restart based on the user's login status. But as soon as I try to put HomeScreen I get null operator used on null value error.
Here is the toggle logic.
class Testing extends StatefulWidget {
const Testing({super.key});
#override
State<Testing> createState() => _TestingState();
}
class _TestingState extends State<Testing> {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: TodoServiceHelper().checkifLoggedIn(),
builder: ((context, snapshot) {
if (!snapshot.hasData) {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
}
if (snapshot.hasError) {
print(snapshot.hasError);
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
}
if (snapshot.data!.isNotEmpty) {
print(snapshot.data);
return RegisterPage();
// returning HomePage gives null check operator used on null value error
} else
return Login();
}),
);
}
}
Here is the HomeScreen
class HomePage extends StatefulWidget {
String? username;
HomePage({this.username});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final GlobalKey<FormState> formKey = GlobalKey();
TextEditingController termController = TextEditingController();
void clearText() {
termController.clear();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
IconButton(
onPressed: () {
User loginUser =
User(username: widget.username.toString(), isLoggedIn: false);
TodoServiceHelper().updateUserName(loginUser);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (BuildContext context) => Login()));
},
icon: Icon(Icons.logout),
color: Colors.white,
)
],
title: FutureBuilder(
future: TodoServiceHelper().getTheUser(widget.username!),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
}
return Text(
'Welcome ${snapshot.data!.username}',
style: TextStyle(color: Colors.white),
);
}),
),
body: SingleChildScrollView(
child: Column(children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Form(
key: formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: termController,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
enabledBorder: OutlineInputBorder(),
labelText: 'search todos',
),
),
TextButton(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ShowingSerachedTitle(
userNamee: widget.username!,
searchTerm: termController.text,
)),
);
print(termController.text);
clearText();
setState(() {});
},
child: Text(
'Search',
)),
Divider(
thickness: 3,
),
],
),
),
),
],
),
Container(
child: Stack(children: [
Positioned(
bottom: 0,
child: Text(
' done Todos',
style: TextStyle(fontSize: 12),
),
),
IconButton(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
CheckingStuff(userNamee: widget.username!)),
);
setState(() {});
},
icon: Icon(Icons.filter),
),
]),
),
Divider(
thickness: 3,
),
Container(
child: TodoListWidget(name: widget.username!),
height: 1000,
width: 380,
)
]),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Color.fromARGB(255, 255, 132, 0),
onPressed: () async {
await showDialog(
barrierDismissible: false,
context: context,
builder: ((context) {
return AddNewTodoDialogue(name: widget.username!);
}),
);
setState(() {});
},
child: Icon(Icons.add),
),
);
}
}
The function used to return user with loginStatus true
Future<List<User>> checkifLoggedIn() async {
final Database db = await initializeDB();
final List<Map<String, Object?>> result = await db.query(
'users',
where: 'isLoggedIn = ?',
whereArgs: ['1'],
);
List<User> filtered = [];
for (var item in result) {
filtered.add(User.fromMap(item));
}
return filtered;
}
the problem is here
you used ! sign on a nullable String , and this string is nullable,
try to use this operation (??) so make it
widget.username??"" by this line you will check if the user name is null it will be replaced by an empty string.
I'm building a chatapp which displays all the available chats as ChatTiles.
A ChatTile shows the name and the last message from the chat using a Stream from Firebase.
By clicking on a ChatTile the Navigator pushes a ConversationScreen Widget.
I would like to somehow pass the stream data along the widget tree to the ConversationScreen. So whenever a message is inserted, the ChatTile shows the last message and the ConversationScreen as well.
But my error is this:
Stream has already been listened to.
Here's a picture:
ChatTiles
ChatTile:
class _ChatRoomTileState extends State<ChatRoomTile> {
Stream<QuerySnapshot> chatRoomStream;
DataFromMessages dataFromMessages;
List<Message> messages;
#override
void initState() {
chatRoomStream =
DB_Service.streamChatRooms(context, widget.dataFromChatRoom.chatRoomId);
getUser();
super.initState();
}
#override
Widget build(BuildContext context) {
if (chatRoomStream == null) return Container();
return StreamProvider<QuerySnapshot>.value(
value: chatRoomStream,
initialData: null,
builder: (context, f) {
QuerySnapshot snapshot = Provider.of<QuerySnapshot>(context);
if (snapshot == null) return Container();
dataFromMessages =
dataFromMessagesFromJson(json.encode(snapshot.docs.first.data()));
messages = dataFromMessages.messages.entries
.map((e) => e.value)
.toList()
.reversed
.toList();
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ConversationScreen(
dataFromChatRoom: widget.dataFromChatRoom,
uid: widget.uid,
chatRoomStream: chatRoomStream,
),
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Row(
...
),
),
);
},
);
}
}
The pushed ConversationScreen:
class _ConversationScreenState extends State<ConversationScreen> {
TextEditingController messageController = TextEditingController();
DataFromMessages dataFromMessages;
List<Message> messages;
#override
Widget build(BuildContext context) {
print("Building ConversationScreen");
if (widget.chatRoomStream == null) return Container();
return StreamBuilder(
stream: widget.chatRoomStream.asBroadcastStream(),
initialData: null,
builder: (context, snap) {
//QuerySnapshot snap = Provider.of<QuerySnapshot>(context);
if (snap == null) return Text(" empty");
dataFromMessages = dataFromMessagesFromJson(
json.encode(snap.data.docs.first.data()));
messages = dataFromMessages.messages.entries
.map((e) => e.value)
.toList()
.reversed
.toList();
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: buildAppBar(context),
body: Column(
children: [
Expanded(
child: messages != null
? ListView.builder(
shrinkWrap: true,
itemCount: messages.length,
itemBuilder: (context, index) {
return ChatTile(
data:
messages[index].data == widget.uid ? 1 : 0,
message: messages[index].message,
timestamp: messages[index].timestamp,
bubbleNip: BubbleNip.no,
);
})
: Container(),
),
TextInput(
uid: widget.uid,
chatRoomId: widget.dataFromChatRoom.chatRoomId,
messages: messages,
),
],
),
bottomNavigationBar: const SizedBox(height: 50),
),
);
});
}
AppBar buildAppBar(BuildContext context) {
return AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(
"Chat with Somebody",
style: TextStyle(
color: Theme.of(context).indicatorColor,
),
),
leading: BackButton(color: Theme.of(context).indicatorColor),
);
}
}
I'm using the Provider package to manage my apps business logic but I've encountered a problem where my entire ListView is rebuilding instead of an individual ListTile. Here's the UI to give you a better understanding:
Currently if I scroll to the bottom of the list, tap the checkbox of the last item, I see no animation for the checkbox toggle and the scroll jumps to the top of the screen because the entire widget has rebuilt. How do I use Provider so that only the single ListTile rebuilds and not every item in the List?
Here's some of the relevant code:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Checklist',
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.indigo[500],
accentColor: Colors.amber[500],
),
home: ChecklistHomeScreen(),
),
providers: [
ChangeNotifierProvider(
create: (ctx) => ChecklistsProvider(),
),
],
);
}
}
class ChecklistHomeScreen extends StatefulWidget {
#override
_ChecklistHomeScreenState createState() => _ChecklistHomeScreenState();
}
class _ChecklistHomeScreenState extends State<ChecklistHomeScreen> {
void createList(BuildContext context, String listName) {
if (listName.isNotEmpty) {
Provider.of<ChecklistsProvider>(context).addChecklist(listName);
}
}
#override
Widget build(BuildContext context) {
final _checklists = Provider.of<ChecklistsProvider>(context).checklists;
final _scaffoldKey = GlobalKey<ScaffoldState>();
ScrollController _scrollController =
PrimaryScrollController.of(context) ?? ScrollController();
return Scaffold(
key: _scaffoldKey,
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
floating: true,
pinned: false,
title: Text('Your Lists'),
centerTitle: true,
actions: <Widget>[
PopupMenuButton(
itemBuilder: (ctx) => null,
),
],
),
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(ctx, i) => _buildListItem(_checklists[i], i),
childCount: _checklists.length,
),
onReorder: (int oldIndex, int newIndex) {
setState(() {
final checklist = _checklists.removeAt(oldIndex);
_checklists.insert(newIndex, checklist);
});
},
),
],
),
drawer: Drawer(
child: null,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: null,
),
);
}
Widget _buildListItem(Checklist list, int listIndex) {
return Dismissible(
key: ObjectKey(list.id),
direction: DismissDirection.endToStart,
background: Card(
elevation: 0,
child: Container(
alignment: AlignmentDirectional.centerEnd,
color: Theme.of(context).accentColor,
child: Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 0.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
child: Card(
child: ListTile(
onTap: null,
title: Text(list.name),
leading: Checkbox(
value: list.completed,
onChanged: (value) {
Provider.of<ChecklistsProvider>(context)
.toggleCompletedStatus(list.id, list.completed);
},
),
trailing: IconButton(
icon: Icon(Icons.more_vert),
onPressed: null,
),
),
),
onDismissed: (direction) {
_onDeleteList(list, listIndex);
},
);
}
void _onDeleteList(Checklist list, int listIndex) {
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(
SnackBar(
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
Provider.of<ChecklistsProvider>(context)
.undoDeleteChecklist(list, listIndex);
},
),
content: Text(
'List deleted',
style: TextStyle(color: Theme.of(context).accentColor),
),
),
);
}
}
class ChecklistsProvider with ChangeNotifier {
final ChecklistRepository _repository = ChecklistRepository(); //singleton
UnmodifiableListView<Checklist> get checklists => UnmodifiableListView(_repository.getChecklists());
void addChecklist(String name) {
_repository.addChecklist(name);
notifyListeners();
}
void deleteChecklist(int id) {
_repository.deleteChecklist(id);
notifyListeners();
}
void toggleCompletedStatus(int id, bool completed) {
final list = checklists.firstWhere((c) => c.id == id);
if(list != null) {
list.completed = completed;
_repository.updateChecklist(list);
notifyListeners();
}
}
}
I should say I understand why this is the current behavior, I'm just not sure of the correct approach to ensure only the list item I want to update gets rebuilt instead of the whole screen.
I've also read about Consumer but I'm not sure how I'd fit it into my implementation.
A Consumer will essentially allow you to consume any changes made to your change notifier. It's best practice to embed the Consumer as deep down as possible in your build method. This way only the wrapped widget will get re-built. This document explains it well: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
Try wrapping your CheckBox widget in a Consumer widget. Only the checkbox should be rebuilt.
Consumer<ChecklistsProvider>(
builder: (context, provider, _) {
return Checkbox(
value: list.completed,
onChanged: (value) {
provider.toggleCompletedStatus(list.id, list.completed);
},
);
},
),
If you'd rather have the ListTile AND the CheckBox be re-built, just wrap the ListTile in the Consumer instead
I'm working on a "search in a document" app . I want when I type the word it the custom scroll view automatically scrolls to the searched word.
I split my document to bunch of Text spans. how do I get the offset of only one of them. Is that even possible?
I tried but it doesn't work
with AutomaticKeepAliveClientMixin<WordViewPage> {
#override
bool get wantKeepAlive => true;
String previewedText;
ScrollController scrollController;
List<String> splitted = [''];
Color customColor = Colors.transparent;
#override
void initState() {
super.initState();
loadAsset().then((String loadedString) {
setState(() {
previewedText = loadedString;
splitter();
});
});
}
#override
Widget build(BuildContext context) {
super.build(context);
List<Widget> he = [wordFile()];
return Scaffold(
body: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
SliverAppBar(
automaticallyImplyLeading: false,
title: searchBar(),
centerTitle: true,
backgroundColor: Color(0xfffc3b398),
),
SliverPadding(
padding: EdgeInsets.all(10),
sliver: SliverList(
delegate: SliverChildListDelegate(he),
),
)
],
));
}
Future<String> loadAsset() async {
return await rootBundle.loadString('assets/test.txt');
}
You can extend your class with Search Delegate in flutter
Detail explanation and code : SearchDelegate
or you can see my code and see the RichText() widget
#override
Widget buildResults(BuildContext context){
return new FutureBuilder(
future: _getUser(query),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
} else {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) => ListTile(
leading: Icon(Icons.person),
title: Text(snapshot.data[index].fName +
" " +
snapshot.data[index].lName),
title: RichText(
text: TextSpan(
text: suggestionList[index].substring(0, query.length),
style: TextStyle(
color: Colors.black, fontWeight: FontWeight.bold)),
),
onTap: () {
},
),
);
}
},
);
}