Correctness First, Optimization Second: My Flutter Interview Lesson

A tale of technical excellence, missed expectations, and the lessons we learn when clarity meets constraints.

Stopwatch Running and devtools analysing the my device performance
Stopwatch Running and devtools analysis

A tale of technical excellence, missed expectations, and the lessons we learn when clarity meets constraints.

The 24-Hour Festival Challenge

It was a festival day. I should have been celebrating with family, sharing sweets, and enjoying the moment. Instead, I was staring at my screen, reading assignment requirements that needed to be completed in 24 hours.

The assignment seemed straightforward: build a Flutter stopwatch app with Start/Stop and Lap functionalities. Simple, right? But simplicity can be deceiving, especially when you’re racing against time during what should be a day of celebration.

The Exact Assignment Requirements

Here’s what they asked for, word for word:

The Assignment:

Please follow the instructions below: 
1. Create a file named stopwatch_screen.dart. 
2. Implement a Stopwatch with Start/Stop and Lap functionalities. 
(Example video ref - drive.google.com/file/d/1kEvUKgqqwVTIRtjox-F6qxEua-nr1MaL/view?usp=sharing) 
3. Do not use any external packages - not even for state management. 
4. Once completed, record the screen of the app running on Android or iOS. 
5. Keep the release APK ready. 
6. Upload the code to a private GitHub repository and grant access to shashikant.durge@flicktv.in 
7. After completing the assignment, fill out this form: forms.gle/FLFuvEnmdNE3r4CFA 
8. Time limit: 36 hours

The UI Details:

1. the stopwatch timer 
2. Two Circular buttons, Lap and Start/Stop stopwatch 
3. List of Laps in desc order of time. Latest First. 
Start-> timer started-> can create laps-> on each lap created update the list of laps. 
On Stop clicked -> we can reset the timer and the list. 
Start / Stop and same button with 2 options. 
And the first lap will have red color, 2nd lap have green and third one will have have white. 
And repeat this pattern based on index.

Notice what’s not mentioned:

  • SOLID principles
  • Specific accuracy requirements
  • Whether to use Stopwatch class or Timer
  • Performance benchmarks
  • Code architecture expectations

This ambiguity would later become the centerpiece of our story.

The Technical Journey Begins

As a developer who loves optimization challenges, I immediately saw an opportunity. A simple stopwatch? Sure. But what if I could make it exceptionally performant?

My GitHub Submission:

GitHub - Satendra9984/f_tv_stopwatch_assignment
Contribute to Satendra9984/f_tv_stopwatch_assignment development by creating an account on GitHub.

The Initial Approach

I started with the most straightforward approach. I used Timer.periodic() — a Dart class that lets you run code repeatedly at set intervals. Every 10 milliseconds (that’s 100 times per second), I updated the timer display.

Here’s what my initial code looked like:

class _StopwatchScreenState extends State<StopwatchScreen> { 
  bool _isRunning = false; 
  Duration _totalElapsedTime = Duration.zero; 
  Duration _currentLapTime = Duration.zero; 
  final List<Duration> _laps = []; 
  Timer? _mainTimer; 
  Timer? _lapTimer; 
}

I created two timers:

  • One for tracking total elapsed time
  • One for tracking the current lap time

This allowed me to reset lap times independently while keeping the main timer running. It seemed like a clean solution.

The Performance Problem

The stopwatch worked perfectly. All functionality was there. But then I measured the performance using Flutter DevTools, and I discovered something alarming.

100 widget rebuilds per second causing frame rate drops.

Let me explain what that means in simple terms: Flutter apps are made of “widgets” — basically, the building blocks of your screen. Every time something changes (like the timer updating), Flutter can rebuild these widgets to show the new information.

In my initial implementation, every 10 milliseconds (100 times per second), Flutter was rebuilding the entire screen:

  • Every button
  • Every lap item in the list
  • Every text display
  • Every color calculation
  • Everything, even if nothing had actually changed

This is called a “widget rebuild.” It’s like repainting your entire house every second, even if only one room’s clock changed.

Why this matters:

  • If you have 50 laps recorded, all 50 lap items rebuild 100 times per second
  • Buttons rebuild unnecessarily
  • High CPU usage
  • Potential battery drain
  • The actual frame rate I measured was only 30–38 FPS (poor performance)
  • The app works, but it’s inefficient

To put this in perspective: Smooth mobile apps should run at 60 FPS. My app was struggling at 30–38 FPS because of all these unnecessary rebuilds happening 100 times every second.

The Optimization Journey

This is where the assignment became interesting for me. I wasn’t just building a stopwatch; I was solving a performance puzzle.

Step 1: Understanding the Problem

I identified the bottleneck: setState() was causing full screen rebuilds. Every time I called setState() to update the timer, the entire widget tree rebuilt.

Here’s what the problematic code looked like:

_lapTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { 
  setState(() {  // ❌ This rebuilds the ENTIRE screen! 
    _currentLapTime = newTime; 
  }); 
});

Step 2: Finding the Solution

I researched Flutter’s built-in tools (remember, no external packages allowed) and found ValueNotifier and ValueListenableBuilder.

Think of it this way:

  • Old way (setState): “Hey Flutter, something changed! Rebuild EVERYTHING!”
  • New way (ValueNotifier): “Hey Flutter, this specific timer value changed. Only rebuild the timer display.”

Step 3: Implementing Selective Rebuilding

I replaced setState() with ValueNotifier:

// Instead of regular variables, I used ValueNotifier 
final ValueNotifier<Duration> _totalElapsedTime = ValueNotifier(Duration.zero); 
final ValueNotifier<Duration> _currentLapTime = ValueNotifier(Duration.zero);

Then, instead of rebuilding everything, I only rebuilt specific parts using ValueListenableBuilder:

// Only the timer text rebuilds, not the entire screen 
ValueListenableBuilder<Duration>( 
  valueListenable: _totalElapsedTime, 
  builder: (context, duration, child) { 
    return Text(_formatDuration(duration), ...); 
  }, 
)

Step 4: Smart List Management

The laps list was the trickiest part. I didn’t want every lap rebuilding constantly. So I made it intelligent:

itemBuilder: (context, index) { 
  final isActiveLap = index == 0 && _isRunning; 
   
  if (isActiveLap) { 
    // Only the active lap rebuilds (the one currently timing) 
    return ValueListenableBuilder<Duration>( 
      valueListenable: _currentLapTime, 
      builder: (context, currentLapDuration, child) { 
        return _buildLapItem(currentLapDuration, index); 
      }, 
    ); 
  } else { 
    // Completed laps never rebuild - they're frozen in time 
    return _buildLapItem(lapTime, index); 
  } 
}

The Result: Stable 60 FPS performance.

Let me break down what this means:

  • Before: 100 widget rebuilds per second causing frame rate drops to 30–38 FPS
  • After: Only 2–3 specific widgets rebuild, achieving smooth 60 FPS
  • Impact: The app runs smoothly even with 100+ laps recorded, maintaining consistent 60 FPS

The Two-Timer Architecture

One of my key architectural decisions was implementing a two-timer system. Why two timers?

The Challenge:

  • The main timer should never reset (it tracks total elapsed time)
  • Lap timers need to reset independently each time you add a lap
  • Both need to update simultaneously

My Solution:

void _startMainTimer() { 
  final DateTime startTime = DateTime.now(); 
  final Duration initialTime = _totalElapsedTime.value; 
   
  _mainTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { 
    _totalElapsedTime.value = initialTime + DateTime.now().difference(startTime); 
  }); 
} 
 
void _startLapTimer() { 
  final DateTime startTime = DateTime.now(); 
  final Duration initialTime = _currentLapTime.value; 
   
  _lapTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { 
    _currentLapTime.value = initialTime + DateTime.now().difference(startTime); 
  }); 
}

When you add a lap:

  1. The current lap time gets saved to the list
  2. The lap timer resets to zero
  3. A new lap timer starts.
  4. The main timer continues uninterrupted

This solved the complex requirement of tracking both total and lap times independently.

The Bug I Found and Fixed

After implementing the optimizations, I discovered a bug. When you stopped the stopwatch, the topmost lap would show an incorrect time — it would display the time from when the lap started, not the time when you stopped.

The Problem:

// When stopped, this condition becomes false 
final isActiveLap = index == 0 && _isRunning; 
 
if (isActiveLap) { 
  // Shows live current lap time when running 
  return ValueListenableBuilder<Duration>(...); 
} else { 
  // Shows frozen time - but it was outdated! 
  return _buildLapItem(lapTime, index); 
}

The Fix:

I updated the code to save the current lap time before stopping:

void _startStopStopwatch() { 
  if (_isRunning) { 
    _mainTimer?.cancel(); 
    _lapTimer?.cancel(); 
     
    // Save current lap time before stopping 
    if (_laps.isNotEmpty) { 
      setState(() { 
        _laps[0] = _currentLapTime.value;  // Freeze at current time 
        _isRunning = false; 
      }); 
    } 
  } 
}

This ensured that when you stop, the lap shows the correct frozen time.

The Submission

I submitted:

  • ✅ Fully functional stopwatch app
  • ✅ Performance optimized from 30–38 FPS (caused by 100 rebuilds/sec) to stable 60 FPS
  • ✅ Comprehensive documentation explaining every decision
  • ✅ A detailed article showing my optimization journey
  • ✅ Screen recording demonstrating smooth performance
  • ✅ Release APK ready for testing

I was confident. Not because the app was complex, but because I had gone above and beyond. I hadn’t just built a stopwatch; I had optimized it, documented it, explained my thinking, and fixed bugs I discovered.

The Feedback That Changed Everything

The response came back: We’ve decided not to move forward at this stage.

The feedback mentioned:

  • SOLID principles (which weren’t in the requirements)
  • Performance-oriented development (which I had focused on extensively)
  • Accuracy of implementation

I was confused. I had optimized the performance to 60 FPS. I had documented my approach. What was missing?

I responded professionally, explaining my work. The evaluator then clarified: ”I recommend using a Stopwatch instead of relying on Timer.”

That was it. The primary feedback was about using Dart’s Stopwatch class instead of Timer.periodic().

Understanding the Difference: Timer vs Stopwatch

Let me explain why this matters, in simple terms:

Timer.periodic() is like setting an alarm clock to ring every 10 seconds. You use it for recurring tasks — checking something periodically, running cleanup routines, etc.

Timer class - dart:async library - Dart API
API docs for the Timer class from the dart:async library, for the Dart programming language.

Stopwatch is like a stopwatch you’d use at a track meet. It’s specifically designed to measure elapsed time. It has built-in methods like start(), stop(), reset(), and elapsed that give you the time directly.

Stopwatch class
API docs for the Stopwatch class from the dart:core library, for the Dart programming language.

Why Stopwatch is better for a stopwatch app:

  • It’s semantically correct (the name matches what you’re building)
  • It handles pause/resume natively
  • It’s more accurate for time measurements
  • Less manual calculation needed
  • It’s what Dart’s standard library provides for this exact purpose

Why I used Timer instead:

  • It works functionally
  • I was focused on performance optimization
  • The assignment didn’t specify which tool to use
  • Under time pressure, I made a pragmatic choice

It’s like using a hammer when you have a screwdriver in your toolbox — it works, but it’s not the right tool.

The Realization

Let me be clear: the evaluator wasn’t wrong. Dart’s Stopwatch class is the semantically correct choice for a stopwatch application. It’s designed for this exact purpose. Using Timer.periodic() was technically a suboptimal choice.

But here’s where the story gets interesting: I had built a highly optimized, performant solution using a slightly incorrect tool.

It’s like building a perfectly tuned race car, but using regular fuel instead of premium. The car runs great, but the fuel choice was wrong.

The Mistakes (On Both Sides)

My Mistakes:

1. Tool Selection: I used Timer instead of Stopwatch. While functional, it wasn’t semantically correct. Under time pressure during a festival day, I made a pragmatic choice instead of researching Dart’s standard library first.

2. Assumption: I assumed optimization would outweigh tool choice. I was wrong. Correctness first, optimization second.

3. Timing: Taking the assignment during a festival day, under time pressure, affected my decision-making. I should have requested a deadline extension or better managed my time.

Their Mistakes:

1. Unclear Requirements: The assignment didn’t specify SOLID principles, specific accuracy expectations, or preferred tools. Asking for “performance-oriented development” without being specific led to mismatched expectations.

2. Overlooking Effort: My extensive optimization work and documentation weren’t acknowledged. I had achieved 60 FPS performance, documented every decision, and created a comprehensive article. None of this was mentioned.

3. Generic Feedback: Initial feedback was vague (“improve accuracy” isn’t actionable). Specific issues were only revealed after my clarification request.

The Learning

What I Learned:

1. Semantics Matter More Than I Thought

Using the right tool for the job isn’t just about functionality — it’s about demonstrating understanding of the platform. Stopwatch vs Timer isn’t just about what works; it’s about what shows you understand Dart’s standard library.

When you’re in an interview situation, using the right tool sends a message: “I know the platform. I understand the standard library. I choose the right tool for the job.”

2. Optimization Alone Isn’t Enough

I optimized the hell out of a solution that used the wrong tool. It’s like polishing the wrong thing perfectly. The lesson: correctness first, optimization second.

You can’t optimize your way out of using the wrong tool. The most optimized wrong solution is still wrong.

3. Requirements Are Everything

Ambiguous requirements lead to mismatched expectations. When I see unclear assignments now, I ask questions. Better to clarify upfront than misunderstand expectations.

A lesson I’ve learned: If requirements aren’t clear, ask. It’s better to seem cautious than to deliver the wrong thing.

4. Time Pressure Affects Decisions

Working during a festival, under 24-hour pressure, I made pragmatic choices. Sometimes, “good enough” isn’t actually good enough. The tool choice was pragmatic but wrong.

Time pressure can lead to shortcut decisions. But in technical interviews, shortcuts can cost you.

5. Research Standard Libraries First

Before architecting a solution, research what the platform provides. Dart has Stopwatch for exactly this purpose. I should have checked first.

What They Might Learn:

1. Clear Requirements Prevent Disappointment

If SOLID principles were important, say so. If specific accuracy was required, specify it. If Stopwatch was preferred, mention it. Vague requirements lead to wasted effort.

Clear requirements help candidates succeed. Unclear requirements help nobody.

2. Acknowledge Good Work

My optimization work was genuine. My documentation was thorough. My problem-solving approach was solid. Not acknowledging this feels like missing the forest for the trees.

Even when rejecting a candidate, acknowledging their strengths helps them grow and maintains goodwill.

3. Specific Feedback Helps

Generic feedback doesn’t help candidates improve. “Use Stopwatch instead of Timer” is actionable. “Improve accuracy” is not.

Actionable feedback helps candidates improve. Generic feedback frustrates everyone.

The Luck Factor

Bad Luck:

  • Assignment during a festival day (24-hour deadline instead of typical 48–72 hours)
  • Unclear requirements (no mention of SOLID, specific accuracy, or tool preferences)
  • Evaluator focusing on tool choice over optimization work
  • Time pressure affecting decision-making

Good Luck:

  • I learned valuable lessons about tool selection
  • I improved my understanding of Dart’s standard library
  • I got to experience a real-world scenario of optimization vs correctness
  • I’m writing this story, sharing insights with others who might face similar situations

The Balanced Perspective

Here’s the truth: Both sides made mistakes.

I should have used Stopwatch. It’s the right tool. My optimization work was excellent, but it was applied to the wrong foundation.

They should have been clearer about requirements. My work deserved acknowledgment, even if it wasn’t what they were looking for.

The real issue: A mismatch between what I thought was being evaluated (performance optimization, problem-solving, documentation) and what was actually being evaluated (tool selection, correctness, adherence to unstated principles).

What Would I Do Differently?

If I had to do it again:

  1. Research first— Check Dart’s standard library for appropriate tools before starting
  2. Ask questions— Clarify unclear requirements before starting
  3. Prioritize correctness — Use the right tool, then optimize
  4. Manage timing — Request deadline extension if needed, especially during festivals
  5. Still optimize, but correctly — Apply my optimization skills to the right foundation

What I wouldn’t change:

  • My focus on performance optimization
  • My documentation and explanation of decisions
  • My problem-solving approach
  • My professional response to feedback

The Takeaway for Other Developers

If you’re doing interview assignments:

  1. Research standard libraries first*— Before coding, check what tools the platform provides
  2. Ask questions — Unclear requirements are red flags. Clarify them.
  3. Use the right tools — Semantic correctness matters as much as functionality
  4. Optimize, but correctly — Don’t optimize the wrong solution
  5. Document, but be clear — Explain why you made choices, not just what you did
  6. Manage expectations — Understand what’s being evaluated

If you’re evaluating assignments:

  1. Be specific — Clear requirements prevent wasted effort
  2. Acknowledge good work— Even if it’s not what you wanted, recognize the effort
  3. Give actionable feedback — Specific, technical feedback helps candidates improve
  4. Balance evaluation — Consider both correctness and problem-solving ability

The Silver Lining

This experience taught me more than a successful interview would have. I learned:

  • The importance of semantic correctness
  • How to balance optimization with accuracy
  • That documentation matters, but tool choice matters more
  • That clear requirements prevent disappointment
  • That feedback, even negative feedback, is a gift when it’s specific

I’m a better developer today because of this “failure.” The optimizations I did were real. The skills I demonstrated were genuine. The lesson I learned was invaluable.

The Real Achievement:

Even though I used the wrong tool (Timer instead of Stopwatch), the optimization work was genuine and demonstrated real Flutter expertise. The architecture was solid. The performance gains were measurable and significant.

Using Stopwatch would have been the right choice. But my optimization work was still excellent.

Conclusion

In 24 hours, during a festival day, I built an optimized stopwatch that improved from 30–38 FPS to stable 60 FPS performance. I documented every decision. I explained my architecture. I solved complex timing problems. I fixed bugs I discovered.

But I used Timer instead of Stopwatch.

Sometimes, the most valuable lessons come from the interviews we don’t pass. This wasn’t just a technical lesson about Dart’s standard library; it was a lesson about the balance between optimization and correctness, between doing things well and doing the right things well.

The stopwatch works perfectly. It’s optimized beautifully. It’s just built on the wrong foundation.

And you know what? That’s okay. Because now I know better. And knowing better is half the battle.

To anyone facing similar interview challenges: Your effort matters. Your problem-solving skills matter. Your ability to learn and adapt matters. Sometimes, things don’t work out not because you’re not good enough, but because expectations didn’t align.

Keep building. Keep optimizing. But always start with the right tool.

— -

P.S.— I’ve since rebuilt the stopwatch using Dart’s Stopwatch class, applying all the same optimizations. It’s still running at 60 FPS, but now it’s built correctly. Sometimes the second attempt is the one that teaches you the most.

The lesson remains: Correctness first, optimization second. But don’t stop optimizing — just make sure you’re optimizing the right thing.

Useful Resources:

Timer class - dart:async library - Dart API
API docs for the Timer class from the dart:async library, for the Dart programming language.
Stopwatch class
API docs for the Stopwatch class from the dart:core library, for the Dart programming language.