Home
Dart
OOP
Flutter
Contact

Dart Coding Challenges

Sharpen your logical thinking by solving mini programs that test loops, conditionals, collections, and algorithmic reasoning.

Why Coding Challenges Matter

Writing real-world Flutter apps requires more than knowing syntax — it demands the ability to decompose problems, think algorithmically, and translate logic into working code. The challenges below are designed to build exactly that kind of reasoning. Each one targets a common pattern you will encounter in interviews, competitive programming, and everyday app development.

To get the most out of these exercises, try to solve each challenge before reading the provided solution. Work through the problem on paper first, then translate your logic into Dart code. The process of struggling with the problem is where the real learning happens.

💡 How to Use This Page Read the problem statement, attempt the solution yourself, then compare your approach with the provided code. Focus on understanding why each solution works, not just what it does. Every challenge includes an explanation of the algorithmic thinking behind it.

These challenges use concepts from Control Flow & Functions and Variables & Data Types. If you are not comfortable with loops, conditionals, lists, or maps, revisit those pages first. The challenges are ordered from easier to harder, but each one introduces a new way of thinking about a problem.

ChallengeCore ConceptDifficulty
Fibonacci SequenceIteration, recursionEasy
Prime Number CheckerLoop control, mathEasy
Palindrome DetectorString manipulationEasy
Anagram CheckerSorting, mapsMedium
Matrix Transpose2D lists, nested loopsMedium
Binary SearchDivide and conquerMedium
FizzBuzz VariationsModular arithmeticEasy
Word Frequency CounterMaps, stringsMedium

1. Fibonacci Sequence Generator

Problem: Write a function that generates the first n numbers of the Fibonacci sequence, where each number is the sum of the two preceding ones, starting from 0 and 1.

Explanation: The Fibonacci sequence is one of the most fundamental patterns in mathematics and computer science. It appears in nature (flower petals, pinecone spirals), financial markets, and algorithmic analysis. The key insight is that every number after the first two is determined entirely by the two before it. This makes it an ideal candidate for both iterative and recursive approaches. The iterative version runs in O(n) time with O(1) space, while a naive recursive approach runs in O(2^n) — a perfect example of why understanding algorithmic complexity matters.

fibonacci.dart
List<int> fibonacci(int n) {
  if (n <= 0) return [];
  if (n == 1) return [0];

  var sequence = [0, 1];
  for (var i = 2; i < n; i++) {
    sequence.add(sequence[i - 1] + sequence[i - 2]);
  }
  return sequence;
}

void main() {
  print(fibonacci(10));
  // Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}

Recursive version — cleaner but less efficient:

fibonacci_recursive.dart
int fib(int n) {
  if (n <= 0) return 0;
  if (n == 1) return 1;
  return fib(n - 1) + fib(n - 2);
}

void main() {
  for (var i = 0; i < 10; i++) {
    print('fib($i) = ${fib(i)}');
  }
  // Output: fib(0)=0, fib(1)=1, fib(2)=1, ..., fib(9)=34
}
🧠 Think About It The recursive version recalculates the same values many times. Could you use a Map or List to cache previously computed results? This technique is called memoization and it transforms the recursive solution from exponential time to linear time.

2. Prime Number Checker

Problem: Write a function that determines whether a given integer is a prime number. Then extend it to generate all primes up to a given limit using the Sieve of Eratosthenes.

Explanation: A prime number is only divisible by 1 and itself. The naive approach checks divisibility from 2 up to n-1, but we only need to check up to the square root of n. If n has a factor larger than its square root, it must also have one smaller. For generating primes in bulk, the Sieve of Eratosthenes is far more efficient — it works by iteratively marking multiples of each prime as composite, leaving only primes unmarked. This is a classic example of trading space for time. Understanding when to optimize for time versus space is a critical skill in software development.

prime_checker.dart
bool isPrime(int n) {
  if (n < 2) return false;
  if (n == 2) return true;
  if (n % 2 == 0) return false;

  for (var i = 3; i * i <= n; i += 2) {
    if (n % i == 0) return false;
  }
  return true;
}

List<int> sieveOfEratosthenes(int limit) {
  var sieve = List.filled(limit + 1, true);
  sieve[0] = false;
  sieve[1] = false;

  for (var i = 2; i * i <= limit; i++) {
    if (sieve[i]) {
      for (var j = i * i; j <= limit; j += i) {
        sieve[j] = false;
      }
    }
  }

  return [
    for (var i = 2; i <= limit; i++)
      if (sieve[i]) i,
  ];
}

void main() {
  print(isPrime(29));  // true
  print(isPrime(15));  // false
  print(sieveOfEratosthenes(30));
  // Output: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
}
📐 Algorithmic Insight The Sieve of Eratosthenes runs in O(n log log n) time, which is nearly linear. Compare this with checking each number individually using isPrime, which would be O(n√n) for the same range. Choosing the right algorithm can mean the difference between milliseconds and minutes for large inputs.

3. Palindrome Detector

Problem: Write a function that checks whether a given string is a palindrome — it reads the same forwards and backwards. Handle case sensitivity and non-alphanumeric characters.

Explanation: A naive palindrome check reverses the entire string and compares it to the original. A more efficient approach uses two pointers starting from each end, moving inward and comparing characters. This halves the number of comparisons. Real-world palindrome checkers must also normalize the input by stripping spaces, punctuation, and converting to lowercase, since "A man, a plan, a canal: Panama" is a valid palindrome when properly cleaned. This kind of input normalization is a common pattern in data validation and text processing.

palindrome.dart
bool isPalindrome(String s) {
  var cleaned = s.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), '');
  var left = 0;
  var right = cleaned.length - 1;

  while (left < right) {
    if (cleaned[left] != cleaned[right]) return false;
    left++;
    right--;
  }
  return true;
}

bool isPalindromeSimple(String s) {
  var cleaned = s.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), '');
  return cleaned == cleaned.split('').reversed.join('');
}

void main() {
  print(isPalindrome('racecar'));          // true
  print(isPalindrome('hello'));            // false
  print(isPalindrome('A man, a plan, a canal: Panama'));  // true
  print(isPalindromeSimple('Was it a car or a cat I saw')); // true
}
🚀 Performance Note The two-pointer approach uses O(1) extra space and runs in O(n) time. The reverse-and-compare approach also runs in O(n) but allocates additional strings. For very long strings, the two-pointer method is preferred.

4. Anagram Checker

Problem: Write a function that determines whether two strings are anagrams of each other — they contain the same characters in a different order.

Explanation: Two strings are anagrams if they have the same character frequency. There are two common approaches: (1) sort both strings and compare — O(n log n) time, or (2) count character frequencies using a map and compare — O(n) time. The frequency-counting approach is faster and more elegant. It works by iterating through each string, incrementing counts for the first and decrementing for the second, then checking if all counts are zero. This pattern of "differential counting" appears frequently in data processing, diff algorithms, and even in reconciling financial transactions.

anagram.dart
bool isAnagram(String s1, String s2) {
  var a = s1.toLowerCase().replaceAll(' ', '');
  var b = s2.toLowerCase().replaceAll(' ', '');

  if (a.length != b.length) return false;

  var freq = <String, int>{};

  for (var ch in a.split('')) {
    freq[ch] = (freq[ch] ?? 0) + 1;
  }

  for (var ch in b.split('')) {
    freq[ch] = (freq[ch] ?? 0) - 1;
    if (freq[ch]! < 0) return false;
  }

  return true;
}

// Sorting approach — simpler but slower
bool isAnagramSort(String s1, String s2) {
  var a = s1.toLowerCase().replaceAll(' ', '');
  var b = s2.toLowerCase().replaceAll(' ', '');

  if (a.length != b.length) return false;

  var aChars = a.split('')..sort();
  var bChars = b.split('')..sort();

  return listEquals(aChars, bChars);
}

void main() {
  print(isAnagram('listen', 'silent'));   // true
  print(isAnagram('hello', 'world'));    // false
  print(isAnagram('Dormitory', 'Dirty Room')); // true
}
📐 When to Use Each Approach The sorting approach is easier to write and read, making it suitable for interviews and quick scripts. The frequency-counting approach is O(n) and better for performance-critical code. In Flutter apps, you might use anagram detection for search functionality or text analysis features.

5. Matrix Transpose

Problem: Write a function that transposes a 2D matrix — rows become columns and columns become rows.

Explanation: Transposing a matrix swaps its rows and columns. For a matrix with dimensions m×n, the result is n×m. This operation is fundamental in linear algebra and appears in image processing, machine learning, and game development. The key insight is that element at position [i][j] moves to [j][i]. For square matrices, you can transpose in-place by swapping elements across the diagonal. For non-square matrices, you need to create a new matrix. Dart's collection-for syntax makes this especially concise and readable.

matrix_transpose.dart
List<List<int>> transpose(List<List<int>> matrix) {
  if (matrix.isEmpty) return [];
  var rows = matrix.length;
  var cols = matrix[0].length;

  return [
    for (var j = 0; j < cols; j++)
      [
        for (var i = 0; i < rows; i++)
          matrix[i][j],
      ],
  ];
}

void printMatrix(List<List<int>> m) {
  for (var row in m) {
    print(row.join('  '));
  }
}

void main() {
  var matrix = [
    [1, 2, 3],
    [4, 5, 6],
  ];

  print('Original:');
  printMatrix(matrix);
  // 1  2  3
  // 4  5  6

  var result = transpose(matrix);
  print('\nTransposed:');
  printMatrix(result);
  // 1  4
  // 2  5
  // 3  6
}
🧠 Extension Exercise Try writing a function that checks if a matrix is a palindromic matrix — it reads the same forwards and backwards both horizontally and vertically. This combines the transpose operation with the palindrome concept from Challenge 3.

7. FizzBuzz Variations

Problem: The classic FizzBuzz prints numbers 1 to 100, but replaces multiples of 3 with "Fizz", multiples of 5 with "Buzz", and multiples of both with "FizzBuzz". Solve this and three extended variations.

Explanation: FizzBuzz is the most common coding interview screening question because it tests the absolute basics: loops, conditionals, and modular arithmetic. Despite its simplicity, many candidates fail it. The key insight is checking the combined condition (divisible by both 3 and 5) before the individual conditions, since if you check divisibility by 3 first, numbers like 15 will print "Fizz" instead of "FizzBuzz". The extended variations below demonstrate how to generalize this pattern using maps and higher-order functions — skills that translate directly to building configurable UI components and business logic in Flutter.

fizzbuzz.dart
// Classic FizzBuzz
void fizzBuzz() {
  for (var i = 1; i <= 100; i++) {
    if (i % 15 == 0) {
      print('FizzBuzz');
    } else if (i % 3 == 0) {
      print('Fizz');
    } else if (i % 5 == 0) {
      print('Buzz');
    } else {
      print(i);
    }
  }
}

// Generalized FizzBuzz with a map
String fizzBuzzGeneral(int n, Map<int, String> rules) {
  var result = '';
  for (var entry in rules.entries) {
    if (n % entry.key == 0) {
      result += entry.value;
    }
  }
  return result.isEmpty ? '$n' : result;
}

// FizzBuzz using join and map
List<String> fizzBuzzFunctional(int n) {
  return [
    for (var i = 1; i <= n; i++)
      fizzBuzzGeneral(i, {3: 'Fizz', 5: 'Buzz'}),
  ];
}

void main() {
  // Classic output for 1-15
  for (var i = 1; i <= 15; i++) {
    print(fizzBuzzGeneral(i, {3: 'Fizz', 5: 'Buzz'}));
  }
  // 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz

  // Custom rules: divisible by 7 = "Foo", by 11 = "Bar"
  print(fizzBuzzGeneral(77, {7: 'Foo', 11: 'Bar'})); // FooBar
}
🚀 From FizzBuzz to Flutter The generalized version using a map is exactly how you would build a configurable validator in a Flutter form field. Instead of hardcoding rules, you pass a map of conditions to a function. This separation of logic from configuration is a core principle in clean architecture.

8. Word Frequency Counter

Problem: Write a function that takes a string of text and returns a map of each word to its frequency count. Handle case insensitivity and punctuation.

Explanation: Word frequency counting is a foundational task in natural language processing, search engines, and data analysis. The algorithm normalizes the text (lowercase, strip punctuation), splits it into words, and counts occurrences using a map. Dart's fold method provides a functional one-liner, while the imperative loop version is easier to debug. This problem also demonstrates the importance of edge cases: empty strings, multiple spaces, and special characters all need to be handled. In a Flutter app, this kind of analysis powers features like tag clouds, search suggestions, and content recommendations.

word_frequency.dart
Map<String, int> wordFrequency(String text) {
  var cleaned = text.toLowerCase().replaceAll(RegExp(r'[^\w\s]'), '');
  var words = cleaned.split(RegExp(r'\s+')).where((w) => w.isNotEmpty).toList();

  var freq = <String, int>{};
  for (var word in words) {
    freq[word] = (freq[word] ?? 0) + 1;
  }
  return freq;
}

// Functional one-liner using fold
Map<String, int> wordFrequencyFold(String text) {
  return text
      .toLowerCase()
      .replaceAll(RegExp(r'[^\w\s]'), '')
      .split(RegExp(r'\s+'))
      .where((w) => w.isNotEmpty)
      .fold<Map<String, int>>({}, (map, word) {
    map[word] = (map[word] ?? 0) + 1;
    return map;
  });
}

// Find the most frequent word
MapEntry<String, int>? mostFrequent(Map<String, int> freq) {
  if (freq.isEmpty) return null;
  return freq.entries.reduce(
    (a, b) => a.value >= b.value ? a : b,
  );
}

void main() {
  var text = 'The dart language is great. Dart is powerful and dart is fun!';
  var freq = wordFrequency(text);

  print(freq);
  // {the: 1, dart: 3, language: 1, is: 3, great: 1, powerful: 1, and: 1, fun: 1}

  var top = mostFrequent(freq);
  print('Most frequent: ${top?.key} (${top?.value} times)');
  // Most frequent: dart (3 times)

  // Sort by frequency descending
  var sorted = freq.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
  for (var e in sorted.take(5)) {
    print('  ${e.key}: ${e.value}');
  }
}
📐 Real-World Application In a Flutter blog or notes app, this function could power a word cloud widget, autocomplete suggestions, or content tagging. The fold approach is particularly useful when chaining multiple data transformations — a pattern you will use extensively with Streams and Future in Flutter.

Summary

These eight challenges cover the core logical thinking patterns every Dart developer should master:

  • Iteration vs Recurrence: Fibonacci and binary search both demonstrate when to use loops versus recursion, and how to think about time complexity.
  • Mathematical Reasoning: Prime checking and modular arithmetic (FizzBuzz) require understanding number properties and divisibility rules.
  • String Processing: Palindromes and anagrams show how to normalize, transform, and compare text data efficiently.
  • Data Structure Manipulation: Matrix transpose and word frequency counting demonstrate working with 2D lists and maps — skills essential for building data-driven Flutter widgets.
  • Algorithmic Thinking: Binary search teaches divide-and-conquer, the foundation for many advanced algorithms used in search, sorting, and graph traversal.
  • Generalization: The FizzBuzz variations show how to move from hardcoded logic to configurable, reusable functions — a key skill for building maintainable Flutter apps.

The ability to solve these kinds of problems is what separates developers who can use Flutter from those who can build with Flutter. When you understand the logic underneath, debugging, optimizing, and creating complex features becomes second nature. Practice these challenges regularly, and you will notice a significant improvement in your ability to think through problems before writing code.

🚀 Next Step Now that you can think through problems logically, learn how to organize your code with object-oriented programming in OOP: Classes & Objects.
K
Kesavaraja Murugesan
Flutter Developer & Educator

Flutter developer with 3 years of experience building production-grade mobile applications. Passionate about teaching clean architecture patterns and helping developers write maintainable, scalable code.

← Control Flow & Functions Next: Design Patterns →