webview_flutter_android
webview_flutter_wkwebview
Thư viện | Mục đích |
---|---|
webview_flutter | Hiển thị nội dung web trong ứng dụng Flutter |
image_picker | Lấy hình ảnh từ camera hoặc thư viện ảnh |
permission_handler | Quản lý quyền truy cập như camera, lưu trữ |
shelf, shelf_static | Tạo và phục vụ server HTTP nội bộ trên thiết bị |
fluttertoast | Hiển thị thông báo dạng toast |
share_plus | Chia sẻ file và văn bản qua các ứng dụng khác |
open_file | Mở file với ứng dụng mặc định của hệ thống |
webview_flutter
: Hiển thị webview trong app Flutter.webview_flutter_android
& webview_flutter_wkwebview
: Tối ưu riêng cho Android và iOS.image_picker
: Cho phép người dùng chọn ảnh hoặc chụp mới.permission_handler
: Quản lý các quyền như truy cập camera, lưu trữ.shelf
& shelf_static
: Tạo server HTTP nội bộ phục vụ tài nguyên web.path
, path_provider
: Quản lý đường dẫn lưu trữ.fluttertoast
: Thông báo dạng toast native.share_plus
: Chia sẻ file và text.open_file
: Mở file với ứng dụng native.assets/web
ở gốc dự án.index.html
, main.js
, main.css
).flutter: assets: - assets/web/
WebViewController _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..enableZoom(true) ..setBackgroundColor(Colors.transparent) ..setNavigationDelegate(NavigationDelegate()) ..platform.setOnPlatformPermissionRequest((request) { debugPrint('Permission requested by web content: ${request.types}'); request.grant(); }) ..loadFlutterAsset('assets/web/index.html');
_controller = WebViewController() ... ..addJavaScriptChannel( 'SaveFile', onMessageReceived: (JavaScriptMessage message) async { List<dynamic> byteList = jsonDecode(message.message); Uint8List pdfBytes = Uint8List.fromList(byteList.cast<int>()); String filename = await saveFile(pdfBytes); Fluttertoast.showToast( msg: "File saved as: $filename", toastLength: Toast.LENGTH_LONG, ); }, ) ...;
savePDFButton.addEventListener('click', async () => { const fileName = document.getElementById('fileName').value; const password = document.getElementById('password').value; const annotationType = document.getElementById('annotationType').value; try { const pdfSettings = { password, saveAnnotation: annotationType }; let blob = await editViewer.currentDocument.saveToPdf(pdfSettings); sendBlobToDart(blob, fileName + `.pdf`); } catch (error) { console.log(error); } document.getElementById("save-pdf").style.display = "none";});
function sendBlobToDart(blob) { const reader = new FileReader(); reader.onload = function () { const arrayBuffer = reader.result; const byteArray = new Uint8Array(arrayBuffer); SaveFile.postMessage(JSON.stringify(Array.from(byteArray))); }; reader.onerror = function (error) { console.error("Error reading blob:", error); }; reader.readAsArrayBuffer(blob);}
Future<String> saveFile(Uint8List pdfBytes) async { final directory = await getApplicationDocumentsDirectory(); String filename = getFileName(); final filePath = '${directory.path}/$filename'; await File(filePath).writeAsBytes(pdfBytes); return filePath;}
String getFileName() { DateTime now = DateTime.now(); String timestamp = '${now.year}${now.month}${now.day}_${now.hour}${now.minute}${now.second}'; return '$timestamp.pdf';}
<input type="file">
trên Android WebViewAndroid WebView theo mặc định không hỗ trợ chọn file trực tiếp.
ImagePicker
if (_controller.platform is AndroidWebViewController) { AndroidWebViewController.enableDebugging(true); (_controller.platform as AndroidWebViewController).setMediaPlaybackRequiresUserGesture(false); (_controller.platform as AndroidWebViewController).setOnShowFileSelector( (params) async { final XFile? image = await _picker.pickImage(source: ImageSource.gallery); if (image != null) { return ['file://${image.path}']; } return ['']; } );}
<uses-permission android:name="android.permission.CAMERA"/>
<key>NSCameraUsageDescription</key><string>This app needs camera access to allow the webpage to use your camera for video input.</string>
Future<void> requestCameraPermission() async { final status = await Permission.camera.request(); if (status == PermissionStatus.granted) { // Đã cấp quyền } else if (status == PermissionStatus.denied) { // Hiện thông báo hoặc xử lý khi bị từ chối } else if (status == PermissionStatus.permanentlyDenied) { // Hướng dẫn người dùng bật lại trong cài đặt }}
@overridevoid initState() { super.initState(); requestCameraPermission();}
Do WebView trên Android không cho phép truy cập camera đầy đủ khi load từ file://, cần dùng phương pháp workaround.
void main() async { WidgetsFlutterBinding.ensureInitialized(); if (Platform.isAndroid) { final dir = await getTemporaryDirectory(); final path = p.join(dir.path, 'web'); final webDir = Directory(path)..createSync(recursive: true); final files = ['index.html', 'full.json', 'main.css', 'main.js']; for (var filename in files) { final ByteData data = await rootBundle.load('assets/web/$filename'); final file = File(p.join(webDir.path, filename)); await file.writeAsBytes(data.buffer.asUint8List()); } final handler = createStaticHandler( webDir.path, defaultDocument: 'index.html', serveFilesOutsidePath: true, ); try { final server = await shelf_io.serve(handler, 'localhost', 8080); print('Serving at http://${server.address.host}:${server.port}'); } catch (e) { print('Failed to start server: $e'); } } runApp(const MyApp());}
if (Platform.isAndroid) { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..enableZoom(true) ..setBackgroundColor(Colors.transparent) ..setNavigationDelegate(NavigationDelegate()) ..addJavaScriptChannel( 'SaveFile', onMessageReceived: (JavaScriptMessage message) { // Xử lý lưu file }, ) ..platform.setOnPlatformPermissionRequest((request) { request.grant(); }) ..loadRequest(Uri.parse('http://localhost:8080/index.html'));}
@overrideWidget build(BuildContext context) { return Scaffold( body: TabBarView( controller: _tabController, physics: const NeverScrollableScrollPhysics(), children: [ HomeView(title: 'Document Viewer', controller: _controller), const HistoryView(title: 'History'), ], ), bottomNavigationBar: TabBar( labelColor: Colors.blue, controller: _tabController, tabs: const [ Tab(icon: Icon(Icons.home), text: 'Home'), Tab(icon: Icon(Icons.history_sharp), text: 'History'), ], ), );}
class HomeView extends StatefulWidget { final WebViewController controller; final String title; const HomeView({super.key, required this.title, required this.controller});
@override State<HomeView> createState() => _HomeViewState();}
class _HomeViewState extends State<HomeView> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.title)), body: WebViewWidget(controller: widget.controller), ); }}
open_file
để mở file, share_plus
để chia sẻ.class HistoryView extends StatefulWidget { const HistoryView({super.key, required this.title}); final String title;
@override State<HistoryView> createState() => _HistoryViewState();}
class _HistoryViewState extends State<HistoryView> { List<String> _results = []; int selectedValue = -1;
@override void initState() { super.initState(); getFiles().then((value) => setState(() => _results = value)); }
Widget createListView(BuildContext context, List<String> results) { return ListView.builder( itemCount: results.length, itemBuilder: (context, index) { return RadioListTile<int>( value: index, groupValue: selectedValue, title: Text(results[index]), onChanged: (int? value) { setState(() => selectedValue = value!); }, ); }, ); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), actions: [ IconButton( icon: const Icon(Icons.info), onPressed: () { if (selectedValue != -1) OpenFile.open(_results[selectedValue]); }, ), IconButton( icon: const Icon(Icons.share), onPressed: () { if (selectedValue != -1) { SharePlus.instance.share( ShareParams( text: 'Check out this image!', files: [XFile(_results[selectedValue])], ), ); } }, ), IconButton( icon: const Icon(Icons.delete), onPressed: () async { if (selectedValue != -1) { bool isDeleted = await deleteFile(_results[selectedValue]); if (isDeleted) { setState(() { _results.removeAt(selectedValue); selectedValue = -1; }); } } }, ), ], ), body: Center( child: _results.isNotEmpty ? createListView(context, _results) : const Text('No documents found.'), ), ); }}