ListView in flutter with sticky side headers - flutter

How to implement a ListView with alphabet shown as sticky side headers, similarly to the Telegram application?
For example, while we look at the countries that begin with the letter "E", the letter "E" is not scrolling as long as there are countries that begin with the letter "F". Then the letter "E" is changed to the letter "F".

Solution with Sticky Side Header
You have a package for that! The flutter_sticky_header package.
Interesting points about the solution:
I used the diacritic package to remove the diacritics when indexing the list of Persons. You wouldn't want to exclude all of Norway's Øyvind, would you?
I used a SplayTreeMap to keep the Map sorted.
I used Flutter Hooks for the Persons mapping, happening only when contacts is changed.
Full source code
import 'dart:collection';
import 'package:diacritic/diacritic.dart';
import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
part '66542479.alphabet.freezed.dart'; // File generated by freezed
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: DirectoryPage(contacts: dummyData),
),
);
}
class DirectoryPage extends HookWidget {
final List<Person> contacts;
const DirectoryPage({Key key, this.contacts}) : super(key: key);
#override
Widget build(BuildContext context) {
final _mappedContacts = useState<SplayTreeMap<String, List<Person>>>(null);
useEffect(() {
_mappedContacts.value = contacts.fold<SplayTreeMap<String, List<Person>>>(
SplayTreeMap<String, List<Person>>(),
(acc, curr) {
final firstChar = removeDiacritics(curr.name)[0];
acc..[firstChar] ??= [];
return acc..[firstChar].add(curr);
},
);
return;
}, [contacts]);
return Scaffold(
body: CustomScrollView(
slivers: _mappedContacts.value.keys
.map(
(firstChar) => _StickyHeaderList(
firstChar: firstChar,
persons: _mappedContacts.value[firstChar]),
)
.toList(),
),
);
}
}
class _StickyHeaderList extends StatelessWidget {
final String firstChar;
final List<Person> persons;
const _StickyHeaderList({
Key key,
this.firstChar,
this.persons,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return SliverStickyHeader(
overlapsContent: true,
header: _SideHeader(text: firstChar),
sliver: SliverPadding(
padding: const EdgeInsets.only(left: 60),
sliver: SliverList(
delegate: SliverChildListDelegate([
...persons.map((person) => PersonTile(person: person)).toList(),
Divider(),
]),
),
),
);
}
}
class _SideHeader extends StatelessWidget {
final String text;
const _SideHeader({
Key key,
this.text,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
child: Text(text, style: TextStyle(fontSize: 36.0)),
);
}
}
class PersonTile extends StatelessWidget {
final Person person;
const PersonTile({Key key, this.person}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListTile(title: Text(person.name));
}
}
final faker = Faker();
final dummyData = [
...List.generate(100, (index) => Person(name: faker.person.name())),
Person(name: 'Øyvind')
];
#freezed
abstract class Person with _$Person {
const factory Person({String name}) = _Person;
}

Here is a solution using Nested ListViews.
Interesting points about the solution:
I used the diacritic package to remove the diacritics when indexing the list of Persons. You wouldn't want to exclude all of Norway's Øyvind, would you?
I used a SplayTreeMap to keep the Map sorted.
I used Flutter Hooks for the ScrollController and the Persons mapping, happening only when contacts is changed.
Full source code
import 'dart:collection';
import 'package:diacritic/diacritic.dart';
import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_file.freezed.dart'; // File generated by freezed
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: DirectoryPage(contacts: dummyData),
),
);
}
class DirectoryPage extends HookWidget {
final List<Person> contacts;
const DirectoryPage({Key key, this.contacts}) : super(key: key);
#override
Widget build(BuildContext context) {
final _scrollController = useScrollController();
final _mappedContacts = useState<SplayTreeMap<String, List<Person>>>(null);
useEffect(() {
_mappedContacts.value = contacts.fold<SplayTreeMap<String, List<Person>>>(
SplayTreeMap<String, List<Person>>(),
(acc, curr) {
final firstChar = removeDiacritics(curr.name)[0];
acc..[firstChar] ??= [];
return acc..[firstChar].add(curr);
},
);
return;
}, [contacts]);
return Scaffold(
body: Scrollbar(
controller: _scrollController,
isAlwaysShown: true,
child: ListView.separated(
controller: _scrollController,
itemBuilder: (context, index) {
final key = _mappedContacts.value.keys.elementAt(index);
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(key, style: TextStyle(fontSize: 36)),
),
const SizedBox(width: 8.0),
Expanded(
child: ListView(
shrinkWrap: true,
children: _mappedContacts.value[key]
.map((person) => PersonTile(person: person))
.toList(),
),
),
],
);
},
separatorBuilder: (_, __) => Divider(),
itemCount: _mappedContacts.value.length,
),
),
);
}
}
class PersonTile extends StatelessWidget {
final Person person;
const PersonTile({Key key, this.person}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListTile(title: Text(person.name));
}
}
final faker = Faker();
final dummyData = [
...List.generate(100, (index) => Person(name: faker.person.name())),
Person(name: 'Øyvind')
];
#freezed
abstract class Person with _$Person {
const factory Person({String name}) = _Person;
}

Related

What we should do when the file gets so long

when I'm done with a page I can extract widget as I can for more readable, But this makes the file longer, So I find a solution I can use "part and part of " I can take extract widgets inside part of (E.g login_view_part.dart), this solves the problem, But I have read opinions about the non-use/bad-use of part and part of.
1- this is a good solution for problems like this? If not what should we
do?
2- we should use part and part of? anywhere. (except code generation)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:second_hand/core/constants/navigation/navigation_constants.dart';
import 'package:second_hand/core/extensions/context_extension.dart';
import 'package:second_hand/core/init/navigation/navigation_service.dart';
import 'package:second_hand/core/init/notifier/product_notifer.dart';
import 'package:second_hand/view/_product/_widgets/textformfield/custom_text_form_field.dart';
import 'package:second_hand/view/app/addproduct/include_some_details/viewmodel/include_some_details_view_model.dart';
enum ProductState {
verybad(name: 'Very Bad'),
bad(name: 'Bad'),
normal(name: 'Normal'),
good(name: 'Good'),
verygood(name: 'Very Good');
const ProductState({required this.name});
final String name;
}
class IncludeSomeDetailsView extends StatefulWidget {
const IncludeSomeDetailsView({super.key});
#override
State<IncludeSomeDetailsView> createState() => IncludeSomeDetailsViewState();
}
class IncludeSomeDetailsViewState extends IncludeSomeDetailsViewModel {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Include some details'),
),
body: Form(
key: formKey,
child: Padding(
padding: context.paddingAllMedium,
child: Column(
children: [
TitleTextFormField(titleController: titleController),
DescriptionTextFormField(describeController: describeController),
DropdownButton<ProductState>(
value: valueProductState,
items: ProductState.values
.map<DropdownMenuItem<ProductState>>(
(value) => DropdownMenuItem<ProductState>(
value: value,
child: Text(value.name),
),
)
.toList(),
onChanged: (productState) {
setState(
() {
stateController.text = productState!.name;
valueProductState = productState;
},
);
},
),
const Spacer(),
NextButton(
formKey: formKey,
titleController: titleController,
stateController: stateController,
describeController: describeController),
],
),
),
),
);
}
}
class TitleTextFormField extends StatelessWidget {
const TitleTextFormField({
Key? key,
required this.titleController,
}) : super(key: key);
final TextEditingController titleController;
#override
Widget build(BuildContext context) {
return Padding(
padding: context.paddingOnlyTopSmall,
child: CustomTextFormField(
controller: titleController,
labelText: 'title',
prefix: const Icon(Icons.title_outlined),
),
);
}
}
class DescriptionTextFormField extends StatelessWidget {
const DescriptionTextFormField({
Key? key,
required this.describeController,
}) : super(key: key);
final TextEditingController describeController;
#override
Widget build(BuildContext context) {
return Padding(
padding: context.paddingOnlyTopSmall,
child: CustomTextFormField(
controller: describeController,
labelText: 'description',
prefix: const Icon(Icons.description),
),
);
}
}
class NextButton extends StatelessWidget {
const NextButton({
Key? key,
required this.formKey,
required this.titleController,
required this.stateController,
required this.describeController,
}) : super(key: key);
final GlobalKey<FormState> formKey;
final TextEditingController titleController;
final TextEditingController stateController;
final TextEditingController describeController;
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
context.read<ProductNotifier>().setProduct(
title: titleController.text,
state: stateController.text,
description: describeController.text,
);
NavigationService.instance.navigateToPage(path: NavigationConstants.UPLOAD_PHOTOS);
}
},
child: const Text(
'Next',
),
);
}
}

How to view list of objects as widgets?

So i have a list of objects with different block_type such as title or text. This what i show in the app:
This is the code :
class AppView extends StatefulWidget {
final List tappedItems;
const AppView({Key? key, required this.tappedItems}) : super(key: key);
#override
_AppViewState createState() => _AppViewState();
}
class _AppViewState extends State<AppView> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
final List listOfItems = [
{"block_type": "title", "block_data": "Books"},
{"block_type": "text", "block_data": "This is the textblock of books"},
{"block_type": "title", "block_data": "Publishers"},
{"block_type": "text", "block_data": "This is the textblock of publishers"},
];
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
key: _scaffoldKey,
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: IconButton(
icon: const Icon(Icons.menu,
size: 40), // change this size and style
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
),
actions: const [
Padding(
padding: EdgeInsets.fromLTRB(5, 20, 80, 5),
)
],
pinned: false,
expandedHeight: 100,
toolbarHeight: 100,
snap: true,
floating: true,
flexibleSpace: FlexibleSpaceBar(),
),
const SliverToBoxAdapter(),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text(
jsonEncode(listOfItems),
style: TextStyle(fontSize: 40, color: Colors.black),
),
);
},
childCount: 1,
),
),
],
),
drawer: const AppMenu(),
),
);
}
}
I also have different widgets for the blocks TitleBlock and TextBlock. This is an example for TitleBlock:
import 'package:flutter/cupertino.dart';
class TitleBlock extends StatefulWidget {
const TitleBlock({Key? key}) : super(key: key);
#override
_TitleBlockState createState() => _TitleBlockState();
}
class _TitleBlockState extends State<TitleBlock> {
#override
Widget build(BuildContext context) {
return Container();
}
}
I want show these seperate block widgets such as TitleBlock and TextBlock based on the block_type of the list of objects. I want to loop through the objects because this list of objects i showed is just an example. In my use case i can have different list of objects.
I want to show the blocks TitleBlock and TextBlock like this:
SliverChildBuilderDelegate gives you access to the builder, just like ListView.builder
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text(listOfItems[index]['block_type']),
subtitle: Text(listOfItems[index]['block_data']),
);
},
childCount: listOfItems.length
),
),
Based on your object's block_type you can call in different widgets with a ternary operator like in place of you ListTile, you can use the following,
listOfItems[index]["block_type"] == "title" ? TitleBlock(listOfItems[index]) : TextBlock(listOfItems[index])
And you can use the object i.e., Map<String, String> in the StatefulWidget (TitleBlock or TextBlock) to add any further info like,
class TitleBlock extends StatefulWidget {
const TitleBlock({Key? key, required this.dataMap}) : super(key: key);
final Map<String, String> dataMap;
#override
_TitleBlockState createState() => _TitleBlockState();
}
class _TitleBlockState extends State<TitleBlock> {
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.dataMap["block_type"]),
subtitle: Text(widget.dataMap["block_data"]),
);
}
}
class TextBlock extends StatefulWidget {
const TextBlock({Key? key, required this.dataMap}) : super(key: key);
final Map<String, String> dataMap;
#override
_TextBlockState createState() => _TextBlockState();
}
class _TextBlockState extends State<TextBlock> {
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.dataMap["block_type"]),
subtitle: Text(widget.dataMap["block_data"]),
);
}
}

Instantiating a List with Provider

I am trying to learn how to use ChangeNotifierProvider and have gotten stuck. I've setup the class as so:
void main() => runApp(
ChangeNotifierProvider(create: (context) => ItemList(),
child: MyApp(),
)
);
class ItemData {
final String title;
final int score;
ItemData({required this.title, required this.score});
}
class ItemList extends ChangeNotifier{
final _items = [];
void add(item){
_items.add(item);
notifyListeners();
}
void update(){
notifyListeners();
}
}
final itemList = ItemList();
Now I want to create the list:
I'm trying to add items by calling:
itemList.add(ItemData({elements}))
but this isn't working. How do I create my list so I can put it into a Listview Builder?
Try this one:
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(
const MyApp(),
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider(
create: (context) => ItemList(), child: const NewHomePage()),
);
}
}
class NewHomePage extends StatefulWidget {
const NewHomePage({Key? key}) : super(key: key);
#override
_NewHomePageState createState() => _NewHomePageState();
}
class _NewHomePageState extends State<NewHomePage> {
#override
Widget build(BuildContext context) {
return Consumer<ItemList>(builder: (context, providerItem, child) {
return Scaffold(
appBar: AppBar(
backgroundColor: const Color(0XFF2e3438),
),
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
providerItem.basketItem.isEmpty
? const Text("No item in the list")
: ListView.builder(
itemCount: providerItem.basketItem.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Title: " + providerItem.basketItem[index].title),
);
}),
ElevatedButton(
onPressed: () {
providerItem.addItem(ItemData(
title: DateTime.now().toString(),
score: DateTime.now().month));
print("data added successfully" +
providerItem.basketItem.length.toString());
},
child: const Text("Add Data")),
],
));
});
}
}
item_data.dart
class ItemData {
final String title;
final int score;
ItemData({required this.title, required this.score});
}
item_list.dart
class ItemList extends ChangeNotifier {
List<ItemData> _items = [];
void addItem(ItemData itemData) {
_items.add(itemData);
notifyListeners();
}
List<ItemData> get basketItem {
return _items;
}
}

Flutter Scroll view to focused widget on a column

I'm developing an app for Android TV, and use DPAD navigation.
I have multiple widgets inside a column. when i navigate to a widget which is outside the view, the widget/view is not moving to reflect the selected widget.
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatelessWidget(),
),
);
}
}
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultTextStyle(
style: textTheme.headline4!,
child: ChangeNotifierProvider<SampleNotifier>(
create: (context) => SampleNotifier(), child: const CardHolder()),
);
}
}
class CardHolder extends StatefulWidget {
const CardHolder({Key? key}) : super(key: key);
#override
_CardHolderState createState() => _CardHolderState();
}
class _CardHolderState extends State<CardHolder> {
late FocusNode _focusNode;
late FocusAttachment _focusAttachment;
#override
void initState() {
super.initState();
_focusNode = FocusNode(debugLabel: "traversal_node");
_focusAttachment = _focusNode.attach(context, onKey: _handleKeyPress);
_focusNode.requestFocus();
}
#override
Widget build(BuildContext context) {
_focusAttachment.reparent();
return Focus(
focusNode: _focusNode,
autofocus: true,
onKey: _handleKeyPress,
child: Consumer<SampleNotifier>(
builder: (context, models, child) {
int listSize = Provider.of<SampleNotifier>(context).listSize;
return SingleChildScrollView(
child: SampleRow(cat: "Test", models: models.modelList),
);
},
),
);
}
KeyEventResult _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print("t:FocusNode: ${node.debugLabel} event: ${event.logicalKey}");
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
Provider.of<SampleNotifier>(context, listen: false).moveRight();
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
Provider.of<SampleNotifier>(context, listen: false).moveLeft();
return KeyEventResult.handled;
}
}
// debugDumpFocusTree();
return KeyEventResult.ignored;
}
}
class SampleCard extends StatefulWidget {
final int number;
final SampleModel model;
final bool focused;
const SampleCard(
{required this.number,
required this.focused,
required this.model,
Key? key})
: super(key: key);
#override
_SampleCardState createState() => _SampleCardState();
}
class _SampleCardState extends State<SampleCard> {
late Color _color;
#override
void initState() {
super.initState();
_color = Colors.red.shade900;
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: widget.focused
? Container(
width: 150,
height: 300,
color: Colors.white,
child: Center(
child: Text(
"${widget.model.text} ${widget.model.num}",
style: TextStyle(color: _color),
),
),
)
: Container(
width: 150,
height: 300,
color: Colors.black,
child: Center(
child: Text(
"${widget.model.text} ${widget.model.num}",
style: TextStyle(color: _color),
),
),
),
);
}
}
class SampleRow extends StatelessWidget {
final String cat;
final List<SampleModel> models;
SampleRow({Key? key, required this.cat, required this.models}) : super(key: key);
#override
Widget build(BuildContext context) {
final int selectedIndex =
Provider.of<SampleNotifier>(context).selectedIndex;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 16, bottom: 8),
),
models.isNotEmpty
? SizedBox(
height: 200,
child: ListView.custom(
padding: const EdgeInsets.all(8),
scrollDirection: Axis.horizontal,
childrenDelegate: SliverChildBuilderDelegate(
(context, index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SampleCard(
focused: index == selectedIndex,
model: models[index],
number: index,
),
),
childCount: models.length,
findChildIndexCallback: _findChildIndex,
),
),
)
: SizedBox(
height: 200,
child: Container(
color: Colors.teal,
),
)
],
);
}
int _findChildIndex(Key key) => models.indexWhere((model) =>
"$cat-${model.text}_${model.num}" == (key as ValueKey<String>).value);
}
class SampleNotifier extends ChangeNotifier {
final List<SampleModel> _models = [
SampleModel(0, "zero"),
SampleModel(1, "one"),
SampleModel(2, "two"),
SampleModel(3, "three"),
SampleModel(4, "four"),
SampleModel(5, "five"),
SampleModel(6, "six"),
SampleModel(7, "seven"),
SampleModel(8, "eight"),
SampleModel(9, "nine"),
SampleModel(10, "ten")
];
int _selectedIndex = 0;
List<SampleModel> get modelList => _models;
int get selectedIndex => _selectedIndex;
int get listSize => _models.length;
void moveRight() {
if (_selectedIndex < _models.length - 1) {
_selectedIndex = _selectedIndex + 1;
}
notifyListeners();
}
void moveLeft() {
if (_selectedIndex > 0) {
_selectedIndex = _selectedIndex - 1;
}
notifyListeners();
}
}
class SampleModel {
int num;
String text;
SampleModel(this.num, this.text);
}
I need a way to move/scroll the widget into view. Is there any way to do this, using the DPAD navigation on android tv
Here is the gist
You could use the scrollable_positioned_list package.
Instead of a ListView.custom which scrolls based on pixels, this widgets its based on index:
final ItemScrollController itemScrollController = ItemScrollController();
ScrollablePositionedList.builder(
itemCount: 500,
itemBuilder: (context, index) => Text('Item $index'),
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
);
So you could maintain an index of the current scroll position and on DPAD press just :
itemScrollController.jumpTo(index: currentItem);
setState((){currentItem++;})

BLoC returning the same data after each event

I have a BLoC whose's job it is to provide a list of models called DrinkCard, which is my custom class. Now, my bloc needs to get the data from another cubit that stores a list of Player models. My code then fills the names of the players that are playing the game into the cards.
Here is my bloc.
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:drinkly/decks/data/repository/deck_repository.dart';
import 'package:drinkly/decks/decks.dart';
import 'package:drinkly/decks/models/card.dart';
import 'package:drinkly/game/data/repository/game_repository.dart';
import 'package:drinkly/players/players.dart';
import 'package:equatable/equatable.dart';
part 'game_event.dart';
part 'game_state.dart';
class GameBloc extends Bloc<GameEvent, GameState> {
GameBloc({required this.repository})
: super(GameInitial());
final GameRepository repository;
List<DrinkCard> _getCards(DeckType deckId, List<Player> players) {
final allCards = DeckRepository.getDeckById(deckId).cards;
final processedCards =
DeckRepository.prepareCards(allCards, playerCubit.state);
return processedCards;
}
#override
Stream<GameState> mapEventToState(
GameEvent event,
) async* {
if (event is GamePrepare) {
yield GameLoaded(cards: _getCards(event.deck, event.players));
}
}
}
Here is my UI implementation, I know it's chunky but I plan to refactor it after I get a MVP working.
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:auto_route/auto_route.dart';
import 'package:awesome_dialog/awesome_dialog.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:tcard/tcard.dart';
import '../../app/injection_container.dart';
import '../../decks/decks.dart';
import '../../players/players.dart';
import '../bloc/game_bloc.dart';
import 'helper/build_cards.dart';
var selectedDeck;
class GamesScreen extends StatelessWidget {
const GamesScreen({Key? key, required this.deck}) : super(key: key);
final DeckType deck;
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<GameBloc>(
create: (ctx) => sl<GameBloc>(),
),
BlocProvider<PlayerCubit>(
create: (ctx) => sl<PlayerCubit>(),
)
],
child: BlocListener<PlayerCubit, List<Player>>(
listener: (context, state) {
context.read<GameBloc>().add(GamePrepare(deck: deck, players: state));
},
child: GamesView(
deck: deck,
),
),
);
}
}
class GamesView extends StatefulWidget {
const GamesView({Key? key, required this.deck}) : super(key: key);
final DeckType deck;
#override
_GamesViewState createState() => _GamesViewState();
}
class _GamesViewState extends State<GamesView> {
#override
Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.height;
selectedDeck = widget.deck;
return Scaffold(
backgroundColor: const Color(0xff2a2438),
body: GameBody(
deck: widget.deck,
),
);
}
}
class GameBody extends StatefulWidget {
const GameBody({
Key? key,
required this.deck,
}) : super(key: key);
final DeckType deck;
#override
_GameBodyState createState() => _GameBodyState();
}
class _GameBodyState extends State<GameBody> {
final _controller = TCardController();
var frontCardIndex = 0;
void callback(int index) {
setState(() {
frontCardIndex = index;
});
}
#override
Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.height;
var players = context.read<PlayerCubit>().state;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: CardStack(
controller: _controller,
callback: callback,
index: frontCardIndex,
deck: widget.deck,
),
),
Expanded(
child: SafeArea(
child: Align(
child: Padding(
padding: const EdgeInsets.fromLTRB(25, 0, 0, 0),
child: BlocBuilder<GameBloc, GameState>(
builder: (context, state) {
if (state is GameLoaded) {
return IconButton(
onPressed: () async {
await showModalBottomSheet(
context: context,
builder: (ctx) {
return BlocProvider(
create: (context) => sl<GameBloc>(),
child: ModalSheetBody(
height: height,
controller: _controller,
index: _controller.index,
callback: () {
var cards = CardHelper.buildCardItems(
state.cards.sublist(_controller.index),
context,
);
setState(() {
_controller.reset(
cards: cards,
);
});
},
),
);
},
);
},
icon: const Icon(
CupertinoIcons.person_add_solid,
size: 34,
),
);
}
return Container();
},
),
),
),
),
),
],
);
}
}
class ModalSheetBody extends StatelessWidget {
ModalSheetBody({
Key? key,
required this.height,
required TCardController controller,
required this.index,
required this.callback,
}) : _controller = controller,
super(key: key);
final double height;
final TCardController _controller;
final int index;
final Function callback;
#override
Widget build(BuildContext context) {
return StatefulBuilder(
builder: (ctx, stateSetter) {
var players = ctx.watch<GameBloc>().playerCubit.state;
return Container(
height: height * 0.4,
),
child: Column(
children: [
name != null
? stateSetter(
() {
context.read<GameBloc>().playerCubit.state.add(
Player(
name: name[0],
),
);
context.read<GameBloc>().add(
GameReloaded(
deck: selectedDeck,
),
);
callback();
},
)
: DoNothingAction();
},
),
],
),
Expanded(
child: GridView.builder(
itemCount: players.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
itemBuilder: (ctx, ind) {
return Chip(
key: UniqueKey(),
onDeleted: () {
stateSetter(() {
context.read<GameBloc>().playerCubit.removePlayer(
name: players[ind].name,
);
});
context.read<GameBloc>().add(
GameReloaded(
deck: selectedDeck,
),
);
callback();
},
deleteIcon: const Icon(
CupertinoIcons.delete,
size: 12,
),
label: Text(
players[ind].name,
style: GoogleFonts.poppins(
fontSize: height * 0.02,
color: Colors.white.withOpacity(0.65),
fontWeight: FontWeight.w500,
),
),
);
},
),
),
],
),
);
},
);
}
}
class CardStack extends StatefulWidget {
const CardStack({
Key? key,
required this.callback,
required TCardController controller,
required this.index,
required this.deck,
}) : _controller = controller,
super(key: key);
final TCardController _controller;
final Function callback;
final int index;
final DeckType deck;
#override
_CardStackState createState() => _CardStackState();
}
class _CardStackState extends State<CardStack> {
#override
Widget build(BuildContext context) {
return BlocBuilder<GameBloc, GameState>(
builder: (context, state) {
print(state.toString());
if (state is GameLoaded) {
return buildCardWidget(context, state);
}
context.read<GameBloc>().add(
GamePrepare(
deck: widget.deck,
players: [
Player(name: 'a'), // temporary initializer.
Player(name: 'b'),
],
),
);
return Container();
},
);
}
Widget buildCardWidget(BuildContext context, GameLoaded state) {
return TCard(
controller: widget._controller,
cards: [...CardHelper.buildCardItems(state.cards, context)],
);
}
}
Now, whenever a player is added, an event gets added to the GameBloc, which is supposed to cause a rebuild in the BlocProvider, and it does. But the list of DrinkCard is always the same, even though I shuffle the cards and there is a zero-chance that the bloc would return a list of the same 25 cards each time.