One of the first decisions you make on a Flutter project is how to organize your files. Get it right and the codebase stays navigable as it grows. Get it wrong and even small changes become slow and risky because nothing has a clear home.
Movie X is an open-source Flutter app built for discovering movies. Beyond what it does, it serves as a practical reference for how to lay out a Flutter project in a way that scales well and stays readable over time.
This article walks through the folder structure, explains the role of each directory, and pulls out the patterns worth carrying into your own projects.
What this covers:
The full directory structure of Movie X
The purpose and conventions of each folder
Code examples from the real codebase
What makes this structure worth following
The Full Directory Structure
lib/
βββ assets/
β βββ icons/
β βββ images/
βββ components/
β βββ app_bottom_nav.dart
β βββ movie_card.dart
β βββ star_rating.dart
β βββ ...
βββ constants/
β βββ constants.dart
β βββ strings.dart
βββ models/
β βββ genre.dart
β βββ movie.dart
βββ screens/
β βββ home_screen.dart
β βββ detail_screen.dart
β βββ search_screen.dart
β βββ ...
βββ services/
β βββ api_service.dart
β βββ movie_service.dart
βββ styles/
β βββ text_styles.dart
βββ theme/
β βββ app_theme.dart
βββ utils/
β βββ string_utils.dart
βββ main.dartEvery folder has a single, clear responsibility. There is no ambiguity about where a new file belongs, which makes onboarding faster and keeps pull request diffs focused.
Breaking Down Each Folder
assets/ β Static Resources
Images and icons live here, separated from application code. Referencing them is straightforward:
Image.asset('lib/assets/images/tmdb_blue_square.png');Keeping all static files in one place makes it easy to audit what assets the app ships with and swap them out without touching component code.
components/ β Reusable UI Elements
Widgets that appear in more than one screen belong in components/. The key rule is that these widgets carry no business logic β they receive data and render it, nothing more.
class StarRating extends StatelessWidget {
final double rating;
const StarRating({required this.rating});
@override
Widget build(BuildContext context) {
// Rendering logic only
}
}This separation means you can update the visual appearance of a MovieCard without touching any screen or service file.
constants/ β Shared Values
Strings, colors, and other values that appear across the app are defined once here and referenced everywhere else:
class AppStrings {
static const String home = 'Home';
static const String search = 'Search';
}The benefit is simple: when a label changes, you update it in one file rather than hunting through the codebase.
models/ β Data Structures
Data models define the shape of the objects your app works with. In Movie X, these map directly to the API response from TMDB:
class Movie {
final int id;
final String title;
final String posterPath;
Movie({required this.id, required this.title, required this.posterPath});
}Well-defined models make it easier to reason about data flow, catch type errors early, and write reliable fromJson and toJson methods.
screens/ β Page-Level Views
Each screen corresponds to a distinct page in the app. Screens are responsible for layout and navigation, not for fetching data or applying business logic directly:
class _HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: screenOptions[selectedIndex],
bottomNavigationBar: AppBottomNav(...),
);
}
}Keeping screens thin makes them easier to test and easier to hand off to another developer who can understand the page layout without needing to know how the data got there.
services/ β API and Business Logic
Network calls and data transformations live in services/. Nothing in a screen or component should talk to an API directly:
class MovieService {
final ApiService _api = ApiService();
Future<List<Movie>> getLatestMovies() async {
final jsonBody = await _api.get('/trending/movie/day');
final List results = jsonBody['results'];
return results.map((json) => Movie.fromJson(json)).toList();
}
}Isolating this layer means you can swap out the API client, mock responses in tests, or change an endpoint without touching a single widget.
styles/ β Text and Visual Rules
Text styles are defined once in styles/ and referenced throughout the app:
class AppTextStyle {
static final TextStyle titleLarge = GoogleFonts.poppins(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.grey[100],
);
}Without this, font sizes and weights tend to drift across screens over time as different developers make slightly different choices.
theme/ β Global Theme Configuration
The theme/ folder wires everything together into a single ThemeData object that the app applies at startup:
ThemeData defaultAppTheme = ThemeData(
brightness: Brightness.dark,
textTheme: TextTheme(
titleLarge: AppTextStyle.titleLarge,
),
colorScheme: ColorScheme.dark(
surface: Colors.grey.shade900,
primary: Colors.amber,
),
);Centralizing the theme means a rebrand or dark mode toggle is a one-file change rather than a project-wide search and replace.
utils/ β Helper Functions
Small, reusable functions that do not belong to any specific feature live in utils/:
String truncateTo({required String text, int to = 18}) {
return text.length > to ? '${text.substring(0, to)}...' : text;
}Utility functions earn their place by being genuinely general. If a function is only used in one screen, it belongs in that screen file until it is needed elsewhere.
main.dart β Entry Point
main.dart stays minimal. It initializes the app, applies the theme, and sets up top-level routing:
void main() {
runApp(const MovieX());
}If main.dart is growing, that is usually a sign that setup logic needs to be moved into its own initialization layer.
Why This Structure Works
The folders in Movie X map directly to the layers of a typical Flutter application: data (models, services), presentation (screens, components), and configuration (theme, styles, constants). Each layer has clear boundaries, which means changes in one layer rarely ripple into another.
This also makes onboarding straightforward. A new developer can open the project, read the folder names, and understand where to find things without needing a guided tour.
The structure scales, too. As the app grows, new screens go in screens/, new API integrations go in services/, and new shared widgets go in components/. The organization does not need to change as the project grows, it just fills in.
Key Takeaways
Organizing by responsibility (data, UI, config) keeps each layer easy to change independently.
Reusable widgets in
components/should hold no business logic.Centralizing strings, styles, and theme settings in dedicated folders prevents inconsistency as the app grows.
Keeping screens thin and services isolated makes both easier to test.
A consistent structure reduces onboarding time and makes code reviews faster.
Conclusion
A thoughtful folder structure is not overhead. It is the foundation that keeps a project maintainable once it grows past a handful of screens.
Movie X is worth studying not because it is complex, but because it is organized clearly enough that the reasoning behind each decision is visible. The full source is on GitHub if you want to explore it directly.
Using a different structure in your Flutter projects? Share your approach in the comments.




