You are in a switch statement. You add a new case because one particular screen needs different behavior from this same method. The other callers do not need it — just this one.
You commit it and move on.
But that if or that new case is a signal. Something is asking to be separated, and you just delayed the conversation.
Most people hear "Single Responsibility Principle" and think it means a class should do one thing.
That is not the SRP.
"Do one thing" is a rule for functions. A function should do one thing well. But a class is larger than a function. A class that only ever had one method would barely be worth calling a class.
SRP is about something different. It is about who demands a change — not how many things the class does.
A class should be responsible to one actor.
| Actor | Noun — A group of people with a shared reason to demand a change. Not a user. Not a role. A group of stakeholders who all want the same kind of thing from the code.
In a typical Flutter app, you have at least three actors:
Now look at the ProfileScreen from Lesson 0 again. Three different actors all have reason to reach into _ProfileScreenState. The backend team, because the API call lives there. The product owner, because the date formatting lives there. The UI team, because the widget tree lives there.
Three actors. One class.
That is an SRP violation.
The most reliable signal is the if that changes behavior for a specific caller.
String formatDate(DateTime date, {bool isAdminView = false}) {
if (isAdminView) {
// Admin wants ISO format
return date.toIso8601String();
}
// Everyone else wants this format
return '${date.day}/${date.month}/${date.year}';
}Two callers with different formatting needs inside the same method. Two actors. One method is now serving both. When the admin format changes, you open this file. When the user format changes, you also open this file. Those changes come from different people with different reasons.
SRP is begging you to split.
Go back to ProfileScreen. Ask: which actor owns each part?
// Before: one class, three actors mixed together
class _ProfileScreenState extends State<ProfileScreen> {
void initState() {
http.get(...) // backend team owns this
.then((res) {
formatDate(...) // product team owns this
setState(...) // UI team owns this
});
}
}Now pull each concern out to where it belongs.
// The backend team owns this
class UserRepository {
Future<User> fetchUser(String id) async {
final res = await http.get(Uri.parse('https://api.example.com/user/$id'));
return User.fromJson(jsonDecode(res.body));
}
}// The product team owns this
class UserPresenter {
String formatJoinDate(DateTime date) =>
'${date.day}/${date.month}/${date.year}';
}// The UI team owns this
class ProfileScreen extends StatelessWidget {
final User user;
final UserPresenter presenter;
const ProfileScreen({
super.key,
required this.user,
required this.presenter,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(user.name),
Text('Joined: ${presenter.formatJoinDate(user.joinedAt)}'),
],
);
}
}Three classes. Three actors. Each one has only one reason to change.
When the backend team renames joinedAt to createdAt, you open UserRepository. Only UserRepository. The presenter and screen do not move.
When product wants ISO date format, you open UserPresenter. Only UserPresenter.
When the designer hands you a new layout, you open ProfileScreen. Only ProfileScreen.
Each change has its own blast radius, and none of them overlap.
Before, any change meant opening the same file and hoping you did not disturb the other two concerns. Now, changes stay where they belong.
[ Backend team ] ──asks to change──► UserRepository
[ Product team ] ──asks to change──► UserPresenter
[ UI team ] ──asks to change──► ProfileScreenOne arrow per actor. No actor reaches across to another class.
A throwaway script. A one-screen prototype. A utility that only one person will ever call.
SRP costs files and indirection. It splits code across multiple places, which means more to navigate. Pay that cost only when more than one actor has a real stake in the code — when changes from different people are genuinely going to collide.
A date formatter used in one screen by one team does not need an actor analysis. The friction is not there yet.
Wait for the friction. Then apply the principle.
UserRepository with fetchUser, saveUser, and deleteUser is fine if all three are changed by the same team for the same reasons.| Concept | What it means | The smell that calls for it |
|---|---|---|
| Single Responsibility Principle | A class should be responsible to one actor — one group of people with a shared reason to demand a change | Multiple teams or concerns reaching into the same file for different reasons |
| Actor | A group of stakeholders who want the same kind of change | When you find an if that changes behavior for one caller and not others |
| Blast radius | The set of files a single change touches | When one change from one person forces edits across unrelated parts of the codebase |
The two ideas every SOLID principle is trying to optimize. Learn them first, and the rest will click.
Start Previous DayAdd new behavior by adding new code, not by editing the code that already works.
Start Next Day