Mobile Development 18 min read

Best Practices for Robust Dart and Flutter Development: Uri, Type Conversion, List Handling, ChangeNotifier, and Resource Management

This article presents a comprehensive guide to writing safer and more maintainable Flutter code by demonstrating proper Uri construction, type checking with 'is' versus 'as', safe List operations using the collection package, disciplined use of ChangeNotifier and its providers, and systematic resource disposal patterns.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Best Practices for Robust Dart and Flutter Development: Uri, Type Conversion, List Handling, ChangeNotifier, and Resource Management

Having reviewed countless code snippets and resolved numerous production bugs, the author shares practical advice to improve the robustness of Dart and Flutter applications.

Using the Uri Class

Instead of concatenating strings to build URLs, use the Uri class which automatically handles encoding and prevents malformed addresses.

/// Assume
outsideInput
comes from an external system and may contain illegal characters.
final someAddress = 'https://www.special.com/?a=
    \\${Uri.encodeFull(outsideInput)}';

// When multiple parameters are needed:
final someAddress = 'https://www.special.com/?a=
    \\${Uri.encodeFull(outsideInput)}&b=
    \\${Uri.encodeFull(outsideInput1)}&c=
    \\${Uri.encodeFull(outsideInput2)}';

// Conditional building:
if (conditionA) {
  someAddress = 'https://www.special.com/?a=
      \\${Uri.encodeFull(outsideInput)}';
} else if (conditionB) {
  someAddress = 'https://www.special.com/?a=
      \\${Uri.encodeFull(outsideInput)}&b=
      \\${otherVariable}';
} else {
  someAddress = 'https://www.special.com';
}

// Preferred way – construct a Uri object:
final someAddress = Uri(
  path: 'some/sys/path/loc',
  queryParameters: {
    'a': '\\${outsideInput}', // automatically percent‑encoded
    'b': '\\${outsideInput1}',
    if (conditionA) 'c': '\\${outsideInput2}',
  },
);

Type Conversion

Use the is operator for safe type checking; it performs an implicit cast without throwing when the check fails, whereas as throws an exception on failure.

class Animal {
  void eat(String food) => print('eat $food');
}

class Bird extends Animal {
  void fly() => print('flying');
}

void main() {
  Object animal = Bird();

  if (animal is Bird) {
    animal.fly(); // implicit cast
  }

  (animal as Animal).eat('meat'); // explicit cast, may throw
}

Safe List Operations

Directly accessing first , last , or firstWhere on an empty list throws exceptions. Use the collection package helpers such as firstOrNull and firstWhereOrNull to avoid crashes.

import 'package:collection/collection.dart';

List
list = [];
list.firstOrNull;          // null instead of exception
list.lastOrNull;
list.firstWhereOrNull((t) => t > 0);
list.singleOrNull;
list.lastWhereOrNull((t) => t > 0);
list.singleWhereOrNull((t) => t > 0);

Alternatively, create a custom SafeList that returns default or placeholder values when an index is out of range.

class SafeList
extends ListMixin
{
  final List
_rawList;
  final T defaultValue;
  final T absentValue;

  SafeList({required this.defaultValue, required this.absentValue, List
? initList})
      : _rawList = List.from(initList ?? []);

  @override
  T operator [](int index) => index < _rawList.length
      ? _rawList[index] ?? defaultValue
      : absentValue;

  @override
  void operator []=(int index, T value) {
    if (_rawList.length == index) {
      _rawList.add(value);
    } else {
      _rawList[index] = value;
    }
  }

  @override
  int get length => _rawList.length;

  @override
  set length(int newLength) => _rawList.length = newLength;

  @override
  T get first => _rawList.isNotEmpty ? _rawList.first ?? defaultValue : absentValue;

  @override
  T get last => _rawList.isNotEmpty ? _rawList.last ?? defaultValue : absentValue;
}

final list = SafeList(defaultValue: 0, absentValue: 100, initList: [1, 2, 3]);
print(list[0]); // 1
print(list[3]); // 100 (out‑of‑range)
list.length = 101;
print(list[100]); // 0 (default after length change)

ChangeNotifier Best Practices

After dispose() a ChangeNotifier must not be used. Guard all accesses with a disposed flag, preferably via a mixin.

mixin Disposed on ChangeNotifier {
  bool _disposed = false;

  @override
  bool get hasListeners => !_disposed && super.hasListeners;

  @override
  void notifyListeners() {
    if (_disposed) return;
    super.notifyListeners();
  }

  @override
  void dispose() {
    _disposed = true;
    super.dispose();
  }
}

class PageNotifier extends ChangeNotifier with Disposed {
  dynamic pageData;

  Future
beginRefresh() async {
    final response = await API.getPageContent();
    if (!response.success) return;
    pageData = response.data;
    notifyListeners(); // safe – will be ignored after dispose
  }
}

Never reuse a single ChangeNotifier instance across unrelated widgets; instead provide it through Provider or get_it at the top of the widget tree.

void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider
.value(value: ShoppingCart.instance),
      ],
      child: const MyApp(),
    ),
  );
}

ChangeNotifierProvider Usage

When using ChangeNotifierProvider.value , you must manually dispose the passed instance because the provider does not own its lifecycle. Use the builder constructor for automatically managed lifecycles.

// Correct – manual disposal
MyChangeNotifier variable;

void initState() {
  super.initState();
  variable = MyChangeNotifier();
}

Widget build(BuildContext context) {
  return ChangeNotifierProvider.value(
    value: variable,
    child: ...,
  );
}

void dispose() {
  variable.dispose();
  super.dispose();
}

// Incorrect – let provider create the instance but pass a pre‑built one
return ChangeNotifierProvider(
  create: (_) => variable, // leads to lifecycle confusion
  child: ...,
);

Resource Disposal Discipline

Many Flutter objects (e.g., Timer , StreamSubscription , ScrollController , TextEditingController ) require explicit disposal. Place dispose() as the first method after the field declarations and keep initState() immediately after, so reviewers can quickly verify that every created resource is released.

// Bad – dispose far from initState
final _controller = TextEditingController();
late Timer _timer;

void initState() {
  super.initState();
  _timer = Timer(...);
}

void dispose() {
  _timer.cancel();
  super.dispose();
}
// Good – dispose placed before initState
void dispose() {
  _controller.dispose();
  _timer.cancel();
  super.dispose();
}

void initState() {
  super.initState();
  _timer = Timer(...);
}

A mixin can automate disposal of arbitrary callbacks:

mixin AutomaticDisposeMixin
on State
{
  final Set
_disposeSet = {};

  void autoDispose(VoidCallback cb) => _disposeSet.add(cb);

  @override
  void dispose() {
    for (final cb in _disposeSet) cb();
    _disposeSet.clear();
    super.dispose();
  }
}

class _PageState extends State
with AutomaticDisposeMixin {
  Future
_refreshPage() async {
    final token = CancelToken();
    autoDispose(() => token.cancel());
    final response = await Dio().get(url, cancelToken: token);
    // use response …
  }
}

StatefulWidget Asynchronous Refresh

When an asynchronous operation updates UI, always check mounted before calling setState to avoid exceptions if the widget has been removed from the tree.

Future
_refreshPage() async {
  final response = await API.getPageDetail();
  if (!response.success) return;
  if (!mounted) return; // guard
  setState(() {
    _data = response.data;
  });
}

For convenience, a mixin can override setState to perform this guard automatically.

mixin Stateable
on State
{
  @override
  void setState(VoidCallback fn) {
    if (!mounted) return;
    super.setState(fn);
  }
}

class SomePageState extends State
with Stateable {
  // async UI updates are now safe without repetitive checks
}

By following these guidelines—using Uri objects, preferring is over as , leveraging the collection package, managing ChangeNotifier lifecycles, systematically disposing resources, and guarding asynchronous UI updates—you can write Flutter code that is more robust, maintainable, and less error‑prone.

DartFlutterstate-managementresource managementBest PracticesChangeNotifierURI
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.