UMVVM library

Set of classes for Flutter app architecture.

Installing

dependencies: 
  umvvm: ^<latest version>

dev_dependencies: 
  umvvm_generator: ^<latest version>

You also need dependency for build_runner if you don't have it yet.

dev_dependencies: 
  build: ^<latest version>
  build_config: ^<latest version>
  build_runner: ^<latest version>

To build/rebuild generated files use:

flutter pub run build_runner build --delete-conflicting-outputs

Examples

Minimal example can be found here.

More complex examples can be found here.

There are small example with all basic components, example of using navigation and example of connecting database.

Docs

Here is small example demonstrating all components:

import 'package:dart_mappable/dart_mappable.dart';
import 'package:dio/dio.dart' as dio;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:umvvm/umvvm.dart';

part 'main.api.dart';
part 'main.mvvm.dart';
part 'main.mapper.dart';

class PostLikedEvent {
  final int id;

  const PostLikedEvent({
    required this.id,
  });
}

@MappableClass()
class Post with PostMappable {
  const Post({
    required this.title,
    required this.body,
    required this.id,
    this.isLiked = false,
  });

  final String? title;
  final String? body;
  final int? id;
  final bool isLiked;

  static const fromMap = PostMapper.fromMap;
}

@MappableClass()
class PostsState with PostsStateMappable {
  const PostsState({
    this.posts,
  });

  final StatefulData<List<Post>>? posts;
}

@mainApi
class Apis with ApisGen {}

@mainApp
class App extends UMvvmApp with AppGen {
  final apis = Apis();

  @override
  Future<void> initialize() async {
    await super.initialize();
  }
}

final app = App();

Future<void> main() async {
  await app.initialize();

  runApp(const MaterialApp(home: PostsListView()));
}

class HttpRequest<T> extends DioRequest<T> {
  @override
  RequestSettings get defaultSettings => RequestSettings(
        logPrint: (message) {
          print(message);
        },
        exceptionPrint: (error, trace) {
          print(error);
          print(trace);
        },
      );
}

@api
class PostsApi {
  HttpRequest<List<Post>> getPosts(int offset, int limit) =>
      HttpRequest<List<Post>>()
        ..method = RequestMethod.get
        ..baseUrl = 'http://jsonplaceholder.typicode.com'
        ..url = '/posts'
        ..parser = (result, headers) async {
          final list = <Post>[];

          result?.forEach((data) {
            list.add(Post.fromMap(data));
          });

          return list;
        };
}

@basicInstance
class PostsInteractor extends BaseInteractor<PostsState, Map<String, dynamic>?> {
  Future<void> loadPosts(int offset, int limit, {bool refresh = false}) async {
    updateState(state.copyWith(posts: const LoadingData()));

    late Response<List<Post>> response;

    if (refresh) {
      response = await executeAndCancelOnDispose(app.apis.posts.getPosts(0, limit));
    } else {
      response = await executeAndCancelOnDispose(app.apis.posts.getPosts(offset, limit));
    }

    if (response.isSuccessful) {
      updateState(state.copyWith(posts: SuccessData(result: response.result ?? [])));
    } else {
      updateState(state.copyWith(posts: ErrorData(error: response.error)));
    }
  }

  @override
  List<EventBusSubscriber> subscribe() => [
        on<PostLikedEvent>((event) {
          // update posts list...
        }),
      ];

  @override
  PostsState get initialState => const PostsState();
}

class PostsListViewState {}

class PostsListViewModel extends BaseViewModel<PostsListView, PostsListViewState> {
  @override
  DependentMvvmInstanceConfiguration get configuration =>
      DependentMvvmInstanceConfiguration(
        dependencies: [
          app.connectors.postsInteractorConnector(),
        ],
      );

  late final postsInteractor = getLocalInstance<PostsInteractor>();

  @override
  void onLaunch() {
    postsInteractor.loadPosts(0, 30, refresh: true);
  }

  void openPost(Post post) {
    // read more about navigation component in docs

    // app.navigation.routeTo(
    //   app.navigation.routes.post(
    //     post: post,
    //   ),
    //   forceGlobal: true,
    // );
  }

  void like(int id) {
    app.eventBus.send(PostLikedEvent(id: id));
  }

  Stream<StatefulData<List<Post>>?> get postsStream => postsInteractor.updates((state) => state.posts);

  @override
  PostsListViewState get initialState => PostsListViewState();
}

class PostsListView extends BaseWidget {
  const PostsListView({
    super.key,
    super.viewModel,
  });

  @override
  State<StatefulWidget> createState() {
    return _PostsListViewWidgetState();
  }
}

class _PostsListViewWidgetState extends BaseView<PostsListView, PostsListViewState, PostsListViewModel> {
  @override
  Widget buildView(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromARGB(255, 232, 232, 232),
      appBar: AppBar(title: const Text('Posts')),
      body: StreamBuilder<StatefulData<List<Post>>?>(
        stream: viewModel.postsStream,
        builder: (context, snapshot) {
          if (snapshot.hasData && snapshot.data != null) {
            return buildList(snapshot.data!);
          }

          return Container();
        },
      ),
    );
  }

  Widget buildList(StatefulData<List<Post>> data) {
    switch (data) {
      case LoadingData():
        return const Center(child: CircularProgressIndicator());
      case SuccessData<List<Post>>(:final result):
        return ListView.builder(
          itemBuilder: (context, index) {
            final item = result[index];

            return PostCard(
              onTap: () {
                viewModel.openPost(item);
              },
              onLikeTap: () {
                viewModel.like(item.id ?? 1);
              },
              title: item.title ?? '',
              body: item.body ?? '',
              isLiked: item.isLiked,
            );
          },
          itemCount: result.length,
        );
      case ErrorData<List<Post>>(:final error):
        return Text(error.toString());
    }
  }

  @override
  PostsListViewModel createViewModel() => PostsListViewModel();
}

// PostCard widget code ... 

Learn about components:

overall

Global layer

Data Layer

Domain Layer

Presentation layer

Utility

Other materials

Important note

To generage test coverage report run sh coverage.sh.

Libraries

annotations/annotations
annotations/api
annotations/main_api
annotations/main_app
annotations/mvvm_instance
arch/base/base
arch/base/base_instance_part
arch/base/base_interactor
arch/base/base_wrapper
arch/base/event_bus
arch/base/event_bus_receiver
arch/base/mvvm_instance
arch/base/observable
arch/base/stateful_instance
arch/base/store
arch/base/umvvm_app
arch/base_events
arch/data/data
arch/data/field_validation_state
arch/data/result_state
arch/data/stateful_data
arch/di/base_scopes
arch/di/connector
arch/di/dependent_instance
arch/di/di
arch/di/instance_collection
arch/di/module
arch/di/scoped_container
arch/exceptions/exceptions
arch/exceptions/illegal_argument_exception
arch/exceptions/illegal_state_exception
arch/exceptions/not_recognized_http_exception
arch/http/apis_caller
arch/http/base_request
arch/http/dio_request
arch/http/http
arch/http/requests_collection
arch/http/simulate_response
arch/navigation/annotations/annotations
arch/navigation/annotations/routes
arch/navigation/base/default_navigation_route_builder
arch/navigation/base/stack/base_navigation_stack
arch/navigation/base/stack/tab_navigation_stack
arch/navigation/base/view/tab_navigator_initializer
arch/navigation/base_navigation_interactor
arch/navigation/declaration/routes_base
arch/navigation/model/route
arch/navigation/model/route_settings
arch/navigation/settings
arch/navigation/utilities/bottom_sheet_route
arch/navigation/utilities/dialog_route
arch/navigation/utilities/willpop_cupertino_page_route
arch/utility/debouncer
arch/utility/form_view_model_mixin
arch/utility/on_become_visible
arch/utility/use_disposable_mixin
arch/view/base_view
arch/view/base_view_model
arch/view/base_widget
arch/view/view
umvvm