Initialize a null safe variable without a default constructor?

Question

Asked by Alex N on November 11, 2021 (source).

I'm sure this problem has been asked before, but I can't figure out how to even properly word it.

I am trying to get Bluetooth Device data into my flutter app. The examples I have found either use non-null safe versions of dart code or they hide all of the important details.

I am trying to build a very simple prototype from scratch so I can get a better grasp on things.

I want to make a stateful widget that updates based on notifyListeners(). The idea is I start out with "noName bluetooth device", then once I have a device connected, I can update the object and display the name of the connected device.

I keep running into this same roadblock and I can't get past it. I want to make a default Bluetooth Device, but the device has no default constructor. The default device cannot be null because of null safety.

Can someone help me figure this out. There is something I know I am fundamentally misunderstanding, but I don't know where to start.

The code below makes a ChangeNotifierProvider the parent of my BlueTesting widget that should display details of the connected Bluetooth Device (I haven't written all of the code yet).

The BTDevices class should update the Bluetooth Device object, and notify the app to display the updated data from "the default empty device" to the new connected device.

Thank you for your help!

import 'package:flutter/material.dart';
import 'package:flutter_blue/flutter_blue.dart';
import 'package:provider/provider.dart';

void main() => runApp(
    ChangeNotifierProvider(create: (_) => BTDevices(), child: BlueTesting()));

class BTDevices extends ChangeNotifier {
  BluetoothDevice device = BluetoothDevice();
  void setDevice(BluetoothDevice newDevice) {
    device = newDevice;
    notifyListeners();
  }
}

UPDATE:

I tried updating the above code to set:

BluetoothDevice device = null;

A value of type 'Null' can't be assigned to a variable of type 'BluetoothDevice'. Try changing the type of the variable, or casting the right-hand type to 'BluetoothDevice'.dartinvalid_assignment

here is the information on the BluetoothDevice definition:

part of flutter_blue;

class BluetoothDevice {
  final DeviceIdentifier id;
  final String name;
  final BluetoothDeviceType type;

  BluetoothDevice.fromProto(protos.BluetoothDevice p)
      : id = new DeviceIdentifier(p.remoteId),
        name = p.name,
        type = BluetoothDeviceType.values[p.type.value];

  BehaviorSubject<bool> _isDiscoveringServices = BehaviorSubject.seeded(false);
  Stream<bool> get isDiscoveringServices => _isDiscoveringServices.stream;

  /// Establishes a connection to the Bluetooth Device.
  Future<void> connect({
    Duration? timeout,
    bool autoConnect = true,
  }) async {
    var request = protos.ConnectRequest.create()
      ..remoteId = id.toString()
      ..androidAutoConnect = autoConnect;

    Timer? timer;
    if (timeout != null) {
      timer = Timer(timeout, () {
        disconnect();
        throw TimeoutException('Failed to connect in time.', timeout);
      });
    }

    await FlutterBlue.instance._channel
        .invokeMethod('connect', request.writeToBuffer());

    await state.firstWhere((s) => s == BluetoothDeviceState.connected);

    timer?.cancel();

    return;
  }

  /// Cancels connection to the Bluetooth Device
  Future disconnect() =>
      FlutterBlue.instance._channel.invokeMethod('disconnect', id.toString());

  BehaviorSubject<List<BluetoothService>> _services =
      BehaviorSubject.seeded([]);

  /// Discovers services offered by the remote device as well as their characteristics and descriptors
  Future<List<BluetoothService>> discoverServices() async {
    final s = await state.first;
    if (s != BluetoothDeviceState.connected) {
      return Future.error(new Exception(
          'Cannot discoverServices while device is not connected. State == $s'));
    }
    var response = FlutterBlue.instance._methodStream
        .where((m) => m.method == "DiscoverServicesResult")
        .map((m) => m.arguments)
        .map((buffer) => new protos.DiscoverServicesResult.fromBuffer(buffer))
        .where((p) => p.remoteId == id.toString())
        .map((p) => p.services)
        .map((s) => s.map((p) => new BluetoothService.fromProto(p)).toList())
        .first
        .then((list) {
      _services.add(list);
      _isDiscoveringServices.add(false);
      return list;
    });

    await FlutterBlue.instance._channel
        .invokeMethod('discoverServices', id.toString());

    _isDiscoveringServices.add(true);

    return response;
  }

  /// Returns a list of Bluetooth GATT services offered by the remote device
  /// This function requires that discoverServices has been completed for this device
  Stream<List<BluetoothService>> get services async* {
    yield await FlutterBlue.instance._channel
        .invokeMethod('services', id.toString())
        .then((buffer) =>
            new protos.DiscoverServicesResult.fromBuffer(buffer).services)
        .then((i) => i.map((s) => new BluetoothService.fromProto(s)).toList());
    yield* _services.stream;
  }

  /// The current connection state of the device
  Stream<BluetoothDeviceState> get state async* {
    yield await FlutterBlue.instance._channel
        .invokeMethod('deviceState', id.toString())
        .then((buffer) => new protos.DeviceStateResponse.fromBuffer(buffer))
        .then((p) => BluetoothDeviceState.values[p.state.value]);

    yield* FlutterBlue.instance._methodStream
        .where((m) => m.method == "DeviceState")
        .map((m) => m.arguments)
        .map((buffer) => new protos.DeviceStateResponse.fromBuffer(buffer))
        .where((p) => p.remoteId == id.toString())
        .map((p) => BluetoothDeviceState.values[p.state.value]);
  }

  /// The MTU size in bytes
  Stream<int> get mtu async* {
    yield await FlutterBlue.instance._channel
        .invokeMethod('mtu', id.toString())
        .then((buffer) => new protos.MtuSizeResponse.fromBuffer(buffer))
        .then((p) => p.mtu);

    yield* FlutterBlue.instance._methodStream
        .where((m) => m.method == "MtuSize")
        .map((m) => m.arguments)
        .map((buffer) => new protos.MtuSizeResponse.fromBuffer(buffer))
        .where((p) => p.remoteId == id.toString())
        .map((p) => p.mtu);
  }

  /// Request to change the MTU Size
  /// Throws error if request did not complete successfully
  Future<void> requestMtu(int desiredMtu) async {
    var request = protos.MtuSizeRequest.create()
      ..remoteId = id.toString()
      ..mtu = desiredMtu;

    return FlutterBlue.instance._channel
        .invokeMethod('requestMtu', request.writeToBuffer());
  }

  /// Indicates whether the Bluetooth Device can send a write without response
  Future<bool> get canSendWriteWithoutResponse =>
      new Future.error(new UnimplementedError());

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is BluetoothDevice &&
          runtimeType == other.runtimeType &&
          id == other.id;

  @override
  int get hashCode => id.hashCode;

  @override
  String toString() {
    return 'BluetoothDevice{id: $id, name: $name, type: $type, isDiscoveringServices: ${_isDiscoveringServices.value}, _services: ${_services.value}';
  }
}

enum BluetoothDeviceType { unknown, classic, le, dual }

enum BluetoothDeviceState { disconnected, connecting, connected, disconnecting }

Answer

Question answered by nvoigt (source).

The default device cannot be null because of null safety.

You got this backwards. Null-safety is never the reason something can or cannot be null. You decide whether something can or cannot be null. It has been this way forever, the programmer decides if a variable sometimes is null. All null-safety does is force the programmer to share this secret arcane knowledge with their compiler, so the compiler can do it's job and warn the programmer if their logic contains mistakes.

So if you think that having no device for the start of the program (sounds reasonable), then you can decide to make it nullable. Use BluetoothDevice? as the type and it is nullable. And now your compiler can tell you all the places where you (mistakenly) assume it never is null. That's the great thing about null-safety. It's here to help you with whatever choice you make, not constrain you in your choices.

ANDROID-BLUETOOTH DART FLUTTER FLUTTER-PROVIDER
SHARE: