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.
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.
| Challenge | Core Concept | Difficulty |
|---|---|---|
| Fibonacci Sequence | Iteration, recursion | Easy |
| Prime Number Checker | Loop control, math | Easy |
| Palindrome Detector | String manipulation | Easy |
| Anagram Checker | Sorting, maps | Medium |
| Matrix Transpose | 2D lists, nested loops | Medium |
| Binary Search | Divide and conquer | Medium |
| FizzBuzz Variations | Modular arithmetic | Easy |
| Word Frequency Counter | Maps, strings | Medium |
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.
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:
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
}
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.
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]
}
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.
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
}
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.
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
}
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.
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
}
6. Binary Search Implementation
Problem: Implement the binary search algorithm to find a target value in a sorted list. Return the index if found, or -1 if not found.
Explanation: Binary search is one of the most important algorithms in computer science. Instead of scanning every element (O(n)), it repeatedly halves the search space by comparing the target to the middle element. If the target is smaller, search the left half; if larger, search the right half. This logarithmic time complexity (O(log n)) makes it indispensable for large datasets. The prerequisite is that the list must be sorted. Binary search is the foundation for more advanced data structures like binary search trees and is used extensively in database indexing and filesystem lookups.
int binarySearch(List<int> sortedList, int target) {
var low = 0;
var high = sortedList.length - 1;
while (low <= high) {
var mid = low + (high - low) ~/ 2;
if (sortedList[mid] == target) return mid;
if (sortedList[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
// Recursive version
int binarySearchRec(List<int> list, int target, [int? low, int? high]) {
low ??= 0;
high ??= list.length - 1;
if (low > high) return -1;
var mid = low + (high - low) ~/ 2;
if (list[mid] == target) return mid;
if (list[mid] < target) {
return binarySearchRec(list, target, mid + 1, high);
} else {
return binarySearchRec(list, target, low, mid - 1);
}
}
void main() {
var data = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91];
print(binarySearch(data, 23)); // 5
print(binarySearch(data, 50)); // -1
print(binarySearchRec(data, 56)); // 7
}
low + (high - low) ~/ 2 instead of (low + high) ~/ 2. The latter can overflow for very large integers. This defensive pattern is used in production code across Google, Apple, and every major tech company. Always prefer the overflow-safe version.
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.
// 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
}
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.
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}');
}
}
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.