ui backbone

This commit is contained in:
Mehul Ahal 2022-12-20 21:52:24 +01:00
parent 3e374d24f6
commit b7045fc242
24 changed files with 918 additions and 73 deletions

View file

@ -12,6 +12,8 @@ abstract class AppSetup {
static Future<void> initialise() async {
WidgetsFlutterBinding.ensureInitialized();
await _setupStrings();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
@ -38,7 +40,7 @@ abstract class AppSetup {
return supportedLocales.first;
}
static Future<void> setupStrings() async {
static Future<void> _setupStrings() async {
await Strings.load(resolveLocale(WidgetsBinding.instance.window.locales, supportedLocales));
}

View file

@ -1,11 +1,11 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:mc_gallery/features/core/services/logging_service.dart';
import 'package:mc_gallery/l10n/generated/l10n.dart';
import '/l10n/generated/l10n.dart';
import '../data/enums/view_model_state.dart';
import '../services/logging_service.dart';
abstract class BaseViewModel<E extends Object?> extends ChangeNotifier {
abstract class BaseViewModel<T extends Object?> extends ChangeNotifier {
final ValueNotifier<bool> _isInitialised = ValueNotifier(false);
ValueListenable<bool> get isInitialised => _isInitialised;
@ -23,12 +23,11 @@ abstract class BaseViewModel<E extends Object?> extends ChangeNotifier {
String get errorMessage => _errorMessage ?? strings.somethingWentWrong;
@mustCallSuper
void initialise(DisposableBuildContext disposableBuildContext, bool Function() mounted,
[E? arguments]) {
void initialise(bool Function() mounted, [T? arguments]) {
_mounted = mounted;
_isInitialised.value = true;
_state.value = ViewModelState.isInitialised;
_loggingService.successfulInit(location: runtimeType.runtimeType.toString());
_loggingService.successfulInit(location: runtimeType.toString());
}
void setBusy(bool isBusy) {
@ -56,7 +55,7 @@ abstract class BaseViewModel<E extends Object?> extends ChangeNotifier {
@override
void dispose() {
super.dispose();
_loggingService.successfulDispose(location: runtimeType.runtimeType.toString());
_loggingService.successfulDispose(location: runtimeType.toString());
}
late final bool Function() _mounted;

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mc_gallery/features/home/views/gallery/gallery_view.dart';
import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view.dart';
import '../../views/error_page_view.dart';
import 'routes.dart';
@ -14,12 +15,21 @@ class McgRouter {
key: state.pageKey,
child: ErrorPageView(error: state.error),
),
// TODO Add Redirect
//todo(mehul): Add Redirect
routes: [
GoRoute(
path: Routes.home.routePath,
name: Routes.home.routeName,
builder: (context, _) => const GalleryView(),
routes: [
GoRoute(
path: Routes.imageCarousel.routePath,
name: Routes.imageCarousel.routeName,
builder: (context, state) => ImageCarouselView(
imageCarouselViewArguments: state.extra as ImageCarouselViewArguments,
),
),
],
),
],
);

View file

@ -4,7 +4,7 @@ enum Routes {
routeName: 'Home',
)),
imageCarousel(RoutesInfo(
routePath: '/image_carousel',
routePath: 'image_carousel',
routeName: 'Image Carousel',
));

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
abstract class ConstColours {
/// Smoke Gray => a neutral grey with neutral warmth/cold
static const imageBackgroundColour = Color.fromRGBO(127, 127, 125, 1.0);
static const galleryBackgroundColour = Color.fromRGBO(127, 127, 125, 1.0);
static const white = Colors.white;
static const black = Colors.black;

View file

@ -4,6 +4,6 @@ abstract class ConstValues {
static const List<String> backendUrlPathSegments = ['user', 'c_v_r'];
static const int numberOfImages = 20;
static const int minImageSize = 100;
static const int maxImageSize = 250;
static const int minImageSize = 50;
static const int maxImageSize = 100;
}

View file

@ -0,0 +1,103 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mc_gallery/locator.dart';
import 'logging_service.dart';
typedef AddLifeCycleListener = void Function({
required void Function(AppLifecycleState appLifecycleState) listener,
required String tag,
bool tryCallListenerOnAdd,
});
typedef RemoveLifeCycleListener = Future<void> Function({required String tag});
/// Used to observe the current app lifecycle state.
class AppLifecycleService with WidgetsBindingObserver {
AppLifecycleService({
required LoggingService loggingService,
}) : _loggingService = loggingService {
WidgetsBinding.instance.addObserver(this);
_appLifeCycleState = WidgetsBinding.instance.lifecycleState;
}
final LoggingService _loggingService;
late final StreamController<AppLifecycleState> _streamController = StreamController.broadcast();
final Map<String, StreamSubscription> _appLifecycleSubscriptions = {};
AppLifecycleState? _appLifeCycleState;
AppLifecycleState? get appLifeCycleState => _appLifeCycleState;
Future<void> dispose() async {
_loggingService.info('Disposing app lifecycle service..');
WidgetsBinding.instance.removeObserver(this);
for (final subscription in _appLifecycleSubscriptions.values) {
await subscription.cancel();
}
_appLifecycleSubscriptions.clear();
_loggingService.good('App lifecycle service disposed!');
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
try {
_appLifeCycleState = state;
_streamController.add(state);
} catch (error, stackTrace) {
_loggingService.error(
'Something went wrong logging ${state.name} inside the didChangeAppLifeCycleState method',
error,
stackTrace,
);
}
super.didChangeAppLifecycleState(state);
}
void addListener({
required void Function(AppLifecycleState appLifecycleState) listener,
required String tag,
bool tryCallListenerOnAdd = true,
}) {
try {
if (_appLifecycleSubscriptions.containsKey(tag)) {
_loggingService.warning('Tag already active, returning!');
} else {
final message = 'Adding $tag appLifecycleState listener';
_loggingService.info('$message..');
if (_appLifeCycleState != null && tryCallListenerOnAdd) listener(_appLifeCycleState!);
_appLifecycleSubscriptions[tag] = _streamController.stream.listen(listener);
_loggingService.good('$message success!');
}
} catch (error, stackTrace) {
_loggingService.error(
'Something went wrong adding $tag appLifecycleState listener!',
error,
stackTrace,
);
}
}
Future<void> removeListener({required String tag}) async {
try {
final message = 'Removing $tag appLifecycleState listener';
_loggingService.info('$message..');
final subscription = _appLifecycleSubscriptions[tag];
if (subscription != null) {
await subscription.cancel();
_appLifecycleSubscriptions.remove(tag);
_loggingService.good('$message success!');
} else {
_loggingService.warning('Subscription was not found!');
}
} catch (error, stackTrace) {
_loggingService.error(
'Something went wrong removing $tag appLifecycleState listener!',
error,
stackTrace,
);
}
}
static AppLifecycleService get locate => Locator.locate();
}

View file

@ -1,5 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view.dart';
import 'package:mc_gallery/locator.dart';
import '../abstracts/router/app_router.dart';
import '../abstracts/router/routes.dart';
@ -11,11 +13,19 @@ class NavigationService {
final McgRouter _mcgRouter;
void pushImageCarouselView(BuildContext context) =>
context.pushNamed(Routes.imageCarousel.routeName);
void pushImageCarouselView(
BuildContext context, {
required ImageCarouselViewArguments imageCarouselViewArguments,
}) =>
context.pushNamed(
Routes.imageCarousel.routeName,
extra: imageCarouselViewArguments,
);
void backToGallery(BuildContext context) => context.pop();
void previous() {}
void next() {}
static NavigationService get locate => Locator.locate();
}

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
@ -9,7 +8,7 @@ import '../services/connection_service.dart';
class McgScaffold extends StatelessWidget {
const McgScaffold({
this.appBar,
this.bodyBuilderCompleter,
this.bodyBuilderWaiter,
this.body,
this.waitingWidget,
this.forceInternetCheck = false,
@ -19,10 +18,10 @@ class McgScaffold extends StatelessWidget {
final AppBar? appBar;
/// Awaits an external signal (complete) before building the body.
final Completer? bodyBuilderCompleter;
final ValueListenable<bool>? bodyBuilderWaiter;
final Widget? body;
/// Custom widget to be used while awaiting [bodyBuilderCompleter].
/// Custom widget to be used while awaiting [bodyBuilderWaiter].
///
/// Defaults to using [PlatformCircularProgressIndicator].
final Widget? waitingWidget;
@ -32,19 +31,12 @@ class McgScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
final Widget loginOptionsBody = bodyBuilderCompleter != null
? FutureBuilder(
future: bodyBuilderCompleter!.future,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return Center(child: waitingWidget ?? const CircularProgressIndicator());
case ConnectionState.done:
return body ?? const SizedBox.shrink();
}
},
final Widget loginOptionsBody = bodyBuilderWaiter != null
? ValueListenableBuilder<bool>(
valueListenable: bodyBuilderWaiter!,
builder: (context, final isReady, child) => !isReady
? Center(child: waitingWidget ?? const CircularProgressIndicator())
: body ?? const SizedBox.shrink(),
)
: body ?? const SizedBox.shrink();

View file

@ -0,0 +1,47 @@
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../abstracts/base_view_model.dart';
class ViewModelBuilder<T extends BaseViewModel> extends StatefulWidget {
const ViewModelBuilder({
required Widget Function(BuildContext context, T model) builder,
required T Function() viewModelBuilder,
dynamic Function()? argumentBuilder,
super.key,
}) : _builder = builder,
_viewModelBuilder = viewModelBuilder,
_argumentBuilder = argumentBuilder;
final Widget Function(BuildContext context, T model) _builder;
final T Function() _viewModelBuilder;
final dynamic Function()? _argumentBuilder;
@override
_ViewModelBuilderState<T> createState() => _ViewModelBuilderState<T>();
}
class _ViewModelBuilderState<T extends BaseViewModel> extends State<ViewModelBuilder<T>> {
late final T _viewModel;
@override
void initState() {
_viewModel = widget._viewModelBuilder();
_viewModel.initialise(() => mounted, widget._argumentBuilder?.call());
super.initState();
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext _) => ChangeNotifierProvider.value(
value: _viewModel,
child: Consumer<T>(
builder: (context, model, _) => widget._builder(context, model),
),
);
}