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
entitlements: Animations
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
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 ofRunner
. For example, ifRunner
requires at least MacOS 10.14, thePods
must also not require a higher version.
Open Xcode. Search for Signing & Capablilities.
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());
}
...
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!);
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);
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);
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
-
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, ), ),
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,
),
),
),
),
);
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
andbottom
are the distances to the left and the upper side of the parent container rectangle. -
RelativeRect
:right
andbottom
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);
}
}
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);
}
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');
}
...
}
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;
}
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 aPositioned.fill
with the visibleCards
. However, I believe that this is at least as expensive asIntrinsicHeight
, as eachRow
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),
),
),
],
),
),
],
);
}
}