native and web - wurzelsand/flutter-memos GitHub Wiki

Native and Web

We want to create an app that saves text to a file both via a browser and natively (desktop or mobile). For this we use the two packages web and file-picker. The web package is only available for the web platform. Therefore we need conditional imports.

5 files in one folder:

  • main.dart
  • multiplatform.dart
  • save_text_stub.dart
  • save_text_web.dart
  • save_text_io.dart

main.dart:

import 'package:flutter/material.dart';

import 'multiplatform.dart';

void main() {
  runApp(const MaterialApp(home: MainApp()));
}

class MainApp extends StatefulWidget {
  const MainApp({super.key});

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  late TextEditingController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
            icon: const Icon(Icons.save),
            onPressed: () {
              saveText(_controller.text, fileName: 'tmp.txt');
            }),
      ),
      body: Center(
        child: TextField(
          controller: _controller,
        ),
      ),
    );
  }
}

multiplatform.dart:

export 'save_text_stub.dart'
    if (dart.library.io) 'package:xxxxx/save_text_io.dart'
    if (dart.library.js_interop) 'package:xxxxx/safe_text_web.dart';

save_text_stub.dart:

void saveText(String text, {required String fileName}) {
  throw UnimplementedError('Neither native nor web');
}

save_text_web.dart:

import 'dart:convert';
import 'package:web/web.dart' as web;

void saveText(String text, {required String fileName}) {
  final bytes = utf8.encode(text);
  final web.HTMLAnchorElement anchor =
      web.document.createElement('a') as web.HTMLAnchorElement
        ..href = "data:application/octet-stream;base64,${base64Encode(bytes)}"
        ..style.display = 'none'
        ..download = fileName;

  web.document.body!.appendChild(anchor);
  anchor.click();
  web.document.body!.removeChild(anchor);
}

save_text_io.dart:

import 'dart:convert';
import 'dart:developer';
import 'dart:io';

import 'package:file_picker/file_picker.dart';

Future<void> saveText(String text, {required String fileName}) async {
  // better: Theme.of(context).platform == TargetPlatform.android...
  if (Platform.isAndroid || Platform.isIOS) {
    return _saveTextMobile(text, fileName);
  } else {
    return _saveTextDesktop(text, fileName);
  }
}

Future<void> _saveTextMobile(String text, String fileName) async {
  final bytes = utf8.encode(text);
  await FilePicker.platform
      .saveFile(dialogTitle: 'Save Text', fileName: fileName, bytes: bytes);
}

Future<void> _saveTextDesktop(String text, String fileName) async {
  final retrievedFileName = await FilePicker.platform
      .saveFile(dialogTitle: 'Save Text', fileName: fileName);
  if (retrievedFileName != null) {
    try {
      File file = File(retrievedFileName);
      file.writeAsString(text);
    } catch (e) {
      log('File dialog cancelled');
    }
  }
}

Better on Desktop: file_selector

Future<void> _saveTextDesktop(String text, String fileName) async {
  final fileSaveLocation = await getSaveLocation(suggestedName: fileName);
  if (fileSaveLocation == null) {
    return;
  }
  final bytes = utf8.encode(text);
  const String mimeType = 'text/plain';
  final textFile = XFile.fromData(bytes, mimeType: mimeType, name: fileName);
  await textFile.saveTo(fileSaveLocation.path);
}

But you have to add this key to DebugProfile.entitlements:

	<key>com.apple.security.files.user-selected.read-write</key>
	<true/>

Unfortunately, Googles official file_selector-package doesn't support saving on Android and iOS. file-picker can do this. But I am using flutter_file_dialog until Google supports saving files on mobile platforms:

Future<void> _saveTextMobile(String text, String fileName) async {
  final bytes = utf8.encode(text);
  final params = SaveFileDialogParams(data: bytes, fileName: fileName);
  await FlutterFileDialog.saveFile(params: params);
}
⚠️ **GitHub.com Fallback** ⚠️