Flutter for Windows Desktop: Getting Started

Learn how to set up a development environment and create a simple Flutter calculator app for Windows Desktop. By Karol Wrótniak.

4.7 (3) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Binding UI with Business Logic

To connect the logic and UI, inject the Calculator into a CalculatorBody:

final _calculator = Calculator();

Prepend the GridView in a column with ValueListenableBuilder. Now the calculator has the ability to display the current state.

Padding(
  padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
  child: ValueListenableBuilder<String>(
    valueListenable: _calculator.displayNotifier,
    //1
    builder: (_, String value, __) => Text(
      value,
      //2
      overflow: TextOverflow.ellipsis,
      maxLines: 1,
      style: const TextStyle(
        fontSize: 36,
        fontWeight: FontWeight.bold,
        color: Color(0xFF158443),  //green
      ),
    ),
  ),
),

Note important code fragments:

  1. The buildercallback fires on each displayNotifier change.
  2. The ellipsis overflow causes the &hellip to display if the text is too long.

Go back to the build method of a CalculatorBody and bind the actions to the buttons:

Tile('7', _calculator.appendDigit),
Tile('7', _calculator.appendDigit),
//rest of the digits
Tile('+', (_) => _calculator.appendOperator(Operator.plus)),
Tile('-', (_) => _calculator.appendOperator(Operator.minus)),

Now you have wired all the actions but the clear AC (I’ll return to this later in the article).

Finally, create the entry point of the app in a main.dart file. After that, you’re able to run the app (press Shift+F10 or a green play button in Android Studio).

Future<void> main() async {
  runApp(const FCalcApp());
}

class FCalcApp extends StatelessWidget {
  const FCalcApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) =>
      MaterialApp(home: Scaffold(body: CalculatorBody()));
}

As a result, you have the simplest, working calculator:
The simplest calculator

Developing Features for Desktop

In the current state, the app works on desktops, just like on mobile platforms. On mobile, however, the default input device is a touchscreen; on desktop, it’s a keyboard and a mouse. You can also connect the keyboard and mouse to smartphones via USB-OTG or Bluetooth. Flutter supports them out of the box.

Supporting Mouse Input

Flutter treats left-button mouse clicks like taps on a touchscreen. You don’t need to do anything special to handle clicks. The onTap and similar callbacks of various widgets will work. But, a feature is visible only when using a mouse. Look what happens with the color if you drag a pointer on the button:

The hover effect
That’s a hover event. The widgets from a standard Flutter library such as an InkWell support the hovers. If you’re implementing your own widgets or involving raw gesture detection, it should also handle hover events. If you need to track the pointer movement, use a special MouseRegion class.

Supporting Keyboard Input

There are three kinds of keyboard input in Flutter:

  1. Entering editable text in text fields.
  2. Focus movement, e.g., Tab to move forward and activate the focused element such as Enter.
  3. Direct keystrokes (not related to editable text input).

In the case of text fields such as TextField), Flutter handles all the low-level work related to keyboard input for you. Users can enter or paste text from the clipboard using the standard system key shortcuts such as Ctrl+C on Windows.

The focus is important in mobile and desktop apps. On the latter, users can move it to the adjacent focusable elements using Tab and Shift+Tab. On mobile, there can be an IME action on a soft keyboard. Also on desktop, the focused element gains the hovered state.

Handling Keystrokes

To intercept arbitrary keystrokes, use the KeyboardListner or Focus widgets. Also, the receiver widget has to be in the focused state (that one related to hover). The entire focus system in Flutter is a broad and advanced topic. You can read more in the official Understanding Flutter’s focus system documentation. The simplest solution is a Focus widget. Add it as the outermost node in the CalculatorBody (in a calculator_body.dart file):

@override
Widget build(BuildContext context) => Focus(
      autofocus: true,
      onKey: _onKey,
      child: Column(...), //existing code
    );

KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
  //TODO handle supported keys
  return KeyEventResult.ignored;
}

Note the autofocus: true, the widget will get focus without clicking it or pressing Tab. The onKey callback has to return the status. Use KeyEventResult.handled when you don’t want the default system behavior to happen. If you’re not interested in the particular keystroke, then return KeyEventResult.ignored. The other consumers will receive it then. Create extension functions on RawKeyEvent in the new keycodes.dart file. Such extensions allow simplifying the code inside the onKey callback.

import 'package:flutter/services.dart';

const _deleteKeyId = 0x10000007F;
const _backspaceKeyId = 0x100000008;
const _cKeyId = 0x00000063;
const _vKeyId = 0x00000076;

extension KeyCodes on RawKeyEvent {
  bool isClear() =>
      logicalKey.keyId == _deleteKeyId || logicalKey.keyId == _backspaceKeyId;

  bool isDigit() {
    //1
    final codeUnit = character?.codeUnitAt(0) ?? 0;
    return codeUnit >= 0x30 && codeUnit <= 0x39;
  }

  bool isCopy() =>
      //2
      (isMetaPressed || isControlPressed) && logicalKey.keyId == _cKeyId;

  bool isPaste() =>
      (isMetaPressed || isControlPressed) && logicalKey.keyId == _vKeyId;
}

Here are key points of the code:

  1. Extract the ASCII code of the character key (fall back to 0 for non-character keys).
  2. Check Control and Meta (aka Cmd) keys so macOS keymap will also work.

Handling Key Events

In the case of keys with a visual representation such as digits, you can use the character property to get the actual keystroke value. For other keys such as Backspace, use the logicalKey. Note the callback fires on both key press and release for each key. Although, a character isn’t null only on press (down) events. For modifier keys, Shift, Control, Alt, Cmd there are special flags such as isControlPressed. When using a Focus widget, you have to detect the key combinations for each operating system separately. There isn’t a built-in method for detecting the paste action. On Windows and Linux, it’s usually Ctrl+C (but also Ctrl+Insert), on macOS, it’s Cmd+C.

Use extensions you created before in _onKey callback inside calculator_body.dart file:

KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
  if (event.isDigit()) {
    _calculator.appendDigit(event.character!);
    return KeyEventResult.handled;
  } else if (event.isClear()) {
    //TODO handle erasing
    return KeyEventResult.handled;
  } else if (event.character == '+') {
    _calculator.appendOperator(Operator.plus);
    return KeyEventResult.handled;
  } else if (event.character == '-') {
    _calculator.appendOperator(Operator.minus);
    return KeyEventResult.handled;
  } else if (event.isCopy()) {
    //TODO handle copying from clipboard
    return KeyEventResult.handled;
  } else if (event.isPaste()) {
    //TODO handle pasing from clipboard
    return KeyEventResult.handled;
  }
  return KeyEventResult.ignored;
}

Let’s run the app (Shift+F10) and watch it work! From now, you can enter digits and operands using the physical keyboard:
Detecting keypresses