DEV Community

Cover image for Building DevFeed — Part 1: MVVM Architecture Before Writing a Single Screen
Yadnesh Teli
Yadnesh Teli

Posted on AI-assisted

Building DevFeed — Part 1: MVVM Architecture Before Writing a Single Screen

Series 2 of 2 — Part 1 of 4

Building DevFeed — Part 1: MVVM Architecture Before Writing a Single Screen

Series: Building DevFeed — A Proper Flutter MVVM App from Day One

When I started DevFeed, I had just finished the painful MVVM refactor of the shopping app. I knew what proper architecture looked like. I knew the difference between a repository and a ViewModel. I had felt the cost of not having that separation.

So this time I set up the entire architecture before I wrote a single screen.

DevFeed is a Hacker News reader app built as a Flutter MVVM practice project during my internship. This series documents how I built it — from a properly structured first commit all the way to shipping v1.0.1 with a GitHub Actions release workflow.

Planning the architecture first

Before opening the first Dart file, I mapped out the layers:

  • Model: ArticleModel to represent a Hacker News story — id, title, url, score, author, and comment count.
  • Repository: HackerNewsRepository responsible for fetching stories from the API. Returns a list of ArticleModel.
  • ViewModel: FeedViewModel using Riverpod AsyncNotifier. Calls the repository, exposes AsyncValue state to the UI.
  • View: FeedScreen that watches the ViewModel. Renders loading, error, and data states. No API knowledge.

This is the same structure I eventually reached in the shopping app. The difference in development experience because of starting with it is the main story of this series.

The first commit: full scaffold

The first commit was a full scaffold — all 35 files. Models, repositories, ViewModels, screens, widgets, routing, theming, and a basic test file. Nothing was fully implemented but the skeleton of every layer was in place.

This approach felt strange at first. You write a lot of boilerplate before you can see anything on screen. But it paid off immediately when I started filling in implementations — because I always knew exactly which file to open and what it should contain.

Riverpod AsyncNotifier setup

I used AsyncNotifier for the feed ViewModel. AsyncNotifier is the right choice when your state is the result of an async operation — it handles loading, error, and data states out of the box.

class FeedViewModel extends AsyncNotifier<List<ArticleModel>> {
  @override
  Future<List<ArticleModel>> build() async {
    return ref.read(hackerNewsRepositoryProvider).fetchTopStories();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(
      () => ref.read(hackerNewsRepositoryProvider).fetchTopStories(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

go_router for navigation

I used go_router from day one. In the shopping app I had used Navigator.push directly, which scattered navigation logic across screens. go_router centralises routing in a single config file and supports declarative navigation, deep links, and route guards. Adding a new route later was a two-line change instead of a hunt through multiple screens.

What starting right feels like

The most noticeable difference compared to the shopping app was the absence of confusion. When I needed to add a feature, I knew where it should go. When I needed to fix a bug, I knew which layer was responsible.

The shopping app taught me what MVVM is. DevFeed taught me what it feels like to work in a well-structured codebase. You need both lessons.

Next: Part 2 — Building the Feed: Hacker News API, AsyncNotifier, and Parallel Fetching

Top comments (1)

Collapse
 
saqrelfirgany profile image
Ahmed ElFirgany •

Scaffolding every layer first is the step most tutorials skip.

One detail in refresh(). Setting state to AsyncLoading wipes the list. So pull to refresh flashes a spinner over stories the user was reading. AsyncLoading().copyWithPrevious(state) keeps the old list visible while it loads.

The Hacker News API returns IDs first, then one call per story. How many requests do you fire in parallel in part 2?