I have a SliverGrid. I have a search field. In my search field onChange event I have a function that searches my local sqlite db based on the keyword entered by the user returns the results and reassigns to a variable and calls notifyListeners(). Now my problem is for some weird reason whenever I search for an item the wrong item is rendered.
I checked the results from my functions by iterating over the list and logging the title and the overall count as well and the results were correct however my view always rendered the wrong items. Not sure how this is possible.
I also noticed something strange, whenever it rendered the wrong item and I went back to my code and hit save, triggering live reload, when I switched back to my emulator it now displayed the right item.
I have tried the release build on an actual phone and it's the same behaviour. Another weird thing is sometimes certain items will duplicate and show twice in my list while the user is typing.
This is my function that searches my sqlite db:
Future<List<Book>> searchBookshelf(String keyword) async {
try {
Database db = await _storageService.database;
final List<Map<String, dynamic>> rows = await db
.rawQuery("SELECT * FROM bookshelf WHERE title LIKE '%$keyword%'; ");
return rows.map((i) => Book.fromJson(i)).toList();
} catch (e) {
print(e);
return null;
}
}
This is my function that calls the above function from my viewmodel:
Future<void> getBooksByKeyword(String keyword) async {
books = await _bookService.searchBookshelf(keyword);
notifyListeners();
}
This is my actual view where i have the SliverGrid:
class BooksView extends ViewModelBuilderWidget<BooksViewModel> {
#override
bool get reactive => true;
#override
bool get createNewModelOnInsert => true;
#override
bool get disposeViewModel => true;
#override
void onViewModelReady(BooksViewModel vm) {
vm.initialise();
super.onViewModelReady(vm);
}
#override
Widget builder(BuildContext context, vm, Widget child) {
var size = MediaQuery.of(context).size;
final double itemHeight = (size.height) / 4.3;
final double itemWidth = size.width / 3;
var heading = Container(
margin: EdgeInsets.only(top: 35),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Align(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Books',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w900),
),
Text(
'Lorem ipsum dolor sit amet.',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 14),
),
],
),
),
);
var searchField = Container(
margin: EdgeInsets.only(top: 5, left: 15, bottom: 15, right: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 1.0,
spreadRadius: 0.0,
offset: Offset(2.0, 1.0), // shadow direction: bottom right
),
],
),
child: TextFormField(
decoration: InputDecoration(
border: InputBorder.none,
prefixIcon: Icon(
FlutterIcons.search_faw,
size: 18,
),
suffixIcon: Icon(
FlutterIcons.filter_fou,
size: 18,
),
hintText: 'Search...',
),
onChanged: (keyword) async {
await vm.getBooksByKeyword(keyword);
},
onFieldSubmitted: (keyword) async {},
),
);
return Scaffold(
body: SafeArea(
child: Container(
padding: EdgeInsets.only(left: 1, right: 1),
child: LiquidPullToRefresh(
color: Colors.amber,
key: vm.refreshIndicatorKey, // key if you want to add
onRefresh: vm.refresh,
showChildOpacityTransition: true,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
heading,
searchField,
],
),
),
SliverToBoxAdapter(
child: SpaceY(15),
),
SliverToBoxAdapter(
child: vm.books.length == 0
? Column(
children: [
Image.asset(
Images.manReading,
width: 250,
height: 250,
fit: BoxFit.contain,
),
Text('No books in your bookshelf,'),
Text('Grab a book from our bookstore.')
],
)
: SizedBox(),
),
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid.count(
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
crossAxisCount: 3,
children: vm.books
.map((book) => BookTile(book: book))
.toList(),
),
)
],
),
))));
}
#override
BooksViewModel viewModelBuilder(BuildContext context) =>
BooksViewModel();
}
Now the reason I am even using SliverGrid in the first place is because I have a search field and a title above the grid and I want all items to scroll along with the page, I didn't want just the list to be scrollable.
I believe this odd behavior can be attributed to you calling vm.getBooksByKeyword() in onChanged. As this is an async method, there is no guarantee that the last result returned will be the result for the final text in the TextFormField. The reason you see the correct results after a live reload is because the method is being called again with the full text currently in the TextFormField.
The quickest way to verify this is to move the function call to onFieldSubmitted or onEditingComplete and see if it behaves correctly.
If you require calling the function with every change to the text, you will need to add a listener to the controller and be sure to only make the call after input has stopped for a specified amount of time, using a Timer, like so:
final _controller = TextEditingController();
Timer _timer;
...
_controller.addListener(() {
_timer?.cancel();
if(_controller.text.isNotEmpty) {
// only call the search method if keyword text does not change for 300 ms
_timer = Timer(Duration(milliseconds: 300),
() => vm.getBooksByKeyword(_controller.text));
}
});
...
#override
void dispose() {
// DON'T FORGET TO DISPOSE OF THE TextEditingController
_controller.dispose();
super.dispose();
}
...
TextFormField(
controller: controller,
...
);
So I found the problem and the solution:
The widget tree is remembering the list items place and providing the
same viewmodel as it had originally. Not only that it also takes every
item that goes into index 0 and provides it with the same data that
was enclosed on the Construction of the object.
Taken from here.
So basically the solution was to add and set a key property for each list item generated:
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
),
delegate: SliverChildListDelegate(vm.books
.map((book) => BookTile(
key: Key(book.id.toString()), book: book))
.toList()),
),
)
And also here:
const BookTile({Key key, this.book}) : super(key: key, reactive: false);
My search works perfectly now. :)
Related
I'm trying to add Applovin Interstitial ads to flutter app. But it keep showing errors. my sdk is sdk:'>=2.12.0 <3.0.0'.
This are the errors displayed
A value of type 'bool?' can't be assigned to a variable of type 'bool'.
Try changing the type of the variable, or casting the right-hand type to 'bool'.
The argument type 'dynamic Function(AppLovinAdListener)' can't be assigned to the parameter type 'dynamic Function(AppLovinAdListener?)'.
This is the sample code:
const CollectionCard();
#override
State<CollectionCard> createState() => _CollectionCardState();
}
class _CollectionCardState extends State<CollectionCard> {
AppLovinListener? get listener => null;
void initState() {
FlutterApplovinMax.initInterstitialAd('91b26a7777e1b455');
super.initState();
}
bool isInterstitialVideoAvailable = false;
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
/*24 is for notifications bar on Android */
final double itemHeight = (size.height - kToolbarHeight - 28) / 2;
final double itemWidth = size.width / 4;
return Container(
child: Padding(
padding: const EdgeInsets.fromLTRB(10.0, 0.0, 10.0, 10.0),
child: Column(
children: <Widget>[
GridView.count(
primary: true,
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
crossAxisSpacing: 10, //Reduce Horizontal Spacing
mainAxisSpacing: 10, //Reduce Vertical Spacing
crossAxisCount: 3,
physics: ScrollPhysics(),
childAspectRatio: (6 / 8),
// (itemWidth / itemHeight),
shrinkWrap: true,
children: <Widget>[
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
elevation: 2,
color: Theme.of(context).scaffoldBackgroundColor,
child: InkWell(
onTap: () async {
isInterstitialVideoAvailable =
await FlutterApplovinMax.isInterstitialLoaded (listener);
if (isInterstitialVideoAvailable) {
FlutterApplovinMax.showInterstitialVideo(
(AppLovinAdListener event) => listener (event));
}
Navigator.push(
context,
MaterialPageRoute(
builder: (ctx) => LearnPage(),
),
);
},
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ImageIcon(
AssetImage('assets/icons/learn.png'),
color: kLightPrimary,
size: 60,
), // Icon(
// layout.icon,
// size: 40,
// ),
Text(
'Learn',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
Below is an image of the code error
bool is a strict boolean - it can be true or false, nothing else.
bool? is a loose boolean or also called nullable - it can be true, false or null. Same goes for the function. The Plugin 'AppLovin' I assume you are using or the package, or the function you declared, doesn't support null-safety, which means that it's functions can return null. In order to solve the issue, you need to make the types nullable by adding a question mark after the type declaration (i.e. bool a; will become - bool? a; and make sure you don't call a method on a null. Since your variables and methods can return null, the editor will inform you before hand that this (the variable) might be null and calling a method on it will throw a MethodNotFoundException. Therefore, you should add your own logic to assure that this doesn't happen and satisfy the compiler by adding null-checks (exclamation marks - '!') to method calls which works as a form of asserting the compiler that this value isn't null, despite it being nullable. If it so happens that you do call a null on a value with a null check, at run time (after you press the run and load your page) you will get the following exception - Null-check used on a null value.
let's say I have a controller like this:
class ProfileController extends GetxController {
Rx<UserFacebookInfo?> facebookInfo = null.obs;
void facebookSync() async {
//
// logic to get user info from facebook
//
facebookInfo.value = UserFacebookInfo.fromFacebookApi(userData);
// facebookInfo = UserFacebookInfo.fromFacebookApi(userData).obs; <=== also tried this
update();
}
}
}
and in widget I have something like this:
Widget buildFacebook() => Padding(
padding: const EdgeInsets.only(top: 30.0, right: 20.0, left: 20.0, bottom: 10.0),
child: Obx(() => (_profileController.facebookInfo.value == null) ? Column(
children: [
IconButton(
icon : const Icon(Icons.facebook_outlined, size: 40.0),
onPressed: () => _profileController.facebookSync()
),
const Text(
'Facebook',
style: TextStyle(color: Colors.white),
)
],
) :
Column(
children: [
UserProfileAvatar(
avatarUrl: _profileController.facebookInfo.value!.facebookAvatar,
radius: 40,
),
Text(
_profileController.facebookInfo.value!.name,
style: const TextStyle(color: Colors.white),
)
],
)
));
and because initial value nullable, it's not working and not changing widget on a fly, but only if I update it from android studio. What is the correct way to initialize it??? I had similar case with nullable string observable so I was able to initialize it lie String string (null as String?).obs` Thanks for any advice
You can initialize nullable observables by using Rxn<Type>().
Therefore use this:
final facebookInfo= Rxn<UserFacebookInfo>();
I have a code whereby the genders of a pet is listed in two separate cards and when the user taps on one of them, it changes color to indicate that it has been selected and is saved in the database. However, the app is letting the user continue to the next page without choosing any one of the values. I want to do a validation whereby the user will have to choose one of the cards to be able to move forward. How can I do this please?
Here is my code:
Expanded(
child: GridView.count(
crossAxisCount: 2,
primary: false,
scrollDirection: Axis.vertical,
children: List.generate(petGenders.length, (index) {
return GestureDetector(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
color:
selectedIndex == index ? primaryColor : null,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
petGenders[petKeys[index]],
SizedBox(
height: 15.0,
),
Text(
petKeys[index],
style: TextStyle(
color: selectedIndex == index
? Colors.white
: null,
fontSize: 18.0,
fontWeight: FontWeight.w600),
),
],
),
),
),
onTap: () {
setState(() {
widget.pet.gender = petKeys[index];
selectedIndex = index;
});
});
}),
),
),
The model:
Map<String, Image> genders() => {
"Male":
Image(image: AssetImage('Assets/images/male.png'), width: 50),
"Female":
Image(image: AssetImage('Assets/images/female.png'), width: 50)
};
Take one variable
bool isGenderSelected = false;
Then change its value to true on tap of card like
onTap: () {
setState(() {
isGenderSelected = true;
widget.pet.gender = petKeys[index];
selectedIndex = index;
});
});
Now check if it's true then only allow the user to go next page or show some message to the user
Scenario like this, I prefer using nullable selectedValue. In this case, I will create nullable int to hold and switch between selection.
int? selectedIndex;
And using color will be like
color: selectedIndex==index? SelectedColor:null,
you can replace null with inactive color.
For validation part, do null check on selectedIndex .
if(selectedIndex!=null){.....}
I have an array which i set as a class like this
class FilterArray {
static var FilterArrayData = [];
}
I am simply adding the values in an array. Issue is i am calling this array in a page when array is null. Then on next Page i am adding values in array. Now issue is when i come back in previous page the array is still null. I need to refresh page for this. Which i dont want thats why i use FutureWidget i though from Future widget when array update it will also update in my screen but thats not working. Need to know what can i do for this here i need to update data when array is update so it can show in a Future Widget.
This is my total code
class _SearchPgState extends State<SearchPg> {
Future getData() async {
var result = FilterArray.FilterArrayData;
if (result.length != 0) {
return result;
} else {
return null;
}
}
#override
Widget build(BuildContext context) {
print(FilterArray.FilterArrayData);
return Scaffold(
appBar: AppBar(
title: Container(
height: 50.0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0),
child: Center(
child: TextField(
onTap: () => Get.to(SearchPgExtra()),
readOnly: true,
decoration: InputDecoration(
hintText: tr('search.search'),
alignLabelWithHint: true,
hintStyle: Theme.of(context).textTheme.subtitle2,
prefixIcon: Icon(Icons.search),
),
),
),
),
),
actions: [
IconButton(
icon: Icon(
FlutterIcons.sort_descending_mco,
color: Theme.of(context).accentColor,
),
onPressed: navigateToSortPage,
),
IconButton(
icon: Icon(
FlutterIcons.filter_fea,
color: Theme.of(context).primaryColor,
),
onPressed: navigateToFilterPage,
),
],
),
body: FutureBuilder(
future: getData(), // async work
builder: (context, projectSnap) {
print(projectSnap.data);
if (projectSnap.hasData) {
return StaggeredGridView.countBuilder(
itemCount: projectSnap.data.length,
crossAxisCount: 4,
staggeredTileBuilder: (int index) => StaggeredTile.fit(2),
mainAxisSpacing: 15.0,
crossAxisSpacing: 15.0,
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 18.0),
itemBuilder: (context, index) {
var product = projectSnap.data[0][index];
return FadeInAnimation(
index,
child: ProductCard2(
product: product,
isHorizontalList: false,
),
);
},
);
} else {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/search.png',
width: MediaQuery.of(context).size.width / 2,
),
SizedBox(height: 15.0),
Text(
'search.title',
style: Theme.of(context).textTheme.headline1,
).tr(),
SizedBox(height: 15.0),
Text(
'search.subtitle',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1,
).tr(),
SizedBox(
height: MediaQuery.of(context).size.height / 5,
),
],
),
);
}
},
),
);
}
}
In start array is null then ill add values in array then comeback nothing change then i reload the screen then its working fine.
This is the how i am adding array
RangeSlider(
values: _currentRangeValues,
min: 0,
max: 10000,
divisions: 10,
labels: RangeLabels(
_currentRangeValues.start.round().toString(),
_currentRangeValues.end.round().toString(),
),
onChanged: (RangeValues values) {
setState(() {
_currentRangeValues = values;
//print(_currentRangeValues);
});
var data = searchArray.searchArrayData;
for (int i = 0; i < data.length; i++) {
var current = data[i];
if(current['Price'] >= _currentRangeValues.start && current['Price'] <= _currentRangeValues.end){
print(data);
FilterArray.FilterArrayData.add(data);
}
}
},
),
when data add to FilterArrayData ill go back on Page array on that page not updating so then i change the page and comeback again in SearchPg then i can see data
Don't do your validation with the length of your array. It is like trying to do a validation with something that doesn't existe yet. Instead of that, try using
if(snapshot.hasData)
{ return ... ; }
then, after that, now you can do another validation, for instance, sometimes what you receive is data, but an empty array. There is where I would place the other two options. Remember, inside of the first if.
if(array.isNotEmpty)
{ return ... ; }
and
else
{ return ... ; }
After the first if, then you can now also validate, what will happen if you didn't receive data at all. Simply with an else.
else
{ return ... ; }
In summary: use one first validation with hasData and then, inside of that, decide what to do with the received information. Outside all that, decide what to do if you didn't receive any information at all.
Such cases are faced by new developers often. The best way to deal with it is state management packages like Provider, Bloc, etc. Visit the link and you will find all the relevant packages. Personally, I have used Provider a lot. Bloc is also a good option. A lot of people use it. But I haven't had the chance to use it. Riverpod is an up and coming package. But it still requires a lot of fixing.
I have this Widget to register. Inside I want to ask for 6 inputs to register, but as not too much space on the screen, I splitted in 2 pair of 3. I show three at first in a form and when the user press the continue button I show the other 3. However, when I press the continue button, the new 3 pair of TextField appear with the same value of the previous ones. And they move position a little under. I don't know why it happens since each of those 6 fields is different Widget function.
I created two variables form1 and form2 to hold the different forms
#override
void initState() {
super.initState();
form1 = <Widget>[
firstForm(),
Text("Or Sign Up with social media"),
SizedBox(
height: 20,
),
socialMediaButtons(),
SizedBox(
height: 50,
),
Text("Have an account? Login")
];
form2 = <Widget>[
secondForm(),
Text("Or Sign Up with social media"),
SizedBox(
height: 20,
),
socialMediaButtons(),
SizedBox(
height: 50,
),
Text("Have an account? Login")
];
}
All the text field have the same format as the text field below, I only changed the variable for their respecting field.
Widget firstNameField() {
return TextFormField(
initialValue: "",
decoration: InputDecoration(
contentPadding: EdgeInsets.only(top: 10, left: 20, right: 20),
border: InputBorder.none,
hintText: "First Name",
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 15)),
onChanged: (val) {
setState(() => firstName = val);
},
);
}
I combined the text fields in two widgets (firstForm and secondForm). (Shown firstForm but it is the same format as second, just called the functions for the other widgets).
Widget firstForm() {
return Column(
children: <Widget>[
emailPassField(),
SizedBox(
height: 50,
),
continueButton(),
SizedBox(
height: 50,
),
],
);
}
Then this is the continue button widget which when pressed. show the second form. I change the step variable to 2 to go to the second form.
Widget continueButton() {
return ButtonTheme(
minWidth: 185.0,
height: 48.0,
child: RaisedButton(
color: Colors.black,
textColor: Colors.white,
child: Text("continue"),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(50.0)),
onPressed: () => setState(() => step = 2),
));
}
When the variable step is changed, I created this function (getForm) to be called and to show the correct form array variable for the children of the column.
#override
Widget build(BuildContext context) {
return Material(
child: Expanded(
child: Stack(
children: <Widget>[
// banner with picture
Positioned(
child: banner(),
),
// Login Elements Container
Positioned(
child: Container(
margin: EdgeInsets.only(top: 300.0),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 5,
blurRadius: 20,
offset: Offset(0, 0))
],
borderRadius: BorderRadius.only(
topRight: Radius.circular(50),
topLeft: Radius.circular(50))),
child: Center(
child: Column(
children: getForm(step),
),
),
),
)
],
),
),
);
}
//functions for switching forms
getForm(int form) {
if (form == 1) {
return form1;
} else if (form == 2) {
return form2;
}
}
This is how the first step of the form appear.
first form
If I don't enter any data in the text fields and press the continue button, the second form with the correct text fields will appear as shown in this below image. You can see that they have the correct hint text.
second form
However if I enter some data on the first step of the form (seen in second form with data step 1), and then press the continue button, in the second step, the text fields will move down a little bit and the same value entered in the previous text fields will appear in the others too(second form with data step 2). can someone help me please, I don't what's going on there? I hope you understand the code and be able to help me please.
second form with data step 1
second form with data step 2
You need to create a TextEditingController for each TextFormField.
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
...
final _cityController = TextEditingController();
...
// initState
...
// dispose of all TextEditingControllers
#override
void dispose {
_emailController.dispose();
...
_cityController.dispose();
super.dispose();
}
// do this for every TextFormField
Widget firstNameField() {
return TextFormField(
// pass the corresponding controller, no need to set initial value if empty
controller: _firstNameController,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(top: 10, left: 20, right: 20),
border: InputBorder.none,
hintText: "First Name",
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 15)),
onChanged: (val) {
setState(() => firstName = val);
},
);
}
Use a unique TextEditingcontroller for each textfield...by this way the values won't overlap each others textfield value.