- Dart cheatsheet
- Flutter Cookbook
- Flutter Templates
- iOS components
- Android components
- Flutter Cookbook GitHub
- Flutter sample repository
- Flutter package repository
- Scaffold class
- Scaffold class in Flutter with Examples
- Using MVVM for your application's architecture
- Start building Flutter Android apps on Windows
Common Errors
-
Resolve Error: The term 'flutter' is not recognized as the name of a cmdlet.
flutter : The term 'flutter' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
For CMD:
flutter doctor flutter doctor -v flutter --disable-analytics dart --disable-analytics
Check If You added
C:\Users\%USERPROFILE%\flutter\bin
to environment variables 'Path':The Dart SDK lives inside the
C:\Users\%USERPROFILE%\flutter\bin\cache\dart-sdk\bin
. -
Resolve Error: Android sdkmanager not found.
flutter : Android sdkmanager not found. Update to the latest Android SDK and ensure that the cmdline-tools are installed to resolve this.
For CMD:
flutter doctor --android-licenses
Open Android Studio, Go to File -> Settings... -> Languages & Frameworks -> Android SDK, Update/Install Android Studio Command-line Tools. Click apply and restart android studio:
-
Press "shift + right-click" in the folder containing vs_enterprise.exe, select "open PowerShell window here" and in the PowerShell window; type "start cmd", press the enter button and run the command below.
VisualStudioSetup.exe --layout c:\localVSlayout --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.ComponentGroup.VC.Tools.142.x86.x64 --includeOptional --lang en-US
-
Resolve Error: Please upgrade your AGP version to at least 8.x.x.
- We need to download latest JDK version, compatible with the relative Gradle version, and install the x64 Installer.
- Find the relative Gradle version: C:\Users<UserName>\source\repos<FlutterApp>\android\gradle\wrapper\gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-x-all.zip
- Find the relative Gradle version: C:\Users<UserName>\source\repos<FlutterApp>\android\gradle\wrapper\gradle-wrapper.properties
- Add the path of JDK,
C:\Program Files\Android\Android Studio\jbr\bin
ORC:\Program Files\Java\jdk-x\bin
, into environment variables 'Path', and add path,C:\Program Files\Android\Android Studio\jbr
ORC:\Program Files\Java\jdk-x
, on the environment variable 'JAVA_HOME'. - Check if everything is working properly. Open command prompt,
java -version
. - We need to download latest Gradle (complete) version.
- Then extract the contents of Gradle zip file in location
C:\Users\%USERPROFILE%\gradle-8.x.x
. - Add the path of Gradle,
C:\Users\%USERPROFILE%\gradle-8.x.x\bin
, into environment variables 'Path'. - Check if everything is working properly. Open command prompt,
gradle -v
.
- We need to download latest JDK version, compatible with the relative Gradle version, and install the x64 Installer.
Flutter CLI
The flutter
command-line tool is how developers (or IDEs on behalf of
developers) interact with Flutter. For Dart related commands,
you can use the dart
command-line tool.
Here's how you might use the flutter
tool to create, analyze, test, and run an
app:
$ flutter create my_app
$ cd my_app
$ flutter analyze
$ flutter test
$ flutter run lib/main.dart
To run pub
commands using the flutter
tool:
$ flutter pub get
$ flutter pub outdated
$ flutter pub upgrade
To view all commands that flutter
supports:
$ flutter --help --verbose
To get the current version of the Flutter SDK, including its framework, engine, and tools:
$ flutter --version
The following table shows which commands you can use with the flutter
tool:
Command | Example of use | More information |
---|---|---|
analyze | flutter analyze -d <DEVICE_ID> |
Analyzes the project's Dart source code. Use instead of dart analyze . |
assemble | flutter assemble -o <DIRECTORY> |
Assemble and build flutter resources. |
attach | flutter attach -d <DEVICE_ID> |
Attach to a running application. |
bash-completion | flutter bash-completion |
Output command line shell completion setup scripts. |
build | flutter build <DIRECTORY> |
Flutter build commands. |
channel | flutter channel <CHANNEL_NAME> |
List or switch flutter channels. |
clean | flutter clean |
Delete the build/ and .dart_tool/ directories. |
config | flutter config --build-dir=<DIRECTORY> |
Configure Flutter settings. To remove a setting, configure it to an empty string. |
create | flutter create <DIRECTORY> |
Creates a new project. |
custom-devices | flutter custom-devices list |
Add, delete, list, and reset custom devices. |
devices | flutter devices -d <DEVICE_ID> |
List all connected devices. |
doctor | flutter doctor |
Show information about the installed tooling. |
downgrade | flutter downgrade |
Downgrade Flutter to the last active version for the current channel. |
drive | flutter drive |
Runs Flutter Driver tests for the current project. |
emulators | flutter emulators |
List, launch and create emulators. |
gen-l10n | flutter gen-l10n <DIRECTORY> |
Generate localizations for the Flutter project. |
install | flutter install -d <DEVICE_ID> |
Install a Flutter app on an attached device. |
logs | flutter logs |
Show log output for running Flutter apps. |
precache | flutter precache <ARGUMENTS> |
Populates the Flutter tool's cache of binary artifacts. |
pub | flutter pub <PUB_COMMAND> |
Works with packages. Use instead of dart pub . |
run | flutter run <DART_FILE> |
Runs a Flutter program. |
screenshot | flutter screenshot |
Take a screenshot of a Flutter app from a connected device. |
symbolize | flutter symbolize --input=<STACK_TRACK_FILE> |
Symbolize a stack trace from the AOT compiled flutter application. |
test | flutter test [<DIRECTORYDART_FILE>] |
Runs tests in this package. Use instead of dart test . |
upgrade | flutter upgrade |
Upgrade your copy of Flutter. |
{:.table .table-striped .nowrap}
For additional help on any of the commands, enter flutter help <command>
or follow the links in the More information column.
You can also get details on pub
commands — for example,
flutter help pub outdated
.
Flutter Common Cheatsheet
Sample program
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hello world!',
home: Scaffold(
body: Center(
child: Text('Hello world'),
),
),
);
}
}
import function
: imports the assets of the specified library from the pubspec.yaml file.void main()
: main function that runs first when the app is compiled every time.class MyApp extends StatelessWidget {}
: Contains a class (similar to java class) and creates a stateless widget.@override
: @override marks an instance member as overriding a superclass member with the same name.build(BuildContext context)
: BuildContext is a locator that is used to track each widget in a tree and locate them and their position in the tree. The BuildContext of each widget is passed to their build method.Scaffold()
: Scaffold is a class in flutter which provides many widgets or we can say APIs like Drawer, SnackBar, BottomNavigationBar etc.body:
: represents the body of the app , wrapped inside a scaffold , where several widgets can be used to layout a UI.Center()
: is a property to wrap around any widget and push the widget layout to be at the center of the screen/widget.child:
: It is a property of the parent widget that allows widgets to be put inside widgets , forming a widget tree.Text()
: It's a widget to pass in text to display it on the current screen.-
The State is information that can read synchronously when the widget is build and might change during the lifetime of the widget. In simpler words, the state of the widget is the information of the objects that its properties (parameters) are holding at the time of its creation (when the widget is painted on the screen). The state can also change when it is used for example the color of RaisedButton widget might change when pressed.
Stateless Widget
Stateless widgets are the widgets that don’t change i.e. they are immutable. Its appearance and properties remain unchanged throughout the lifetime of the widget. In simple words, Stateless widgets cannot change their state during the runtime of the app, which means the widgets cannot be redrawn while the app is in action. Examples: Icon, IconButton, and Text are examples of stateless widgets.
class MyCounter extends StatelessWidget {
final int count;
const MyCounter({super.key, required this.count});
@override
Widget build(BuildContext context) {
return Text('$count');
}
}
Stateful Widget
Stateful Widgets are the ones that change its properties during run-time. They are dynamic i.e., they are mutable and can be drawn multiple times within its lifetime. It can change its appearance in response to events triggered by user interactions or when it receives data. Examples : Checkbox, Radio Button, Slider, InkWell, Form, and TextField are examples of Stateful widgets
class MyCounter extends StatefulWidget {
const MyCounter({super.key});
@override
State<MyCounter> createState() => _MyCounterState();
}
class _MyCounterState extends State<MyCounter> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
TextButton(
onPressed: () {
setState(() {
count++;
});
},
child: Text('Increment'),
)
],
);
}
}
Inherited Widget
Manually passing data down the widget tree can be verbose and cause unwanted boilerplate code, so Flutter provides InheritedWidget, which provides a way to efficiently host data in a parent widget so that child widgets can get access them without storing them as a field.
To use InheritedWidget, extend the InheritedWidget class and implement the static method of() using dependOnInheritedWidgetOfExactType. A widget calling of() in a build method creates a dependency that is managed by the Flutter framework, so that any widgets that depend on this InheritedWidget rebuild when this widget re-builds with new data and updateShouldNotify returns true.
class MyState extends InheritedWidget {
const MyState({
super.key,
required this.data,
required super.child,
});
final String data;
static MyState of(BuildContext context) {
// This method looks for the nearest `MyState` widget ancestor.
final result = context.dependOnInheritedWidgetOfExactType<MyState>();
assert(result != null, 'No MyState found in context');
return result!;
}
@override
// This method should return true if the old widget's data is different
// from this widget's data. If true, any widgets that depend on this widget
// by calling `of()` will be re-built.
bool updateShouldNotify(MyState oldWidget) => data != oldWidget.data;
}
// Next, call the of() method from the build()method of the widget that needs access to the shared state:
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
var data = MyState.of(context).data;
return Scaffold(
body: Center(
child: Text(data),
),
);
}
}
Required and default props
import 'package:flutter/material.dart';
class SomeComponent extends StatelessWidget {
SomeComponent({
@required this.foo,
this.bar = 'some string',
});
final String foo;
final String bar;
@override
Widget build(BuildContext context) {
return Container(
child: Text('$foo $bar'),
);
}
}
Android Ink effect
InkWell(
child: Text('Button'),
onTap: _onTap,
onLongPress: _onLongPress,
onDoubleTap: _onDoubleTap,
onTapCancel: _onTapCancel,
);
Detecting Gestures
GestureDetector(
onTap: _onTap,
onLongPress: _onLongPress,
child: Text('Button'),
);
Loading indicator
class SomeWidget extends StatefulWidget {
@override
_SomeWidgetState createState() => _SomeWidgetState();
}
class _SomeWidgetState extends State<SomeWidget> {
Future future;
@override
void initState() {
future = Future.delayed(Duration(seconds: 1));
super.initState();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
return snapshot.connectionState == ConnectionState.done
? Text('Loaded')
: CircularProgressIndicator();
},
);
}
}
Platform specific code
import 'dart:io' show Platform;
if (Platform.isIOS) {
doSmthIOSSpecific();
}
if (Platform.isAndroid) {
doSmthAndroidSpecific();
}
Hide status bar
import 'package:flutter/services.dart';
void main() {
SystemChrome.setEnabledSystemUIOverlays([]);
}
Lock orientation
import 'package:flutter/services.dart';
void main() async {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
runApp(App());
}
Make http request
dependencies:
http: ^0.12.0
import 'dart:convert' show json;
import 'package:http/http.dart' as http;
http.get(API_URL).then((http.Response res) {
final data = json.decode(res.body);
print(data);
});
Async Await
Future<int> doSmthAsync() async {
final result = await Future.value(42);
return result;
}
class SomeClass {
method() async {
final result = await Future.value(42);
return result;
}
}
JSON
import 'dart:convert' show json;
json.decode(someString);
json.encode(encodableObject);
Check if dev
bool isDev = false;
assert(isDev == true);
if (isDev) {
doSmth();
}
Navigation
import 'package:flutter/material.dart';
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text('Go to SecondScreen'),
onPressed: () => Navigator.pushNamed(context, '/second'),
),
);
}
}
class SecondScreen extends StatelessWidget {
void _pushSecondScreen(context) {
Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
RaisedButton(
child: Text('Go back!'),
onPressed: () => Navigator.pop(context),
),
RaisedButton(
child: Text('Go to SecondScreen... again!'),
onPressed: () => _pushSecondScreen(context),
),
],
);
}
}
void main() {
runApp(MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => FirstScreen(),
'/second': (context) => SecondScreen(),
},
));
}
Show alert
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Alert Title'),
content: Text('My Alert Msg'),
actions: <Widget>[
FlatButton(
child: Text('Ask me later'),
onPressed: () {
print('Ask me later pressed');
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('Cancel'),
onPressed: () {
print('Cancel pressed');
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('OK'),
onPressed: () {
print('OK pressed');
Navigator.of(context).pop();
},
),
],
);
},
);
Arrays
final length = items.length;
final newItems = items..addAll(otherItems);
final allEven = items.every((item) => item % 2 == 0);
final filled = List<int>.filled(3, 42);
final even = items.where((n) => n % 2 == 0).toList();
final found = items.firstWhere((item) => item.id == 42);
final index = items.indexWhere((item) => item.id == 42);
final flat = items.expand((_) => _).toList();
final mapped = items.expand((item) => [item + 1]).toList();
items.forEach((item) => print(item));
items.asMap().forEach((index, item) => print('$item, $index'));
final includes = items.contains(42);
final indexOf = items.indexOf(42);
final joined = items.join(',');
final newItems = items.map((item) => item + 1).toList();
final item = items.removeLast();
items.add(42);
final reduced = items.fold({}, (acc, item) {
acc[item.id] = item;
return acc;
});
final reversed = items.reversed;
items.removeAt(0);
final slice = items.sublist(15, 42);
final hasOdd = items.any((item) => item % 2 == 0);
items.sort((a, b) => a - b);
items.replaceRange(15, 42, [1, 2, 3]);
items.insert(0, 42);
Changing the app icon in a Flutter app is a great way to customize your app's appearance. Here’s a simple guide to help you out:
-
Prepare Your Icons: You need to create your app icon in different sizes for various devices. You can use an online tool like App Icon Generator to generate these.
-
Add Icons to Your Project:
- iOS:
- In your Flutter project, navigate to the
ios
folder and then toRunner
. - Replace the icons in
Assets.xcassets/AppIcon.appiconset
with your generated icons.
- In your Flutter project, navigate to the
- Android:
- Navigate to the
android
folder, then toapp/src/main/res
. - Replace the icons in the
mipmap
folders (likemipmap-hdpi
,mipmap-mdpi
, etc.) with your generated icons.
- Navigate to the
- iOS:
-
Update the pubspec.yaml: Ensure that your
pubspec.yaml
file includes the updated assets if necessary. -
Run the App: Finally, run your Flutter app to see the new app icon in action!
To update the icon of a Flutter Windows desktop application before packaging use the following instructions:
In the Flutter project, navigate to windows\runner\resources. Replace the app_icon.ico with the desired icon. If the name of the icon is other than app_icon.ico, proceed to change the IDI_APP_ICON value in the windows\runner\Runner.rc file to point to the new path.
for loop, forEach, map, cascade & spread operator in Dart
In Dart, you have several ways to iterate over a list or apply functions to elements, such as using a for
loop, forEach
, and map
. Let me show you how each one works:
The traditional for
loop is useful when you need to iterate by index or when you want to modify the list in some way.
void main() {
List<String> fruits = ['Apple', 'Banana', 'Orange', 'Mango'];
// Using for loop
for (int i = 0; i < fruits.length; i++) {
print(fruits[i]);
}
}
forEach
is a method that allows you to apply a function to each element in the list. It’s convenient when you don’t need the index, just the item.
void main() {
List<String> fruits = ['Apple', 'Banana', 'Orange', 'Mango'];
// Using forEach
fruits.forEach((fruit) {
print(fruit);
});
}
This is functionally similar to the for-in
loop but with a cleaner syntax for passing the operation as a function.
map
transforms the elements of the list into a new list. It’s useful when you want to apply a function to each element and return a new list of transformed items.
void main() {
List<String> fruits = ['Apple', 'Banana', 'Orange', 'Mango'];
// Using map
List<String> uppercasedFruits = fruits.map((fruit) => fruit.toUpperCase()).toList();
print(uppercasedFruits); // ['APPLE', 'BANANA', 'ORANGE', 'MANGO']
}
Here, the map
function takes a function as an argument and returns a new list where each element is the result of applying the function. We used toUpperCase()
as an example, but you can perform any transformation.
for
loop: Iterates through a list using an index.forEach
: Iterates through each element in a list and performs an action.map
: Transforms each element and returns a new list.
Sure thing! Let's start with examples in Dart using the cascade operator (..
) and the spread operator (...
).
Here's a simple example of using the cascade operator to chain multiple operations on an object:
class Car {
String color;
int year;
void setColor(String color) {
this.color = color;
}
void setYear(int year) {
this.year = year;
}
}
void main() {
var myCar = Car()
..setColor('Red')
..setYear(2021);
print('Car color: ${myCar.color}, Year: ${myCar.year}');
}
Now, an example using the spread operator to combine lists:
void main() {
var list1 = [1, 2, 3];
var list2 = [4, 5, 6];
var combinedList = [...list1, ...list2];
print(combinedList); // Output: [1, 2, 3, 4, 5, 6]
}
C# doesn't have a direct equivalent for the cascade operator, but you can achieve similar results using method chaining:
public class Car
{
public string Color { get; set; }
public int Year { get; set; }
public Car SetColor(string color)
{
Color = color;
return this;
}
public Car SetYear(int year)
{
Year = year;
return this;
}
}
public class Program
{
public static void Main()
{
var myCar = new Car()
.SetColor("Red")
.SetYear(2021);
Console.WriteLine($"Car color: {myCar.Color}, Year: {myCar.Year}");
}
}
For the spread operator, C# also doesn't have a direct equivalent. However, you can combine lists using LINQ:
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 4, 5, 6 };
var combinedList = list1.Concat(list2).ToList();
Console.WriteLine(string.Join(", ", combinedList)); // Output: 1, 2, 3, 4, 5, 6
}
}
How to fetch authorized data from a web service.
To fetch data from most web services, you need to provide
authorization. There are many ways to do this,
but perhaps the most common uses the Authorization
HTTP header.
The http
package provides a
convenient way to add headers to your requests.
Alternatively, use the HttpHeaders
class from the dart:io
library.
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
// Send authorization headers to the backend.
headers: {
HttpHeaders.authorizationHeader: 'Basic your_api_token_here',
},
);
This example builds upon the Fetching data from the internet recipe.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
Future<Album> fetchAlbum() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
// Send authorization headers to the backend.
headers: {
HttpHeaders.authorizationHeader: 'Basic your_api_token_here',
},
);
final responseJson = jsonDecode(response.body) as Map<String, dynamic>;
return Album.fromJson(responseJson);
}
class Album {
final int userId;
final int id;
final String title;
const Album({
required this.userId,
required this.id,
required this.title,
});
factory Album.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'userId': int userId,
'id': int id,
'title': String title,
} =>
Album(
userId: userId,
id: id,
title: title,
),
_ => throw const FormatException('Failed to load album.'),
};
}
}
How to fetch data over the internet using the http package.
Fetching data from the internet is necessary for most apps.
Luckily, Dart and Flutter provide tools, such as the
http
package, for this type of work.
:::note
You should avoid directly using dart:io
or dart:html
to make HTTP requests.
Those libraries are platform-dependent
and tied to a single implementation.
:::
This recipe uses the following steps:
- Add the
http
package. - Make a network request using the
http
package. - Convert the response into a custom Dart object.
- Fetch and display the data with Flutter.
The http
package provides the
simplest way to fetch data from the internet.
To add the http
package as a dependency,
run flutter pub add
:
$ flutter pub add http
Import the http package.
import 'package:http/http.dart' as http;
{% render docs/cookbook/networking/internet-permission.md %}
This recipe covers how to fetch a sample album from the
JSONPlaceholder using the http.get()
method.
Future<http.Response> fetchAlbum() {
return http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
}
The http.get()
method returns a Future
that contains a Response
.
Future
is a core Dart class for working with async operations. A Future object represents a potential value or error that will be available at some time in the future.- The
http.Response
class contains the data received from a successful http call.
While it's easy to make a network request, working with a raw
Future<http.Response>
isn't very convenient.
To make your life easier,
convert the http.Response
into a Dart object.
First, create an Album
class that contains the data from the
network request. It includes a factory constructor that
creates an Album
from JSON.
Converting JSON using pattern matching is only one option. For more information, see the full article on JSON and serialization.
class Album {
final int userId;
final int id;
final String title;
const Album({
required this.userId,
required this.id,
required this.title,
});
factory Album.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'userId': int userId,
'id': int id,
'title': String title,
} =>
Album(
userId: userId,
id: id,
title: title,
),
_ => throw const FormatException('Failed to load album.'),
};
}
}
Now, use the following steps to update the fetchAlbum()
function to return a Future<Album>
:
- Convert the response body into a JSON
Map
with thedart:convert
package. - If the server does return an OK response with a status code of
200, then convert the JSON
Map
into anAlbum
using thefromJson()
factory method. - If the server does not return an OK response with a status code of 200,
then throw an exception.
(Even in the case of a "404 Not Found" server response,
throw an exception. Do not return
null
. This is important when examining the data insnapshot
, as shown below.)
Future<Album> fetchAlbum() async {
final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
Hooray! Now you've got a function that fetches an album from the internet.
Call the fetchAlbum()
method in either the
initState()
or didChangeDependencies()
methods.
The initState()
method is called exactly once and then never again.
If you want to have the option of reloading the API in response to an
InheritedWidget
changing, put the call into the
didChangeDependencies()
method.
See State
for more details.
class _MyAppState extends State<MyApp> {
late Future<Album> futureAlbum;
@override
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
// ···
}
This Future is used in the next step.
To display the data on screen, use the
FutureBuilder
widget.
The FutureBuilder
widget comes with Flutter and
makes it easy to work with asynchronous data sources.
You must provide two parameters:
- The
Future
you want to work with. In this case, the future returned from thefetchAlbum()
function. - A
builder
function that tells Flutter what to render, depending on the state of theFuture
: loading, success, or error.
Note that snapshot.hasData
only returns true
when the snapshot contains a non-null data value.
Because fetchAlbum
can only return non-null values,
the function should throw an exception
even in the case of a "404 Not Found" server response.
Throwing an exception sets the snapshot.hasError
to true
which can be used to display an error message.
Otherwise, the spinner will be displayed.
FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
)
Although it's convenient,
it's not recommended to put an API call in a build()
method.
Flutter calls the build()
method every time it needs
to change anything in the view,
and this happens surprisingly often.
The fetchAlbum()
method, if placed inside build()
, is repeatedly
called on each rebuild causing the app to slow down.
Storing the fetchAlbum()
result in a state variable ensures that
the Future
is executed only once and then cached for subsequent
rebuilds.
For information on how to test this functionality, see the following recipes:
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Album> fetchAlbum() async {
final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
class Album {
final int userId;
final int id;
final String title;
const Album({
required this.userId,
required this.id,
required this.title,
});
factory Album.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'userId': int userId,
'id': int id,
'title': String title,
} =>
Album(
userId: userId,
id: id,
title: title,
),
_ => throw const FormatException('Failed to load album.'),
};
}
}
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Future<Album> futureAlbum;
@override
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fetch Data Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: Scaffold(
appBar: AppBar(
title: const Text('Fetch Data Example'),
),
body: Center(
child: FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
),
),
),
);
}
}
How to use the http package to send data over the internet.
Sending data to the internet is necessary for most apps.
The http
package has got that covered, too.
This recipe uses the following steps:
- Add the
http
package. - Send data to a server using the
http
package. - Convert the response into a custom Dart object.
- Get a
title
from user input. - Display the response on screen.
To add the http
package as a dependency,
run flutter pub add
:
$ flutter pub add http
Import the http
package.
import 'package:http/http.dart' as http;
{% render docs/cookbook/networking/internet-permission.md %}
This recipe covers how to create an Album
by sending an album title to the
JSONPlaceholder using the
http.post()
method.
Import dart:convert
for access to jsonEncode
to encode the data:
import 'dart:convert';
Use the http.post()
method to send the encoded data:
Future<http.Response> createAlbum(String title) {
return http.post(
Uri.parse('https://jsonplaceholder.typicode.com/albums'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'title': title,
}),
);
}
The http.post()
method returns a Future
that contains a Response
.
Future
is a core Dart class for working with asynchronous operations. A Future object represents a potential value or error that will be available at some time in the future.- The
http.Response
class contains the data received from a successful http call. - The
createAlbum()
method takes an argumenttitle
that is sent to the server to create anAlbum
.
While it's easy to make a network request,
working with a raw Future<http.Response>
isn't very convenient. To make your life easier,
convert the http.Response
into a Dart object.
First, create an Album
class that contains
the data from the network request.
It includes a factory constructor that
creates an Album
from JSON.
Converting JSON with pattern matching is only one option. For more information, see the full article on JSON and serialization.
class Album {
final int id;
final String title;
const Album({required this.id, required this.title});
factory Album.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'id': int id,
'title': String title,
} =>
Album(
id: id,
title: title,
),
_ => throw const FormatException('Failed to load album.'),
};
}
}
Use the following steps to update the createAlbum()
function to return a Future<Album>
:
- Convert the response body into a JSON
Map
with thedart:convert
package. - If the server returns a
CREATED
response with a status code of 201, then convert the JSONMap
into anAlbum
using thefromJson()
factory method. - If the server doesn't return a
CREATED
response with a status code of 201, then throw an exception. (Even in the case of a "404 Not Found" server response, throw an exception. Do not returnnull
. This is important when examining the data insnapshot
, as shown below.)
Future<Album> createAlbum(String title) async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/albums'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'title': title,
}),
);
if (response.statusCode == 201) {
// If the server did return a 201 CREATED response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
// If the server did not return a 201 CREATED response,
// then throw an exception.
throw Exception('Failed to create album.');
}
}
Hooray! Now you've got a function that sends the title to a server to create an album.
Next, create a TextField
to enter a title and
a ElevatedButton
to send data to server.
Also define a TextEditingController
to read the
user input from a TextField
.
When the ElevatedButton
is pressed, the _futureAlbum
is set to the value returned by createAlbum()
method.
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _controller,
decoration: const InputDecoration(hintText: 'Enter Title'),
),
ElevatedButton(
onPressed: () {
setState(() {
_futureAlbum = createAlbum(_controller.text);
});
},
child: const Text('Create Data'),
),
],
)
On pressing the Create Data button, make the network request,
which sends the data in the TextField
to the server
as a POST
request.
The Future, _futureAlbum
, is used in the next step.
To display the data on screen, use the
FutureBuilder
widget.
The FutureBuilder
widget comes with Flutter and
makes it easy to work with asynchronous data sources.
You must provide two parameters:
- The
Future
you want to work with. In this case, the future returned from thecreateAlbum()
function. - A
builder
function that tells Flutter what to render, depending on the state of theFuture
: loading, success, or error.
Note that snapshot.hasData
only returns true
when
the snapshot contains a non-null data value.
This is why the createAlbum()
function should throw an exception
even in the case of a "404 Not Found" server response.
If createAlbum()
returns null
, then
CircularProgressIndicator
displays indefinitely.
FutureBuilder<Album>(
future: _futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return const CircularProgressIndicator();
},
)
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Album> createAlbum(String title) async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/albums'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'title': title,
}),
);
if (response.statusCode == 201) {
// If the server did return a 201 CREATED response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
// If the server did not return a 201 CREATED response,
// then throw an exception.
throw Exception('Failed to create album.');
}
}
class Album {
final int id;
final String title;
const Album({required this.id, required this.title});
factory Album.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'id': int id,
'title': String title,
} =>
Album(
id: id,
title: title,
),
_ => throw const FormatException('Failed to load album.'),
};
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
final TextEditingController _controller = TextEditingController();
Future<Album>? _futureAlbum;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Create Data Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: Scaffold(
appBar: AppBar(
title: const Text('Create Data Example'),
),
body: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8),
child: (_futureAlbum == null) ? buildColumn() : buildFutureBuilder(),
),
),
);
}
Column buildColumn() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _controller,
decoration: const InputDecoration(hintText: 'Enter Title'),
),
ElevatedButton(
onPressed: () {
setState(() {
_futureAlbum = createAlbum(_controller.text);
});
},
child: const Text('Create Data'),
),
],
);
}
FutureBuilder<Album> buildFutureBuilder() {
return FutureBuilder<Album>(
future: _futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return const CircularProgressIndicator();
},
);
}
}
How to perform a task in the background.
By default, Dart apps do all of their work on a single thread. In many cases, this model simplifies coding and is fast enough that it does not result in poor app performance or stuttering animations, often called "jank."
However, you might need to perform an expensive computation, such as parsing a very large JSON document. If this work takes more than 16 milliseconds, your users experience jank.
To avoid jank, you need to perform expensive computations like this in the background. On Android, this means scheduling work on a different thread. In Flutter, you can use a separate Isolate. This recipe uses the following steps:
- Add the
http
package. - Make a network request using the
http
package. - Convert the response into a list of photos.
- Move this work to a separate isolate.
First, add the http
package to your project.
The http
package makes it easier to perform network
requests, such as fetching data from a JSON endpoint.
To add the http
package as a dependency,
run flutter pub add
:
$ flutter pub add http
This example covers how to fetch a large JSON document
that contains a list of 5000 photo objects from the
JSONPlaceholder REST API,
using the http.get()
method.
Future<http.Response> fetchPhotos(http.Client client) async {
return client.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
}
:::note
You're providing an http.Client
to the function in this example.
This makes the function easier to test and use in different environments.
:::
Next, following the guidance from the
Fetch data from the internet recipe,
convert the http.Response
into a list of Dart objects.
This makes the data easier to work with.
First, create a Photo
class that contains data about a photo.
Include a fromJson()
factory method to make it easy to create a
Photo
starting with a JSON object.
class Photo {
final int albumId;
final int id;
final String title;
final String url;
final String thumbnailUrl;
const Photo({
required this.albumId,
required this.id,
required this.title,
required this.url,
required this.thumbnailUrl,
});
factory Photo.fromJson(Map<String, dynamic> json) {
return Photo(
albumId: json['albumId'] as int,
id: json['id'] as int,
title: json['title'] as String,
url: json['url'] as String,
thumbnailUrl: json['thumbnailUrl'] as String,
);
}
}
Now, use the following instructions to update the
fetchPhotos()
function so that it returns a
Future<List<Photo>>
:
- Create a
parsePhotos()
function that converts the response body into aList<Photo>
. - Use the
parsePhotos()
function in thefetchPhotos()
function.
// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
final parsed =
(jsonDecode(responseBody) as List).cast<Map<String, dynamic>>();
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client
.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
// Synchronously run parsePhotos in the main isolate.
return parsePhotos(response.body);
}
If you run the fetchPhotos()
function on a slower device,
you might notice the app freezes for a brief moment as it parses and
converts the JSON. This is jank, and you want to get rid of it.
You can remove the jank by moving the parsing and conversion
to a background isolate using the compute()
function provided by Flutter. The compute()
function runs expensive
functions in a background isolate and returns the result. In this case,
run the parsePhotos()
function in the background.
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client
.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
// Use the compute function to run parsePhotos in a separate isolate.
return compute(parsePhotos, response.body);
}
Isolates communicate by passing messages back and forth. These messages can
be primitive values, such as null
, num
, bool
, double
, or String
, or
simple objects such as the List<Photo>
in this example.
You might experience errors if you try to pass more complex objects,
such as a Future
or http.Response
between isolates.
As an alternate solution, check out the worker_manager
or
workmanager
packages for background processing.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client
.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
// Use the compute function to run parsePhotos in a separate isolate.
return compute(parsePhotos, response.body);
}
// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
final parsed =
(jsonDecode(responseBody) as List).cast<Map<String, dynamic>>();
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
class Photo {
final int albumId;
final int id;
final String title;
final String url;
final String thumbnailUrl;
const Photo({
required this.albumId,
required this.id,
required this.title,
required this.url,
required this.thumbnailUrl,
});
factory Photo.fromJson(Map<String, dynamic> json) {
return Photo(
albumId: json['albumId'] as int,
id: json['id'] as int,
title: json['title'] as String,
url: json['url'] as String,
thumbnailUrl: json['thumbnailUrl'] as String,
);
}
}
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const appTitle = 'Isolate Demo';
return const MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late Future<List<Photo>> futurePhotos;
@override
void initState() {
super.initState();
futurePhotos = fetchPhotos(http.Client());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FutureBuilder<List<Photo>>(
future: futurePhotos,
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(
child: Text('An error has occurred!'),
);
} else if (snapshot.hasData) {
return PhotosList(photos: snapshot.data!);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
class PhotosList extends StatelessWidget {
const PhotosList({super.key, required this.photos});
final List<Photo> photos;
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: photos.length,
itemBuilder: (context, index) {
return Image.network(photos[index].thumbnailUrl);
},
);
}
}
How to connect to a web socket.
In addition to normal HTTP requests,
you can connect to servers using WebSockets
.
WebSockets
allow for two-way communication with a server
without polling.
In this example, connect to a test WebSocket server sponsored by Lob.com. The server sends back the same message you send to it. This recipe uses the following steps:
- Connect to a WebSocket server.
- Listen for messages from the server.
- Send data to the server.
- Close the WebSocket connection.
The web_socket_channel
package provides the
tools you need to connect to a WebSocket server.
The package provides a WebSocketChannel
that allows you to both listen for messages
from the server and push messages to the server.
In Flutter, use the following line to
create a WebSocketChannel
that connects to a server:
final channel = WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
);
Now that you've established a connection, listen to messages from the server.
After sending a message to the test server, it sends the same message back.
In this example, use a StreamBuilder
widget to listen for new messages, and a
Text
widget to display them.
StreamBuilder(
stream: channel.stream,
builder: (context, snapshot) {
return Text(snapshot.hasData ? '${snapshot.data}' : '');
},
)
The WebSocketChannel
provides a
Stream
of messages from the server.
The Stream
class is a fundamental part of the dart:async
package.
It provides a way to listen to async events from a data source.
Unlike Future
, which returns a single async response,
the Stream
class can deliver many events over time.
The StreamBuilder
widget connects to a Stream
and asks Flutter to rebuild every time it
receives an event using the given builder()
function.
To send data to the server,
add()
messages to the sink
provided
by the WebSocketChannel
.
channel.sink.add('Hello!');
The WebSocketChannel
provides a
StreamSink
to push messages to the server.
The StreamSink
class provides a general way to add sync or async
events to a data source.
After you're done using the WebSocket, close the connection:
channel.sink.close();
import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const title = 'WebSocket Demo';
return const MaterialApp(
title: title,
home: MyHomePage(
title: title,
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
super.key,
required this.title,
});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController _controller = TextEditingController();
final _channel = WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Form(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(labelText: 'Send a message'),
),
),
const SizedBox(height: 24),
StreamBuilder(
stream: _channel.stream,
builder: (context, snapshot) {
return Text(snapshot.hasData ? '${snapshot.data}' : '');
},
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _sendMessage,
tooltip: 'Send message',
child: const Icon(Icons.send),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
void _sendMessage() {
if (_controller.text.isNotEmpty) {
_channel.sink.add(_controller.text);
}
}
@override
void dispose() {
_channel.sink.close();
_controller.dispose();
super.dispose();
}
}
Implementing MVVM Architecture in Flutter
Generated by Medium to Markdown
A Guide to Flutter Architecture Patterns: Folder Structures for MVC, MVVM, BLoC, and More
Master the art of building scalable, testable, and maintainable Flutter apps using the MVVM architecture.
Flutter, with its powerful and efficient development environment, has become a top choice for mobile app developers. As apps become more complex, maintaining clean, testable, and scalable code is crucial. One of the best ways to ensure this is by adopting the MVVM (Model-View-ViewModel) architecture, which provides a structured and organized approach to building apps.
In this article, we’ll walk through how to implement the MVVM architecture in Flutter, step by step, from a beginner-friendly perspective to advanced considerations. By the end, you’ll have a clear understanding of how to create clean, maintainable Flutter apps using MVVM, with real-world examples and practical tips.
MVVM (Model-View-ViewModel) is a software design pattern that separates the app’s business logic from the UI, making the codebase more modular and testable. Here’s a quick breakdown of each component:
- Model: Represents the app’s data layer, such as network requests, databases, and APIs.
- View: The UI component, which listens for changes from the ViewModel and updates the UI accordingly.
- ViewModel: Serves as the intermediary between the View and the Model, holding the app’s logic and managing the data flow between the two.
By separating concerns, MVVM helps developers write cleaner, more maintainable code that is easier to test, debug, and scale.
Flutter’s reactive nature makes it an ideal candidate for implementing MVVM. In Flutter, widgets are responsible for the UI, and by pairing them with a ViewModel, developers can efficiently separate the UI from business logic. This approach ensures that the code is both scalable and easy to test.
- Separation of concerns: Each part of the architecture has a distinct responsibility, making it easier to manage larger apps.
- Testability: The ViewModel contains all the app’s logic, which can be tested in isolation from the UI.
- Scalability: As the app grows, MVVM ensures that the architecture remains clean and modular.
Let’s walk through a step-by-step guide to implementing MVVM in a Flutter project with practical code snippets.
Create a new Flutter project using the following command:
flutter create mvvm_example
To keep the architecture clean, structure your Flutter project into models
, views
, viewmodels
, and services
. This separation ensures that each component has a well-defined role.
Here’s an example folder structure for a Flutter MVVM project:
lib/
│
├── models/
├── services/
├── viewmodels/
├── views/
Let’s assume we are building a simple app that fetches a list of books. The model will represent the book data.
// models/book_model.dart
class Book {
final String title;
final String author;
Book({required this.title, required this.author});
}
The service will handle the network requests and provide data to the ViewModel. In this example, we’ll use a simple service to simulate fetching data.
// services/book_service.dart
import 'package:mvvm_example/models/book_model.dart';
class BookService {
Future<List<Book>> fetchBooks() async {
// Simulating network request
await Future.delayed(Duration(seconds: 2));
return [ Book(title: 'Clean Code', author: 'Robert C. Martin'),
Book(title: 'Effective Dart', author: 'Google'),
];
}
}
The ViewModel will manage the state and communicate with the service to fetch the data. The ViewModel will then expose the necessary data to the View.
// viewmodels/book_viewmodel.dart
import 'package:flutter/foundation.dart';
import 'package:mvvm_example/models/book_model.dart';
import 'package:mvvm_example/services/book_service.dart';
class BookViewModel extends ChangeNotifier {
final BookService _bookService = BookService();
List<Book> _books = [];
bool _loading = false;
List<Book> get books => _books;
bool get loading => _loading;
Future<void> fetchBooks() async {
_loading = true;
notifyListeners();
_books = await _bookService.fetchBooks();
_loading = false;
notifyListeners();
}
}
In Flutter, the View is simply a widget that listens to the ViewModel and updates accordingly. We’ll use the Provider package to manage state in this example.
// views/book_view.dart
import 'package:flutter/material.dart';
import 'package:mvvm_example/viewmodels/book_viewmodel.dart';
import 'package:provider/provider.dart';
class BookView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bookViewModel = Provider.of<BookViewModel>(context);
return Scaffold(
appBar: AppBar(
title: Text('Books'),
),
body: bookViewModel.loading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: bookViewModel.books.length,
itemBuilder: (context, index) {
final book = bookViewModel.books[index];
return ListTile(
title: Text(book.title),
subtitle: Text(book.author),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
bookViewModel.fetchBooks();
},
child: Icon(Icons.refresh),
),
);
}
}
In the main file, set up the Provider
to manage the state of the BookViewModel
.
// main.dart
import 'package:flutter/material.dart';
import 'package:mvvm_example/viewmodels/book_viewmodel.dart';
import 'package:mvvm_example/views/book_view.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [ ChangeNotifierProvider(create: (_) => BookViewModel()),
],
child: MaterialApp(
home: BookView(),
),
);
}
}
- Use Dependency Injection: =============================
To make the ViewModel more testable and modular, consider injecting services using packages like get_it or riverpod.
- Testing ViewModels: =======================
With the MVVM pattern, testing becomes straightforward. You can test the ViewModel without involving the UI by simply mocking the data layer.
- Error Handling: ===================
In real-world applications, make sure to handle errors gracefully in the ViewModel. You can provide additional states for errors and update the View accordingly.
- Code Organization: ======================
As your app scales, consider organizing ViewModels and Services into feature-specific folders to keep the project manageable.
By following the MVVM architecture in Flutter, you ensure that your codebase remains clean, modular, and easy to maintain. Whether you’re a beginner or an experienced developer, adopting MVVM will help you build scalable applications while keeping the business logic separate from the UI.
Implementing this architecture may take some initial setup time, but the benefits in terms of code organization, testability, and scalability are well worth it. Happy coding!
Flutter MVVM (Model-View-ViewModel) todo application with REST API CRUD operations
A comprehensive guide to creating a Flutter MVVM (Model-View-ViewModel) todo application with REST API CRUD operations. This app will include a login screen, a list of todo items, and detailed views for each item.
-
Create a new Flutter project:
flutter create todo_app cd todo_app
-
Add dependencies in
pubspec.yaml
:dependencies: flutter: sdk: flutter provider: ^6.0.0 http: ^0.14.0 shared_preferences: ^2.0.6
flutter pub get
lib/
├── models/
│ └── todo.dart
├── viewmodels/
│ ├── login_viewmodel.dart
│ └── todo_viewmodel.dart
├── views/
│ ├── login_view.dart
│ ├── todo_list_view.dart
│ └── todo_detail_view.dart
├── services/
│ ├── api_service.dart
│ └── auth_service.dart
└── main.dart
models/todo.dart:
class Todo {
final int id;
final String title;
final int userId;
final bool completed;
Todo({
required this.id,
required this.title,
required this.userId,
required this.completed,
});
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'],
title: json['title'],
userId: json['userId'],
completed: json['completed'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'userId': userId,
'completed': completed,
};
}
}
services/api_service.dart:
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/todo.dart';
class ApiService {
final String baseUrl = 'https://jsonplaceholder.typicode.com/todos';
Future<List<Todo>> fetchTodos() async {
final response = await http.get(Uri.parse(baseUrl));
if (response.statusCode == 200) {
List jsonResponse = json.decode(response.body);
return jsonResponse.map((todo) => Todo.fromJson(todo)).toList();
} else {
throw Exception('Failed to load todos');
}
}
Future<Todo> fetchTodoById(int id) async {
final response = await http.get(Uri.parse('$baseUrl/$id'));
if (response.statusCode == 200) {
return Todo.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to load todo');
}
}
Future<void> createTodo(Todo todo) async {
final response = await http.post(
Uri.parse(baseUrl),
headers: {'Content-Type': 'application/json'},
body: json.encode(todo.toJson()),
);
if (response.statusCode != 201) {
throw Exception('Failed to create todo');
}
}
Future<void> updateTodo(Todo todo) async {
final response = await http.put(
Uri.parse('$baseUrl/${todo.id}'),
headers: {'Content-Type': 'application/json'},
body: json.encode(todo.toJson()),
);
if (response.statusCode != 200) {
throw Exception('Failed to update todo');
}
}
Future<void> deleteTodo(int id) async {
final response = await http.delete(Uri.parse('$baseUrl/$id'));
if (response.statusCode != 200) {
throw Exception('Failed to delete todo');
}
}
}
services/auth_service.dart:
import 'package:shared_preferences/shared_preferences.dart';
class AuthService {
Future<void> login(String username, String password) async {
// Simulate a login API call
if (username == 'user' && password == 'password') {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isLoggedIn', true);
} else {
throw Exception('Invalid credentials');
}
}
Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('isLoggedIn');
}
Future<bool> isLoggedIn() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('isLoggedIn') ?? false;
}
}
viewmodels/login_viewmodel.dart:
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
class LoginViewModel extends ChangeNotifier {
final AuthService _authService = AuthService();
bool _isLoading = false;
bool get isLoading => _isLoading;
Future<void> login(String username, String password) async {
_isLoading = true;
notifyListeners();
try {
await _authService.login(username, password);
} catch (e) {
rethrow;
} finally {
_isLoading = false;
notifyListeners();
}
}
}
viewmodels/todo_viewmodel.dart:
import 'package:flutter/material.dart';
import '../models/todo.dart';
import '../services/api_service.dart';
class TodoViewModel extends ChangeNotifier {
final ApiService _apiService = ApiService();
List<Todo> _todos = [];
bool _isLoading = false;
List<Todo> get todos => _todos;
bool get isLoading => _isLoading;
TodoViewModel() {
fetchTodos();
}
Future<void> fetchTodos() async {
_isLoading = true;
notifyListeners();
try {
_todos = await _apiService.fetchTodos();
} catch (e) {
print(e);
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> addTodo(Todo todo) async {
await _apiService.createTodo(todo);
_todos.add(todo);
notifyListeners();
}
Future<void> updateTodo(Todo todo) async {
await _apiService.updateTodo(todo);
final index = _todos.indexWhere((t) => t.id == todo.id);
if (index != -1) {
_todos[index] = todo;
notifyListeners();
}
}
Future<void> deleteTodoById(int id) async {
await _apiService.deleteTodo(id);
_todos.removeWhere((todo) => todo.id == id);
notifyListeners();
}
}
views/login_view.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../viewmodels/login_viewmodel.dart';
class LoginView extends StatelessWidget {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
final viewModel = Provider.of<LoginViewModel>(context);
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _usernameController,
decoration: InputDecoration(labelText: 'Username'),
),
TextField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
SizedBox(height: 20),
viewModel.isLoading
? CircularProgressIndicator()
: ElevatedButton(
onPressed: () async {
try {
await viewModel.login(
_usernameController.text,
_passwordController.text,
);
Navigator.pushReplacementNamed(context, '/todos');
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Login failed')),
);
}
},
child: Text('Login'),
),
],
),
),
);
}
}
views/todo_list_view.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../viewmodels/todo_viewmodel.dart';
import '../models/todo.dart';
class TodoListView extends StatelessWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context) {
final viewModel = Provider.of<TodoViewModel>(context);
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: viewModel.isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: viewModel.todos.length,
itemBuilder: (context, index) {
final todo = viewModel.todos[index];
return ListTile(
title: Text(todo.title),
subtitle: Text(todo.userId.toString()),
trailing: Checkbox(
value: todo.completed,
onChanged: (bool? value) {
final updatedTodo = Todo(
id: todo.id,
title: todo.title,
userId: todo.userId,
completed: value ?? false,
);
viewModel.updateTodo(updatedTodo);
},
),
onTap: () {
Navigator.pushNamed(
context,
'/todoDetail',
arguments: todo,
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.pushNamed(context, '/addTodo');
},
child: Icon(Icons.add),
),
);
}
}
views/todo_detail_view.dart:
import 'package:flutter/material.dart';
import '../models/todo.dart';
class TodoDetailView extends StatelessWidget {
const TodoDetailView({super.key});
@override
Widget build(BuildContext context) {
final Todo todo = ModalRoute.of(context)!.settings.arguments as Todo;
return Scaffold(
appBar: AppBar(title: Text(todo.title)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
todo.title,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(
todo.userId.toString(),
style: TextStyle(fontSize: 18),
),
SizedBox(height: 20),
Row(
children: [
Text('Completed: '),
Checkbox(
value: todo.completed,
onChanged: (bool? value) {
// Handle checkbox change
},
),
],
),
],
),
),
);
}
}
main.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'viewmodels/login_viewmodel.dart';
import 'viewmodels/todo_viewmodel.dart';
import 'views/login_view.dart';
import 'views/todo_list_view.dart';
import 'views/todo_detail_view.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => LoginViewModel()),
ChangeNotifierProvider(create: (_) => TodoViewModel()),
],
child: MaterialApp(
title: 'Flutter MVVM Todo App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => LoginView(),
'/todos': (context) => TodoListView(),
'/todoDetail': (context) => TodoDetailView(),
},
),
);
}
}
flutter run
This Flutter MVVM todo application includes:
- A login screen where users can log in.
- A list of todo items fetched from a REST API.
- Detailed views for each todo item.
- CRUD operations (Create, Read, Update, Delete) for managing todo items.
You can further enhance this application by adding more features like user registration, better error handling, and more sophisticated UI elements. If you have any questions or need further assistance, feel free to ask!