I am building a list (dynamically, throught the result of an HTTP request to an API) but I want to add a 'add' card first, before the others elements.
Like this :
I'm building this with a builder
But the way I do it is not good, there's a problem.
Here, account.trips is a list of Trip object and AddNewTrip is the 'add' card.
Because of my if, the first element of the list is always not displayed
So, is there another way to put my 'add' card as the first element of the list, without hiding one element ?
return Builder(
builder: (BuildContext context) {
if (account.trips.indexOf(i) == 0) {
return new AddNewTrip(account);
}
return Padding(
padding: const EdgeInsets.only(
top: 20.0, bottom: 40.0, left: 5.0, right: 5.0),
child: Stack(
...
EDIT : here is the full code :
Expanded(
child: ScopedModelDescendant<Account>(
builder: (context, child, _account){
print(_account.profile);
return new CarouselSlider(
enableInfiniteScroll: false,
enlargeCenterPage: true,
height: MediaQuery.of(context).size.height,
initialPage: 1,
items: itemBuilder(_account)
);
},
),
)
List<Widget> itemBuilder(Account account) {
if (account.trips != null)
return account.trips.map((i) {
return Builder(
builder: (BuildContext context) {
if (account.trips.indexOf(i) == 0) {
return new AddNewTrip(account);
}
return Padding(
padding: const EdgeInsets.only(
top: 20.0, bottom: 40.0, left: 5.0, right: 5.0),
child: Stack(
children: <Widget>[
Hero(
tag: i.id,
child: Material(
type: MaterialType.transparency,
child: InkWell(
child: Container(
height: 200,
margin: EdgeInsets.symmetric(horizontal: 5.0),
decoration: cardImage(i.imageUrl),
),
borderRadius: BorderRadius.circular(20.0),
onTap: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
return TripDetails(i);
}));
},
),
),
),
Positioned(
width: MediaQuery.of(context).size.width * 0.7,
height: MediaQuery.of(context).size.height * 0.48,
top: MediaQuery.of(context).size.height * 0.2,
left: MediaQuery.of(context).size.width * 0.035,
child: Hero(
tag: i.id + 'container', //should be an ID
child: Material(
type: MaterialType.transparency,
child: Container(
decoration: cardDecoration,
child: new TripContent(i, true),
),
),
),
)
],
),
);
},
);
}).toList();
else
return [
new AddNewTrip(account)
];
}
The previous answers already provide a good general advice on how to do this.
For your specific example you would have to do something like this:
When looping over your accounts, prepend the result with your "Add" card like this:
List<Widget> itemBuilder(Account account) {
return [
new AddNewTrip(account),
...(account.trips?.map((i) {/** same as before without your index check **/}) ?? []),
];
}
return account.trips.map((i) {
change to:
return [[null], account.trips].expand((x) => x).toList().map((i) {
how about this you build the card first then build rest of your list..
List<Widget> itemBuilder(Account account) {
return [
AddNewTrip(account),
if (account.trips != null)
return account.trips.map((i) {
return Builder(
builder: (BuildContext context) {
return Padding(
//rest of your code
);
});
);
}];}
You can add a dummy list item in your list at 0th index and later you can perform your logic.
Below is sample code can help you out:
var list = ["1","3","4"];
list.insert(0,"5");
Output:5 1 3 4
Related
I am currently modifying my app to support large devices. In this app I want to have a list of categories at the left and when I tap the list tile I want to show the list of the items which the category holds on the left. By the approach I am using the list updates in memory but doesnt change the UI. This is my current approach ->
class _OrderCategoryScreenState extends State<OrderCategoryScreen> {
List<FoodItem> filteredLists = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Row(
children: [
ValueListenableBuilder<Box<FoodCategory>>(
valueListenable: Boxes.getFoodCategories().listenable(),
builder: (context, box, child) {
final categories = box.values.toList().cast<FoodCategory>();
return categories.length == 0
? Container(
color: Color(0xFFFCF9F9),
height: double.infinity,
width: MediaQuery.of(context).size.height * 0.30,
child: Center(
child: Text("No Categories"),
))
: Container(
height: double.infinity,
width: MediaQuery.of(context).size.height * 0.35,
child: Drawer(
backgroundColor: Color(0xFFFCF9F9),
elevation: 0,
child: ListView(
children: <Widget>[
DrawerHeader(
child: ListView(
children: [
Container(
height: MediaQuery.of(context).size.height,
child: ValueListenableBuilder<
Box<FoodCategory>>(
builder: (context, value, child) {
final foodCategories = box.values
.toList()
.cast<FoodCategory>();
return ListView.builder(
itemCount: foodCategories.length,
itemBuilder: (context, index) {
return ListTile(
tileColor: Colors.green,
onTap: () {
filteredLists = Boxes
.getFoodItems()
.values
.where((element) =>
element.categoryName ==
foodCategories[index]
.name)
.toList();
print(filteredLists.length);
print("object");
},
title: Text(
foodCategories[index].name),
);
},
);
},
valueListenable: Boxes.getFoodCategories()
.listenable(),
),
)
],
),
),
],
),
));
},
),
Container(
width: MediaQuery.of(context).size.width * 0.5,
height: MediaQuery.of(context).size.height * 0.5,
color: Colors.amberAccent,
child: filteredLists.isEmpty
? Center(
child: Text("Choose A Category"),
)
: ListView.builder(itemCount: filteredLists.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(filteredLists[index].itemName),
);
},
),
),
],
)
),
);
}
}
When I hot reload using this approach it works on the UI as well.
Please Help.
Try calling setState to update the UI.
onTap: () {
filteredLists = Boxes
.getFoodItems()
.values
.where((element) =>
element.categoryName ==
foodCategories[index]
.name)
.toList();
print(filteredLists.length);
print("object");
setState((){}); //this
},
When I transition to a screen where I get a list of information via an API, it initially gives an error:
_CastError (Null check operator used on a null value)
and only after loading the information, the screen is displayed correctly.
I am declaring the variables like this:
#observable
ObservableFuture<Model?>? myKeys;
#action
getKeys() {
myKeys = repository.getKeys().asObservable();
}
How can I enter the page only after loading the information?
In button action I tried this but to no avail!
await Future.wait([controller.controller.getKeys()]);
Modular.to.pushNamed('/home');
This is the page where the error occurs momentarily, but a short time later, that is, when the api call occurs, the data appears on the screen.
class MyKeyPage extends StatefulWidget {
const MyKeyPage({Key? key}) : super(key: key);
#override
State<MyKeyPage> createState() => _MyKeyPageState();
}
class _MyKeyPageState
extends ModularState<MyKeyPage, KeyController> {
KeyController controller = Modular.get<KeyController>();
Widget countKeys() {
return FutureBuilder(
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
final count =
controller.myKeys?.value?.data!.length.toString();
if (snapshot.connectionState == ConnectionState.none &&
!snapshot.hasData) {
return Text('..');
}
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: 1,
itemBuilder: (context, index) {
return Text(count.toString() + '/5');
});
},
future: controller.getCountKeys(),
);
}
#override
Widget build(BuildContext context) {
Size _size = MediaQuery.of(context).size;
return controller.getCountKeys() != "0"
? TesteScaffold(
removeHorizontalPadding: true,
onBackPressed: () => Modular.to.navigate('/exit'),
leadingIcon: ConstantsIcons.trn_arrow_left,
title: '',
child: Container(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.only(left: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Keys',
style: kHeaderH3Bold.copyWith(
color: kBluePrimaryTrinus,
),
),
countKeys(),
],
),
),
),
body: Observer(builder: (_) {
return Padding(
padding: const EdgeInsets.only(bottom: 81),
child: Container(
child: ListView.builder(
padding: EdgeInsets.only(
left: 12.0,
top: 2.0,
right: 12.0,
),
itemCount:
controller.myKeys?.value?.data!.length,
itemBuilder: (context, index) {
var typeKey = controller
.myKeys?.value?.data?[index].type
.toString();
var id =
controller.myKeys?.value?.data?[index].id;
final value = controller
.myKeys?.value?.data?[index].value
.toString();
return GestureDetector(
onTap: () {
.
.
},
child: CardMeyKeys(
typeKey: typeKey,
value: value!.length > 25
? value.substring(0, 25) + '...'
: value,
myKeys: pixController
.minhasChaves?.value?.data?[index].type
.toString(),
),
);
},
),
),
);
}),
bottomSheet: ....
)
: TesteScaffold(
removeHorizontalPadding: true,
onBackPressed: () => Modular.to.navigate('/exit'),
leadingIcon: ConstantsIcons.trn_arrow_left,
title: '',
child: Container(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.only(left: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'...',
style: kHeaderH3Bold.copyWith(
color: kBluePrimaryTrinus,
),
),
],
),
),
),
body: Padding(
padding: const EdgeInsets.only(bottom: 81),
child: Container(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.asset(
'assets/images/Box.png',
fit: BoxFit.cover,
width: 82.75,
height: 80.91,
),
SizedBox(
height: 10,
),
],
),
), //Center
),
),
bottomSheet: ...
);
}
List<ReactionDisposer> disposers = [];
#override
void initState() {
super.initState();
controller.getKeys();
}
#override
void dispose() {
disposers.forEach((toDispose) => toDispose());
super.dispose();
}
}
Initially the error occurs in this block
value: value!.length > 25
? value.substring(0, 25) + '...'
: value,
_CastError (Null check operator used on a null value)
I appreciate if anyone can help me handle ObservableFuture correctly!
You need to call the "future" adding
Future.wait
(the return type of getKeys) keys=await Future.wait([
controller.getKeys();
]);
The problem is your getKeys function isn't returning anything, so there's nothing for your code to await. You need to return a future in order to await it.
Future<Model?> getKeys() {
myKeys = repository.getKeys().asObservable();
return myKeys!; // Presumably this isn't null anymore by this point.
}
...
await controller.controller.getKeys();
Modular.to.pushNamed('/home');
I'm trying to implement page with Google map and i put ListView over this map. When i scroll this vertical ListView, map is actually scrolls too. Is there any way to prevent this behaviour and scroll map only when cursor located out of listview boundaries? Thanks[![Scroll conflict behind listview and google map][1]][1]
/// Layout that describes part with ListView
Positioned.fill(
child: Align(
alignment: Alignment.bottomRight,
child: sessions.maybeWhen(
orElse: () => const SizedBox.shrink(),
data: (items) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: items.length <= 3
? items.length * 130
: mediaQuery.size.width < 720 ? 150 : mediaQuery.size.height * 0.7,
),
child: ScrollablePositionedList.separated(
itemScrollController: controller,
scrollDirection: mediaQuery.size.width < 720
? Axis.horizontal
: Axis.vertical,
padding: EdgeInsets.all(mediaQuery.size.width < 720 ? 5 : 20.0),
itemCount: items.length,
separatorBuilder: (_, __) => mediaQuery.size.width < 720
? const SizedBox(width: 6.0)
: const SizedBox(height: 6.0),
itemBuilder: (_, i) {
return AdaptiveLayout(
smallLayout: Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: mediaQuery.size.width - 32,
height: 100.0,
child: SessionCard(
session: items[i],
selected: selected == items[i],
onPressed: () {
if (selected == items[i]) {
onSelected?.call(null, null);
} else {
onSelected?.call(items[i], i);
}
},
),
),
),
largeLayout: FractionallySizedBox(
widthFactor: 0.4,
alignment: Alignment.bottomRight,
child: SessionCard(
session: items[i],
selected: selected == items[i],
onPressed: () {
if (selected == items[i]) {
onSelected?.call(null, null);
} else {
onSelected?.call(items[i], i);
}
},
),
),
);
},
),
);
},
),
),
);
///Part with Stack that contains map and ListView
AdaptiveLayout(
smallLayout: Scaffold(
body: Stack(
children: [
MapWidget(
onMapCreated: onMapCreated,
onMarkerSelected: onSessionSelected,
),
// const SearchBar(),
MapToolbar(
controller: mapController.value,
handlePermission: () async {
final provider = ref.read(positionProvider.notifier);
if (await (provider.handlePermission(context))) {
provider.getCurrentLocation();
}
},
),
ValueListenableBuilder<dynamic>(
valueListenable: selectedSession, /// Contains ListView
builder: (_, value, __) {
return SessionCards(
onSelected: onSessionSelected,
selected: value,
controller: scrollController.value,
);
},
),
ValueListenableBuilder<bool>(
valueListenable: showLoading,
builder: (_, value, __) {
return LoadingOverlay(
showing: value,
);
},
),
],
),
),
I am building a very simple to-do list and I would like to divide it into two groups: the pending tasks above and the completed tasks below by marking a title that clearly separates them.
Then, I generated two lists (_pendientesList and _completasList) and I put a ListView.builder on each one, however when I display the Column a first Scroll of _completasList is observed, then the Text of "Complete" and then a second Scroll of _pendientesList. What I would like to achieve is that the three components are part of the same Scroll.
The code to generate the list from a StreamBuilder is the following:
Widget _crearListado(BuildContext context, AlarmaBloc alarmaBloc) {
final _size = MediaQuery.of(context).size;
List<AlarmaModel> _completasList = new List();
List<AlarmaModel> _pendientesList = new List();
return Column(
children: <Widget>[
Container(
height: _size.height * 0.5,
child: Padding(
padding: const EdgeInsets.only(bottom: 70.0/2, left: 10.0, right: 10.0, top:0.0), //fondo gris del listado. Bottom
child: StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
if (tareasList.length == 0) return _imagenInicial(context);
tareasList.sort((a, b) => a.fechaVencimiento.compareTo(b.fechaVencimiento));
_completasList = tareasList.where((element) => element.completa==true).toList();
_pendientesList = tareasList.where((element) => element.completa==false).toList();
return Column(
children: <Widget>[
Expanded(
child: ListView.builder(
itemCount: _pendientesList.length,
itemBuilder: (context, i) =>_crearItem(context, alarmaBloc, _pendientesList[i]),
padding: EdgeInsets.only(left: 10.0, right: 10.0, top: 10.0, bottom: 20.0),
),
),
SizedBox(height: 10.0,),
Text('Completas '),
Expanded(
child: ListView.builder(
itemCount: _completasList.length,
itemBuilder: (context, i) =>_crearItem(context, alarmaBloc, _completasList[i]),
padding: EdgeInsets.only(left: 10.0, right: 10.0, top: 10.0, bottom: 20.0),
),
),
],
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center (child: Image(image: AssetImage('Preloader.gif'), height: 200.0,));
},
),
),
),
],
);
}
I tried wrapping the Column in a SingleChildScrollView, but it didn't work.
How can I have both ListView.builder and the text (or button in the future) within the same Scroll?
Instead of two ListView.builder, you can do somewhat like this. The two are on the same scroll listview all the widgets pendient is above widgets completas.
ListView(
children: [
for (var itemPendiente in _pendientesList)
_crearItem(context, alarmaBloc, itemPendiente),
for (var itemCompletas in _completasList)
_crearItem(context, alarmaBloc, itemCompletas),
],
);
I try to put a Corousel Slider but with a List that is updated automatically in another part of the app.
The documentation for the slider is with static photos. I want to access to the items inside a list and iterate.
var itemss = [];
for (var e = 0; e < model.listFireforce.length; e++) {
itemss.add(model.listFireforce[e].image);
}
return CarouselSlider(
height: 350.0,
items: [ itemss // HERE IS THE PROBLEM !!
].map((i) {
return Builder(
builder: (BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
margin: EdgeInsets.symmetric(horizontal: 2.0),
decoration: BoxDecoration(color: Colors.white),
child:
Image.network(
i,
fit: BoxFit.cover,
));
},
);
}).toList());
i itearate only 1 time, but no for all the items inside. I try forEach() but i have no solution.
Thanks!
Your problem is that you wrap your itemss array in another array and map over that. You may want to try this:
return CarouselSlider(
height: 350.0,
items: itemss.map((i) {
return Builder(
builder: (BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
margin: EdgeInsets.symmetric(horizontal: 2.0),
decoration: BoxDecoration(color: Colors.white),
child: Image.network(
i,
fit: BoxFit.cover,
));
},
);
}).toList(),
);