Authentication
Stream binding with login/logout operations.
Repository
dart
class AuthRepository {
final _userController = StreamController<User?>.broadcast();
Stream<User?> get userStream => _userController.stream;
Future<void> login(String email, String password) async {
await Future.delayed(Duration(seconds: 1)); // Simulate network
if (email == 'test@test.com' && password == 'password') {
_userController.add(User(id: '1', email: email, name: 'Test User'));
} else {
throw Exception('Invalid credentials');
}
}
Future<void> logout() async {
await Future.delayed(Duration(milliseconds: 500));
_userController.add(null);
}
void dispose() => _userController.close();
}ViewModel
dart
import 'package:piper/piper.dart';
class AuthViewModel extends ViewModel {
final AuthRepository _auth;
AuthViewModel(this._auth);
// Bind user stream — updates automatically
late final user = bind(_auth.userStream, initial: null);
// Async state for login operation
late final loginState = asyncState<void>();
bool get isLoggedIn => user.value != null;
void login(String email, String password) {
load(loginState, () => _auth.login(email, password));
}
void logout() {
load(loginState, () => _auth.logout());
}
void clearError() {
loginState.setEmpty();
}
}Auth Gate Widget
dart
class AuthGate extends StatelessWidget {
@override
Widget build(BuildContext context) {
final vm = context.vm<AuthViewModel>();
return vm.user.build((user) {
if (user == null) {
return LoginPage();
}
return HomePage(user: user);
});
}
}Login Page
dart
class LoginPage extends StatefulWidget {
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final vm = context.vm<AuthViewModel>();
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
SizedBox(height: 24),
vm.loginState.build(
(state) => switch (state) {
AsyncLoading() => CircularProgressIndicator(),
AsyncError(:final message) => Column(
children: [
Text(message, style: TextStyle(color: Colors.red)),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => vm.login(
_emailController.text,
_passwordController.text,
),
child: Text('Try Again'),
),
],
),
_ => ElevatedButton(
onPressed: () => vm.login(
_emailController.text,
_passwordController.text,
),
child: Text('Login'),
),
},
),
],
),
),
);
}
}Home Page
dart
class HomePage extends StatelessWidget {
final User user;
const HomePage({required this.user});
@override
Widget build(BuildContext context) {
final vm = context.vm<AuthViewModel>();
return Scaffold(
appBar: AppBar(
title: Text('Home'),
actions: [
IconButton(
icon: Icon(Icons.logout),
onPressed: vm.logout,
),
],
),
body: Center(
child: Text('Welcome, ${user.name}!'),
),
);
}
}Setup
dart
void main() {
final authRepo = AuthRepository();
runApp(
ViewModelScope(
create: [() => AuthViewModel(authRepo)],
child: MaterialApp(home: AuthGate()),
),
);
}What's Happening
bind()— Binds the user stream to a StateHolderasyncState<void>()— Tracks login operation state (loading/error)load()— Manages the async operation lifecycle- Reactive UI — AuthGate rebuilds when user changes
- Auto cleanup — Stream subscription cancels on ViewModel dispose
