Flutter: Scraping a site with JavaScript generated content without dart:html - dart

How do I scrape a web page that generates some of its content with a JavaScript element? It is a local court booking site for my squash club and from what I can see, this is the JavaScript script it calls to retrieve the court bookings:
function load_js(url) {
var e = document.createElement("script");
e.src = url;
e.type = "text/javascript";
if (document.getElementsByTagName("head")[0].lastChild.src == e.src) {
document.getElementsByTagName("head")[0].replaceChild(e, document.getElementsByTagName("head")[0].lastChild);
}
else {
document.getElementsByTagName("head")[0].appendChild(e);
}
}
onload = function () {
setInterval("load_js('/js/bookings_reload.js.php')", 30000);
};
How could I replicate this in Flutter? I can scrape the HTML and parse it to create an HTML document, but since Flutter disallows the use of the dart:html library, I can't create a ScriptElement to duplicate the JavaScript script. Is there an alternative in Flutter?
Here is my Dart function so far:
var httpClient = createHttpClient();
getData() async {
// Send POST request to get authorized cookie
var response = await httpClient.post('http://bookings.squashgym.co.nz/login',
body: {'username': username, 'password': password});
// Send get request with authenticated cookie to get bookings
var courts = await httpClient.get('http://bookings.squashgym.co.nz/booking-sheet',
headers: {'cookie': response.headers['set-cookie']});
Document data = parse(courts.body);
httpClient.close();
}
Thanks!

I agree on using WebView to load the URL needed. Here is an example implementation of WebView using the webview_flutter.
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'dart:async';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final Completer<WebViewController> _controller =
Completer<WebViewController>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: WebView(
initialUrl: "https://flutter.dev/",
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
},
javascriptMode: JavascriptMode.unrestricted,
),
);
}
}
This is how it will look like:
And when the page is rendered you will be able to get the DOM from the WebView. There is also an available example of getting DOM from WebView. Check it here.

Related

how can i pass a variable to a class and call that variable in any other screen without it being reset

i want to be able to call an empty variable from a class, assign a value to it and make it persistent, anything aside provider e.t.c would be help, i don't want to overhaul the entire app again to do some bloc, provider e.t.c
NB: all screens are stateful widgets
i have tried creating a class with an empty string and passing a value to it from another screen, but this doesn't seem to work
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
class MethodA {
// id(user, context){
// var name =user.email;
// }
String identity;
MethodA({this.iD});
bool isLoggedIn() {
if (FirebaseAuth.instance.currentUser() != null) {
return true;
} else {
return false;
}
}
Future<void> addUserA( userinfo) async {
//this.iD=id;
Firestore.instance
.collection('user')
.document('furtherinfo').collection(identity).document('Personal Info')
.setData(userdoc)
.catchError((e) {
print(e);
});
}
each time i pass the argument to i.e foo='bar';
and i import that class in another screen, i.e screen 9, foo is automatically set to null, but i would want foo to be bar
I would suggest that you use the Provider since it is the easiest way for me to manage state throughout the app. Flutter starts with one component on top of the widget tree so i would place my provider here.
Example
void main() {runApp(MyApp());}
class MyApp extends StatelessWidget {
MyApp();
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged, // Provider to manage user throughout the app.
),
],
child: MaterialApp(
title: 'My App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.green,
primarySwatch: Colors.green,
accentColor: Colors.yellow,
),
home: MainPage(),
),
);
}
}
Then in your class you can do the following
class MethodAService with ChangeNotifier {
String _identity = null;
FirebaseUser _user = null;
// constructor with the (new changes )
MethodAService(FirebaseUser user){
this._user = user;
}
get identity => _identity ;
setIdentity(String identity) {
_identity = identity ;
notifyListeners(); // required to notify the widgets of your change
}
}
Then when you want to use it anywhere in your app just do the following in the build method
#override
Widget build(BuildContext context) {
final user = Provider.of<FirebaseUser>(context); // to get the current user
final methodA = Provider.of<MethodAService>(context); // get your service with identity
// now you can set the string using
methodA.setIdentity('new identity');
// or just use it like this
if(methodA.identity.isNotEmpty()){
print(methodA.identity);
}else{
print('Identity is empty');
}
return ChangeNotifierProvider<MethodAService>(
builder: (context) => MethodAService(user), // Your provider to manage your object, sending the Firebase user in
child: loggedIn ? HomePage() : LoginPage(), );
}
References
Provider Package
Fireship 185 Provider
Great Youtube video explaining the code
Update for comment
For getting the user uid you can just do user.uid
Changed code above to fit the
I'm not sure put the whole app in a StreamProvider is the best choice. That means the app will be rebuilt on each stream value.
To make a Widget available on all screens, you need a TransitionBuilder in your MaterialApp.
To avoid the external dependency you can also use an InheritedWidget
signed_user.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class SignedUser extends InheritedWidget {
final FirebaseUser user;
SignedUser({#required this.user, #required Widget child})
: super(child: child);
#override
bool updateShouldNotify(SignedUser oldWidget) => true;
static SignedUser of(BuildContext context) =>
context.inheritFromWidgetOfExactType(SignedUser);
}
my_transition_builder.dart
class MyTransitionBuilder extends StatefulWidget {
final Widget child;
const MyTransitionBuilder({Key key, this.child}) : super(key: key);
#override
_MyTransitionBuilderState createState() => _MyTransitionBuilderState();
}
class _MyTransitionBuilderState extends State<MyTransitionBuilder> {
StreamBuilder<FirebaseUser> _builder;
#override
void initState() {
super.initState();
_builder = StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
return SignedUser(
child: widget.child,
user: snapshot.data,
);
});
}
#override
Widget build(BuildContext context) {
return _builder;
}
}
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
// this will make your inherited widget available on all screens of your app
builder: (context, child) {
return MyTransitionBuilder(child: child);
},
routes: {
'/editAccount': (context) => new EditAccountPage(),
},
theme: ThemeData(
primarySwatch: Colors.green,
),
home: MyHomePage(),
);
}
}
usage in edit_account_page.dart
#override
Widget build(BuildContext context) {
var user = SignedUser.of(context).user;
return Scaffold(
body: FutureBuilder<DocumentSnapshot>(
future: Firestore.instance.document('users/${user.uid}').get(),

How can I use BottomNavigationBar with BLoC?

When I use BottomNavigationBar with BLoC pattern, it causes the error, Bad state: Stream has already been listened to.
I may listen a stream of a BLoC at just one place.
My code is the following.
main.dart
import 'package:flutter/material.dart';
import 'package:bottom_tab_bloc/app_state_bloc.dart';
import 'package:bloc_provider/bloc_provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return BlocProvider(
creator: (context, _bag) => AppStateBloc(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String activeTab = "tab1";
final bottomTabs = ["tab1", "tab2"];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(activeTab),
),
body: buildTab(activeTab),
bottomNavigationBar: BottomNavigationBar(
currentIndex: bottomTabs.indexOf(activeTab),
onTap: (int index) {
setState(() {
activeTab = bottomTabs[index];
});
},
items: bottomTabs.map((tab) =>
buildBnbItem(tab)
).toList(),
),
);
}
Widget buildTab(String tab) {
if (tab == "tab1") {
return TabOne();
} else if (tab == "tab2") {
return TabTwo();
}
}
BottomNavigationBarItem buildBnbItem (String tab) {
assert(bottomTabs.contains(tab));
if (tab == "tab1") {
return BottomNavigationBarItem(
title: Text('Tab1'),
icon: Icon(Icons.looks_one),
);
} else if (tab == "tab2") {
return BottomNavigationBarItem(
title: Text('Tab1'),
icon: Icon(Icons.looks_two),
);
}
}
}
class TabOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
final AppStateBloc bloc = BlocProvider.of<AppStateBloc>(context);
return StreamBuilder(
stream: bloc.outValue1,
builder: (context, snapshot) =>
Center(child: Text(snapshot.data.toString())),
);
}
}
class TabTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
final AppStateBloc bloc = BlocProvider.of<AppStateBloc>(context);
return StreamBuilder(
stream: bloc.outValue2,
builder: (context, snapshot) =>
Center(child: Text(snapshot.data.toString())),
);
}
}
app_state_bloc.dart
import 'dart:async';
import 'package:bloc_provider/bloc_provider.dart';
class AppStateBloc implements Bloc {
StreamController<int> _value1Controller
= StreamController<int>();
Sink<int> get _inValue1 => _value1Controller.sink;
Stream<int> get outValue1 => _value1Controller.stream;
StreamController<int> _updateValue1Controller
= StreamController<int>();
Sink<int> get updateValue1 =>
_updateValue1Controller.sink;
StreamController<int> _value2Controller
= StreamController<int>();
Sink<int> get _inValue2 => _value2Controller.sink;
Stream<int> get outValue2 => _value2Controller.stream;
StreamController<int> _updateValue2Controller
= StreamController<int>();
Sink<int> get updateValue2 =>
_updateValue2Controller.sink;
AppStateBloc(){
_inValue1.add(1);
_updateValue1Controller.stream.listen(_updateValue1);
_inValue2.add(2);
_updateValue2Controller.stream.listen(_updateValue2);
}
#override
void dispose() {
_value1Controller.close();
_updateValue1Controller.close();
_value2Controller.close();
_updateValue2Controller.close();
}
void _updateValue1(int value1) {
_inValue1.add(value1);
}
void _updateValue2(int value2) {
_inValue2.add(value2);
}
}
I can go to the TabTwo from TabOne only at the first time, but the error occurs when I go back to TabOne .
I also tried using StreamController<int>.broadcast() in app_state_bloc.dart, but snapshot.data is always null.
How can I implement BottomNavigationBar with BLoC pattern?
Why the streams are called more than twice, though I write each stream at just one place?
Is AppStateBloc.dispose() is called in this code? When and where AppStateBloc.dispose() is called?
Why broadcast stream's snapshot.data is always null?
Why the streams are called more than twice, though I write each stream
at just one place?
The error is related to the number of subscribers to the stream. StreamController by default only allows one subscriber. That's why TabOne works fine the first time, but breaks afterwards.
Is AppStateBloc.dispose() is called in this code? When and where
AppStateBloc.dispose() is called?
It would be called when the BlocProvider widget gets removed, but since it's being used as the app root, I guess this only happens when the app is closed.
Why broadcast stream's snapshot.data is always null?
Broadcast streams do not buffer events when there is no listener. Since you're writing to the stream before TabOne is created, the event is lost and you get null.
How can I implement BottomNavigationBar with BLoC pattern?
I guess it depends on your use case, but for this particular example, if you replace StreamController with rxdart's BehaviorSubject, it works fine, because then you'd have a broadcast stream that always sends you the last event.

How to load theme at beginning in Flutter

I want to users can change and save the theme color in my app. However, I have no ideas how to load the saved theme color when the app starts running. For example, I want to load the saved theme color directly in the comment place below. I tried SharedPreference. However, the SharedPreference instance needs to run with await. It seems can't be used here. Is there any way I can load the saved theme here directly instead of using setState or something like it?
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: // how to load saved theme here?
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
This answer goes a bit further. It shows how to load and save theme preferences, how to build a ThemeData, and how to change the theme from a page of your app.
Save the user preferences (which theme is selected) using the shared_preferences plugin.
Use the "controller pattern" that is used throughout the Flutter framework to provide the currently selected theme (and changes to it) to your app.
Use an InheritedWidget to use the controller in any part of your app.
Here is how the controller looks like:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// provides the currently selected theme, saves changed theme preferences to disk
class ThemeController extends ChangeNotifier {
static const themePrefKey = 'theme';
ThemeController(this._prefs) {
// load theme from preferences on initialization
_currentTheme = _prefs.getString(themePrefKey) ?? 'light';
}
final SharedPreferences _prefs;
String _currentTheme;
/// get the current theme
String get currentTheme => _currentTheme;
void setTheme(String theme) {
_currentTheme = theme;
// notify the app that the theme was changed
notifyListeners();
// store updated theme on disk
_prefs.setString(themePrefKey, theme);
}
/// get the controller from any page of your app
static ThemeController of(BuildContext context) {
final provider = context.inheritFromWidgetOfExactType(ThemeControllerProvider) as ThemeControllerProvider;
return provider.controller;
}
}
/// provides the theme controller to any page of your app
class ThemeControllerProvider extends InheritedWidget {
const ThemeControllerProvider({Key key, this.controller, Widget child}) : super(key: key, child: child);
final ThemeController controller;
#override
bool updateShouldNotify(ThemeControllerProvider old) => controller != old.controller;
}
Here is how you would use the controller and InheritedWidget in your app:
void main() async {
// load the shared preferences from disk before the app is started
final prefs = await SharedPreferences.getInstance();
// create new theme controller, which will get the currently selected from shared preferences
final themeController = ThemeController(prefs);
runApp(MyApp(themeController: themeController));
}
class MyApp extends StatelessWidget {
final ThemeController themeController;
const MyApp({Key key, this.themeController}) : super(key: key);
#override
Widget build(BuildContext context) {
// use AnimatedBuilder to listen to theme changes (listen to ChangeNotifier)
// the app will be rebuilt when the theme changes
return AnimatedBuilder(
animation: themeController,
builder: (context, _) {
// wrap app in inherited widget to provide the ThemeController to all pages
return ThemeControllerProvider(
controller: themeController,
child: MaterialApp(
title: 'Flutter Demo',
theme: _buildCurrentTheme(),
home: MyHomePage(),
),
);
},
);
}
// build the flutter theme from the saved theme string
ThemeData _buildCurrentTheme() {
switch (themeController.currentTheme) {
case "dark":
return ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.orange,
);
case "light":
default:
return ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.blue,
);
}
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(),
body: Center(
child: Column(
children: <Widget>[
RaisedButton(
onPressed: () {
// thanks to the inherited widget, we can access the theme controller from any page
ThemeController.of(context).setTheme('light');
},
child: Text('Light Theme'),
),
RaisedButton(
onPressed: () {
ThemeController.of(context).setTheme('dark');
},
child: Text('Dark Theme'),
)
],
),
),
);
}
}
You have a few options as to how you'd load it. The first is as Gunter said in a comment - you make MyApp into a stateful widget and load it with initState(), then setState it.
That would look something like this:
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
ThemeData theme = ThemeData.dark(); // whatever your default is
#override
void initState() {
super.initState();
SharedProperties.getInstance().then((prefs) {
ThemeData theme = ThemeData.light(); // load from prefs here
setState(() => this.theme = theme);
});
}
...
}
The second option is to use a FutureBuilder.
class MyApp extends StatelessWidget {
final Future<ThemeData> loadThemeData = SharedPreferences.getInstance().then((prefs) {
... get theme from prefs
return ThemeData.light();
});
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: loadThemeData,
builder: (context, snapshot) {
return MaterialApp(
theme: snapshot.data,
);
},
initialData: ThemeData.dark(), // whatever you want your default theme to be
);
}
}
The third option is to do the loading before you actually start your app - in your main method. I don't know if this is really recommended as if sharedpreferences takes a while it could delay the start of your app, but realistically it should be very quick and you probably want to avoid a flash different theme showing anyways.
main() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
ThemeData theme = ThemeData.dark(); // get theme from prefs
runApp(MyApp(
theme: theme,
));
}
class MyApp extends StatelessWidget {
final ThemeData theme;
const MyApp({Key key, #required this.theme}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: theme,
....
);
}
}
Load theme data from local storage in main function as await

How to listen to Drawer open/close animation in Flutter

Being new to Flutter, I'm doing a learning exercise by re-creating my existing Android app. However I'm having trouble to produce a 'spinning, growing home icon', which should be animated in sync with the drawer open/close animation.
The desired drawer/home-icon behaviour looks like this:
I made this in Android by implementing
DrawerListener.onDrawerSlide(View drawerView, float slideOffset)
My naive approach to do this in Flutter, is to use a ScaleTransition and a RotationTransition that listen to the same Animation that opens/closes the Drawer.
I can see that ScaffoldState has a DrawerControllerState, but it is private.
final GlobalKey<DrawerControllerState> _drawerKey = new GlobalKey<DrawerControllerState>();
And even if I could somehow access the DrawerControllerState (which I don't know how), I then couldn't access _animationChanged() and _controller because both are private members of DrawerControllerState.
I feel that I'm coming at this in the wrong way, and that there is an better approach that's more natural to Flutter, that I'm unable to see.
Please can anyone describe the Flutter way of implementing this?
You can first refer to other people's replies on stackoverflow here
My solve:
get Drawer status on DrawerWidget
initState() : open drawer
dispose() : close drawer
Stream drawer status by DrawerService Provider
see full code
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:provider/provider.dart';
void main() {
runApp(
MultiProvider(
providers: [
Provider(create: (_) => DrawerService()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
DrawerService _drawerService;
String drawerStatus = 'close';
#override
void initState() {
super.initState();
_drawerService = Provider.of(context, listen: false);
_listenDrawerService();
}
_listenDrawerService() {
_drawerService.status.listen((status) {
if(status) {
drawerStatus = 'open';
} else {
drawerStatus = 'close';
}
setState(() { });
});
}
#override
Widget build(BuildContext context) {
Color bgColor = Colors.yellow;
if(drawerStatus == 'open') {
bgColor = Colors.red;
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: DrawerWidget(),
body: Container(
decoration: BoxDecoration(color: bgColor),
height: 300,
child: Center(child: Text(drawerStatus),),
),
);
}
}
class DrawerWidget extends StatefulWidget {
#override
_DrawerWidgetState createState() => _DrawerWidgetState();
}
class _DrawerWidgetState extends State<DrawerWidget> {
DrawerService _drawerService;
#override
void initState() {
super.initState();
_drawerService = Provider.of(context, listen: false);
_drawerService.setIsOpenStatus(true);
}
#override
Widget build(BuildContext context) {
return Drawer(
child: Center(child: Text('drawer'),),
);
}
#override
void dispose() {
super.dispose();
_drawerService.setIsOpenStatus(false);
}
}
class DrawerService {
StreamController<bool> _statusController = StreamController.broadcast();
Stream<bool> get status => _statusController.stream;
setIsOpenStatus(bool openStatus) {
_statusController.add(openStatus);
}
}
hope to help some body

How do I allow users to select text from a Text widget?

On Android, I'm used to TextView's textIsSelectable attribute but I didn't see that in the Text docs.
Currently I'm using a TextField (editable) and not saving any changes to the displayed text. My primary need is to allow copy-paste.
I solved this in one of my projects with the following (simplified) code:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
String _text = 'TestString';
/// Pastes given String to the clipboard and shows Popup-Snackbar
void copyToClipboard(String toClipboard) {
ClipboardData data = new ClipboardData(text: toClipboard);
Clipboard.setData(data);
_scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text(toClipboard + ' copied to clipboard.'),
));
}
#override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text('TestProject'),
),
body: new InkWell(
onLongPress: () => copyToClipboard(_text),
child: new Center(
child: new Text(_text),
),
),
);
}
}
So just wrap your text in a widget that can detect gestures or use a GestureDetector to call the clipboard method.
Hope it helps.

Resources