turbo_mvvm 1.1.0 copy "turbo_mvvm: ^1.1.0" to clipboard
turbo_mvvm: ^1.1.0 copied to clipboard

A lightweight MVVM state management solution inspired by the FilledStacks Stacked package.

example/lib/main.dart

import 'dart:async';

import 'package:turbo_mvvm_example/views/second/second_veto_view.dart';
import 'package:turbo_mvvm_example/views/second/second_veto_view_arguments.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:turbo_mvvm/turbo_mvvm.dart';

import 'data/constants/const_durations.dart';
import 'data/enums/mounted_status.dart';

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

class FirstVetoView extends StatelessWidget {
  const FirstVetoView({super.key});
  static const String route = 'first-veto-view';

  /// Original width of the fictional design file that we got from our designer.
  ///
  /// Play around with this value to demonstrate the [TViewModel.scaledWidth] property.
  static const _originalDesignWidth = 390.0;

  /// Original height of the fictional design file that we got from our designer.
  ///
  /// Play around with this value to demonstrate the [TViewModel.scaledHeight] property.
  static const _originalDesignHeight = 844.0;

  @override
  Widget build(BuildContext context) {
    return TViewModelBuilder<FirstVetoViewModel>(
      builder: (context, model, isInitialised, child) => MediaQuery(
        data: model.media.copyWith(
          textScaler: TextScaler.linear(
            model.textScaler.scale(1.0).clamp(1, 1.35),
          ),
        ),
        child: Scaffold(
          appBar: AppBar(title: const Text('Turbo MVVM Test App')),
          body: GestureDetector(
            onTap: model.focusNode.unfocus,
            child: Stack(
              children: [
                Positioned.fill(
                  child: LayoutBuilder(
                    builder: (context, constraints) => AnimatedSwitcher(
                      duration: ConstDurations.defaultAnimationDuration,
                      child: isInitialised
                          ? ValueListenableBuilder<bool>(
                              valueListenable: model.isBusy,
                              builder: (context, isBusy, child) => AnimatedSwitcher(
                                duration:
                                    ConstDurations.defaultAnimationDuration,
                                child: isBusy
                                    ? Column(
                                        mainAxisAlignment:
                                            MainAxisAlignment.center,
                                        children: [
                                          ValueListenableBuilder<int>(
                                            valueListenable: model.busySeconds,
                                            builder:
                                                (
                                                  context,
                                                  busySeconds,
                                                  child,
                                                ) => Text(
                                                  'This model is busy for another $busySeconds seconds...',
                                                  style: model
                                                      .textTheme
                                                      .titleMedium!
                                                      .copyWith(
                                                        fontWeight:
                                                            FontWeight.bold,
                                                      ),
                                                ),
                                          ),
                                          const SizedBox(height: 16),
                                          const CircularProgressIndicator(),
                                        ],
                                      )
                                    : ValueListenableBuilder<bool>(
                                        valueListenable: model.hasError,
                                        builder: (context, hasError, child) => AnimatedContainer(
                                          height: constraints.maxHeight,
                                          curve: Curves.decelerate,
                                          duration: ConstDurations
                                              .defaultAnimationDuration,
                                          decoration: BoxDecoration(
                                            color: hasError
                                                ? Colors.red
                                                : Colors.transparent,
                                          ),
                                          child: Container(
                                            margin: const EdgeInsets.all(16),
                                            decoration: BoxDecoration(
                                              color: model.theme.canvasColor,
                                              borderRadius:
                                                  BorderRadius.circular(32),
                                            ),
                                            child: SingleChildScrollView(
                                              physics:
                                                  const BouncingScrollPhysics(),
                                              child: Column(
                                                mainAxisAlignment:
                                                    MainAxisAlignment.start,
                                                children: <Widget>[
                                                  SizedBox(
                                                    height: model.textScaled(
                                                      value: 40,
                                                      context: context,
                                                    ),
                                                  ),
                                                  Text(
                                                    'First Veto View',
                                                    style: model
                                                        .textTheme
                                                        .headlineSmall!
                                                        .copyWith(
                                                          fontWeight:
                                                              FontWeight.bold,
                                                        ),
                                                  ),
                                                  Text(
                                                    'Made by Yyhwach_ and me',
                                                    style: model
                                                        .textTheme
                                                        .titleSmall!
                                                        .copyWith(
                                                          fontWeight:
                                                              FontWeight.bold,
                                                        ),
                                                  ),
                                                  SizedBox(
                                                    height: model.textScaled(
                                                      value: 16,
                                                      context: context,
                                                    ),
                                                  ),
                                                  Padding(
                                                    padding:
                                                        const EdgeInsets.symmetric(
                                                          horizontal: 16,
                                                        ),
                                                    child: Row(
                                                      mainAxisAlignment:
                                                          MainAxisAlignment
                                                              .spaceEvenly,
                                                      children: [
                                                        Flexible(
                                                          child: Column(
                                                            children: [
                                                              Text(
                                                                'ViewModel',
                                                                maxLines: 1,
                                                                overflow:
                                                                    TextOverflow
                                                                        .ellipsis,
                                                                style: Theme.of(context)
                                                                    .textTheme
                                                                    .bodyLarge!
                                                                    .copyWith(
                                                                      fontWeight:
                                                                          FontWeight
                                                                              .bold,
                                                                    ),
                                                              ),
                                                              Text(
                                                                '${model.modelCounter}',
                                                                style: Theme.of(
                                                                  context,
                                                                ).textTheme.headlineMedium,
                                                              ),
                                                            ],
                                                          ),
                                                        ),
                                                        ValueListenableBuilder<
                                                          int
                                                        >(
                                                          valueListenable: model
                                                              .valueListenableCounter,
                                                          builder:
                                                              (
                                                                context,
                                                                valueListenableCounter,
                                                                child,
                                                              ) => Flexible(
                                                                child: Column(
                                                                  children: [
                                                                    Text(
                                                                      'ValueNotifier',
                                                                      maxLines:
                                                                          1,
                                                                      overflow:
                                                                          TextOverflow
                                                                              .ellipsis,
                                                                      style: Theme.of(context)
                                                                          .textTheme
                                                                          .bodyLarge!
                                                                          .copyWith(
                                                                            fontWeight:
                                                                                FontWeight.bold,
                                                                          ),
                                                                    ),
                                                                    Text(
                                                                      valueListenableCounter
                                                                          .toString(),
                                                                      style: Theme.of(
                                                                        context,
                                                                      ).textTheme.headlineMedium,
                                                                    ),
                                                                  ],
                                                                ),
                                                              ),
                                                        ),
                                                      ],
                                                    ),
                                                  ),
                                                  SizedBox(
                                                    height: model.textScaled(
                                                      value: 16,
                                                      context: context,
                                                    ),
                                                  ),
                                                  Padding(
                                                    padding:
                                                        const EdgeInsets.symmetric(
                                                          horizontal: 16,
                                                        ),
                                                    child: Row(
                                                      mainAxisAlignment:
                                                          MainAxisAlignment
                                                              .spaceBetween,
                                                      children: [
                                                        FloatingActionButton(
                                                          onPressed: model
                                                              .incrementModelCounter,
                                                          backgroundColor:
                                                              Colors.red,
                                                          heroTag: '1',
                                                          child: const Icon(
                                                            Icons.add,
                                                          ),
                                                        ),
                                                        FloatingActionButton(
                                                          onPressed: model
                                                              .incrementValueNotifierCounter,
                                                          backgroundColor:
                                                              Colors.blue,
                                                          heroTag: '2',
                                                          child: const Icon(
                                                            Icons.add,
                                                          ),
                                                        ),
                                                      ],
                                                    ),
                                                  ),
                                                  Padding(
                                                    padding:
                                                        const EdgeInsets.symmetric(
                                                          vertical: 16,
                                                        ),
                                                    child: ElevatedButton(
                                                      onPressed: model.reset,
                                                      child: const Text(
                                                        'Reset',
                                                      ),
                                                    ),
                                                  ),
                                                  Padding(
                                                    padding:
                                                        const EdgeInsets.symmetric(
                                                          horizontal: 16,
                                                        ),
                                                    child: Row(
                                                      mainAxisAlignment:
                                                          MainAxisAlignment
                                                              .spaceBetween,
                                                      children: [
                                                        FloatingActionButton(
                                                          onPressed: model
                                                              .decrementModelCounter,
                                                          backgroundColor:
                                                              Colors.red,
                                                          heroTag: '3',
                                                          child: const Icon(
                                                            Icons.remove,
                                                          ),
                                                        ),
                                                        FloatingActionButton(
                                                          onPressed: model
                                                              .decrementValueNotifierCounter,
                                                          backgroundColor:
                                                              Colors.blue,
                                                          heroTag: '4',
                                                          child: const Icon(
                                                            Icons.remove,
                                                          ),
                                                        ),
                                                      ],
                                                    ),
                                                  ),
                                                  const SizedBox(height: 16),
                                                  ElevatedButton(
                                                    style: ButtonStyle(
                                                      backgroundColor:
                                                          WidgetStateProperty.all(
                                                            Colors.pink,
                                                          ),
                                                    ),
                                                    onPressed:
                                                        model.pushSecondView,
                                                    child: const Text(
                                                      'Push Arguments',
                                                    ),
                                                  ),
                                                  const SizedBox(height: 16),
                                                  Padding(
                                                    padding:
                                                        const EdgeInsets.symmetric(
                                                          horizontal: 16,
                                                        ),
                                                    child: Row(
                                                      children: [
                                                        ValueListenableBuilder<
                                                          MountedStatus
                                                        >(
                                                          valueListenable: model
                                                              .mountedStatus,
                                                          builder:
                                                              (
                                                                context,
                                                                mountedStatus,
                                                                child,
                                                              ) => Expanded(
                                                                child: Text(
                                                                  'Mounted: ${mountedStatus.name.capitalizeFirstLetter}',
                                                                ),
                                                              ),
                                                        ),
                                                        ElevatedButton(
                                                          style: ButtonStyle(
                                                            backgroundColor:
                                                                WidgetStateProperty.all(
                                                                  Colors.orange,
                                                                ),
                                                          ),
                                                          onPressed: model
                                                              .updateMountedStatus,
                                                          child: const Text(
                                                            'Check status',
                                                          ),
                                                        ),
                                                      ],
                                                    ),
                                                  ),
                                                  const SizedBox(height: 16),
                                                  ElevatedButton(
                                                    style: ButtonStyle(
                                                      backgroundColor:
                                                          WidgetStateProperty.all(
                                                            Colors.yellow,
                                                          ),
                                                    ),
                                                    onPressed:
                                                        model.triggerBusy,
                                                    child: Text(
                                                      'Trigger Busy',
                                                      style: model
                                                          .textTheme
                                                          .bodyLarge!
                                                          .copyWith(
                                                            color: Colors.black,
                                                          ),
                                                    ),
                                                  ),
                                                  const SizedBox(height: 16),
                                                  Padding(
                                                    padding:
                                                        const EdgeInsets.symmetric(
                                                          horizontal: 16,
                                                        ),
                                                    child: ValueListenableBuilder<bool>(
                                                      valueListenable:
                                                          model.hasError,
                                                      builder: (context, hasError, child) => Row(
                                                        mainAxisAlignment:
                                                            MainAxisAlignment
                                                                .spaceBetween,
                                                        children: [
                                                          AnimatedOpacity(
                                                            duration: ConstDurations
                                                                .defaultAnimationDuration,
                                                            opacity: hasError
                                                                ? 0.3
                                                                : 1,
                                                            child: IgnorePointer(
                                                              ignoring:
                                                                  hasError,
                                                              child: ElevatedButton(
                                                                style: ButtonStyle(
                                                                  backgroundColor:
                                                                      WidgetStateProperty.all(
                                                                        Colors
                                                                            .black,
                                                                      ),
                                                                ),
                                                                onPressed: model
                                                                    .triggerError,
                                                                child: const Text(
                                                                  'Trigger Error',
                                                                ),
                                                              ),
                                                            ),
                                                          ),
                                                          AnimatedOpacity(
                                                            duration: ConstDurations
                                                                .defaultAnimationDuration,
                                                            opacity: !hasError
                                                                ? 0.3
                                                                : 1,
                                                            child: IgnorePointer(
                                                              ignoring:
                                                                  !hasError,
                                                              child: ElevatedButton(
                                                                style: ButtonStyle(
                                                                  backgroundColor:
                                                                      WidgetStateProperty.all(
                                                                        Colors
                                                                            .black,
                                                                      ),
                                                                ),
                                                                onPressed: model
                                                                    .removeError,
                                                                child: const Text(
                                                                  'Remove Error',
                                                                ),
                                                              ),
                                                            ),
                                                          ),
                                                        ],
                                                      ),
                                                    ),
                                                  ),
                                                  AnimatedContainer(
                                                    duration: ConstDurations
                                                        .defaultAnimationDuration,
                                                    margin:
                                                        const EdgeInsets.only(
                                                          top: 16,
                                                        ),
                                                    decoration: BoxDecoration(
                                                      color: Colors.blueAccent,
                                                      borderRadius:
                                                          BorderRadius.circular(
                                                            32,
                                                          ),
                                                    ),
                                                    width: model.scaledWidth(
                                                      value: 200,
                                                      originalDesignWidth:
                                                          _originalDesignWidth,
                                                    ),
                                                    height: model.scaledHeight(
                                                      value: 20,
                                                      originalDesignHeight:
                                                          _originalDesignHeight,
                                                    ),
                                                    child: Row(
                                                      children: [
                                                        Expanded(
                                                          child: Text(
                                                            'Scaled Container',
                                                            textAlign: TextAlign
                                                                .center,
                                                            style: model
                                                                .textTheme
                                                                .titleSmall!
                                                                .copyWith(
                                                                  color: Colors
                                                                      .white,
                                                                ),
                                                          ),
                                                        ),
                                                      ],
                                                    ),
                                                  ),
                                                ],
                                              ),
                                            ),
                                          ),
                                        ),
                                      ),
                              ),
                            )
                          : const Center(child: CircularProgressIndicator()),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
      viewModelBuilder: () => FirstVetoViewModel.locate,
    );
  }
}

class FirstVetoViewModel extends TBaseViewModel<Object?>
    with TBusyManagement, TErrorManagement, TViewModelHelpers {
  int _modelCounter = 0;
  int get modelCounter => _modelCounter;

  final ValueNotifier<int> _valueNotifierCounter = ValueNotifier(0);
  ValueListenable<int> get valueListenableCounter => _valueNotifierCounter;

  final ValueNotifier<MountedStatus> _mountedStatus = ValueNotifier(
    MountedStatus.unknown,
  );
  ValueListenable<MountedStatus> get mountedStatus => _mountedStatus;

  final ValueNotifier<int> _busySeconds = ValueNotifier(0);
  ValueListenable<int> get busySeconds => _busySeconds;

  Timer? _busyTimer;

  @override
  Future<void> initialise({bool doSetInitialised = true}) async {
    _log('Initialising..');
    _log('Calling addPostFrameCallback with dummy log to showcase usage!');
    await wait(1000);
    addPostFrameCallback((timeStamp) {
      _log('addPostFrameCallback dummy log!');
    });
    _log('Logging the media value to showcase it\'s usage:');
    _log('model.media: $media');
    await super.initialise(doSetInitialised: doSetInitialised);
    _log('Initialised!');
  }

  @override
  Future<void> dispose() async {
    _valueNotifierCounter.dispose();
    super.dispose();
  }

  /// Increments the [modelCounter], and then rebuilds the UI.
  void incrementModelCounter() {
    _modelCounter++;
    rebuild();
  }

  /// If the [modelCounter] is greater than 0, decrement the [modelCounter] and rebuild the UI.
  void decrementModelCounter() {
    if (modelCounter > 0) {
      _modelCounter--;
      rebuild();
    }
  }

  /// Increments the value of the [_valueNotifierCounter] by one.
  void incrementValueNotifierCounter() => _valueNotifierCounter.value++;

  /// If the value of the [_valueNotifierCounter] is greater than 0 decrement the counter.
  void decrementValueNotifierCounter() {
    if (_valueNotifierCounter.value > 0) {
      _valueNotifierCounter.value--;
    }
  }

  /// Resets both counters and mounted status to zero.
  void reset() {
    _valueNotifierCounter.value = 0;
    _modelCounter = 0;
    _mountedStatus.value = MountedStatus.unknown;
    setError(false);
    rebuild();
  }

  /// Pushes the second view with counter arguments.
  void pushSecondView() {
    Navigator.of(context!).push(
      MaterialPageRoute(
        builder: (context) => SecondVetoView(
          secondVetoViewArguments: SecondVetoViewArguments(
            firstCounterValue: _modelCounter,
            secondCounterValue: _valueNotifierCounter.value,
          ),
        ),
      ),
    );
  }

  /// Updates the current mounted status to [_mountedStatus] for demonstration purposes.
  void updateMountedStatus() => _mountedStatus.value = isMounted
      ? MountedStatus.mounted
      : MountedStatus.unmounted;

  /// Triggers the busy state for demonstration purposes.
  void triggerBusy() {
    _busySeconds.value = 3;
    setBusy(true);
    _busyTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
      _busySeconds.value--;
      if (busySeconds.value == 0) {
        _busyTimer!.cancel();
        _busyTimer = null;
        setBusy(false);
      }
    });
  }

  /// Triggers the error state for demonstration purposes.
  void triggerError() => setError(true);

  /// Removes the busy state for demonstration purposes.
  void removeError() => setError(false);

  void _log(Object message) => debugPrint('💡 [INFO] $message');

  /// Provides the current [TViewModelBuilderState]'s [ThemeData].
  ThemeData get theme => Theme.of(context!);

  /// Provides the current [TViewModelBuilderState]'s [TextTheme].
  TextTheme get textTheme => Theme.of(context!).textTheme;

  /// Provides the current [TViewModelBuilderState]'s [MediaQueryData].
  MediaQueryData get media => MediaQuery.of(context!);

  /// Provides the current [TViewModelBuilderState]'s [MediaQueryData.textScaler].
  TextScaler get textScaler => MediaQuery.of(context!).textScaler;

  /// Provides the current [TViewModelBuilderState]'s [MediaQueryData.textScaler] scale factor.
  @Deprecated('Use textScaler.scale(1.0) instead')
  double get textScaleFactor => textScaler.scale(1.0);

  /// Provides a scaled value based on given [value] and [textScaler].
  double textScaled({required double value, BuildContext? context}) =>
      value *
      (context == null
          ? textScaler.scale(1.0)
          : MediaQuery.textScalerOf(context).scale(1.0));

  /// Provides the current [TViewModelBuilderState]'s [FocusNode].
  FocusNode get focusNode => FocusScope.of(context!);

  /// Provides the current [TViewModelBuilderState]'s [MediaQueryData]'s [Size.width].
  double get width => MediaQuery.of(context!).size.width;

  /// Provides the current [TViewModelBuilderState]'s [MediaQueryData]'s [Size.height].
  double get height => MediaQuery.of(context!).size.height;

  /// Provides a design scaled value based on given [value], [width] and given [originalDesignWidth].
  ///
  /// Where [originalDesignWidth] is the width of the screen in your original UI design file.
  double scaledWidth({
    required double value,
    required double originalDesignWidth,
  }) => value * (width / originalDesignWidth);

  /// Provides a design scaled value based on given [value], [height] and given [originalDesignHeight].
  ///
  /// Where [originalDesignHeight] is the height of the screen in your original UI design file.
  double scaledHeight({
    required double value,
    required double originalDesignHeight,
  }) => value * (height / originalDesignHeight);

  static FirstVetoViewModel get locate => FirstVetoViewModel();
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Turbo MVVM',
      theme: ThemeData(primarySwatch: Colors.green),
      home: const FirstVetoView(),
    );
  }
}

extension on String {
  String get capitalizeFirstLetter => '${this[0].toUpperCase()}${substring(1)}';
}
0
likes
160
points
113
downloads

Publisher

verified publisherultrawideturbodev.com

Weekly Downloads

A lightweight MVVM state management solution inspired by the FilledStacks Stacked package.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, provider

More

Packages that depend on turbo_mvvm