In Flutter, the ViewModel is typically used in conjunction with the MVVM (Model-View-ViewModel) architecture pattern. It acts as a bridge between the UI (View) and the data layer (Model). Here are some common tasks you can do in a ViewModel in Flutter, along with examples:
- State Management The ViewModel manages the state of the UI components. This includes fetching data, managing loading states, and updating the UI.
Example:
class CounterViewModel extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
- Data Fetching The ViewModel can handle data fetching from APIs or databases.
Example:
class UserViewModel extends ChangeNotifier {
final ApiService apiService;
List<User> users = [];
bool isLoading = false;
UserViewModel(this.apiService);
Future<void> fetchUsers() async {
isLoading = true;
notifyListeners();
try {
users = await apiService.getUsers();
} catch (e) {
print("Error fetching users: $e");
} finally {
isLoading = false;
notifyListeners();
}
}
}
- Data Transformation The ViewModel can transform raw data into a format that is easier to use in the UI.
Example:
class ProductViewModel extends ChangeNotifier {
List<Product> products = [];
List<Product> get discountedProducts {
return products.where((product) => product.isDiscounted).toList();
}
void setProducts(List<Product> newProducts) {
products = newProducts;
notifyListeners();
}
}
- Error Handling The ViewModel can manage error states and provide error messages to the UI.
Example:
class LoginViewModel extends ChangeNotifier {
final AuthService authService;
bool isLoading = false;
String? errorMessage;
LoginViewModel(this.authService);
Future<void> login(String email, String password) async {
isLoading = true;
errorMessage = null;
notifyListeners();
try {
await authService.login(email, password);
} catch (e) {
errorMessage = "Login failed: $e";
} finally {
isLoading = false;
notifyListeners();
}
}
}
- Validation The ViewModel can perform validation logic for user inputs.
Example:
class FormViewModel extends ChangeNotifier {
String _email = '';
String _password = '';
String? _emailError;
String? _passwordError;
String? get emailError => _emailError;
String? get passwordError => _passwordError;
void setEmail(String email) {
_email = email;
_emailError = _validateEmail(email);
notifyListeners();
}
void setPassword(String password) {
_password = password;
_passwordError = _validatePassword(password);
notifyListeners();
}
String? _validateEmail(String email) {
if (email.isEmpty) {
return 'Email cannot be empty';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(email)) {
return 'Enter a valid email';
}
return null;
}
String? _validatePassword(String password) {
if (password.isEmpty) {
return 'Password cannot be empty';
}
if (password.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
}
}
- Dependency Injection The ViewModel can use dependency injection to get instances of services or repositories.
Example:
class HomeViewModel extends ChangeNotifier {
final UserService userService;
final ProductService productService;
HomeViewModel(this.userService, this.productService);
Future<void> initialize() async {
await fetchUsers();
await fetchProducts();
}
Future<void> fetchUsers() async {
// Fetch users logic
}
Future<void> fetchProducts() async {
// Fetch products logic
}
}
- Navigation The ViewModel can handle navigation logic, making it easier to test and manage.
Example:
class NavigationViewModel extends ChangeNotifier {
final NavigationService navigationService;
NavigationViewModel(this.navigationService);
void navigateToDetailPage(int itemId) {
navigationService.navigateTo('detail/$itemId');
}
}
Using ViewModel in the Widget Tree
Make sure you provide the ViewModel in the widget tree and use it in your widgets.
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterViewModel()),
ChangeNotifierProvider(create: (_) => UserViewModel(ApiService())),
// Add other ViewModels here
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counterViewModel = Provider.of<CounterViewModel>(context);
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Counter: ${counterViewModel.counter}'),
ElevatedButton(
onPressed: counterViewModel.increment,
child: Text('Increment'),
),
],
),
),
);
}
}
Filtering On feteched Api Data
Filtering Fetched Data in ViewModel
Suppose you have an API that fetches a list of products, and you want to filter the products based on their category. Here’s how you can do it:
Define the Product Model:
class Product {
final int id;
final String name;
final String category;
Product({
required this.id,
required this.name,
required this.category,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'],
name: json['name'],
category: json['category'],
);
}
}
Create the Product API Service:
class ApiService {
Future<List<Product>> fetchProducts() async {
final response = await http.get(Uri.parse('https://api.example.com/products'));
if (response.statusCode == 200) {
List<dynamic> data = json.decode(response.body);
return data.map((item) => Product.fromJson(item)).toList();
} else {
throw Exception('Failed to load products');
}
}
}
Implement the ViewModel:
class ProductViewModel extends ChangeNotifier {
final ApiService apiService;
List<Product> _products = [];
List<Product> _filteredProducts = [];
bool isLoading = false;
ProductViewModel({required this.apiService});
List<Product> get products => _filteredProducts;
Future<void> fetchProducts() async {
isLoading = true;
notifyListeners();
try {
_products = await apiService.fetchProducts();
_filteredProducts = _products;
} catch (e) {
print('Error fetching products: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
void filterProducts(String category) {
if (category == 'All') {
_filteredProducts = _products;
} else {
_filteredProducts = _products.where((product) => product.category == category).toList();
}
notifyListeners();
}
}
Use the ViewModel in the Widget Tree:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ProductViewModel(apiService: ApiService())),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ProductScreen(),
);
}
}
Build the Product Screen:
class ProductScreen extends StatefulWidget {
@override
_ProductScreenState createState() => _ProductScreenState();
}
class _ProductScreenState extends State<ProductScreen> {
String _selectedCategory = 'All';
@override
void initState() {
super.initState();
Future.microtask(() => Provider.of<ProductViewModel>(context, listen: false).fetchProducts());
}
@override
Widget build(BuildContext context) {
final productViewModel = Provider.of<ProductViewModel>(context);
return Scaffold(
appBar: AppBar(
title: Text('Products'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: DropdownButton<String>(
isExpanded: true,
value: _selectedCategory,
onChanged: (newValue) {
setState(() {
_selectedCategory = newValue!;
productViewModel.filterProducts(_selectedCategory);
});
},
items: ['All', 'Category1', 'Category2', 'Category3'].map((category) {
return DropdownMenuItem<String>(
value: category,
child: Text(category),
);
}).toList(),
),
),
Expanded(
child: productViewModel.isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: productViewModel.products.length,
itemBuilder: (context, index) {
final product = productViewModel.products[index];
return ListTile(
title: Text(product.name),
subtitle: Text(product.category),
);
},
),
),
],
),
);
}
}
Top comments (0)