{"id":74735,"date":"2025-09-24T05:59:35","date_gmt":"2025-09-24T00:29:35","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=74735"},"modified":"2025-11-19T11:19:40","modified_gmt":"2025-11-19T05:49:40","slug":"flutter-state-management-with-viewmodel-cubit","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/flutter-state-management-with-viewmodel-cubit\/","title":{"rendered":"Flutter State Management with ViewModel + Cubit"},"content":{"rendered":"<h1>Introduction<\/h1>\n<p>State management is one of the most debated topics in Flutter. Some engineers like Bloc, others prefer Riverpod. But after working on large-scale Flutter apps with dozens of screens and business rules, one thing becomes obvious:<br \/>\nWhat matters most is not which library you pick, but how you organise responsibilities between UI, business logic, and state.<br \/>\nIn this article, I\u2019ll walk you through a ViewModel-driven architecture using Cubit.<\/p>\n<h1><strong>Why ViewModel?<\/strong><\/h1>\n<p>In most projects, a single Cubit or Bloc class ends up doing everything:<\/p>\n<ul>\n<li>Fetching and transforming data<\/li>\n<li>Validating inputs<\/li>\n<li>Managing UI state<\/li>\n<li>Emitting states<\/li>\n<\/ul>\n<p>As your app grows, this approach becomes hard to scale, test, or reason about. Your Cubit becomes a God class, doing too much, too often.<\/p>\n<h1><strong>Solution: ViewModel-Driven Cubit Architecture<\/strong><\/h1>\n<p>To solve this, we can introduce a ViewModel layer<\/p>\n<p><strong>What is a ViewModel?<\/strong><br \/>\nA ViewModel is a boundary object that manages the state and logic for one specific UI component.<\/p>\n<p>For example:<br \/>\n<strong>TeacherViewModel<\/strong> handles teacher-related logic.<br \/>\n<strong>StudentViewModel<\/strong> handles student-related logic.<br \/>\n<strong>MainViewModel<\/strong> interacts with both ViewModels and connects with the Cubit.<\/p>\n<p>Each ViewModel has two main responsibilities:<br \/>\n<strong>refresh(object) \u2192<\/strong> update internal fields with fresh data<br \/>\n<strong>build() \u2192<\/strong> return a UI-ready immutable state after necessary logic is performed.<br \/>\nNote:- The view model can contain other logic methods as well.<\/p>\n<h1><strong>How It Fits with Cubit:-<\/strong><\/h1>\n<ul>\n<li><strong>Instead of having Cubit<\/strong> directly manipulate every field, the flow is:<\/li>\n<li><strong>API\/UI<\/strong> provides objects\/data \u00a0(Teacher, Student).<\/li>\n<li><strong>Cubit calls MainViewModel.refresh(&#8230;),<\/strong> passing those objects\/data.<\/li>\n<li><strong>MainViewModel<\/strong> delegates to child ViewModels.<\/li>\n<li>Cubit calls <strong>MainViewModel.build(), <\/strong>and it calls other view models&#8217; build method internally and emits the resulting immutable MainState.<\/li>\n<\/ul>\n<p style=\"padding-left: 40px;\"><em>MainState build() =&gt; MainState(<\/em><br \/>\n<em>\u00a0 \u00a0teacher: _teacherVM.build(),<\/em><br \/>\n<em>\u00a0 \u00a0student: _studentVM.build(),<\/em><br \/>\n<em>);<\/em><br \/>\n<strong><br \/>\n<\/strong>This keeps a <strong>single entry point<\/strong> (MainViewModel) between Cubit and the business logic.<\/p>\n<p><strong>By splitting logic into ViewModels: &#8211;<\/strong><\/p>\n<ul>\n<li>Cubits become more readable and manageable.<\/li>\n<li>Each ViewModel can be unit-tested in isolation.<\/li>\n<li>Teams can scale, one dev works on TeacherViewModel, another on StudentViewModel, without stepping on each other.<\/li>\n<\/ul>\n<p><strong>Below is the code example:-<\/strong><\/p>\n<p><strong>UI States:-<\/strong><br \/>\nState will contain UI variables.<\/p>\n<blockquote>\n<p style=\"text-align: left;\">@freezed<br \/>\nclass <strong>TeacherState<\/strong> with _$TeacherState {<br \/>\nconst factory TeacherState({<br \/>\nrequired String name,<br \/>\nrequired String subject,<br \/>\nrequired int experience,<br \/>\n}) = _TeacherState;<\/p>\n<p style=\"text-align: left;\">factory TeacherState.empty() =&gt;<br \/>\nconst TeacherState(name: &#8221;, subject: &#8221;, experience: 0);<br \/>\n}<\/p>\n<p>@freezed<br \/>\nclass <strong>StudentState<\/strong> with _$StudentState {<br \/>\nconst factory StudentState({<br \/>\nrequired String name,<br \/>\nrequired int grade,<br \/>\nrequired int age,<br \/>\n}) = _StudentState;<\/p>\n<p>factory StudentState.empty() =&gt;<br \/>\nconst StudentState(name: &#8221;, grade: 0, age: 0);<br \/>\n}<\/p>\n<p>@freezed<br \/>\nclass <strong>MainState<\/strong> with _$MainState {<br \/>\nconst factory MainState({<br \/>\nrequired TeacherState teacher,<br \/>\nrequired StudentState student,<br \/>\n}) = _MainState;<\/p>\n<p>factory MainState.empty() =&gt; MainState(<br \/>\nteacher: TeacherState.empty(),<br \/>\nstudent: StudentState.empty(),<br \/>\n);<br \/>\n}<\/p><\/blockquote>\n<h1><strong>ViewModels<\/strong><\/h1>\n<p>Each ViewModel mutates private fields and exposes a fresh state from build().<\/p>\n<blockquote><p>class <strong>TeacherViewModel<\/strong> {<br \/>\nString _name = &#8221;;<br \/>\nString _subject = &#8221;;<br \/>\nint _experience = 0;<\/p>\n<p>void refresh(Teacher teacher) {<br \/>\n_name = teacher.name;<br \/>\n_subject = teacher.subject;<br \/>\n_experience = teacher.experience;<br \/>\n}<\/p>\n<p>TeacherState build() =&gt; TeacherState(<br \/>\nname: _name,<br \/>\nsubject: _subject,<br \/>\nexperience: _experience,<br \/>\n);<br \/>\n}<\/p>\n<p>class <strong>StudentViewModel<\/strong> {<br \/>\nString _name = &#8221;;<br \/>\nint _grade = 0;<br \/>\nint _age = 0;<\/p>\n<p>void refresh(Student student) {<br \/>\n_name = student.name;<br \/>\n_grade = student.grade;<br \/>\n_age = student.age;<br \/>\n}<\/p>\n<p>StudentState build() =&gt; StudentState(<br \/>\nname: _name,<br \/>\ngrade: _grade,<br \/>\nage: _age,<br \/>\n);<br \/>\n}<\/p><\/blockquote>\n<h1><strong>MainViewModel<\/strong><\/h1>\n<p>Cubit never talks to TeacherViewModel or StudentViewModel directly.<\/p>\n<blockquote><p>class <strong>MainViewModel<\/strong> {<br \/>\nfinal TeacherViewModel _teacherVM = TeacherViewModel();<br \/>\nfinal StudentViewModel _studentVM = StudentViewModel();<\/p>\n<p>void refresh({required Teacher teacher, required Student student}) {<br \/>\n_teacherVM.refresh(teacher);<br \/>\n_studentVM.refresh(student);<br \/>\n}<\/p>\n<p>MainState build() =&gt; MainState(<br \/>\nteacher: _teacherVM.build(),<br \/>\nstudent: _studentVM.build(),<br \/>\n);<br \/>\n}<\/p><\/blockquote>\n<h1><strong>Cubit<\/strong><\/h1>\n<blockquote><p>class <strong>MainCubit<\/strong> extends Cubit&lt;MainState&gt; {<br \/>\nfinal MainViewModel _mainVM = MainViewModel();<\/p>\n<p>MainCubit() : super(MainState.empty());<\/p>\n<p>void loadData() {<br \/>\nfinal teacher = TeacherState(name: &#8216;Mr. Smith&#8217;, subject: &#8216;Math&#8217;, experience: 12);<br \/>\nfinal student = StudentState(name: &#8216;John&#8217;, grade: 8, age: 14);<\/p>\n<p>_mainVM.refresh(teacher: teacher, student: student);<br \/>\nemit(_mainVM.build());<br \/>\n}<br \/>\n}<\/p><\/blockquote>\n<h1><strong>UI<\/strong><\/h1>\n<p><strong>TeacherSection UI<\/strong><\/p>\n<blockquote><p>class <strong>TeacherSection<\/strong> extends StatelessWidget {<br \/>\nfinal TeacherState teacher;<\/p>\n<p>const TeacherSection({super.key, required this.teacher});<\/p>\n<p>@override<br \/>\nWidget build(BuildContext context) {<br \/>\nreturn Card(<br \/>\nmargin: const EdgeInsets.symmetric(vertical: 8),<br \/>\nchild: ListTile(<br \/>\ntitle: Text(&#8216;Teacher: ${teacher.name}&#8217;),<br \/>\nsubtitle: Text(<br \/>\n&#8216;Subject: ${teacher.subject}\\nExperience: ${teacher.experience} yrs&#8217;,<br \/>\n),<br \/>\ntrailing: IconButton(<br \/>\nicon: const Icon(Icons.refresh),<br \/>\nonPressed: () {<br \/>\n\/\/ Example action &#8211; reload teacher data<br \/>\ncontext.read&lt;MainCubit&gt;().loadData();<br \/>\n},<br \/>\n),<br \/>\n),<br \/>\n);<br \/>\n}<br \/>\n}<\/p><\/blockquote>\n<p>&nbsp;<\/p>\n<h1><strong>StudentSection UI<\/strong><\/h1>\n<p>class StudentSection extends StatelessWidget {<br \/>\nfinal StudentState student;<\/p>\n<blockquote><p>const StudentSection({super.key, required this.student});<\/p>\n<p>@override<br \/>\nWidget build(BuildContext context) {<br \/>\nreturn Card(<br \/>\nmargin: const EdgeInsets.symmetric(vertical: 8),<br \/>\nchild: ListTile(<br \/>\ntitle: Text(&#8216;Student: ${student.name}&#8217;),<br \/>\nsubtitle: Text(<br \/>\n&#8216;Grade: ${student.grade}\\nAge: ${student.age}&#8217;,<br \/>\n),<br \/>\ntrailing: IconButton(<br \/>\nicon: const Icon(Icons.edit),<br \/>\nonPressed: () {<br \/>\n\/\/ Add logic here<br \/>\n},<br \/>\n),<br \/>\n),<br \/>\n);<br \/>\n}<br \/>\n}<\/p><\/blockquote>\n<h1><strong>TeacherStudentScreen<\/strong><\/h1>\n<blockquote><p>class TeacherStudentScreen extends StatelessWidget {<br \/>\nconst TeacherStudentScreen({super.key});<\/p>\n<p>@override<br \/>\nWidget build(BuildContext context) {<br \/>\nreturn BlocProvider(<br \/>\ncreate: (_) =&gt; MainCubit()..loadData(),<br \/>\nchild: BlocBuilder&lt;MainCubit, MainState&gt;(<br \/>\nbuilder: (context, state) {<br \/>\nreturn Scaffold(<br \/>\nappBar: AppBar(title: const Text(&#8216;Teacher &amp; Student&#8217;)),<br \/>\nbody: Padding(<br \/>\npadding: const EdgeInsets.all(16),<br \/>\nchild: Column(<br \/>\nchildren: [<br \/>\nTeacherSection(teacher: state.teacher),<br \/>\nStudentSection(student: state.student),<br \/>\n],<br \/>\n),<br \/>\n),<br \/>\n);<br \/>\n},<br \/>\n),<br \/>\n);<br \/>\n}<br \/>\n}<\/p><\/blockquote>\n<h1><strong>Advantages of the view model<\/strong><\/h1>\n<p><strong>Encapsulation<\/strong> \u2192 Each ViewModel is a focused unit and will handle specific UI logic\/state.<br \/>\n<strong>Testability<\/strong> \u2192 You can unit test TeacherViewModel without Cubit or UI.<br \/>\n<strong>Composable<\/strong> \u2192 MainViewModel is just a facade add more child VMs without touching Cubit.<br \/>\n<strong>Predictable<\/strong> flow \u2192 All updates through refresh() \u2192 build() \u2192 emit().<\/p>\n<p><strong> Example Unit Test<\/strong><\/p>\n<p style=\"padding-left: 40px;\">test(&#8216;TeacherViewModel builds correct state&#8217;, () {<br \/>\nfinal vm = TeacherViewModel();<br \/>\nvm.refresh(const Teacher(name: &#8216;Dr. Ray&#8217;, subject: &#8216;Chemistry&#8217;, experience: 10));<br \/>\nfinal s = vm.build();<br \/>\nexpect(s.name, &#8216;Dr. Ray&#8217;);<br \/>\nexpect(s.subject, &#8216;Chemistry&#8217;);<br \/>\nexpect(s.experience, 10);<br \/>\n});<!--more--><\/p>\n<h1 style=\"text-align: left;\"><strong>Conclusion<\/strong><\/h1>\n<p style=\"text-align: left;\">This isn\u2019t about inventing yet another state management library. It\u2019s about discipline:<\/p>\n<ul>\n<li>Keep Cubit clean and focused.<\/li>\n<li>Move logic into ViewModels.<\/li>\n<li>Let UI just display the data.<\/li>\n<\/ul>\n<p>By applying this ViewModel-driven Cubit architecture, your Flutter codebase becomes cleaner, easier to test, and ready to scale \u2014 no matter how complex your app grows.<\/p>\n<p><!--more--><\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction State management is one of the most debated topics in Flutter. Some engineers like Bloc, others prefer Riverpod. But after working on large-scale Flutter apps with dozens of screens and business rules, one thing becomes obvious: What matters most is not which library you pick, but how you organise responsibilities between UI, business logic, [&hellip;]<\/p>\n","protected":false},"author":2140,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":19},"categories":[518],"tags":[4969,4968],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/74735"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/2140"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=74735"}],"version-history":[{"count":16,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/74735\/revisions"}],"predecessor-version":[{"id":76855,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/74735\/revisions\/76855"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=74735"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=74735"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=74735"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}