| |

Dart 43 🎯 Async Generators (async*, yield) and Sync Generators (sync*, yield*)

A generator is a function that produces a sequence of values lazily. Instead of building a list and returning it, the function yields values one at a time, and the consumer receives them as they are produced. Dart has two kinds of generators: synchronous generators marked with sync*, which return an Iterable, and asynchronous generators marked with async*, which return a Stream. The two share the yield keyword for emitting a value and the yield* keyword for delegating to another generator, but their execution models are different. A synchronous generator runs when its iterator is advanced; an asynchronous generator runs when its stream is listened to, and it can await between yields.

The distinction between a generator and a normal function is the distinction between producing a collection and producing a sequence. A function that returns a List must compute every element before it returns. A generator computes each element only when it is needed, and it can produce an unbounded sequence — a sequence that never ends — without consuming infinite memory. This laziness is the point. A generator that produces the first ten even numbers is as easy to write as one that produces ten million, and the consumer decides how many to take.

The yield* keyword is the delegation operator. yield* otherGenerator() emits every value from another generator as if the current generator had yielded them itself. It is the generator equivalent of a loop that yields each element of another sequence, but it is more concise and it preserves the laziness. In an asynchronous generator, yield* can delegate to a stream or to another asynchronous generator, and it awaits each value as it is produced. In a synchronous generator, yield* delegates to an iterable.

This chapter covers three areas. First, why generators exist — the laziness problem and the difference between producing a collection and producing a sequence. Second, how synchronous generators work — sync*, yield, yield*, and the Iterable return type. Third, how asynchronous generators work — async*, yield, yield*, await between yields, and the Stream return type. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the generator execution.

Key point: A sync* function returns an Iterable and runs when its iterator is advanced. An async* function returns a Stream and runs when its stream is listened to. yield emits a value; yield* delegates to another generator. An async* function can await between yields, which makes it the standard way to produce a stream from asynchronous operations.


Why generators exist

The laziness problem. A function that returns a List must compute every element before it returns. If the list is large, the computation is expensive, and the memory is allocated up front. If the list is unbounded — the sequence of all integers, the stream of all primes — the function cannot return at all. A generator solves this by producing values lazily. The consumer asks for the next value, and the generator computes it and yields it. The generator is suspended between yields, and its state is preserved. The consumer controls the pace, and the generator does only the work that is requested.

The sequence problem. A generator is the natural way to express a sequence that is defined by a rule rather than by a collection. The sequence of Fibonacci numbers, the sequence of dates in a range, the sequence of lines in a file, and the sequence of pages in a paginated API are all defined by a rule. A generator expresses the rule directly: “yield the first value, then yield the next value computed from the previous ones.” The consumer receives the values without the generator needing to know how many will be requested.

The asynchronous-sequence problem. An asynchronous generator is the asynchronous version of the same idea. The sequence is produced over time, and each value may require an await before it is available. A paginated API returns a page of results and a cursor; the next page requires another request. A file read returns chunks; the next chunk requires another read. An asynchronous generator expresses this as a loop that awaits the next value and yields it. The consumer receives a Stream and can process each value as it arrives.

The delegation problem. A generator often needs to include the values of another generator. A generator that produces all the files in a directory tree needs to include the values of the generators for the subdirectories. A generator that produces a filtered sequence needs to include the values of the source generator that pass the filter. The yield* keyword is the delegation operator. It emits every value from another generator as if the current generator had yielded them, and it preserves the laziness and the suspension semantics.

The state-preservation problem. A generator preserves its state between yields. The local variables, the position in the code, and the loop counters are all suspended and restored. This is what makes a generator different from a function that returns an iterator. The generator’s code is written as a normal sequence of statements, and the suspension is implicit. The consumer does not see the state; it only sees the values. The implementation moves the state to the heap, but the source code remains simple.

The trade-off. A generator is lazy, which means the work is deferred until the consumer requests it. If the consumer never requests the values, the work is never done. This is an advantage for unbounded sequences and for expensive computations that may not be needed. It is a disadvantage when the values must be produced eagerly, or when the generator’s state is large and the suspension is expensive. The trade-off is between the flexibility of lazy production and the simplicity of eager production. For sequences, the laziness is usually the right choice.


a. Synchronous generators

A synchronous generator is a function marked with sync*. It returns an Iterable, and it uses yield to emit values.

Iterable<int> countTo(int n) sync* {
  for (var i = 1; i <= n; i++) {
    yield i;
  }
}

void main() {
  final numbers = countTo(5);
  for (final n in numbers) {
    print(n); // 1, 2, 3, 4, 5
  }
}

The countTo function returns an Iterable<int>. The yield statement emits a value, and the function is suspended until the next value is requested. The for loop in the caller advances the iterator, and each iteration resumes the generator.

A synchronous generator runs when its iterator is advanced. The body of the function does not execute when the function is called; it executes when the first value is requested, and it continues until the next yield or the end of the function. This is the key difference from a normal function, which runs to completion when it is called.

yield* delegates to another iterable or generator:

Iterable<int> countTo(int n) sync* {
  yield 0;
  yield* countFrom(1, n);
}

Iterable<int> countFrom(int start, int end) sync* {
  for (var i = start; i <= end; i++) {
    yield i;
  }
}

The yield* countFrom(1, n) emits every value from countFrom(1, n) as if countTo had yielded them. The delegation is lazy: the values from countFrom are produced only as they are requested.

A synchronous generator can be used anywhere an Iterable is expected. The for-in loop, the toList method, the map and where methods, and the spread operator all work with the result.

final numbers = countTo(5);
final doubled = numbers.map((n) => n * 2);
final list = numbers.toList();

The generator is lazy, but the toList call makes it eager: it requests every value and collects them into a list. This is the pattern for converting a lazy sequence into a concrete collection.


b. Asynchronous generators

An asynchronous generator is a function marked with async*. It returns a Stream, and it uses yield to emit values. It can await between yields, which makes it the standard way to produce a stream from asynchronous operations.

Stream<int> countTo(int n) async* {
  for (var i = 1; i <= n; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    yield i;
  }
}

void main() async {
  await for (final n in countTo(5)) {
    print(n); // 1, 2, 3, 4, 5, with 100ms between each
  }
}

The countTo function returns a Stream<int>. The await pauses the generator until the future completes, and the yield emits the value. The consumer receives the values over time, with the delays applied.

An asynchronous generator runs when its stream is listened to. The body of the function does not execute when the function is called; it executes when the first listener attaches, and it continues until the next yield, the next await, or the end of the function. The generator is suspended between yields, and the stream’s subscription controls the pace.

yield* delegates to another stream or asynchronous generator:

Stream<int> countTo(int n) async* {
  yield 0;
  yield* countFrom(1, n);
}

Stream<int> countFrom(int start, int end) async* {
  for (var i = start; i <= end; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    yield i;
  }
}

The yield* countFrom(1, n) emits every value from countFrom(1, n) as if countTo had yielded them. The delegation is asynchronous: each value from countFrom is awaited and emitted in order.

An asynchronous generator can be consumed with await for, with listen, or with any of the stream operators:

await for (final n in countTo(5)) {
  print(n);
}

countTo(5).listen((n) => print(n));

countTo(5).where((n) => n.isEven).map((n) => n * 2);

The await for loop is the most readable way to consume a stream. The listen method is the lower-level API. The stream operators compose the stream into a pipeline.

An asynchronous generator can produce a stream from a paginated API:

Stream<List<Item>> fetchPages(String url) async* {
  String? nextUrl = url;
  while (nextUrl != null) {
    final response = await http.get(Uri.parse(nextUrl));
    final data = jsonDecode(response.body);
    yield data['items'] as List<Item>;
    nextUrl = data['next'];
  }
}

The generator fetches the first page, yields the items, and continues with the next page. The consumer receives each page as it arrives, and the generator stops when there is no next page. This is the standard pattern for consuming a paginated API as a stream.


c. The differences and the shared syntax

The two generators share the yield and yield* keywords, but their execution models are different.

The return type. A sync* function returns an Iterable. An async* function returns a Stream. The return type is determined by the marker, not by the yield statements.

The execution trigger. A sync* function runs when its iterator is advanced. An async* function runs when its stream is listened to. Neither runs when the function is called; the call returns the Iterable or the Stream immediately.

The suspension. A sync* function is suspended at each yield and resumed when the next value is requested. An async* function is suspended at each yield and at each await, and resumed when the value is requested or the future completes.

The await support. A sync* function cannot use await. An async* function can, and the await is the mechanism for producing values from asynchronous operations.

The consumption. A sync* result is consumed with a for-in loop, the toList method, or the iterable operators. An async* result is consumed with await for, the listen method, or the stream operators.

The laziness. Both are lazy. A sync* generator produces values only when they are requested. An async* generator produces values only when the stream is listened to and the subscription is active. The difference is the pacing: the synchronous generator is driven by the iterator, and the asynchronous generator is driven by the stream’s events.

The error handling. A sync* generator can throw an exception, which propagates to the iterator. An async* generator can throw an exception, which is delivered as an error event on the stream. The consumer handles the error with a try/catch around the await for loop or with the onError callback of listen.

The shared syntax is the point. The yield and yield* keywords mean the same thing in both generators: emit a value and delegate to another generator. The difference is the execution model, and the marker determines which model applies.


Complete Example Session

// ============================================
// PART 1: A SYNCHRONOUS GENERATOR
// ============================================

Iterable<int> countTo(int n) sync* {
  for (var i = 1; i <= n; i++) {
    yield i;
  }
}

void main() {
  for (final n in countTo(5)) {
    print(n); // 1, 2, 3, 4, 5
  }
}


// ============================================
// PART 2: LAZINESS
// ============================================

Iterable<int> infinite() sync* {
  var i = 0;
  while (true) {
    yield i++;
  }
}

void main() {
  final firstFive = infinite().take(5).toList();
  print(firstFive); // [0, 1, 2, 3, 4]
}

// The generator is unbounded, but take(5) requests
// only five values.


// ============================================
// PART 3: yield* DELEGATION
// ============================================

Iterable<int> countFrom(int start, int end) sync* {
  for (var i = start; i <= end; i++) {
    yield i;
  }
}

Iterable<int> countTo(int n) sync* {
  yield 0;
  yield* countFrom(1, n);
}

void main() {
  print(countTo(5).toList()); // [0, 1, 2, 3, 4, 5]
}


// ============================================
// PART 4: A RECURSIVE GENERATOR
// ============================================

Iterable<int> treeValues(TreeNode node) sync* {
  yield node.value;
  for (final child in node.children) {
    yield* treeValues(child);
  }
}

// The generator yields the node's value and then
// delegates to the generators for each child.
// This is a depth-first traversal.


// ============================================
// PART 5: AN ASYNCHRONOUS GENERATOR
// ============================================

Stream<int> countTo(int n) async* {
  for (var i = 1; i <= n; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    yield i;
  }
}

void main() async {
  await for (final n in countTo(5)) {
    print(n); // 1, 2, 3, 4, 5, with 100ms between each
  }
}


// ============================================
// PART 6: ASYNC GENERATOR WITH yield*
// ============================================

Stream<int> countFrom(int start, int end) async* {
  for (var i = start; i <= end; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    yield i;
  }
}

Stream<int> countTo(int n) async* {
  yield 0;
  yield* countFrom(1, n);
}

void main() async {
  await for (final n in countTo(5)) {
    print(n); // 0, 1, 2, 3, 4, 5
  }
}


// ============================================
// PART 7: A PAGINATED API AS A STREAM
// ============================================

Stream<List<Item>> fetchPages(String url) async* {
  String? nextUrl = url;
  while (nextUrl != null) {
    final response = await http.get(Uri.parse(nextUrl));
    final data = jsonDecode(response.body);
    yield data['items'] as List<Item>;
    nextUrl = data['next'] as String?;
  }
}

void main() async {
  await for (final page in fetchPages('https://api.example.com/items')) {
    for (final item in page) {
      print(item.name);
    }
  }
}

// Each page is yielded as it arrives.
// The generator stops when there is no next page.


// ============================================
// PART 8: COMBINING GENERATORS
// ============================================

Stream<int> source() async* {
  for (var i = 0; i < 10; i++) {
    yield i;
  }
}

Stream<int> evens() async* {
  await for (final n in source()) {
    if (n.isEven) {
      yield n;
    }
  }
}

void main() async {
  await for (final n in evens()) {
    print(n); // 0, 2, 4, 6, 8
  }
}


// ============================================
// PART 9: ERROR HANDLING IN AN ASYNC GENERATOR
// ============================================

Stream<int> failing() async* {
  yield 1;
  yield 2;
  throw Exception('boom');
  // yield 3; // never reached
}

void main() async {
  try {
    await for (final n in failing()) {
      print(n); // 1, 2
    }
  } catch (e) {
    print('caught: $e'); // caught: Exception: boom
  }
}


// ============================================
// PART 10: THE COMPLETE EXAMPLE
// ============================================

Iterable<int> fibonacci() sync* {
  var a = 0;
  var b = 1;
  while (true) {
    yield a;
    final next = a + b;
    a = b;
    b = next;
  }
}

Stream<Map<String, dynamic>> fetchAllPages(String baseUrl) async* {
  var page = 1;
  while (true) {
    final response = await http.get(Uri.parse('$baseUrl?page=$page'));
    final data = jsonDecode(response.body);
    yield data as Map<String, dynamic>;
    if (data['next'] == null) break;
    page++;
  }
}

void main() async {
  // Synchronous generator: first 10 Fibonacci numbers
  final fibs = fibonacci().take(10).toList();
  print(fibs); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

  // Asynchronous generator: all pages
  await for (final page in fetchAllPages('https://api.example.com')) {
    print('Page: ${page['page']}');
  }
}

The ten parts show a synchronous generator, laziness, yield* delegation, a recursive generator, an asynchronous generator, an asynchronous generator with yield*, a paginated API as a stream, combining generators, error handling, and the complete example.


Quick Reference

The Two Generators

Aspectsync*async*
ReturnsIterable<T>Stream<T>
Runs whenIterator is advancedStream is listened to
Can awaitNoYes
Consumed withfor-in, toListawait for, listen
Error handlingtry/catchtry/catch around await for

yield and yield*

KeywordEffect
yield valueEmit a value
yield* iterableEmit every value from an iterable
yield* streamEmit every value from a stream
yield* generator()Delegate to another generator

Common Patterns

PatternGenerator
Unbounded sequencesync* or async*
Recursive traversalsync* with yield*
Paginated APIasync* with await
File chunksasync* with await
Filtered sequenceasync* with await for
Fibonaccisync*

Laziness

OperationEager or lazy
Calling the generatorLazy (returns immediately)
for-in loopLazy (one value at a time)
await for loopLazy (one value at a time)
toList()Eager (collects all)
listen()Lazy (one value at a time)

Best Practices

✅ Do This:

// Use sync* for sequences
Iterable<int> countTo(int n) sync* { for (var i = 1; i <= n; i++) yield i; } // ✅
// Use async* for asynchronous sequences
Stream<int> fetchAll() async* { while (...) { yield await fetch(); } }        // ✅
// Use yield* for delegation
yield* otherGenerator();                                                      // ✅
// Use take for bounded consumption
generator().take(5).toList();                                                 // ✅
// Handle errors with try/catch around await for
try { await for (final v in stream) { ... } } catch (e) { ... }               // ✅
// Use await for for readable stream consumption
await for (final v in stream) { ... }                                         // ✅

❌ Don’t Do This:

// Don't use async* when a sync* generator works
Stream<int> count() async* { yield 1; } // unnecessary async                  // ❌
// Don't build a list and return it
List<int> count(int n) { final list = []; for (...) list.add(i); return list; } // use sync* // ❌
// Don't use yield in a normal function
int f() { yield 1; } // syntax error                                          // ❌
// Don't forget the return type
countTo(int n) sync* { yield 1; } // missing Iterable<int>                    // ❌
// Don't use await in a sync* generator
Iterable<int> f() sync* { await ...; } // syntax error                        // ❌
// Don't consume an unbounded generator with toList
infinite().toList(); // hangs                                                  // ❌

Common Pitfalls

PitfallWhy It HappensFix
Generator does not runNever iterated or listened toConsume the result
toList on an infinite generatorEager consumptionUse take
await in a sync* generatorWrong markerUse async*
Yield in a normal functionMissing markerAdd sync* or async*
Return type mismatchWrong markersync* → Iterable, async* → Stream
Error not caughtNo try/catchWrap the await for loop
Values out of orderConcurrent processingUse await for for order

Real-World Examples

1. Synchronous Generator

Iterable<int> countTo(int n) sync* { for (var i = 1; i <= n; i++) yield i; }

2. Infinite Generator

Iterable<int> infinite() sync* { var i = 0; while (true) yield i++; }

3. yield* Delegation

Iterable<int> combined() sync* { yield 0; yield* other(); }

4. Recursive Generator

Iterable<int> tree(TreeNode node) sync* {
  yield node.value;
  for (final child in node.children) yield* tree(child);
}

5. Asynchronous Generator

Stream<int> countTo(int n) async* { for (var i = 1; i <= n; i++) yield i; }

6. Paginated API

Stream<Page> fetchAll(String url) async* { while (...) { yield await fetch(); } }

7. Filtered Stream

Stream<int> evens() async* { await for (final n in source()) if (n.isEven) yield n; }

8. Error Handling

try { await for (final v in stream) { } } catch (e) { }

9. Take from Infinite

infinite().take(5).toList();

10. Complete Generator

Iterable<int> fibonacci() sync* { var a = 0, b = 1; while (true) { yield a; final n = a + b; a = b; b = n; } }

Visual

Synchronous Generator Execution

┌──────────────────────────────────────────────────────────────┐
│  SYNCHRONOUS GENERATOR EXECUTION                             │
│                                                              │
│  Iterable<int> countTo(int n) sync* {                        │
│    for (var i = 1; i <= n; i++) {                            │
│      yield i;                                                │
│    }                                                         │
│  }                                                           │
│                                                              │
│  Caller:                        Generator:                   │
│  ┌──────────────────────┐      ┌──────────────────────┐      │
│  │  countTo(3)          │─────►│  (suspended)         │      │
│  │                      │      │                      │      │
│  │  iterator.moveNext() │─────►│  i=1, yield 1        │      │
│  │  iterator.current    │◄─────│  (suspended)         │      │
│  │                      │      │                      │      │
│  │  iterator.moveNext() │─────►│  i=2, yield 2        │      │
│  │  iterator.current    │◄─────│  (suspended)         │      │
│  │                      │      │                      │      │
│  │  iterator.moveNext() │─────►│  i=3, yield 3        │      │
│  │  iterator.current    │◄─────│  (suspended)         │      │
│  │                      │      │                      │      │
│  │  iterator.moveNext() │─────►│  loop ends, done     │      │
│  └──────────────────────┘      └──────────────────────┘      │
│                                                              │
│  The generator runs one step per moveNext() call.            │
│  The state is suspended between yields.                      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Asynchronous Generator Execution

┌──────────────────────────────────────────────────────────────┐
│  ASYNCHRONOUS GENERATOR EXECUTION                            │
│                                                              │
│  Stream<int> countTo(int n) async* {                         │
│    for (var i = 1; i <= n; i++) {                            │
│      await Future.delayed(Duration(ms: 100));                │
│      yield i;                                                │
│    }                                                         │
│  }                                                           │
│                                                              │
│  Time ──────────────────────────────────────────────►        │
│                                                              │
│  0ms    100ms   200ms   300ms                                │
│  │      │       │       │                                    │
│  │      │       │       └── yield 3 ──► listener            │
│  │      │       └────────── yield 2 ──► listener            │
│  │      └────────────────── yield 1 ──► listener            │
│  └───────────────────────── (start)                          │
│                                                              │
│  The generator awaits between yields.                        │
│  The values arrive over time.                                │
│  The stream delivers each value as it is produced.           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

yield* Delegation

┌──────────────────────────────────────────────────────────────┐
│  yield* DELEGATION                                           │
│                                                              │
│  Iterable<int> outer() sync* {                               │
│    yield 0;                                                  │
│    yield* inner();                                           │
│    yield 99;                                                 │
│  }                                                           │
│                                                              │
│  Iterable<int> inner() sync* {                               │
│    yield 1;                                                  │
│    yield 2;                                                  │
│  }                                                           │
│                                                              │
│  outer() produces: [0, 1, 2, 99]                             │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  yield 0    → 0                                      │    │
│  │  yield* inner() → 1, 2 (from inner)                  │    │
│  │  yield 99   → 99                                     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The yield* delegates to the inner generator.                │
│  The values are emitted as if outer had yielded them.        │
│  The delegation is lazy: inner runs only as requested.       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Paginated API as a Stream

┌──────────────────────────────────────────────────────────────┐
│  PAGINATED API AS A STREAM                                   │
│                                                              │
│  Stream<List<Item>> fetchPages(String url) async* {          │
│    String? next = url;                                       │
│    while (next != null) {                                    │
│      final response = await http.get(next);                  │
│      final data = jsonDecode(response.body);                 │
│      yield data['items'];                                    │
│      next = data['next'];                                    │
│    }                                                         │
│  }                                                           │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Page 1 ──► await ──► yield ──► listener             │    │
│  │  Page 2 ──► await ──► yield ──► listener             │    │
│  │  Page 3 ──► await ──► yield ──► listener             │    │
│  │  no next ──► stream closes                           │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The consumer receives each page as it arrives.              │
│  The generator stops when there is no next page.             │
│  This is the standard pattern for consuming a paginated API. │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
sync*Synchronous generator; returns Iterable
async*Asynchronous generator; returns Stream
yieldEmit a value
yield*Delegate to another generator
ExecutionLazy; runs when iterated or listened to
awaitAllowed only in async*
Consumptionfor-in (sync), await for (async)
Error handlingtry/catch
Unbounded sequencesSupported; use take to bound

Key takeaways:

  • A generator produces values lazily. A sync* function returns an Iterable and runs when its iterator is advanced. An async* function returns a Stream and runs when its stream is listened to. Neither runs when the function is called; the call returns the iterable or the stream immediately.
  • yield emits a value; yield* delegates. The yield keyword produces a single value. The yield* keyword produces every value from another generator, iterable, or stream, as if the current generator had yielded them.
  • An async* generator can await between yields. This is what makes it the standard way to produce a stream from asynchronous operations. The await pauses the generator until the future completes, and the yield emits the result.
  • The two generators share the syntax but differ in execution. The yield and yield* keywords mean the same thing in both. The difference is the return type, the execution trigger, and the support for await.
  • A synchronous generator is consumed with for-in or toList. The iterator drives the generator, and each value is produced on demand. The toList method makes the generator eager and should not be used with an unbounded sequence.
  • An asynchronous generator is consumed with await for or listen. The stream’s subscription drives the generator, and each value is produced as the subscription is ready. The await for loop is the most readable way to consume a stream.
  • A generator can be recursive. A generator that yields a node’s value and then delegates to the generators for its children performs a depth-first traversal. The yield* keyword makes the recursion concise.
  • Errors propagate through the generator. A sync* generator throws an exception that propagates to the iterator. An async* generator throws an exception that is delivered as an error event on the stream. The consumer handles the error with try/catch or with the onError callback.

Remember: A generator is a function that produces a sequence of values lazily. The sync* marker creates a synchronous generator that returns an Iterable; the async* marker creates an asynchronous generator that returns a Stream. The yield keyword emits a value, and the yield* keyword delegates to another generator. An async* generator can await between yields, which makes it the standard way to produce a stream from asynchronous operations. The generators share the syntax but differ in execution: the synchronous generator is driven by its iterator, and the asynchronous generator is driven by its stream’s subscription. The result is a lazy, composable way to produce sequences, whether the values are computed synchronously or fetched asynchronously.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!