getting the old scroll offset using the Package flutter_webview_plugin - flutter

i am trying to integrate a Webview in a Test app using the Package flutter_webview_plugin
my goal is to hide a bottomNavigationBar when the User scroll up in the Webview and show it, when the user scroll down.
in the mentioned Package there is a listner to listen to vertical Schroll changes :
final flutterWebviewPlugin = new FlutterWebviewPlugin();
flutterWebviewPlugin.onScrollYChanged.listen((double offsetY) { // latest offset value in vertical scroll
// compare vertical scroll changes here with old value
});
the offsetY value, is the current value, but how can't i get the old value, to compare it with the new value ? any idea ?

ok, i have implemented a solution to this.
i have defined a variable oldOffset = 0.0 and in the method trackOffsetChange i checked if oldOffset value smaller than the currentOffset value. if it's the case then oldOffset get the value of currentOffset and with setState i rebuild the widget to hide the BottomNavBar, else show the BottomNavBar.
here is the whole code of the test App, if someone new like me, is interested to see the source code:
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.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: WebInApp(),
);
}
}
class WebInApp extends StatefulWidget {
#override
_WebInAppState createState() => _WebInAppState();
}
class _WebInAppState extends State<WebInApp> {
bool _isVisible = true;
double oldOffset = 0.0;
final flutterWebOlugin = FlutterWebviewPlugin();
void trackOffsetChange(double currentOffset) {
print('current Offset --->> $currentOffset');
print('old Offset --->> $oldOffset');
if (oldOffset < currentOffset) {
print('old Offset In -- IF --->> $oldOffset');
oldOffset = currentOffset;
setState(() {
_isVisible = false;
});
} else {
setState(() {
_isVisible = true;
});
print('old Offset In -- ESLE --->> $oldOffset');
oldOffset = currentOffset;
}
}
#override
void initState() {
super.initState();
flutterWebOlugin.onScrollYChanged.listen((double yOffset) {
trackOffsetChange(yOffset);
});
}
#override
Widget build(BuildContext context) {
return WebviewScaffold(
url: "https://play.google.com/store/apps",
// hidden: true,
appBar: AppBar(
title: Text('WebView'),
),
bottomNavigationBar: AnimatedContainer(
duration: Duration(microseconds: 300),
height: _isVisible ? 60.0 : 0.0,
child: bottomNav(),
),
);
}
}
class bottomNav extends StatelessWidget {
const bottomNav({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.forward),
title: Text('back'),
),
BottomNavigationBarItem(
icon: Icon(Icons.arrow_back),
title: Text('forward'),
),
],
);
}
}

Related

setState not really settng the state [flutter]

So I have this code that is supposed to draw to the canvas.
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
);
}
}
// offsets
List<Offset?> offsets = [];
class Home extends StatefulWidget {
const Home({
Key? key,
}) : super(key: key);
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
// offsets
List<Offset?> offsets = [];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Drawing'),
),
body: GestureDetector(
onPanDown: (details) {
setState(() {
offsets.add(details.localPosition);
});
},
onPanUpdate: (details) {
setState(() {
offsets.add(details.localPosition);
});
},
onPanEnd: (details) {
setState(() {
offsets.add(null);
});
},
child: Container(
width: double.infinity,
height: MediaQuery.of(context).size.height,
color: Colors.grey,
child: CustomPaint(
foregroundPainter: Painter(offsets: offsets),
),
),
),
);
}
}
class Painter extends CustomPainter {
Painter({required this.offsets});
List<Offset?> offsets;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..strokeWidth = 2
..style = PaintingStyle.stroke;
// for every offset and the next offset
for (int i = 0; i < offsets.length - 1; i++) {
// if both are not null
if (offsets[i] != null && offsets[i + 1] != null) {
canvas.drawLine(offsets[i]!, offsets[i + 1]!, paint);
} else if (offsets[i] != null && offsets[i + 1] == null) {
canvas.drawPoints(PointMode.points, [offsets[i]!], paint);
}
}
print('paint called');
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
How this is working is that it detects the gestures on the container and when there is a swipe on the container, it records the offsets of that and adds them to the offsets array. Later that array is being passed to the custom painter where we go through all the offsets and render the lines. So when I add the offsets to the array I use setState so that the new offsets are added, the screen is rendered again, new offsets are passed to the custom painter and it renders those lines. But it's not working when I make a swipe but the lines appear when I make a hot reload. Why is this? Why I need to manually hot reload to see those lines? Any help will be appreciated.
Edit: It seems like the offsets are being added properly and the setState is also working fine but somehow the custom painter is not being called.
NVM, returning true from shouldRepaint method solve the issue.
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}

Is it possible to have separate BuildContext for two dialogs in Flutter?

I want to control how I close specific dialogs in Flutter. I know if I call Navigator.of(context).pop() it will close the latest dialog.
However my situation is that I can have two dialogs opened at the same time in different order (a -> b or b -> a) and I want to explicitly close one of them.
I know that showDialog builder method provides a BuildContext that I can reference and do Navigator.of(storedDialogContext).pop() but that actually doesn't really help since this context shares same navigation stack.
Update: Vandan has provided useful answer. One solution is to use Overlay widget but it has its downsides, see this answer
My example is on dartpad.dev, example code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#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({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Completer<BuildContext>? _dialog1Completer;
Completer<BuildContext>? _dialog2Completer;
bool _opened1 = false;
bool _opened2 = false;
#override
void initState() {
super.initState();
Timer(const Duration(seconds: 3), () {
_openDialog1();
debugPrint('Opened dialog 1. Dialog should read: "Dialog 1"');
Timer(const Duration(seconds: 2), () {
_openDialog2();
debugPrint('Opened dialog 2. Dialog should read: "Dialog 2"');
Timer(const Duration(seconds: 3), () {
_closeDialog1();
debugPrint('Closed dialog 1. Dialog should read: "Dialog 2"');
Timer(const Duration(seconds: 5), () {
_closeDialog2();
debugPrint('Closed dialog 2. You should not see any dialog at all.');
});
});
});
});
}
Future<void> _openDialog1() async {
setState(() {
_opened1 = true;
});
_dialog1Completer = Completer<BuildContext>();
await showDialog(
barrierDismissible: false,
context: context,
routeSettings: const RouteSettings(name: 'dialog1'),
builder: (dialogContext) {
if (_dialog1Completer?.isCompleted == false) {
_dialog1Completer?.complete(dialogContext);
}
return CustomDialog(title: 'Dialog 1', timeout: false, onClose: _closeDialog1);
});
}
Future<void> _openDialog2() async {
setState(() {
_opened2 = true;
});
_dialog2Completer = Completer<BuildContext>();
await showDialog(
barrierDismissible: false,
context: context,
routeSettings: const RouteSettings(name: 'dialog1'),
builder: (dialogContext) {
if (_dialog2Completer?.isCompleted == false) {
_dialog2Completer?.complete(dialogContext);
}
return CustomDialog(title: 'Dialog 2', timeout: false, onClose: _closeDialog2);
});
}
Future<void> _closeDialog1() async {
final ctx = await _dialog1Completer?.future;
if (ctx == null) {
debugPrint('Could not closed dialog 1, no context.');
return;
}
Navigator.of(ctx, rootNavigator: true).pop();
setState(() {
_dialog1Completer = null;
_opened1 = false;
});
}
Future<void> _closeDialog2() async {
final ctx = await _dialog2Completer?.future;
if (ctx == null) {
debugPrint('Could not closed dialog 2, no context.');
return;
}
Navigator.of(ctx, rootNavigator: true).pop();
setState(() {
_dialog2Completer = null;
_opened2 = false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
TextButton(onPressed: _openDialog1, child: const Text('Open 1')),
TextButton(onPressed: _openDialog2, child: const Text('Open 2')),
const Spacer(),
Align(
alignment: Alignment.bottomCenter,
child: Text('Opened 1? $_opened1\nOpened 2? $_opened2'),
),
],
),
),
);
}
}
class CustomDialog extends StatefulWidget {
const CustomDialog({
Key? key,
required this.timeout,
required this.title,
required this.onClose,
}) : super(key: key);
final bool timeout;
final String title;
final void Function() onClose;
#override
createState() => _CustomDialogState();
}
class _CustomDialogState extends State<CustomDialog>
with SingleTickerProviderStateMixin {
late final Ticker _ticker;
Duration? _elapsed;
final Duration _closeIn = const Duration(seconds: 5);
late final Timer? _timer;
#override
void initState() {
super.initState();
_timer = widget.timeout ? Timer(_closeIn, widget.onClose) : null;
_ticker = createTicker((elapsed) {
setState(() {
_elapsed = elapsed;
});
});
_ticker.start();
}
#override
void dispose() {
_ticker.dispose();
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.title),
content: SizedBox(
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: Text([
'${_elapsed?.inMilliseconds ?? 0.0}',
if (widget.timeout) ' / ${_closeIn.inMilliseconds}',
].join('')))),
actions: [
TextButton(onPressed: widget.onClose, child: const Text('Close'))
],
);
}
}
If you were to run this code and observe console you can see steps being printed, on step #3 you can observe unwanted behaviour:
opened dialog 1 - OK
opened dialog 2 - OK
closed dialog 1 - not OK
I think I understand the problem, Navigator.of(dialogContext, rootNavigator: true) searches for nearest navigator and then calls .pop() method on it, removing the latest route/dialog on its stack.
I would need to remove specific dialog.
What would be the solution here? Multiple Navigator objects?
I highly suggest that in this case you use Overlay in Flutter. Overlays are rendered independently of widgets on the screen and have their own lifetimes. They appear when you ask them to and you can control when and which one of them should disappear at which time.

How to avoid child rebuilding if parent updates

I have a strange requirement. I saw this this question on SO but i can't make it work for my case.
I have an animated container which represent my screen. On pressing the ADD icon. I'm transforming the screen like this (The column in right side image is below the homescreen) . But inside that AnimatedContainer there is a LIST(as child).
Every time I do transfromation. The list is re building itself. Is there any way i can avoid it. ?
You can imagine that the homescreen is pinned to wall with two nails in the left and right top. As I press FAB. The left top nail is pulled out and the screen hangs on right nail support. and when i again press FAB, the left top is again pinned with nail.
This is the widget I'm using
https://pub.dev/packages/matrix4_transform
Here is minimal code to see the rebuilding
import 'package:flutter/material.dart';
import 'package:matrix4_transform/matrix4_transform.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key? key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
_MyStatefulWidgetState? home;
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
DrawerManager drawerManager = DrawerManager();
callSetState() {
setState(() {});
}
#override
Widget build(BuildContext context) {
print('Rebuild');
home = this;
return AnimatedContainer(
transform: Matrix4Transform()
.translate(x: drawerManager.xOffSet, y: drawerManager.yOffSet)
.rotate(drawerManager.angle)
.matrix4,
duration: Duration(milliseconds: 500),
child: Scaffold(
body: MyList(drawerManager),
),
);
}
}
class MyList extends StatelessWidget {
final Data myData = Data();
final DrawerManager drawerManager;
MyList(this.drawerManager);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: myData.data.length+1,
itemBuilder: (context, index) {
print('Building list' + index.toString());
if(index == 4){
return GestureDetector(
child: IconButton(
icon: Icon(Icons.add),
onPressed: () {
drawerManager.callback(drawerManager.isOpened);
}),
);
}
else{return ListTile(
title: Text(myData.data[index]),
);}
},
),
);
}
}
class Data {
List<String> data = ['Hello1', 'Hello2', 'Hello3', 'Hello4'];
}
class DrawerManager {
double xOffSet = 0;
double yOffSet = 0;
double angle = 0;
bool isOpened = false;
void callback(bool isOpen) {
print('Tapped');
if (isOpen) {
xOffSet = 0;
yOffSet = 0;
angle = 0;
isOpened = false;
} else {
xOffSet = 150;
yOffSet = 80;
angle = -0.2;
isOpened = true;
}
callSetState();
}
void callSetState() {
home!.callSetState();
}
}
You can see that When you press that + icon. Screen transforms and the lists are rebuilding.
please use this class ,
https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html
Performance optimizations
If your builder function contains a subtree that does not depend on
the animation, it's more efficient to build that subtree once instead
of rebuilding it on every animation tick.
If you pass the pre-built subtree as the child parameter, the
AnimatedBuilder will pass it back to your builder function so that you
can incorporate it into your build.
You can easily achieve this using provider library.
Give it try πŸ‘
I will recommend to use Getx if you r beginner and if
have experience in app development then I will
Recommend to use Bloc library check both of them on
Pub.

Flutter : No SuchMethodError when I called Stateful Widget

I'm new to flutter. I want to make an app that shows a page selected by BottomNavigationBar.
But when I'm trying to run the app, it throws an Exception. The following is Error Log.
════════ Exception caught by widgets library
The following NoSuchMethodError was thrown building Builder:
The method '_debugTypesAreRight' was called on null.
Receiver: null
Tried calling: _debugTypesAreRight(Instance of 'MainPages')
The relevant error-causing widget was:
MaterialApp file:///C:/Users/jango/AndroidStudioProjects/study_and_statistic/lib/main.dart:49:14
When the exception was thrown, this was the stack:
0 Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5)
1 new StatefulElement.<anonymous closure> (package:flutter/src/widgets/framework.dart:4309:19)
2 new StatefulElement (package:flutter/src/widgets/framework.dart:4320:6)
3 StatefulWidget.createElement (package:flutter/src/widgets/framework.dart:809:38)
4 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3189:40)
and my code is here
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class AppConfig {
static double width;
static double height;
static double blockSize;
static double blockSizeVertical;
static double statusBarHeight;
static double getAppbarHeight(){
double ratioHeight = blockSizeVertical*9;
return (ratioHeight>60)? 60 : ratioHeight;
}
static double getGap(){
double ratioGap = width/20;
return (ratioGap>30)? 30 : ratioGap;
}
static double getFontsize_content(){
double ratioSize = (blockSize>blockSizeVertical)?blockSizeVertical*6:blockSize*6;
return (ratioSize > 18)? 18: ratioSize;
}
static double getFontsize_appBar(){
double ratioSize = (blockSize>blockSizeVertical)?blockSizeVertical*7:blockSize*7;
return (ratioSize > 20)? 20: ratioSize;
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (){
FocusScope.of(context).unfocus();
},
child: MaterialApp(
title: 'STUDY',
theme: ThemeData(
fontFamily: 'NanumBarunGothic',
primaryColor: Color(0XFF5dc19b),
),
home: MainPages() //Here is the problem, maybe..
)
);
}
}
PreferredSize DailyAppBar(){
//My customAppBar
//AppConfig used here
}
class SubjectListTile extends StatelessWidget{
//My custom ListTile
//AppConfig used here
}
class SubjectList extends StatefulWidget{
#override
State createState() => SubjectListState();
}
class SubjectListState extends State<SubjectList>{
//My custom Listview
}
class MainPages extends StatefulWidget{
const MainPages({ Key key }) : super(key: key);
#override
_MainPagesState createState() {
_MainPagesState();
}
}
class _MainPagesState extends State<MainPages>{
int _currentIndex = 0;
final List<Widget> pages = [
SubjectList(),
StudyPage(),
StaticPage(),
];
void init_AppConfig(BuildContext context){
AppConfig.width = MediaQuery.of(context).size.width;
AppConfig.height = MediaQuery.of(context).size.height;
AppConfig.blockSize = AppConfig.width / 100;
AppConfig.blockSizeVertical = AppConfig.height / 100;
AppConfig.statusBarHeight = MediaQuery.of(context).padding.top;
double width = AppConfig.width;
double height = AppConfig.height;
print('width: $width');
print('height: $height');
}
void _onItemTapped(int index){
setState((){
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
init_AppConfig(context);
return Scaffold(
appBar: DailyAppBar(),
body : pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: _onItemTapped,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.check_box),
title: Text('였늘의 곡뢀'),
),
BottomNavigationBarItem(
icon: Icon(Icons.chrome_reader_mode),
title: Text('집쀑λͺ¨λ“œ'),
),
BottomNavigationBarItem(
icon: Icon(Icons.show_chart),
title: Text('기둝'),
),
],
),
);
}
}
class StaticPage extends StatelessWidget{ //Not impleted yet
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child:Text("StaticPage")),
);
}
}
class StudyPage extends StatelessWidget{ //Not impleted yet
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child:Text("StudyPage")),
);
}
}
In MyApp, MainPages() is called as home of MaterialApp. At that time, it throws an Exception.
In MainPagesState class, build() function initializes App Configuration first.
And then it builds Scaffold Widget, which includes DailyAppBar(my custom Appbar), pages[_currentIndex], bottomNavigationBar. Daily AppBar and pages[0] use AppConfig Data.
Is there a mistake when using init_appConfig or bottomNavigationBar?
Appconfig, SubjectListTile, SubjectList and State, DailyAppBar worked well when I put SubjectList() in body of Scaffold directly.
You have missed the return statement.
#override
_MainPagesState createState() {
return _MainPagesState();
}
or just use arrow function
#override
_MainPagesState createState() => _MainPagesState();

How to reset ListView children

I am using a ListView with selectable items similar to this example.
Each stateful widget in the ListView has a _selected boolean to determine it's selected status which is flipped when the item is tapped.
When the user is in selection mode, there is a "back" option in the app bar. Determining when the back button is pressed and handling underlying core logic is working fine. I just want to reset the _selected flag on each individual list item so that they no long display as selected. You can see in the included gif that once back is pressed, the ListView items remain selected.
I am obviously missing something extremely basic.
The underlying question is, how do I trigger a reset of a ListView children items programatically.
Edit: Sample code added
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'List selection demo',
home: new MyHomePage(title: 'List selection demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<String> playerList = [
"Player 1",
"Player 2",
"Player 3",
"Player 4"
];
List<String> selectedPlayers = [];
bool longPressFlag = false;
void longPress() {
setState(() {
if (selectedPlayers.isEmpty) {
longPressFlag = false;
} else {
longPressFlag = true;
}
});
}
void clearSelections(){
setState(() {
selectedPlayers.clear();
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(selectedPlayers.length == 0?widget.title: selectedPlayers.length.toString() + " selected"),
leading: selectedPlayers.length == 0? null: new IconButton(
icon: new Icon(Icons.arrow_back),
onPressed: () {clearSelections();
})),
body: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new PlayerItem(
playerName: playerList[index],
longPressEnabled: longPressFlag,
callback: () {
if (selectedPlayers.contains(playerList[index])) {
selectedPlayers.remove(playerList[index]);
} else {
selectedPlayers.add(playerList[index]);
}
longPress();
});
},
itemCount: playerList.length,
));
}
}
class PlayerItem extends StatefulWidget {
final String playerName;
final bool longPressEnabled;
final VoidCallback callback;
const PlayerItem(
{Key key, this.playerName, this.longPressEnabled, this.callback})
: super(key: key);
#override
_PlayerItemState createState() => new _PlayerItemState();
}
class _PlayerItemState extends State<PlayerItem> {
bool selected = false;
#override
Widget build(BuildContext context) {
return new GestureDetector(
onLongPress: () {
setState(() {
selected = !selected;
});
widget.callback();
},
onTap: () {
if (widget.longPressEnabled) {
setState(() {
selected = !selected;
});
widget.callback();
} else {
final snackBar = SnackBar(content: Text(widget.playerName + " tapped"));
Scaffold.of(context).showSnackBar(snackBar);
}
},
child: new Card(
color: selected ? Colors.grey[300] : Colors.white,
elevation: selected ? 4.0 : 1.0,
margin: const EdgeInsets.all(4.0),
child: new ListTile(
leading: new CircleAvatar(
child: new Text(widget.playerName.substring(0, 1)),
),
title: new Text(widget.playerName),
)));
}
}
You have to call following method for reset widgets.
setState(() {
});