i have an app that renders a webpage inside a webview. this actually has a bunch of crypto addresses.
I want them to be automatically made clickable. and when they are clicked - i want to show a popup (some information about the addresses).
can this be done ?
im very unsure if this thing about changing the UI is possible...but in desktop web world, there are extensions that do this. if there are any examples of flutter webview codebases that do this, that would be helpful
the second point - communicating back and forth with the webpage is even more confusing. can this be done at all ? can i receive the data of the click back to main flutter app and then do something ?
Here's a working example
This is achieved as I said by Injecting Js when the webpage loads, as for communication with Flutter, I used the flutter Webview plugin provided JavascriptChannel
The Javascript code looks for a specific element firstly on Page load and secondly while scrolling the webpage (to account for newly created dynamic elements)
Here's how the flow works
JS: assigns the element a new css style (Or in your case make it look like a button) or even create a button and insert it into the webpage
JS: assign on click to the element to call the Flutter JS Channel.
Flutter: Receive message Display a snackbar - you can deeplink or do whatever you want.
As the comments on the JS code say. the scrolling behavior calls every time which is not always ideal, you can use another function make it only trigger on a specific scroll distance
Full working example
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
dynamic ctrl;
// if you have multiple elements, just use querySelector All and loop
const jsCodeToForAnElement = """
// Choose the element
watch();
// I would add dealy to calculate delta between scrolls
// Meaning this code wont watch on every scroll
window.onscroll = watch();
function watch(){
let elm = document.querySelector("main h1");
try{
// Style it
elm.style.cssText = "background: red";
// Add on click
elm.onclick = (event)=> {
var walletHTMLElement = event.target;
// Use native API to communicate with Flutter
jsMessager.postMessage('Wallet clicked: ' + walletHTMLElement.innerHTML);
};
}catch(e){
jsMessager.postMessage('Error: ' + e);
}
}
""";
void main() {
runApp(
MaterialApp(
home: WebViewExample(),
),
);
}
class WebViewExample extends StatefulWidget {
#override
WebViewExampleState createState() => WebViewExampleState();
}
class WebViewExampleState extends State<WebViewExample> {
#override
void initState() {
super.initState();
// Enable virtual display.
if (Platform.isAndroid) WebView.platform = AndroidWebView();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: WebView(
initialUrl: 'https://flutter.dev/',
debuggingEnabled: true,
onWebViewCreated: (WebViewController webViewController) {
ctrl = webViewController;
},
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: <JavascriptChannel>{
JavascriptChannel(
name: 'jsMessager',
onMessageReceived: (jsMessager) async {
if (jsMessager.message.contains("Wallet")) {
var snackBar = SnackBar(
content: Text(jsMessager.message),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
// Do Other Thing like deeplinking to crypto app
// Deeplink to wallet LaunchApp(wallet) <-- clean wallet string first
}
}),
},
onPageStarted: (String url) async {
ctrl.runJavascript(jsCodeToForAnElement);
},
onPageFinished: (String url) async {},
),
);
}
}
Original Answer
This would be possible by Injecting Javascript into Webviews (This is one idea)
1 - I would wait for the page to load
2 - Modify the HTML content using Javascript
Should be pretty straight forward.
Refer to this Answer to see how it is done in Flutter.
https://stackoverflow.com/a/73240357/6151564
Related
I'm trying to add a firebase analytics track screen to my app.
I tried to enter it in the menu like in the picture below but it always doesn't read
I've tried some code but it doesn't work. For now, the code I'm using is as follows:
FirebaseAnalyticsObserver(analytics: analytics);
onGenerateRoute: widget.appRouter.onGenerateRoute,
builder: EasyLoading.init(),
initialRoute: splashScreenRoute,
navigatorObservers: <NavigatorObserver>[
// FirebaseAnalyticsObserver(analytics: _analytics),
observer
]
and on each screen I add code like this below on each initState()
analytics.setCurrentScreen(screenName: 'Page Detail Mobil');
i have re-run the app but it doesn't work track screen and put in firebase analytic. please help me thank you
I have used different function to track my screens. I have called my logScreens function on routing or you can call it on initState of each pages.
This is my analytic_service.dart
class AnalyticsService {
final FirebaseAnalytics _analytics = FirebaseAnalytics();
Future logScreens({#required String? name}) async {
await _analytics.setCurrentScreen(screenName: name);
}
}
and this is locator.dart
import 'package:get_it/get_it.dart';
import 'analytic_service.dart';
GetIt locator = GetIt.instance;
void setupLocator() {
if (!locator.isRegistered<AnalyticsService>()) {
locator.registerLazySingleton(() => AnalyticsService());
}
}
Call the logScreens function when needed.
locator<AnalyticsService>().logScreens(name: "Dashboard");
And this will be logged in analytics like this.
Try this on DebugView. Click on the screen_view. It will show the screen like this.
Here is my sample React component:
const OwnerView = () => {
const [monthlyCharge, setMonthlyCharge] = useState(0)
useEffect(() => {
getPerMonthCharges(ownerPhoneNumber, vehicles.length)
}, [])
async function getPerMonthCharges(ownerPhoneNumber, noOfCars) {
console.log(`inside getPerMonthCharges`);
try {
const serviceProviderChargesDoc = await firestore().collection(`${serviceProviderId}_charges`).doc(`${ownerPhoneNumber}`).get()
if (serviceProviderChargesDoc?.data()?.chargesPerMonth > 0) {
setMonthlyCharge(serviceProviderChargesDoc?.data()?.chargesPerMonth)
return
}
} catch (error) {
console.log(`Error while fetching monthly charge ${error}`);
}
setMonthlyCharge(noOfCars * perMonthGeneralCharge)
console.log(`done with getPerMonthCharges`);
}
}
There is a possibility that OwnerView gets unmounted even before getPerMonthCharges() completes its execution. Therefore in case OwnerView gets unmounted I receive a warning that am doing state update on an unmounted component and this is a non-op. Can someone please highlight what is your observation and right way to write this piece of code?
There are many ways to address this
You can check if the component is still Mounted, a bit ugly approach I agree, but quite a standard one (I would just use something like useAsync from react-use, which essentially does the same, but hides the ugliness)
Move loading logic outside of UI and make part of the global state (Redux, MobX, Apollo, or any other state management library), it would be in lines of separation of concerns and should make your code more readable.
The worst would be to prevent your user from any actions, while content is loading - making your app seem clunky, but React would not complain anymore.
The closest to the right way would be 2, but this can sparkle religious debates and some witch-burning, which I'm not a fan of.
You can refer to this: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup
You can have a variable to keep track whether your component has unmount, let isMounted = true inside useEffect and set it to false as soon as the component is unmounted.
The code will be:
useEffect(() => {
let isMounted = true;
async function getPerMonthCharges(ownerPhoneNumber, noOfCars) {
console.log(`inside getPerMonthCharges`);
try {
const serviceProviderChargesDoc = await firestore().collection(`${serviceProviderId}_charges`).doc(`${ownerPhoneNumber}`).get()
if (serviceProviderChargesDoc?.data()?.chargesPerMonth > 0 && isMounted) { // add conditional check
setMonthlyCharge(serviceProviderChargesDoc?.data()?.chargesPerMonth)
return
}
} catch (error) {
console.log(`Error while fetching monthly charge ${error}`);
}
if (isMounted) setMonthlyCharge(noOfCars * perMonthGeneralCharge) // add conditional check
console.log(`done with getPerMonthCharges`);
}
getPerMonthCharges(ownerPhoneNumber, vehicles.length)
return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []);
According to the Flutter's official deep linking page, we do not require any plugin or native Android/iOS code for handling deep links.
But it doesn't really tell us how we can get the data from that link. I'm talking from coding perspective. Sure, they have written in there that:
But this does not tell me where should I write what code to actually get the complete link. I've looked for examples/tutorials but I'm unable to find anything that is not using a plugin for handling deep linking.
Right now, all I've done is add <intent-filter> tags in AndroidManifest.xml file and on clicking the link, my app has started to show up. But I don't know how to extract data from that link.
Is there anyone who can guide me here? Thanks in advance.
You need platform specific code to handle deep linking. If you follow link mention in documention, you will find complete example.
private val CHANNEL = "poc.deeplink.flutter.dev/channel"
private var startString: String? = null
override fun configureFlutterEngine(#NonNull flutterEngine:FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "initialLink") {
if (startString != null) {
result.success(startString)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = getIntent()
startString = intent.data?.toString()
}
Flutter Code:
class DeepLinkBloc extends Bloc {
//Event Channel creation
static const stream = const
EventChannel('poc.deeplink.flutter.dev/events');
//Method channel creation
static const platform = const
MethodChannel('poc.deeplink.flutter.dev/channel');
StreamController<String> _stateController = StreamController();
Stream<String> get state => _stateController.stream;
Sink<String> get stateSink => _stateController.sink;
//Adding the listener into contructor
DeepLinkBloc() {
//Checking application start by deep link
startUri().then(_onRedirected);
//Checking broadcast stream, if deep link was clicked in opened appication
stream.receiveBroadcastStream().listen((d) => _onRedirected(d));
}
_onRedirected(String uri) {
// Here can be any uri analysis, checking tokens etc, if it’s necessary
// Throw deep link URI into the BloC's stream
stateSink.add(uri);
}
#override
void dispose() {
_stateController.close();
}
Future<String> startUri() async {
try {
return platform.invokeMethod('initialLink');
} on PlatformException catch (e) {
return "Failed to Invoke: '${e.message}'.";
}
}
}
Follow this link for more detail.
https://medium.com/flutter-community/deep-links-and-flutter-applications-how-to-handle-them-properly-8c9865af9283
The Flutter way to do that, assuming you've already made the steps in the guide you posted, is to create a onGenerateRoute and/or onGenerateInitialRoutes handlers in your MaterialApp so that these handlers deals with the routes passed or pushed by the framework according to the described behaviors. You can even create an expected named route coming from a deeplink on the routes property of MaterialApp, even though I believe the dynamic generation of routes is more appropriate due to the dynamic nature of deeplinking, specially if you're dealing with "authentication needed content" inside your app.
Or, if you don't want to pass trough the platform specific code, you could use firebase dynamic links. That would allow to easily listen to links coming from both platforms and you also get the advantage that your link would bring up the store listing page if the user doesn't have the app installed.
I've written a full example here: https://gbaccetta.medium.com/flutter-deep-linking-with-firebase-dynamic-links-and-bloc-architecture-660f0517fbc2
Do for android as said in the web page: "Add a metadata tag and intent filter to AndroidManifest.xml inside the tag with the ".MainActivity" name". Do what required for ios too.
Then use onGenerateRoute in the usual way in MaterialApp, don't use "routes:". For example:
onGenerateRoute: (settings) {
print("settings.name " + settings.name.toString());
if (settings.name == '/') return MaterialPageRoute(builder: (_) => ScreenStart());
return MaterialPageRoute(builder: (_) => ScreenUnknown());
},
Then to simulate I did:
cd /Users/Utente/AppData/Local/Android/Sdk/platform-tools
adb shell
am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "http://theaddressichoosed.com/helloworld?byebye"
And
print("settings.name " + settings.name.toString());
printed
settings.name /helloworld?byebye
After spending some time on this, here's my take using the Navigator 2 API. It also shows how to perform query and path arguments parsing. Hope it will save someone the time I spent researching this.
Obviously you also need to edit your platform-specific build files (such as AndroidManifest.xml for Android) as shown in the Flutter Deep Linking page.
A special note for Android 12 and above: you'll also need to securely approve the app's domain in the Google Play Developer Console for deep linking to work.
class App extends StatelessWidget {
const App({super.key});
#override
Widget build(BuildContext context) => MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScaffold(),
'/route1': (context) => const RouteOneScaffold(),
'/route2': (context) => const RouteTwoScaffold(),
// Other routes which don't need any sort of query parsing
},
onGenerateRoute: (settings) {
// This is executed to determine which route to follow if no adequate entry is found in the `routes` array above.
// Here we can parse path and query parameters as we like.
final fullRoute = settings.name;
if (fullRoute == null) {
return null;
}
final routeData = Uri.tryParse(fullRoute);
if (routeData == null) {
return null;
}
final pathParameters = routeData.pathSegments;
final queryParameters = routeData.queryParameters;
// Here you can write your route handling logic
return MaterialPageRoute(builder: (context) => RouteThreeScaffold(pathParameters,queryParameters));
},
);
}
I am building a comics app. In order to make comic chapters load faster. I want images to load one by one consequtively. When an image finishes loading, it is presented directly into a pageview. I do not know how to load images one by one and edit the page view to present them.
In order to make load image faster you can try use cached_network_image package like this:
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
It will show placeholder until the image loaded, so the user can see how many image there is and wait until it loaded, and errorWidget to show if some image were getting trouble. So I suggest to use it, it's easier. Unless you want some complicated code you can try use ScrollController put it inside your ListView.builder. Initialize your scroll controller in initState like this :
final _scrollController = ScrollController();
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
// scroll has reach end, now load more images.
loadMore();
}
});
}
You can't use setState inside initState so that's why I create another method to call it like this:
Future<void> loadMore() async {
final response = await api.get("curated?per_page=50&page=$_currentPage");
if (response.statusCode == 200) {
var tempList = Pages.fromJson(response.data);
setState(() {
isLoading = false;
wallpaper.addAll(tempList.photos);
});
}
return null;
}
So there you go I hope you can understand, and yes it is more complicated, so like I said I suggest you use CachedNetworkImage.
use a StreamBuilder it will do all the work for u.
I'm building an app that basically is a YouTube clone. I use the official video_player plugin for playback and chewie for controls. I'd like to implement a quality switcher, so the user can decide what quality they want the video to be streamed at
I've built a bottom sheet with switches and I run changeQuality() when the user selects the desired quality. What it should do is simply giving a new source file to the old player and keep playing from where the video left.
This is the video player and chewie player that run on initState():
videoPlayer = VideoPlayerController.network(data == null
? dataAll[indexNo]["video"]["480"]
: data[indexNo]["video"]["480"]);
chewieController = ChewieController(
videoPlayerController: videoPlayer,
aspectRatio: 16 / 9,
autoPlay: true,
allowedScreenSleep: false,
placeholder: data == null
? Image(
image: NetworkImage(dataAll[indexNo]["thumbnail"]),
)
: Image(
image: NetworkImage(data[indexNo]["thumbnail"]),
)
);
And the changeQuality() function:
changeQuality(String newQuality) {
setState(() {
position = videoPlayer.value.position;
chewieController.pause();
videoPlayer = new VideoPlayerController.network(data == null
? dataAll[indexNo]["video"]["$newQuality"]
: data[indexNo]["video"]["$newQuality"]);
chewieController = ChewieController(
videoPlayerController: videoPlayer,
aspectRatio: 16 / 9,
autoPlay: true,
allowedScreenSleep: false,
startAt: position,
);
});
Navigator.of(context).pop();
}
I've also tried disposing the old video player and then setting the new value, but I get an error that variables cannot be used after being disposed.
The switcher works a bit, because it changes the quality around 4 to 5 times and then it runs into an error and won't play anything.
I expand upon this solution for video_player and extend it to also cover chewie.
Key parts of this solution
You need two widgets. MyVideoPlayer that encapsulates video_player and chewie and an outer widget where you react to user input or state changes and swap out MyVideoPlayer with a new one.
This solution roundabouts the whole question in one way. I doesn't solve how to change video of video_player or chewie. Instead it follows the documented principal on how to use chewie for the whole life cycle of a host widget (MyVideoPlayer) and swap that one out to change video url.
You can stuff in more things in the outer widget as you see fit if you don't want to dedicate it just to containing MyVideoPlayer. Ie. if you want a description text adjacent to it based on app state.
Outer Widget
I write with this. but it can be omitted in Dart code.
class QuizVideoPlayer extends StatefulWidget {
#override
_QuizVideoPlayerState createState() => _QuizVideoPlayerState();
}
class _QuizVideoPlayerState extends State<QuizVideoPlayer> {
Word _url;
UniqueKey _urlKey;
// Call this method from button or in reaction to model change etc.
// I call it from Provider.of in didChangeDependencies, but I don't think it is
// a necessary detail of the answer as it depends on how you do state management.
// The key in this solution is that state management occur in the outer widget and
// due to some trigger call _changeUrl() which changes _url and _urlKey which then
// swaps out MyVideoPlayer.
#override
void _changeUrl(String newUrl) async {
this.setState(() {
// Rebuild MyVideoPlayer with a new instance => eventually dispose old controllers
this._url = newUrl;
this._urlKey = UniqueKey();
});
}
#override
Widget build(BuildContext context) {
return
/* ... */
this._url != null
? MyVideoPlayer(
this._url,
this._urlKey,
)
: AspectRatio(
aspectRatio: 3 / 2,
child: Container(color: Colors.black),
)
/* ... */
);
}
}
MyVideoPlayer
I write with this. but it can be omitted in Dart code.
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
class MyVideoPlayer extends StatefulWidget {
final String videoUrl;
final UniqueKey newKey;
MyVideoPlayer(this.videoUrl, this.newKey): super(key: newKey); // passing Unique key to dispose old class instance and create new with new data
#override
_MyVideoPlayerState createState() => _MyVideoPlayerState();
}
class _MyVideoPlayerState extends State<MyVideoPlayer> {
VideoPlayerController _controller;
ChewieController _chewie;
#override
void initState() {
this._initControllers(this.widget.videoUrl);
super.initState();
}
void _initControllers(String url) {
this._controller = VideoPlayerController.network(url);
this._chewie = ChewieController(
videoPlayerController: this._controller,
autoPlay: true,
);
}
#override
void dispose() {
this._controller?.dispose();
this._chewie?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Chewie(controller: this._chewie);
}
}