Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Optimistic State" architecture cookbook recipe #11394

Merged
merged 17 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Take our settings from the example_utils analysis_options.yaml file.
# If necessary for a particular example, this file can also include
# overrides for individual lints.

include: package:example_utils/analysis.yaml
172 changes: 172 additions & 0 deletions examples/app-architecture/optimistic-state/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
// #docregion SubscribeButton
child: SubscribeButton(
viewModel: SubscribeButtonViewModel(
subscriptionRepository: SubscriptionRepository(),
),
),
// #enddocregion SubscribeButton
),
),
);
}
}

/// A button that simulates a subscription action.
/// For example, subscribing to a newsletter or a streaming channel.
// #docregion Widget
class SubscribeButton extends StatefulWidget {
const SubscribeButton({
super.key,
required this.viewModel,
});

/// Subscribe button view model.
final SubscribeButtonViewModel viewModel;

@override
State<SubscribeButton> createState() => _SubscribeButtonState();
}
// #enddocregion Widget

class _SubscribeButtonState extends State<SubscribeButton> {
// #docregion listener1
@override
void initState() {
super.initState();
widget.viewModel.addListener(_onViewModelChange);
}

@override
void dispose() {
widget.viewModel.removeListener(_onViewModelChange);
super.dispose();
}
// #enddocregion listener1

// #docregion build
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.viewModel,
builder: (context, _) {
return FilledButton(
onPressed: widget.viewModel.subscribe,
style: widget.viewModel.subscribed
? SubscribeButtonStyle.subscribed
: SubscribeButtonStyle.unsubscribed,
child: widget.viewModel.subscribed
? const Text('Subscribed')
: const Text('Subscribe'),
);
},
);
}
// #enddocregion build

// #docregion listener2
/// Listen to ViewModel changes.
void _onViewModelChange() {
// If the subscription action has failed
if (widget.viewModel.error) {
// Reset the error state
widget.viewModel.error = false;
// Show an error message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to subscribe'),
),
);
}
}
// #enddocregion listener2
}

// #docregion style
class SubscribeButtonStyle {
static const unsubscribed = ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Colors.red),
);

static const subscribed = ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Colors.green),
);
}
// #enddocregion style

// #docregion ViewModelFull
/// Subscribe button View Model.
/// Handles the subscribe action and exposes the state to the subscription.
// #docregion ViewModelStart
class SubscribeButtonViewModel extends ChangeNotifier {
SubscribeButtonViewModel({
required this.subscriptionRepository,
});

final SubscriptionRepository subscriptionRepository;
// #enddocregion ViewModelStart

// #docregion States
// Whether the user is subscribed
bool subscribed = false;

// Whether the subscription action has failed
bool error = false;
// #enddocregion States

// #docregion subscribe
// Subscription action
Future<void> subscribe() async {
// Ignore taps when subscribed
if (subscribed) {
return;
}

// Optimistic state.
// It will be reverted if the subscription fails.
subscribed = true;
// Notify listeners to update the UI
notifyListeners();

try {
await subscriptionRepository.subscribe();
} catch (e) {
print('Failed to subscribe: $e');
// Revert to the previous state
subscribed = false;
// Set the error state
error = true;
} finally {
notifyListeners();
}
}
// #enddocregion subscribe
}
// #enddocregion ViewModelFull

/// Repository of subscriptions.
// #docregion SubscriptionRepository
class SubscriptionRepository {
/// Simulates a network request and then fails.
Future<void> subscribe() async {
// Simulate a network request
await Future.delayed(const Duration(seconds: 1));
// Fail after one second
throw Exception('Failed to subscribe');
}
}
// #enddocregion SubscriptionRepository
27 changes: 27 additions & 0 deletions examples/app-architecture/optimistic-state/lib/starter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';

// #docregion Starter
class SubscribeButton extends StatefulWidget {
const SubscribeButton({
super.key,
});

@override
State<SubscribeButton> createState() => _SubscribeButtonState();
}

class _SubscribeButtonState extends State<SubscribeButton> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

class SubscribeButtonViewModel extends ChangeNotifier {

}

class SubscriptionRepository {

}
// #enddocregion Starter
16 changes: 16 additions & 0 deletions examples/app-architecture/optimistic-state/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: optimistic_state
description: Example for optimistic_state cookbook recipe

environment:
sdk: ^3.5.0

dependencies:
flutter:
sdk: flutter

dev_dependencies:
example_utils:
path: ../../example_utils

flutter:
uses-material-design: true
10 changes: 10 additions & 0 deletions src/content/app-architecture/cookbook/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: Architecture Cookbook Recipes
description: A catalog of recipes for application architecture patterns.
---

This cookbook contains recipes for Flutter apps following the architecture
guidelines. Each recipe is self-contained and can be used as a reference to
help you build up an application.

- [Optimistic State](/app-architecture/cookbook/optimistic-state)
Loading
Loading