Appearance
Integrating LIQA into Flutter App
Below is the guide for integrating LIQA into a Flutter application using Dart language.
WebView with LIQA Integrated
Here is a ready-to-use implementation of a WebView with integrated LIQA functionality.
Production Recommendation:
For simplicity, the WebView loads the HTML page from a local string containing the HTML content.
In a real-world application, you may consider loading the page from a CDN managed by your organization. Check the Hosting the HTML Page section for more details.
IMPORTANT
The WebView should be opened over HTTPS. Without HTTPS, the camera access will not be requested, and the camera cannot be used. When hosting locally, you can open the WebView using https://localhost/ as the baseUrl.
dart
liqaWebViewController.loadHtmlString(
_getHtmlPage(),
baseUrl: "https://localhost/"
);liqaWebViewController.loadHtmlString(
_getHtmlPage(),
baseUrl: "https://localhost/"
);dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
// ignore: depend_on_referenced_packages
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
class LiqaWebView extends StatefulWidget {
const LiqaWebView({super.key, required this.onLiqaEvent});
final Function(String, Map<String, dynamic>?) onLiqaEvent;
@override
State<LiqaWebView> createState() => _LiqaWebViewState();
}
class _LiqaWebViewState extends State<LiqaWebView> {
WebViewController? liqaWebViewController;
@override
void initState() {
super.initState();
_initializeLiqaWebView();
}
@override
Widget build(BuildContext context) {
return liqaWebViewController != null
? WebViewWidget(
controller: liqaWebViewController!,
)
: const Center(
child: CircularProgressIndicator(
color: Colors.blueGrey,
),
);
}
Future<void> _initializeLiqaWebView() async {
// Request camera permission before loading the WebView
await _requestCameraPermission();
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
setState(() {
liqaWebViewController = WebViewController.fromPlatformCreationParams(
params,
onPermissionRequest: (request) async {
// Handle camera permission specifically
if (request.types.first.name == 'camera') {
final granted = await _requestCameraPermission();
if (granted) {
request.grant();
} else {
request.deny();
}
}
},
)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'FlutterWebView',
onMessageReceived: (JavaScriptMessage message) {
try {
print("Received message from LIQA: ${message.message}");
final messageObject =
jsonDecode(message.message) as Map<String, dynamic>;
final name = messageObject['name'] as String;
final payload = messageObject['payload'] as Map<String, dynamic>?;
// Pass the event to the external handler
widget.onLiqaEvent(name, payload);
} catch (e) {
print('Failed to parse message: $e');
}
},
)
..loadHtmlString(_getHtmlPage(), baseUrl: "https://localhost/");
});
}
Future<bool> _requestCameraPermission() async {
var status = await Permission.camera.status;
if (!status.isGranted) {
await Permission.camera.request();
}
return status.isGranted;
}
String _getHtmlPage() {
return """
PUT THE HTML CONTENT HERE
""";
}
}import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
// ignore: depend_on_referenced_packages
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
class LiqaWebView extends StatefulWidget {
const LiqaWebView({super.key, required this.onLiqaEvent});
final Function(String, Map<String, dynamic>?) onLiqaEvent;
@override
State<LiqaWebView> createState() => _LiqaWebViewState();
}
class _LiqaWebViewState extends State<LiqaWebView> {
WebViewController? liqaWebViewController;
@override
void initState() {
super.initState();
_initializeLiqaWebView();
}
@override
Widget build(BuildContext context) {
return liqaWebViewController != null
? WebViewWidget(
controller: liqaWebViewController!,
)
: const Center(
child: CircularProgressIndicator(
color: Colors.blueGrey,
),
);
}
Future<void> _initializeLiqaWebView() async {
// Request camera permission before loading the WebView
await _requestCameraPermission();
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
setState(() {
liqaWebViewController = WebViewController.fromPlatformCreationParams(
params,
onPermissionRequest: (request) async {
// Handle camera permission specifically
if (request.types.first.name == 'camera') {
final granted = await _requestCameraPermission();
if (granted) {
request.grant();
} else {
request.deny();
}
}
},
)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'FlutterWebView',
onMessageReceived: (JavaScriptMessage message) {
try {
print("Received message from LIQA: ${message.message}");
final messageObject =
jsonDecode(message.message) as Map<String, dynamic>;
final name = messageObject['name'] as String;
final payload = messageObject['payload'] as Map<String, dynamic>?;
// Pass the event to the external handler
widget.onLiqaEvent(name, payload);
} catch (e) {
print('Failed to parse message: $e');
}
},
)
..loadHtmlString(_getHtmlPage(), baseUrl: "https://localhost/");
});
}
Future<bool> _requestCameraPermission() async {
var status = await Permission.camera.status;
if (!status.isGranted) {
await Permission.camera.request();
}
return status.isGranted;
}
String _getHtmlPage() {
return """
PUT THE HTML CONTENT HERE
""";
}
}WebView with LIQA preload
Preload instructions
Preload implementation leverages the flutter_inappwebview package to enhance performance and user experience.
Ensure you have the following packages in your pubspec.yaml file:
yaml
dependencies:
flutter_inappwebview: ^6.1.5dependencies:
flutter_inappwebview: ^6.1.5WebView preloading with HeadlessInAppWebView
dart
HeadlessInAppWebView? headlessWebView;
@override
void initState() {
super.initState();
headlessWebView = HeadlessInAppWebView(
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
useHybridComposition: true,
cacheEnabled: true,
),
initialData: InAppWebViewInitialData(
data: _getHtmlPage(licenseKey),
baseUrl: WebUri.uri(Uri.parse("https://localhost/")),
mimeType: 'text/html',
encoding: 'utf-8',
),
onLoadStop: (controller, url) {
_injectBridgeScript(controller);
setState(() {
isWebViewReady = true;
});
},
)..run();
}HeadlessInAppWebView? headlessWebView;
@override
void initState() {
super.initState();
headlessWebView = HeadlessInAppWebView(
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
useHybridComposition: true,
cacheEnabled: true,
),
initialData: InAppWebViewInitialData(
data: _getHtmlPage(licenseKey),
baseUrl: WebUri.uri(Uri.parse("https://localhost/")),
mimeType: 'text/html',
encoding: 'utf-8',
),
onLoadStop: (controller, url) {
_injectBridgeScript(controller);
setState(() {
isWebViewReady = true;
});
},
)..run();
}JavaScript and Flutter Communication
Establish a bridge between JavaScript and Flutter to handle LIQA events.
dart
void _injectBridgeScript(InAppWebViewController controller) {
controller.evaluateJavascript(source: """
const postMessage = (name, payload) => {
window.flutter_inappwebview.callHandler('flutterHandler', JSON.stringify({ name, payload }));
}
const liqa = document.querySelector("hautai-liqa");
liqa?.addEventListener("ready", () => postMessage("ready"));
liqa?.addEventListener("error", ({ detail: error }) =>
postMessage("error", {
code: error.code,
message: error.message,
cause: error.cause?.message,
}),
);
liqa?.addEventListener("capture", async ({ detail: capture }) =>
postMessage("capture", {
source: capture.source,
data: await blobToBase64(await capture.blob()),
}),
);
function blobToBase64(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
""");
controller.addJavaScriptHandler(
handlerName: 'flutterHandler',
callback: (args) {
if (args.isNotEmpty && args[0] is String) {
_handleWebMessage(args[0]);
}
return null;
},
);
}void _injectBridgeScript(InAppWebViewController controller) {
controller.evaluateJavascript(source: """
const postMessage = (name, payload) => {
window.flutter_inappwebview.callHandler('flutterHandler', JSON.stringify({ name, payload }));
}
const liqa = document.querySelector("hautai-liqa");
liqa?.addEventListener("ready", () => postMessage("ready"));
liqa?.addEventListener("error", ({ detail: error }) =>
postMessage("error", {
code: error.code,
message: error.message,
cause: error.cause?.message,
}),
);
liqa?.addEventListener("capture", async ({ detail: capture }) =>
postMessage("capture", {
source: capture.source,
data: await blobToBase64(await capture.blob()),
}),
);
function blobToBase64(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
""");
controller.addJavaScriptHandler(
handlerName: 'flutterHandler',
callback: (args) {
if (args.isNotEmpty && args[0] is String) {
_handleWebMessage(args[0]);
}
return null;
},
);
}Before integrating these snippets into your app, please ensure that:
- Camera permissions are requested and granted before loading the
WebView. - JavaScript is enabled in the WebView (
setJavaScriptMode(JavaScriptMode.unrestricted)). WebViewloads the page over HTTPS.- You have installed the necessary dependencies:
bash
flutter pub add webview_flutter permission_handlerflutter pub add webview_flutter permission_handlerApplication Integration Example
Here is an app’s entry point, showcasing a sample integration of LiqaWebView:
dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'liqa_web_view.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: HomeScreen());
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String? capturedImage;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('LIQA Flutter Integration'),
actions: [
if (capturedImage != null)
IconButton(
onPressed: showCapturedImage, icon: const Icon(Icons.image))
],
),
body: LiqaWebView(
onLiqaEvent: handleLiqaEvent,
),
);
}
void handleLiqaEvent(String name, Map<String, dynamic>? payload) {
print('LIQA Event: $name');
if (payload != null) {
print('Payload: ${jsonEncode(payload)}');
}
if (name == 'capture') {
setState(() {
capturedImage = payload?['data'];
});
}
}
void showCapturedImage() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LiqaImageScreen(
base64String: capturedImage!
.replaceFirst('data:image/jpeg;base64,', ''),
),
),
);
}
}
class LiqaImageScreen extends StatelessWidget {
const LiqaImageScreen({super.key, required this.base64String});
final String base64String;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Captured Image'),
),
body: Image.memory(base64Decode(base64String)),
);
}
}import 'dart:convert';
import 'package:flutter/material.dart';
import 'liqa_web_view.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: HomeScreen());
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String? capturedImage;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('LIQA Flutter Integration'),
actions: [
if (capturedImage != null)
IconButton(
onPressed: showCapturedImage, icon: const Icon(Icons.image))
],
),
body: LiqaWebView(
onLiqaEvent: handleLiqaEvent,
),
);
}
void handleLiqaEvent(String name, Map<String, dynamic>? payload) {
print('LIQA Event: $name');
if (payload != null) {
print('Payload: ${jsonEncode(payload)}');
}
if (name == 'capture') {
setState(() {
capturedImage = payload?['data'];
});
}
}
void showCapturedImage() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LiqaImageScreen(
base64String: capturedImage!
.replaceFirst('data:image/jpeg;base64,', ''),
),
),
);
}
}
class LiqaImageScreen extends StatelessWidget {
const LiqaImageScreen({super.key, required this.base64String});
final String base64String;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Captured Image'),
),
body: Image.memory(base64Decode(base64String)),
);
}
}Application Permissions Configuration
For LiqaWebView to function correctly, you’ll need to configure camera permissions in your configuration files.
Android
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- Camera feature -->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<!-- Application Configuration -->
<application android:label="LIQA WebView Integration">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest><manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- Camera feature -->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<!-- Application Configuration -->
<application android:label="LIQA WebView Integration">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>iOS
xml
<key>NSCameraUsageDescription</key>
<string>We need camera access to analyze skin quality.</string><key>NSCameraUsageDescription</key>
<string>We need camera access to analyze skin quality.</string>Podfile
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
]
end
end
endpost_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
]
end
end
endFurther Considerations
- Secure the
WebView: Ensure that theWebViewis secured against potential vulnerabilities, such as cross-site scripting (XSS). - Test Permissions Thoroughly: Verify that the app requests and handles permissions correctly on both iOS and Android.
- For more details, refer to the Flutter WebView Documentation.
Need Assistance?
If you have any questions regarding the integration or need further assistance, please contact Haut.AI Support Team with this request via the Support Desk.