The method nextPage() and previousPage() in PageController doesn't work sometimes - dart

Here is my code:
I response key event in Android MainActivity, and use BasicMessageChannel to post key message:
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "scroll";
private static final String KEY_LEFT = "keyLeft";
private static final String KEY_RIGHT = "keyRight";
private BasicMessageChannel messageChannel;
private FlutterView flutterView;
private String[] getArgsFromIntent(Intent intent) {
// Before adding more entries to this list, consider that arbitrary
// Android applications can generate intents with extra data and that
// there are many security-sensitive args in the binary.
ArrayList<String> args = new ArrayList<String>();
if (intent.getBooleanExtra("trace-startup", false)) {
args.add("--trace-startup");
}
if (intent.getBooleanExtra("start-paused", false)) {
args.add("--start-paused");
}
if (intent.getBooleanExtra("enable-dart-profiling", false)) {
args.add("--enable-dart-profiling");
}
if (!args.isEmpty()) {
String[] argsArray = new String[args.size()];
return args.toArray(argsArray);
}
return null;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
flutterView = new FlutterView(this);
String[] args = getArgsFromIntent(getIntent());
FlutterMain.ensureInitializationComplete(getApplicationContext(), args);
flutterView.runFromBundle(FlutterMain.findAppBundlePath(getApplicationContext()), null);
messageChannel = new BasicMessageChannel<>(flutterView, CHANNEL, StringCodec.INSTANCE);
}
#Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
Log.d(TAG, "dispatchKeyEvent: ACTION_DOWN keyCode = " + event.getKeyCode());
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
messageChannel.send(KEY_LEFT);
return true;
case KeyEvent.KEYCODE_DPAD_RIGHT:
messageChannel.send(KEY_RIGHT);
return true;
default:
break;
}
}
return super.dispatchKeyEvent(event);
}
#Override
protected void onDestroy() {
if (flutterView != null) {
flutterView.destroy();
}
super.onDestroy();
}
#Override
protected void onPause() {
super.onPause();
flutterView.onPause();
}
#Override
protected void onPostResume() {
super.onPostResume();
flutterView.onPostResume();
}
}
When Flutter receives the message, I call nextPage() and previousPage() to scroll PageView, it doesn't work. But I find if I call nextPage() and previousPage() in onTap() method of the GestureDetector, it works:
class _Page {
_Page({
this.imagePath,
});
final String imagePath;
}
final List<_Page> _allPages = <_Page>[
new _Page(imagePath: 'images/1.jpg'),
new _Page(imagePath: 'images/2.jpg'),
new _Page(imagePath: 'images/3.jpg'),
new _Page(imagePath: 'images/4.jpg'),
new _Page(imagePath: 'images/5.jpg'),
new _Page(imagePath: 'images/6.jpg'),
];
class ScrollablePageDemo extends StatefulWidget {
#override
_ScrollablePageDemoState createState() => new _ScrollablePageDemoState();
}
class _ScrollablePageDemoState extends State<ScrollablePageDemo>
with SingleTickerProviderStateMixin {
PageController _controller;
static const String _channel = 'scroll';
static const String _emptyMessage = '';
static const String KEY_LEFT = "keyLeft";
static const String KEY_RIGHT = "keyRight";
static const BasicMessageChannel<String> platform =
const BasicMessageChannel<String>(_channel, const StringCodec());
#override
void initState() {
super.initState();
_controller = new PageController(
initialPage: 0, keepPage: true, viewportFraction: 1.0);
platform.setMessageHandler(changePage);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
Future<String> changePage(String message) async {
setState(() {
print(message);
if (message == KEY_RIGHT) {
//here doesn't work
_controller.nextPage(duration: kTabScrollDuration, curve: Curves.ease);
print("tab right: page = " + _controller.page.toString());
} else if (message == KEY_LEFT) {
//here doesn't work
_controller.previousPage(duration: kTabScrollDuration, curve: Curves.ease);
print("tab left: page = " + _controller.page.toString());
}
});
return _emptyMessage;
}
PageView buildPageView() {
return new PageView(
controller: _controller,
children: _allPages.map((_Page page) {
return new Container(
key: new ObjectKey(page.imagePath),
padding: const EdgeInsets.all(12.0),
child: new Card(
child: new Image.asset(page.imagePath, fit: BoxFit.fill)));
}).toList(),
);
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onTap: () {
print('Listen PageView');
//here works
_controller.nextPage(duration: kTabScrollDuration, curve: Curves.ease);
},
child: new Scaffold(
appBar: new AppBar(
title: new Text('PageView'),
),
body: buildPageView()));
}
}
void main() {
runApp(new MaterialApp(
title: 'Flutter Study',
home: new ScrollablePageDemo(),
));
}

The problem is with setState method inside changePage. The setState invokes build method whenever the state set. Thus the page gets built again whenever setState called. You can just remove the setState from changePage method.
example:
Future<String> changePage(String message) async {
print(message);
if (message == KEY_RIGHT) {
_controller.nextPage(duration: kTabScrollDuration, curve: Curves.ease);
print("tab right: page = " + _controller.page.toString());
} else if (message == KEY_LEFT) {
_controller.previousPage(duration: kTabScrollDuration, curve: Curves.ease);
print("tab left: page = " + _controller.page.toString());
}
return _emptyMessage;
}
Hope that helped!

Related

StateError (Bad state: No element) on IOS only

This error does not occur on Android or web but only on IOS. It seem very trivial but I can't figure out what's wrong.
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'dart:convert';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class ScanQrPage extends StatefulWidget {
#override
_ScanQrPageState createState() => _ScanQrPageState();
}
class _ScanQrPageState extends State<ScanQrPage> {
final qrKey = GlobalKey();
late QRViewController qrViewController;
late Barcode barcode;
// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
#override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
qrViewController.pauseCamera();
} else if (Platform.isIOS) {
qrViewController.resumeCamera();
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
Navigator.of(context).pop("");
return new Future(() => true);
},
child: Scaffold(
body: Stack(
children: [
buildQrView(context),
],
),
),
);
}
Widget buildQrView(BuildContext context) {
return QRView(
onQRViewCreated: onQRViewCreated,
key: qrKey,
overlay: QrScannerOverlayShape(
cutOutSize: MediaQuery.of(context).size.width * 0.8),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
);
}
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
log('${DateTime.now().toIso8601String()}_onPermissionSet $p');
if (!p) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('no Permission')),
);
}
}
#override
void dispose() {
qrViewController.dispose();
super.dispose();
}
void onQRViewCreated(QRViewController qrViewController) {
setState(() {
this.qrViewController = qrViewController;
});
qrViewController.scannedDataStream.listen((event) {
setState(() {
this.barcode = event;
if (Platform.isAndroid) {
qrViewController.pauseCamera();
} else if (Platform.isIOS) {
qrViewController.resumeCamera();
}
String rawData = event.code;
Uri data = Uri.dataFromString(rawData);
String para1 = data.queryParameters["buy"] ??
""; //get parameter with attribute "para1"
Codec<String, String> stringToBase64 = utf8.fuse(base64);
if (para1 != "") {
placer = stringToBase64.decode(para1);
}
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
Navigator.pop(context, placer);
});
});
});
}
}
I've tried all the solutions with the same error found on stackoverflow (addPostFrameCallback and Future(Duration.zero)) but none of them are exactly the same and does not seem to fix my problem.
I don't think I have having the same issue as any of the other questions.
The exception is happening on the Navigator.pop(context, placer);
Does anyone have any idea how to overcome this?
Why does this only happen on IOS?

Flutter don't update UI from iOS using setState() method

I am invoking a method from native Swift code by using the platform channel like this:
channel.invokeMethod(METHOD_NAME, arguments: STRING_ARGUMENT)
in my Flutter class I handle the respective method call by using a callback
platform.setMethodCallHandler(_receiveFromHost);
The setMethodCallHandler() requires the callback to return a Future and I set state in _receiveFromHost method in Dart.
Problem is that my UI is not updated when Swift invokes the callback. Why?
call.method catch method string, state is set, but UI don't update.
video
This is my code in Swift:
let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
let flutterViewController = FlutterViewController(engine: flutterEngine!, nibName: nil, bundle: nil)
let channel = FlutterMethodChannel(name: "flutter_apple_pay", binaryMessenger: flutterViewController.binaryMessenger)
channel.invokeMethod("sendDidFinishAdding", arguments: "payment success")
And this is my code in Flutter:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'dart:convert';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text("Native Code from Dart"),
),
body: new MyHomePage(title: ""),
);
}
}
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> {
String _state = "Waiting results...";
static const platform = const MethodChannel("flutter_apple_pay");
_MyHomePageState() {
platform.setMethodCallHandler(_receiveFromHost);
}
// #override
// void initState() {
// platform.setMethodCallHandler(_receiveFromHost);
// super.initState();
// }
Future<dynamic> _receiveFromHost(MethodCall call) async {
List certificatesBase64Encoded;
String nonceBase64Encoded;
String nonceSignatureBase64Encoded;
String state;
try {
print(call.method);
if (call.method == "sendGenerateRequestWithData") {
final String data = call.arguments;
final jData = jsonDecode(data);
certificatesBase64Encoded = jData['certificatesBase64Encoded'];
nonceBase64Encoded = jData['nonceBase64Encoded'];
nonceSignatureBase64Encoded = jData['nonceSignatureBase64Encoded'];
print(certificatesBase64Encoded);
print(nonceBase64Encoded);
print(nonceSignatureBase64Encoded);
state = "sendGenerateRequestWithData";
platform.invokeMethod("SendTokenizationData", "TokenizationDataString");
} else if (call.method == "sendDidFinishAdding") {
final String result = call.arguments;
print(result);
state = result;
}
} on PlatformException catch (e) {
print(e);
}
setState(() {
_state = state;
});
print(_state);
}
Future<void> _checkCardState() async {
String cardState;
const primaryAccountIdentifiers = ["a", "b"];
try {
final String result = await platform.invokeMethod(
"checkCardState", primaryAccountIdentifiers);
cardState = result;
} on PlatformException catch (e) {
cardState = "Failed to Invoke: '${e.message}'.";
}
setState(() {
_state = cardState;
});
}
Future<void> _startApplePay() async {
const _cardNetwork = "cn/AMEX";
const _cardHolderName = "pero peric";
const _primaryAccountIdentifier = "a";
const _primaryAccountSuffix = "1234";
const _localizedDescription = "AMEX";
Map<String, dynamic> resultMap = Map();
resultMap['cardNetwork'] = _cardNetwork;
resultMap['cardHolderName'] = _cardHolderName;
resultMap['primaryAccountIdentifier'] = _primaryAccountIdentifier;
resultMap['primaryAccountSuffix'] = _primaryAccountSuffix;
resultMap['localizedDescription'] = _localizedDescription;
platform.invokeMethod("startApplePay", resultMap);
}
#override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: Text('Check Card State'),
onPressed: _checkCardState,
),
ElevatedButton(
child: Text('Start Apple Pay'),
onPressed: _startApplePay,
),
Text(_state),
],
),
),
);
}
}
Thanks!

Get data from JSON and add into the List on initState()

I have JSON file below.
Data.json
[{
"rownum": 1,
"total": 10.99793271,
"total2": 106.65666751,
}, {
"rownum": 2,
"total": 10.99793271,
"total2": 106.65666751,
}]
and the class Item and List
List <Item> item;
class Item {
String row;
String total;
String total2;
Student({this.row, this.total, this.total2});
}
How can I get data from data.json and add them into List <Item> item on the initState()?
Like this
class MyAppState extends State<MyApp> {
#override
void initState() {
Future<String> _loadAStudentAsset() async {
return await rootBundle.loadString('assets/data.json');
}
//....some code to add value into list
super.initState();
}
That solution is also valid for you:
Flutter: How to display a short text file from assets on screen of phone?
If we make another example with same template:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
void main() {
runApp(Test());
}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
Future _future;
Future<String> loadString() async =>
await rootBundle.loadString('assets/data.json');
#override
void initState() {
_future = loadString();
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FutureBuilder(
future: _future,
builder: (context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return Text('Loading...');
}
List<dynamic> parsedJson = jsonDecode(snapshot.data);
items = parsedJson.map((element) {
return Item(
row: element['rownum'].toString(),
total: element['total'].toString(),
total2: element['total2'].toString(),
);
}).toList();
;
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Column(
children: <Widget>[
Text(item.row),
Text(item.total),
Text(item.total2),
],
);
},
);
},
),
),
);
}
}
List<Item> items;
class Item {
String row;
String total;
String total2;
Item({this.row, this.total, this.total2});
}
class Item {
static final String db_row = "rownum";
static final String db_total = "total";
static final String db_total2 = "total2";
int row;
double total;
double total2;
Item({this.row, this.total, this.total2});
Item.fromMap(Map map) {
this.row = map[Item.db_row];
this.total = map[Item.db_total];
this.total2 = map[Item.db_total2];
}
Map toMap() =>
{Item.db_row: row, Item.db_total: total, Item.db_total2: total2};
static List<Item> fromMapList(mapList) {
List<Item> items = new List();
new List.from(mapList).forEach((mapItem) {
items.add(Item.fromMap(mapItem));
});
return items;
}
}
And
List <Item> items = Item.fromMapList(await rootBundle.loadString('assets/data.json'));

Flutter Cancel Geolocator Listener

I am using Geolocator library to detect the location of the user. It works well however, I would like the listener to stop receiving any updates when the particular screen requesting the location updates is closed. I have no idea how to achieve this. Here is my code
class _CheckLoactionScreenState extends State<CheckLocationScreen>{
String _geoHash = "No Geo Hash";
String _placeId = "No Place Detected";
String _coordinates = "";
var geolocator = Geolocator();
var locationOptions = LocationOptions(accuracy: LocationAccuracy.high, distanceFilter: 10);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: _body(),
);
}
_body(){
return Center(
child: Text(_geoHash),
),
);
}
_getLocation(){
geolocator.getPositionStream(locationOptions).listen(
(Position position) {
//print("${position.toString()}");
var newGeoHash = Geohash.encode(position.latitude, position.longitude).substring(0,8);
if(newGeoHash != _geoHash){
setState((){
_geoHash = newGeoHash;
});
}
_coordinates = position.toString();
print(_geoHash);
});
}
#override
void initState() {
super.initState();
_getLocation();
}
#override
void dispose() {
print("**** dispose");
geolocator.getPositionStream(null).listen(null);
super.dispose();
}
}
I attempt to cancel the listener in the dispose method but the listener still persists.
StreamSubscription _getPositionSubscription;
_getLocation(){
_getPositionSubscription = geolocator.getPositionStream(locationOptions).listen(
...
}
#override
void dispose() {
_getPositionSubscription?.cancel();
super.dispose();
}

Flutter: Scratch card

Here is the example of a scratch card in react-native, I want to implement in Flutter, But I am not getting any way to do this. I tried with blendMode but it is not working, and even there is no clear functionality are given in CustomPaint in the flutter. https://github.com/aleksik/react-scratchcard.
Future<Null> main() async {
runApp(MaterialApp(home: Scaffold(body: new Roller())));
}
class Roller extends StatefulWidget {
#override
_ScratchCardState createState() => new _ScratchCardState();
}
class _ScratchCardState extends State<Roller> {
ui.Image _image;
String _urlImage = 'assets/BedRoom.png';
List<Offset> _points = <Offset>[];
#override
void initState() {
// TODO: implement initState
super.initState();
super.initState();
load(_urlImage).then((j) {
_image = j;
print('image:${_image}');
});
}
Future<ui.Image> load(String asset) async {
ByteData data = await rootBundle.load(asset);
ui.Codec codec = await ui.instantiateImageCodec(
data.buffer.asUint8List(),
);
ui.FrameInfo fi = await codec.getNextFrame();
return fi.image;
}
void _onPanStart(DragStartDetails dt) {
print('drag start ');
setState(() {
});
}
Offset _localPosition;
void _onPanUpdate(DragUpdateDetails details) {
setState(() {
RenderBox object = context.findRenderObject();
_localPosition = object.globalToLocal(details.globalPosition);
_points = new List.from(_points)..add(_localPosition);
});
}
_onPanEnd(DragEndDetails dt) {
_points.add(null);
}
#override
void dispose() {
super.dispose();
}
Widget _scale(BuildContext context) {
return GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
onDoubleTap: () {
setState(() {
_points.clear();
});
},
child: Container(
child: CustomPaint(
painter: ScratchCard(
imagePath: _image, points: _points,),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: _scale(context),
);
}
}
class ScratchCard extends CustomPainter {
final ui.Image imagePath;
List<Offset> points;
ScratchCard({Key key, this.imagePath, this.points}) : super();
Paint _paint = Paint();
Rect rect, inputSubrect, outputSubrect;
Path path = new Path();
Paint paint1 = new Paint();
#override
void paint(Canvas canvas, Size size) {
_paint.blendMode = BlendMode.src;
if (imagePath != null)
canvas.drawImage(imagePath, Offset(10.0, 100.0), _paint);
Paint paint = new Paint()
..color = Colors.white
..strokeCap = StrokeCap.round
..strokeWidth = 30.0;
_paint.blendMode = BlendMode.clear;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
path.reset();
}
}
}
#override
bool shouldRepaint(ScratchCard oldDelegate) {
return true;
}
}
I've created a library for scratch cards in Flutter which supports both - color and image scratchable overlays. You can get it from https://pub.dev/packages/scratcher Feedback is more than welcome!
Example:
Scratcher(
brushSize: 30,
threshold: 50,
color: Colors.red,
onChange: (value) { print("Scratch progress: $value%"); },
onThreshold: () { print("Threshold reached, you won!"); },
child: Container(
height: 300,
width: 300,
child: Text("Hidden text"),
),
)

Resources