ScaleTransition looks like as it was sliding from right to left - flutter

I'm trying to make a widget that scale down and reappears at the different side, I was expecting it to scale down and scale up regardless of its alignment. But when I try, it looks like as it was sliding from the right to the left.
Tried removing the ListTile from the _buildRightAlignedListTile and uses the text directly and the ValueKey assigned to it but it still looks the same.
Is there any way to prevent this?
H̶e̶r̶e̶ ̶i̶s̶ ̶t̶h̶e̶ ̶s̶c̶r̶i̶p̶t̶ ̶y̶o̶u̶ ̶c̶a̶n̶ ̶t̶r̶y̶ ̶t̶o̶ ̶r̶u̶n̶ ̶i̶n̶ ̶[̶D̶a̶r̶t̶P̶a̶d̶]̶(̶h̶t̶t̶p̶s̶:̶/̶/̶d̶a̶r̶t̶p̶a̶d̶.̶d̶e̶v̶)̶,̶ ̶p̶l̶e̶a̶s̶e̶ ̶h̶a̶v̶e̶ ̶a̶ ̶l̶o̶o̶k̶.̶
EDIT
Finally found a way to share it, please use this DartPad link to reproduce the issue.
You can also copy and paste the script below just in case the link is dead. It was made directly from the dartpad.
EDIT 2
I apologize if my explanation is confusing, I'm having trouble trying to find the right words to explain it. I'm not a native speaker.
I want the right aligned widget to stay on the right and scale down in place until it disappear completely. And then the left aligned widget to scale up on the left, instead of scaling up from the middle to the left.
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _active = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: SwitchingWidget(active: _active),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
child: const Icon(Icons.check)
),
),
);
}
void _toggle() {
setState(() {
_active = !_active;
});
}
}
class SwitchingWidget extends StatefulWidget {
const SwitchingWidget({super.key, this.active = false});
final bool active;
#override
State<SwitchingWidget> createState() => _SwitchingWidgetState();
}
class _SwitchingWidgetState extends State<SwitchingWidget> {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) {
return ScaleTransition(scale: animation, child: child);
},
child: widget.active
? _buildLeftAlignedListTile()
: _buildRightAlignedListTile(),
),
);
}
Widget _buildLeftAlignedListTile() {
return const ListTile(
key: ValueKey(1),
leading: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
),
title: Text(
'Left aligned',
style: TextStyle(color: Colors.white),
),
);
}
Widget _buildRightAlignedListTile() {
return const ListTile(
key: ValueKey(2),
title: Text(
'Right aligned',
textAlign: TextAlign.right,
style: TextStyle(color: Colors.white),
),
);
}
}

Found a solution to this while tinkering around with another stuff. The solution is to use separate AnimatedSwitcher for each widget I want to animate instead of using one and placing it directly with a conditional statement.
The script above has slightly modified and it works perfectly just like what I wanted. You can also copy and paste to run it on the DartPad since it was made from there to try it out.
Please let me know if there is a better approach!
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _active = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: SwitchingWidget(active: _active),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
child: const Icon(Icons.check)
),
),
);
}
void _toggle() {
setState(() {
_active = !_active;
});
}
}
class SwitchingWidget extends StatefulWidget {
const SwitchingWidget({super.key, this.active = false});
final bool active;
#override
State<SwitchingWidget> createState() => _SwitchingWidgetState();
}
class _SwitchingWidgetState extends State<SwitchingWidget> {
final scaleDuration = const Duration(milliseconds: 200);
Widget transitionBuilder(child, animation) {
return ScaleTransition(child: child, scale: animation);
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: scaleDuration,
transitionBuilder: transitionBuilder,
child: widget.active
? const SizedBox()
: _buildLeftAlignedWidget(),
),
AnimatedSwitcher(
duration: scaleDuration,
transitionBuilder: transitionBuilder,
child: widget.active
? _buildRightAlignedWidget()
: const SizedBox(),
),
],
),
);
}
Widget _buildLeftAlignedWidget() {
return Row(
key: const ValueKey(1),
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.only(right: 16),
width: 20,
height: 20,
child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
),
const Text(
'Left aligned',
style: TextStyle(color: Colors.white),
),
],
);
}
Widget _buildRightAlignedWidget() {
return const Text(
key: ValueKey(2),
'Right aligned',
textAlign: TextAlign.right,
style: TextStyle(color: Colors.white),
);
}
}

Related

How can I use a provider in a custom widget to change the variables within a provider?

I tried to create a function to adjust the font size on the setting screen, so I made a provider.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class CountPage extends ChangeNotifier{
double _font = 40;
double get font => _font;
change_font_1()
{
_font = 30;
notifyListeners();
}
change_font_2()
{
_font = 35;
notifyListeners();
}
change_font_3()
{
_font = 40;
notifyListeners();
}
And I created an option screen custom widget
Code related to font size:
class Option_page extends StatelessWidget {
late CountPage _countPage;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red[200],
centerTitle: true,
title: Text("설정"),
),
body: Center(
child: Container(
child: Column(children: [
Container(
height: 50,
child: Row(children: [
Container(child: Text('글자크기')),
Container(
width: 300,
color: Colors.cyan,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () {
_countPage.change_font_1();
},
icon: Icon(Icons.looks_one_outlined)),
IconButton(
onPressed: () {
_countPage.change_font_2();
},
icon: Icon(Icons.looks_two_outlined)),
IconButton(
onPressed: () {
_countPage.change_font_3();
},
icon: Icon(Icons.looks_3_outlined)),
],
),
)
]))
]))));
}
}
I connected with the main class:
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,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
home:ChangeNotifierProvider(
create: (BuildContext context) => CountPage()
,child: Quote() ,)
);
}
}
Code related to font size in the main class:
class Quote extends StatelessWidget {
Quote({Key? key}) : super(key: key);
late CountPage _countPage;
#override
Widget build(BuildContext context) {
_countPage = Provider.of<CountPage>(context, listen: true);
return Scaffold(
body: Container(
color: Colors.white,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Center(
child: IconButton(
onPressed: () {
_countPage.page_down();
},
icon: Icon(Icons.chevron_left))),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: Container(
color: Colors.white,
height: 400,
width: 350,
child: Center(
child: Text(
list[_countPage.page]["message"],
style: TextStyle(
fontSize: _countPage.font,
fontFamily: 'snow',
),
textAlign: TextAlign.center,
))),
),
Container(
height: 40,
width: 300,
color: Colors.white,
child: Center(
child: Text(
list[_countPage.page]["author"],
style: TextStyle(fontSize: 20, color: Colors.black),
)))
],
),
],
),
),
);
}
}
The font size does not change even if you run it on Chrome and press the font size change button. I don't know what the problem is.
You can wrap the MaterialApp to access provider on all route
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => CountPage(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
home: Quote(),
),
);
}
}
From current snippet, you didnt assign the CountPage on Option_page.
class Option_page extends StatelessWidget {
Widget build(BuildContext context) {
CountPage _countPage = Provider.of<CountPage>(context, listen: true); //this ibe
return Scaffold(
appBar: AppBar(

AnimatedContainers with Row child not animating

I would like to use multiple animated containers, one for padding and another for height, but when I use a row, it stops animating. In my simplified code sample, you can see that the "Card View With Row..." card is not animating where the "Card View No Row..." is animating.
I imagine that it has something to do with the change in width and the row. Is there something I need to wrap my row in to make it compatible with the animated containers?
My desired outcome, I want
The padding around the column to animate
The height of the cards' headers to animate
The content to stay the same
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool cardView = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: AnimatedCardList(
isCardView: cardView,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
cardView = !cardView;
});
},
backgroundColor: Colors.black,
child: Icon(
cardView ? Icons.check_rounded : Icons.edit,
size: 40,
color: Colors.white,
),
),
),
);
}
}
class AnimatedCardList extends StatelessWidget {
final bool isCardView;
const AnimatedCardList({
super.key,
required this.isCardView,
});
#override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(seconds: 2),
padding: isCardView ? EdgeInsets.zero : const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
BasicCard(
isCardView: isCardView,
header: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text("Card View With Row Not Animating"),
Text("Edit >"),
],
),
),
const SizedBox(height: 50),
BasicCard(
isCardView: isCardView,
header: const Text("Card View No Row IS Animating"),
),
],
),
);
}
}
class BasicCard extends StatelessWidget {
final Widget header;
const BasicCard({
Key? key,
required this.isCardView,
required this.header,
}) : super(key: key);
final bool isCardView;
#override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Column(
children: [
AnimatedSize(
duration: const Duration(seconds: 2),
child: Container(
color: Colors.blue,
constraints: !isCardView
? const BoxConstraints(
maxHeight: double.infinity,
)
: const BoxConstraints(
maxHeight: 0.0,
),
child: header,
),
),
const Text("Card Content")
],
),
);
}
}
The problem:
Rows or Columns don't work well when their "cross axis" length is changed while wrapped in an animated widget. They already animate when their children are animating.
The solution:
Animate each of the children by wrapping them with the animation you want.
I created a AnimatedHeightCollapse widget that collapses based on a parameter. This should work for Column if you swich height with width respectively in the code.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool cardView = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: AnimatedCardList(
isCardView: cardView,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
cardView = !cardView;
});
},
backgroundColor: Colors.black,
child: Icon(
cardView ? Icons.check_rounded : Icons.edit,
size: 40,
color: Colors.white,
),
),
),
);
}
}
class AnimatedCardList extends StatelessWidget {
final bool isCardView;
const AnimatedCardList({
super.key,
required this.isCardView,
});
#override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(seconds: 2),
padding: isCardView ? EdgeInsets.zero : const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
BasicCard(
header: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AnimatedHeightCollapse(
visible: isCardView,
child: const Text("Card View With Row IS Animating"),
),
AnimatedHeightCollapse(
visible: isCardView,
child: const Text("Edit >"),
),
],
),
),
const SizedBox(height: 50),
BasicCard(
header: AnimatedHeightCollapse(
visible: isCardView,
child: const Text("Card View No Row IS Animating"),
),
),
],
),
);
}
}
class BasicCard extends StatelessWidget {
final Widget header;
const BasicCard({
Key? key,
required this.header,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Column(
children: [header, const Text("Card Content")],
),
);
}
}
class AnimatedHeightCollapse extends StatelessWidget {
final bool visible;
final Widget child;
const AnimatedHeightCollapse({
super.key,
required this.visible,
required this.child,
});
#override
Widget build(BuildContext context) {
return AnimatedSize(
duration: const Duration(seconds: 2),
child: Container(
color: Colors.blue,
constraints: !visible
? const BoxConstraints(
maxHeight: double.infinity,
)
: const BoxConstraints(
maxHeight: 0.0,
),
child: child,
),
);
}
}

How can i make a moveable overlay?

I want to show a minimize moveable calling screen in top of the app
I tried with stack it does not meet my expectation
#Raiyan, you have to use picture-in-picture concept to implement such floating child.
In flutter, multiple plugins are there, that we can use for the, some are as follows:
https://pub.dev/packages/pip_view
https://pub.dev/packages/floating
https://pub.dev/packages/easy_pip
floating package will fit in your case, it provides picture in Picture mode management for Flutter.
Sadly the gif is not working... But by on taping and draging on the green window will make the green window move.
Try this:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return OverlayWindow(
overlayChild: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Overlay Window",
style: TextStyle(fontSize: 20),
),
Icon(
Icons.android,
size: 80,
),
],
),
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
),
);
}
}
class OverlayWindow extends StatefulWidget {
const OverlayWindow(
{Key? key, required this.overlayChild, required this.child})
: super(key: key);
final Widget overlayChild;
final Widget child;
#override
State<OverlayWindow> createState() => _OverlayWindowState();
}
class _OverlayWindowState extends State<OverlayWindow> {
double _top = 0;
double _left = 0;
#override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
Positioned(
top: _top,
left: _left,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_top = max(0, _top + details.delta.dy);
_left = max(0, _left + details.delta.dx);
});
},
child: Container(
height: 300,
width: 200,
color: Colors.green,
child: widget.overlayChild,
),
),
)
],
);
}
}
More about things like that, you can find here:
https://docs.flutter.dev/development/ui/advanced/gestures

Expandable button overflowing top of container

I'm trying to make an expandable button, a bit like the expandable fab, except it's not a fab as it is not floating. This is the expandable fab for perspective:
What I'm trying to achieve though is to have a self contained button that expands above it with a menu. Self contained is in bold because I'd like the widget to be used easily without having to modify the parents structure.
So if you copy paste the code below in dartpad you'll see a yellow bar at the bottom. However if you uncomment the lines which are commented, which represents the menu expanding, you'll see that the bottom bar is pushed to the top.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
Expanded(child: Container(color: Colors.purple)),
MyWidget(),
]
),
),
),
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SizedOverflowBox(
size: Size(double.infinity, 100),
child: Stack(
children: [
Container(color: Colors.amber, height: 100),
// Transform.translate(
// offset: Offset(0, -400),
// child: Container(color: Colors.lightBlue, height: 400, width: 80),
// ),
]
)
);
}
}
So my questions are:
How do I achieve the required result where the bottom bar does not move and a menu above it (light blue container); modifying only MyWidget and not MyApp ?
Why in the current code the bar is pushed above ?
Overlay and OverlayEntry can help to achieve this:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
Expanded(child: Container(color: Colors.purple)),
MyWidget(),
]
),
),
),
);
}
}
class MyWidget extends StatelessWidget {
OverlayEntry? _overlayEntry;
_hideMenu() {
_overlayEntry?.remove();
}
_showMenu(BuildContext context) {
final overlay = Overlay.of(context);
_overlayEntry = OverlayEntry(
builder: (ctx) => Stack(
children: [
GestureDetector(
onTap: () => _hideMenu(),
child: Container(color: Colors.grey.withAlpha(100)),
),
Positioned(
bottom: 100,
left: 50,
child: Container(color: Colors.pink, height: 200, width: 50,),
),
],
)
);
overlay?.insert(_overlayEntry!);
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _showMenu(context),
child: Container(color: Colors.amber, height: 100)
);
}
}
Try this, run this code in dartpad.
It contains one parent, three child which can be called using the menu buttons,
The FloatingActionButton.extended used in this code can be replaced by any custom Widget, you can give onTap methods for clicks,
I have used simple widgets, Let me know wether you were looking for something like that, or something different.
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'I am Parent'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool showButtons = false;
var index = 0;
List<Widget> childList = [Child1(), Child2(), Child3()];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: childList[index],
),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
Visibility(
visible: showButtons,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.extended(
heroTag: 'btn1',
onPressed: () {
setState(() {
index = 0;
});
},
label: Text(
"Sub Btn1",
style: TextStyle(color: Colors.black),
),
elevation: 3,
backgroundColor: Colors.yellowAccent,
),
Padding(
padding: EdgeInsets.only(top: 3),
child: FloatingActionButton.extended(
heroTag: 'btn1',
onPressed: () {
setState(() {
index = 1;
});
},
label: Text(
"Sub Btn2",
style: TextStyle(color: Colors.black),
),
elevation: 3,
backgroundColor: Colors.yellowAccent,
)),
Padding(
padding: EdgeInsets.only(top: 3),
child: FloatingActionButton.extended(
heroTag: 'btn3',
onPressed: () {
setState(() {
index = 2;
});
},
label: Text(
"Sub Btn3",
style: TextStyle(color: Colors.black),
),
elevation: 3,
backgroundColor: Colors.yellowAccent,
))
],
),
),
RaisedButton(
onPressed: () {
setState(() {
showButtons = !showButtons;
});
},
child: Text("Self Contained"),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
color: Colors.yellow,
),
],
) // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class Child1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text("I am Child 1"),
);
}
}
class Child2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text("I am Child 2"),
);
}
}
class Child3 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text("I am Child 3"),
);
}
}

Get the rack to update after shuffle

In the example below, what is the best construct to use to get the rack to update after a shuffle?
It seems to me that when a StatefulWidget is created, with its corresponding State Object (SO), any method that you can call from elsewhere is a method that's attached to the widget itself (not to the SO).
But, to get the widget to update its display, the SetState() method can only go in the SO's method(s). So how does the method on the widget call a method on its SO?
import 'package:flutter/material.dart';
List<Block> g_blocks = [Block(Colors.red), Block(Colors.green), Block(Colors.blue)];
Rack g_rack = new Rack();
void main() => runApp(MyApp());
// This widget is the root of your application.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
fontFamily: 'PressStart',
),
home: MyHomeScreen(),
);
}
}
class MyHomeScreen extends StatefulWidget {
MyHomeScreen({Key key}) : super(key: key);
createState() => MyHomeScreenState();
}
class MyHomeScreenState extends State<MyHomeScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Center(child: Text('Thanks for your help')),
backgroundColor: Colors.pink,
),
body: Center(
child: g_rack,
),
bottomNavigationBar: SizedBox(
height: 100.0,
child: BottomNavigationBar(
currentIndex: 0,
iconSize: 48.0,
backgroundColor: Colors.lightBlue[100],
items: [
BottomNavigationBarItem(
label: 'Shuffle',
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: 'Shuffle',
icon: Icon(Icons.home),
),
],
onTap: (int indexOfItem) {
setState(() {
g_blocks.shuffle;
rack.updateScreen(); // ** How to get the rack to update? **
});
},
),
),
);
} // build
} // End class MyHomeScreenState
class Rack extends StatefulWidget {
#override
_rackState createState() => _rackState();
}
class _rackState extends State<Rack> {
#override
Widget build(BuildContext context) {
return Container(
height: 150.0,
color: Colors.yellow[200],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: g_blocks),
);
}
void updateRack(){
setState(() {
g_blocks.shuffle;
});
}
}
class Block extends StatelessWidget {
final Color color;
Block(this.color);
#override
Widget build(BuildContext context) {
return Container(height:50,width:50, color: color,);
}
}
Here is a solution where I try to decouple the State Management and Business Logic of the application from the User Interface.
I used the following packages:
freezed for the Domain Entities
hooks_riverpod for the State Management
1. Domain Layer: Entities
We need two Entities to model our Racks of Blocks.
Blocks are defined by their color.
Blocks have no business logic.
Racks are ordered lists of Blocks.
Racks can get shuffled.
Racks can be randomly created for a (random or given) number of Blocks
#freezed
abstract class Block with _$Block {
const factory Block({Color color}) = _Block;
}
#freezed
abstract class Rack implements _$Rack {
const factory Rack({List<Block> blocks}) = _Rack;
const Rack._();
static Rack create([int nbBlocks]) => Rack(
blocks: List.generate(
nbBlocks ?? 4 + random.nextInt(6),
(index) => Block(
color: Color(0x66000000 + random.nextInt(0xffffff)),
),
),
);
Rack get shuffled => Rack(blocks: blocks..shuffle());
}
We use the freeze package to have immutability and the precious copyWith method to manage our States.
2. Application Layer: State Management
We use Hooks Riverpod for our State Management. We just need one StateNotifier and its provider.
This StateNotifierProvider gives access to both the Rack State and the core functionalities that are deal() and shuffle().
class RackStateNotifier extends StateNotifier<Rack> {
static final provider =
StateNotifierProvider<RackStateNotifier>((ref) => RackStateNotifier());
RackStateNotifier([Rack state]) : super(state ?? Rack.create());
void shuffle() {
state = state.shuffled;
}
void deal() {
state = Rack.create();
}
}
3. Presentation Layer: User Interface
The User Interface is made of four Widgets:
AppWidget [StatelessWidget]
HomePage [HookWidget]
RackWidget [StatelessWidget]
BlockWidget [StatelessWidget]
As you see, the only Widget that really cares about the State of the Application is the HomePage.
3.1 AppWidget
class AppWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.amber,
accentColor: Colors.black87,
),
home: HomePage(),
);
}
}
3.2 HomePage
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final rack = useProvider(RackStateNotifier.provider.state);
return Scaffold(
appBar: AppBar(
title: Row(
children: const [
Icon(Icons.casino_outlined),
SizedBox(
width: 8.0,
),
Text('Rack Shuffler'),
],
),
),
body: Center(
child: RackWidget(rack: rack),
),
bottomNavigationBar: BottomAppBar(
color: Theme.of(context).primaryColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
icon: Icon(Icons.refresh),
iconSize: 48,
onPressed: () => context.read(RackStateNotifier.provider).deal(),
),
IconButton(
icon: Icon(Icons.shuffle),
iconSize: 48,
onPressed: () =>
context.read(RackStateNotifier.provider).shuffle(),
),
],
),
),
);
}
}
rack is provided by our StateNotifierProvider, in watch mode:
final rack = useProvider(RackStateNotifier.provider.state);
The Racks are dealt and shuffled using the same provider, in read mode:
...
context.read(RackStateNotifier.provider).deal(),
...
context.read(RackStateNotifier.provider).shuffle(),
...
3.3 RackWidget
class RackWidget extends StatelessWidget {
final Rack rack;
const RackWidget({Key key, this.rack}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: LayoutBuilder(
builder: (context, constraints) {
return Row(
children: rack.blocks
.map((block) => BlockWidget(
block: block,
size: constraints.biggest.width / rack.blocks.length))
.toList(),
);
},
),
);
}
}
Basic StatelessWidget. We use a LayoutBuilder to define the size of the BlockWidgets.
3.4 BlockWidget
class BlockWidget extends StatelessWidget {
final Block block;
final double size;
const BlockWidget({
Key key,
this.block,
this.size,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: Padding(
padding: EdgeInsets.all(size / 10),
child: Container(
decoration: BoxDecoration(
color: block.color,
border: Border.all(color: Colors.black87, width: size / 20),
borderRadius: BorderRadius.circular(size / 15),
),
),
),
);
}
}
Another basic StatelessWidget.
Full Application Code
Just copy-paste the following to try it out.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
part '66053795.shuffle.freezed.dart';
Random random = Random();
void main() => runApp(ProviderScope(child: AppWidget()));
class AppWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.amber,
accentColor: Colors.black87,
),
home: HomePage(),
);
}
}
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final rack = useProvider(RackStateNotifier.provider.state);
return Scaffold(
appBar: AppBar(
title: Row(
children: const [
Icon(Icons.casino_outlined),
SizedBox(
width: 8.0,
),
Text('Rack Shuffler'),
],
),
),
body: Center(
child: RackWidget(rack: rack),
),
bottomNavigationBar: BottomAppBar(
color: Theme.of(context).primaryColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
icon: Icon(Icons.refresh),
iconSize: 48,
onPressed: () => context.read(RackStateNotifier.provider).deal(),
),
IconButton(
icon: Icon(Icons.shuffle),
iconSize: 48,
onPressed: () =>
context.read(RackStateNotifier.provider).shuffle(),
),
],
),
),
);
}
}
class RackWidget extends StatelessWidget {
final Rack rack;
const RackWidget({Key key, this.rack}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: LayoutBuilder(
builder: (context, constraints) {
return Row(
children: rack.blocks
.map((block) => BlockWidget(
block: block,
size: constraints.biggest.width / rack.blocks.length))
.toList(),
);
},
),
);
}
}
class BlockWidget extends StatelessWidget {
final Block block;
final double size;
const BlockWidget({
Key key,
this.block,
this.size,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: Padding(
padding: EdgeInsets.all(size / 10),
child: Container(
decoration: BoxDecoration(
color: block.color,
border: Border.all(color: Colors.black87, width: size / 20),
borderRadius: BorderRadius.circular(size / 15),
),
),
),
);
}
}
class RackStateNotifier extends StateNotifier<Rack> {
static final provider =
StateNotifierProvider<RackStateNotifier>((ref) => RackStateNotifier());
RackStateNotifier([Rack state]) : super(state ?? Rack.create());
void shuffle() {
state = state.shuffled;
}
void deal() {
state = Rack.create();
}
}
#freezed
abstract class Block with _$Block {
const factory Block({Color color}) = _Block;
}
#freezed
abstract class Rack implements _$Rack {
const factory Rack({List<Block> blocks}) = _Rack;
const Rack._();
static Rack create([int nbBlocks]) => Rack(
blocks: List.generate(
nbBlocks ?? 4 + random.nextInt(6),
(index) => Block(
color: Color(0x66000000 + random.nextInt(0xffffff)),
),
),
);
Rack get shuffled => Rack(blocks: blocks..shuffle());
}
Here is a solution using a GlobalKey.
It feels pretty inelegant. It surprises me that with the close relationship between the widget and its state object, there's no easy way for a widget's method to call a method on the SO. The "widget.blah" construct provides a way for the SO to access the widget's data, is there a reason for not having a similar "state.myMethod" construct?
Anyway, the following works:
import 'package:flutter/material.dart';
List<Block> g_blocks = [Block(Colors.red), Block(Colors.green),
Block(Colors.blue), Block(Colors.purple)];
GlobalKey g_key = GlobalKey();
Rack g_rack = new Rack(key: g_key);
void main() => runApp(MyApp());
// This widget is the root of your application.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
fontFamily: 'PressStart',
),
home: MyHomeScreen(),
);
}
}
class MyHomeScreen extends StatefulWidget {
MyHomeScreen({Key key}) : super(key: key);
createState() => MyHomeScreenState();
}
class MyHomeScreenState extends State<MyHomeScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Center(child: Text('Thanks for your help')),
backgroundColor: Colors.pink,
),
body: Center(
child: g_rack,
),
bottomNavigationBar: SizedBox(
height: 100.0,
child: BottomNavigationBar(
currentIndex: 0,
iconSize: 48.0,
backgroundColor: Colors.lightBlue[100],
items: [
BottomNavigationBarItem(
label: 'Shuffle',
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: 'Shuffle',
icon: Icon(Icons.home),
),
],
onTap: (int index) {
g_blocks.shuffle();
g_key.currentState.setState(() {
});
}
),
),
);
} // build
} // End class MyHomeScreenState
class Rack extends StatefulWidget {
Rack({Key key}) : super(key: key);
#override
_rackState createState() => _rackState();
}
class _rackState extends State<Rack> {
#override
Widget build(BuildContext context) {
return Container(
height: 150.0,
color: Colors.yellow[200],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: g_blocks),
);
}
void updateRack(){
setState(() {});
}
}
class Block extends StatelessWidget {
final Color color;
Block(this.color);
#override
Widget build(BuildContext context) {
return Container(height:50,width:50, color: color,);
}
}