Mobile Development 13 min read

Implementing ViewModel with Riverpod in Flutter for UI‑Logic Separation

This article introduces the ViewModel concept and demonstrates how to use Riverpod 2.0's NotifierProvider in Flutter to separate UI from business logic, providing step‑by‑step code examples, including model, ViewModel, view, and a complete login page implementation.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing ViewModel with Riverpod in Flutter for UI‑Logic Separation

The article explains how to introduce the ViewModel pattern into a Flutter project and use Riverpod 2.0's NotifierProvider to achieve a clean separation between UI and business logic.

Concept : ViewModel acts as a bridge between the View (widgets) and the Model (data). It holds the data required by the UI and exposes commands (e.g., callbacks) while listening to model changes. This follows the MVVM architecture.

Model example :

class UserModel {
  final String id;
  final String name;
  final String email;

  UserModel({required this.id, required this.name, required this.email});

  factory UserModel.fromJson(Map
json) {
    return UserModel(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }
}

ViewModel example (extends ChangeNotifier and fetches data with Dio):

class UserViewModel extends ChangeNotifier {
  UserModel? _user;

  UserModel? get user => _user;

  void fetchUserData() async {
    var response = await Dio().get("https://mock.apifox.com/m1/4081539-3719383-default/flutter_article/testUser");
    Map
? responseObject = response.data;
    _user = UserModel.fromJson(responseObject?['data']);
    notifyListeners();
  }
}

View example using ChangeNotifierProvider to bind the ViewModel to the UI:

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

class MvvmApp extends StatelessWidget {
  const MvvmApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => UserViewModel(),
      child: const MaterialApp(home: UserView()),
    );
  }
}

class UserView extends StatelessWidget {
  const UserView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final viewModel = Provider.of
(context);
    return Scaffold(
      appBar: AppBar(title: const Text('User')),
      body: Center(
        child: viewModel.user == null
            ? ElevatedButton(
                onPressed: () => viewModel.fetchUserData(),
                child: const Text('Load User'),
              )
            : Text("Hello, ${viewModel.user!.name}, your email is ${viewModel.user!.email}"),
      ),
    );
  }
}

The article then moves to a practical login‑page example. It defines a LoginInfo model, a LoginPageState that holds controllers and optional login data, and a Riverpod @riverpod annotated LoginPageVM that manages the state and performs the login request.

class LoginInfo {
  final String userName;
  final String loginTime;

  LoginInfo({required this.userName, required this.loginTime});

  factory LoginInfo.fromJson(Map
json) {
    return LoginInfo(
      userName: json['userName'],
      loginTime: json['loginTime'],
    );
  }
}

class LoginPageState {
  final TextEditingController userNameController;
  final TextEditingController passwordController;
  final LoginInfo? loginInfo;

  LoginPageState({this.loginInfo, required this.userNameController, required this.passwordController});

  LoginPageState.initial()
      : userNameController = TextEditingController(),
        passwordController = TextEditingController(),
        loginInfo = null;

  LoginPageState copyWith({TextEditingController? userNameController, TextEditingController? passwordController, LoginInfo? loginInfo}) {
    return LoginPageState(
      userNameController: userNameController ?? this.userNameController,
      passwordController: passwordController ?? this.passwordController,
      loginInfo: loginInfo ?? this.loginInfo,
    );
  }
}

@riverpod
class LoginPageVM extends _${'$'}LoginPageVM {
  @override
  LoginPageState build() => LoginPageState.initial();

  Future
login() async {
    final userName = state.userNameController.text;
    final password = state.passwordController.text;
    if (userName.isEmpty || password.isEmpty) {
      showSnackBar("请输入帐号或密码");
    } else {
      var response = await Dio().post(
        "https://mock.apifox.com/m1/4081539-3719383-default/flutter_article/testLogin",
        data: {'username': userName, 'password': password},
      );
      var data = response.data['data'];
      if (response.data['errorCode'] == 200) {
        var loginInfo = LoginInfo.fromJson(data);
        state = state.copyWith(loginInfo: loginInfo);
        showSnackBar("登录成功");
        pop(result: loginInfo);
      } else {
        showSnackBar("登录失败");
      }
    }
  }
}

The UI page ( LoginPage ) uses ref.watch(loginPageVMProvider) to obtain the current state and ref.watch(loginPageVMProvider.notifier) to trigger the login() method. The main app defines a global navigatorKey to allow showing snackbars and popping routes from anywhere.

final GlobalKey
navigatorKey = GlobalKey
();

void showSnackBar(String message) {
  if (navigatorKey.currentContext != null) {
    ScaffoldMessenger.of(navigatorKey.currentContext!).showSnackBar(SnackBar(content: Text(message)));
  }
}

void pop
({T? result}) {
  if (navigatorKey.currentContext != null) {
    if (result != null) {
      Navigator.pop(navigatorKey.currentContext!, result);
    } else {
      Navigator.pop(navigatorKey.currentContext!);
    }
  }
}

void main() {
  ApiClient.init("https://mock.apifox.com/m1/4081539-3719383-default/flutter_article/");
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: const HomePage(), navigatorKey: navigatorKey);
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State
createState() => _HomePageState();
}

class _HomePageState extends State
{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: Consumer(builder: (context, ref, child) {
          LoginInfo? loginInfo = ref.watch(loginPageVMProvider.select((value) => value.loginInfo));
          return loginInfo == null
              ? ElevatedButton(
                  onPressed: () {
                    Navigator.push(context, MaterialPageRoute(builder: (context) => const LoginPage()));
                  },
                  child: const Text('去登录'),
                )
              : Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text('用户名:${loginInfo.userName}'),
                    Text('登录时间:${loginInfo.loginTime}'),
                  ],
                );
        }),
      ),
    );
  }
}

Finally, the article summarizes that using Riverpod’s lazy loading and AutoDisposeNotifier ensures providers are only instantiated when needed and automatically cleared when no longer observed, providing efficient state management for Flutter mobile applications.

Fluttermobile developmentViewModelstate managementMVVMRiverpod
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.