Home - wurzelsand/flutter-memos GitHub Wiki

Just for me

Offline Flutter Documentation (600 MB; entpackt > 8 GB):

wget https://api.flutter.dev/offline/flutter.docset.tar.gz

Online Flutter Documentation

MacOS app cannot load assets from internet?

entitlements: Animations

Desktop app window title, window size?

before runApp:

import 'package:window_size/window_size.dart';
...

if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
  WidgetsFlutterBinding.ensureInitialized();
  setWindowTitle('Provider Counter');
  setWindowMinSize(const Size(windowWidth, windowHeight));
  setWindowMaxSize(const Size(windowWidth, windowHeight));
  getCurrentScreen().then((screen) {
    setWindowFrame(Rect.fromCenter(
      center: screen!.frame.center,
      width: windowWidth,
      height: windowHeight,
    ));
  });
}

pubspec.yaml under dependencies:

window_size:
git:
    url: https://github.com/google/flutter-desktop-embedding.git
    path: plugins/window_size

Build process failed

The macOS deployment target 'MACOSX_DEPLOYMENT_TARGET' is set to ... but the range of supported deployment target versions is ...

Add the minimal deployment target to Podfile:

platform :osx, '10.14'
inhibit_all_warnings! # optional
...
post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_macos_build_settings(target)
    # >>>
    target.build_configurations.each do |config|
      config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET'
      # config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.14'
    end
    # <<<
  end
end
  • There are Pods which could actually be deployed on older MacOS versions, e.g. MacOS 10.9. However, newer Xcode versions cannot compile for older MacOS, e.g. not for MacOS 10.9 but only for MacOS 10.13 and above. So the minimum deployment target must be raised, e.g. from MacOS 10.9 to 10.14. However, the minimum deployment target must not exceed that of Runner. For example, if Runner requires at least MacOS 10.14, the Pods must also not require a higher version.

Keychain problems using Firebase on MacOS.

Open Xcode. Search for Signing & Capablilities.

Empty Flutter project with Dart only repository

flutter create --empty flutter_with_repository
cd flutter_with_repository
mkdir packages
cd packages
dart create --template=package my_repository

Excerpt:

├── pubspec.yaml
├── lib
│   └── main.dart
└── packages
    └── my_repository
        ├── lib
        │   ├── my_repository.dart
        │   └── src
        │       └── my_repository_base.dart
        └── pubspec.yaml

pubspec.yaml at top file level:

name: flutter_with_repository
description: A new Flutter project.
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=3.0.5 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  my_repository:
    path: packages/my_repository

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

main.dart:

import 'package:flutter/material.dart';
import 'package:my_repository/my_repository.dart';

void main() {
  runApp(const MainApp());
}

...

Open Zip-File in web and save extracted file in memory

import 'dart:typed_data';

import 'package:archive/archive.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:file_selector/file_selector.dart';

XTypeGroup typeGroup = XTypeGroup(label: 'Zip', extensions: ['zip']);
// open native dialog:
XFile? xFile = await openFile(acceptedTypeGroups: [typeGroup]);
Uint8List bytes = await xFile!.readAsBytes();
Archive archive = ZipDecoder().decodeBytes(bytes);
Uint8List? content = archive.files[0].rawContent?.toUint8List();
MemoryFileSystem fileSystem = MemoryFileSystem();
// no dart:io, therefore can be used in web applications:
File memoryFile = fileSystem.file('xyz.m4a');
await memoryFile.writeAsBytes(content!);

Record audio on web, put it into zip archive and save it on disk

import 'dart:convert';
import 'dart:typed_data';

import 'package:archive/archive.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:record/record.dart';
import 'package:http/http.dart' as http;
import 'package:web/web.dart' as web;

//---- Start recording audio ----//
RecordConfig config =
    RecordConfig(encoder: AudioEncoder.opus, numChannels: 1);
AudioRecorder recorder = AudioRecorder();
if (!await recorder.hasPermission()) {
  return;
}
recorder.start(config, path: 'ignored');

//---- Stop recording audio and get bytes ----//
// gives a BLOB-URL on web:
String? path = await recorder.stop();
Uri uri = Uri.parse(path!);
http.Client client = http.Client();
http.Response request = await client.get(uri);
Uint8List bytes = request.bodyBytes;

//---- Put audio into archive ----//
final archiveFile =
    ArchiveFile.noCompress('xyz.opus', bytes.lengthInBytes, bytes);
Archive archive = Archive();
archive.addFile(archiveFile);

//---- Play audio from archive file ----//
AudioPlayer player = AudioPlayer()..setReleaseMode(ReleaseMode.stop);
final source = BytesSource(archiveFile.content);
player.play(source);

//---- Save zip file on disk ----//
final zipEncoder = ZipEncoder();
List<int>? zipBytes =
    zipEncoder.encode(archive, level: Deflate.NO_COMPRESSION);
web.HTMLAnchorElement anchor =
    web.document.createElement('a') as web.HTMLAnchorElement
      ..href = "data:application/octet-stream;base64,${base64Encode(zipBytes!)}"
      ..style.display = 'none'
      ..download = 'audio_files.zip';
web.document.body!.appendChild(anchor);
anchor.click();
web.document.body!.removeChild(anchor);

Record audio on MacOS, put it into zip archive and save it on disk

import 'dart:io';
import 'dart:typed_data';

import 'package:archive/archive_io.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:file_selector/file_selector.dart';
import 'package:path/path.dart' as pth;
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';

//---- Start recording audio ----//
const config = RecordConfig(encoder: AudioEncoder.aacLc, numChannels: 1);
AudioRecorder recorder = AudioRecorder();
if (!await recorder.hasPermission()) {
  return;
}
Directory temporaryDirectory = await getTemporaryDirectory();
String audioFileName = 'xyz.m4a';
recorder.start(config,
    path: pth.join(temporaryDirectory.path, audioFileName));

//---- Stop recording audio ----//
// gives a BLOB-URL on web:
String? path = await recorder.stop();
File file = File(path!);
Uint8List bytes = await file.readAsBytes();

//---- Put audio into archive ----//
Archive archive = Archive();
audioFileName = pth.basename(path);
// noCompress might be not necassary, because the whole archive will not be compressed
final archiveFile =
    ArchiveFile.noCompress(audioFileName, bytes.lengthInBytes, bytes);
archive.addFile(archiveFile);

//---- Play audio from archive file ----//
AudioPlayer player = AudioPlayer()..setReleaseMode(ReleaseMode.stop);
final source = BytesSource(archiveFile.content, mimeType: 'audio/mp4');
player.play(source);

//---- Save zip file on disk ----//
final zipEncoder = ZipEncoder();
List<int>? zipBytes =
    zipEncoder.encode(archive, level: Deflate.NO_COMPRESSION);
FileSaveLocation? location =
    await getSaveLocation(suggestedName: 'audio_files.zip');
await File(location!.path).writeAsBytes(zipBytes!);
// String mimeType = 'application/octet-stream';
// final XFile zipFile =
//     XFile.fromData(zipBytes! as Uint8List, mimeType: mimeType);
// await zipFile.saveTo(location!.path);

Entitlements:

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

Context Menu

  • add package context_menus

  • To distinguish between mobile and desktop:

    bool isMobile(BuildContext context) =>
        Theme.of(context).platform == TargetPlatform.android ||
        Theme.of(context).platform == TargetPlatform.iOS;
  • So that when it runs in the browser, no context menu interferes:

    @override
    void initState() {
      super.initState();
      if (kIsWeb) {
        BrowserContextMenu.disableContextMenu();
      }
    }
    
    @override
    void dispose() {
      if (kIsWeb) {
        BrowserContextMenu.enableContextMenu();
      }
      super.dispose();
    }
  • Wrap a widget at the top with ContextMenuOverlay:

    @override
    Widget build(BuildContext context) {
      return ContextMenuOverlay(
        child: MaterialApp(...),
      )
    }
  • Wrap the wanted widget with ContextMenuRegion:

    child: ContextMenuRegion(
      behavior: isMobile(context)
          ? [ContextMenuShowBehavior.longPress]
          : [ContextMenuShowBehavior.secondaryTap],
      contextMenu: GenericContextMenu(
        buttonConfigs: [
          ContextMenuButtonConfig('Info', onPressed: () => ()),
          ContextMenuButtonConfig('Copy', onPressed: () => ()),
        ],
      ),
      child: Container(
        color: Colors.yellow,
        width: 100,
        height: 100,
      ),
    ),

Context Menu without additional packages

ContextMenuController class

It is also easy to create a context menu without an additional package, namely using AdaptiveTextSelectionToolbar.buttonItems and ContextMenuController:

final ContextMenuController _contextMenuController = ContextMenuController();

Now we need to use GestureDetector to determine when the context menu should be displayed. We should also save the offset so that we can pass it to AdaptiveTextSelectionToolbar.buttonItems, which displays the individual elements of the context menu on the screen:

// Within GestureDetector:
void _onSecondaryTapUp(TapUpDetails details) {
  position = details.globalPosition;
}

_contextMenuController.show(
  context: context,
  contextMenuBuilder: (BuildContext context) {
    return contextMenuBuilder(context, position);
  },
);

Widget contextMenuBuilder(BuildContext context, Offset offset) {
  return AdaptiveTextSelectionToolbar.buttonItems(
    anchors: TextSelectionToolbarAnchors(
      primaryAnchor: offset,
    ),
    buttonItems: const <ContextMenuButtonItem>[
      ContextMenuButtonItem(
        onPressed: ContextMenuController.removeAny,
        label: 'Info',
      ),
      ContextMenuButtonItem(
        onPressed: ContextMenuController.removeAny,
        label: 'Copy',
      ),
    ],
  );  
}

The whole can be reused if we create the class ContextMenuRegion (slightly modified from the Flutter documentation).

Add to pubspec.yaml (note the indentation!):

context_menu_region:
  git: 
    url: https://github.com/wurzelsand/context_menu_region.git

ContextMenuRegion can now wrap any widget:

import 'package:context_menu_region/context_menu_region.dart';

...

return MaterialApp(
  home: Scaffold(
    body: Center(
      child: ContextMenuRegion(
        contextMenuBuilder: (BuildContext context, Offset offset) {
          // The custom context menu will look like the default context menu
          // on the current platform
          return AdaptiveTextSelectionToolbar.buttonItems(
            anchors: TextSelectionToolbarAnchors(
              primaryAnchor: offset,
            ),
            buttonItems: const <ContextMenuButtonItem>[
              ContextMenuButtonItem(
                onPressed: ContextMenuController.removeAny,
                label: 'Info',
              ),
              ContextMenuButtonItem(
                onPressed: ContextMenuController.removeAny,
                label: 'Copy',
              ),
            ],
          );
        },
        child: Container(
          color: Colors.yellow,
          width: 100,
          height: 100,
        ),
      ),
    ),
  ),
);

RelativeRect

Both Rect and RelativeRect have a constructor with the name fromLTRB(left, top, right, bottom). In both cases, left and top represent the distances to the left and top of the parent container rectangle, e.g. a widget. However, right and bottom have different meanings:

  • Rect: right and bottom are the distances to the left and the upper side of the parent container rectangle.

  • RelativeRect: right and bottom are the distances to the right and bottom sides of the parent container rectangle.

The height and width of a RelativeRect is therefore only known when the size of the container rectangle is known. For the height and width of a Rect, on the other hand, the size of the container rectangle is meaningless.

Extract from the implementation:

class RelativeRect {
  ...
  Rect toRect(Rect container) {
    return Rect.fromLTRB(left, top, container.width - right, container.height - bottom);
  }
}

Async initializer

I have an initializer that is only completed after a certain time. The Future-getter initialized monitors the initialization: As long as the initialization continues, you can wait for the completion via await initialized. After initialization, each subsequent await initialized is left immediately. BehaviorSubject is part of rxdart.

import 'dart:async';
import 'package:rxdart/subjects.dart';

// Like StreamController, but capturing last item:
final _notifyWhenInitializedSubject = BehaviorSubject<void>();

Future<void> get initialized async {
  return await _notifyWhenInitializedSubject.stream.first;
}

late int result;

Future<void> initialize() async {
  await Future.delayed(Duration(seconds: 1));
  result = 42;

  // ignore: void_checks
  _notifyWhenInitializedSubject.add(());
}

void main() async {
  print('start');
  initialize();

  await initialized;
  print(result);
  await initialized;
  print(result);
}

Simpler:

import 'dart:async';

Future<void>? _future;

late int result;

Future<void> initialize() async {
  Future<void> initializeResult() async {
    await Future.delayed(Duration(seconds: 1));
    result = 42;
  }

  _future ??= initializeResult();
  return _future;
}

void main() async {
  print('start');
  initialize();

  await initialize();
  print(result);
  await initialize();
  print(result);
}

ReorderableListView, proxyDecorator, Semantics

You can draw the elements of a ReorderableListView using a proxyDecorator function. One parameter of this function is child. The documentation states: "The proxy is created with the original list item as its child." For example, if the list is composed of Card objects, you might expect this original list item to be of type Card. However, the list item is actually embedded in a Semantics widget tree. If you look at the implementation of ReorderableListView, you will see that there are three ways in which the list item can be wrapped, for example depending on the platform. You can now try to extract the embedded Card object. Since the implementation can change, I would not recommend it:

Widget proxyDecorator(Widget child, int index, Animation<double> animation) {
  final semantics = child as Semantics;
  final Card card;
  switch (semantics.child) {
    case Stack stack when stack.children[0] is Card:
      card = stack.children[0] as Card;
    case ReorderableDelayedDragStartListener listener
        when listener.child is Card:
      card = listener.child as Card;
    case KeyedSubtree tree when tree.child is Card:
      card = tree.child as Card;
    default:
      throw Exception('Card widget not found in Semantics');
  }
  ...
}

Save Map in SharedPreferences

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final prefs = SharedPreferencesAsync();

  final Map<String, String> inputMap = {
    '001': 'red',
    '002': 'green',
    '003': 'blue'
  };
  final String json = jsonEncode(inputMap);
  prefs.setString('my_map', json);

  final Map<String, String> outputMap = await getMap(prefs, 'my_map');
  print(outputMap); // {001: red, 002: green, 003: blue}

  await prefs.remove('my_map');

  final Map<String, String> removedMap = await getMap(prefs, 'my_map');
  print(removedMap); // {}
}

Future<Map<String, String>> getMap(
    SharedPreferencesAsync prefs, String key) async {
  final Map<String, String> outputMap = {};
  final jsonString = await prefs.getString(key);
  if (jsonString != null) {
    final map = jsonDecode(jsonString) as Map<Object, Object?>;
    for (final entry in map.entries) {
      final key = entry.key as String;
      final value = entry.value as String;
      outputMap.putIfAbsent(key, () => value);
    }
  }
  return outputMap;
}

Intrinsic Height vs Stack

A ListView consists of several Rows. I want Row to consist of two Cards of the same size, even if their texts are of different lengths. I can only see two solutions for this:

  • IntrinsicHeight, which according to the documentation should be avoided because it is relatively expensive.

  • A Stack consisting of an invisible proxy widget to set the height, and a Positioned.fill with the visible Cards. However, I believe that this is at least as expensive as IntrinsicHeight, as each Row has to be created twice.

class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: const [
          IntrinsicHeightRow(),
          StackRow(),
        ],
      ),
    );
  }
}

const shortText = 'Lorem';
const longText = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.';

class IntrinsicHeightRow extends StatelessWidget {
  const IntrinsicHeightRow({super.key});

  @override
  Widget build(BuildContext context) {
    return const IntrinsicHeight(
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(
            child: Card(
              child: Text(shortText),
            ),
          ),
          Expanded(
            child: Card(
              child: Text(longText),
            ),
          ),
        ],
      ),
    );
  }
}

class StackRow extends StatelessWidget {
  const StackRow({super.key});

  @override
  Widget build(BuildContext context) {
    return const Stack(
      children: [
        Visibility(
          visible: false,
          maintainSize: true,
          maintainState: true,
          maintainAnimation: true,
          child: Row( // Proxy Widget
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Expanded(
                child: Card(
                  child: Text(longText),
                ),
              ),
              Expanded(
                child: Card(
                  child: Text(shortText),
                ),
              ),
            ],
          ),
        ),
        Positioned.fill(
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.stretch, // possible now!
            children: [
              Expanded(
                child: Card(
                  child: Text(longText),
                ),
              ),
              Expanded(
                child: Card(
                  child: Text(shortText),
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}
⚠️ **GitHub.com Fallback** ⚠️