Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BaseViewModel with common loading state and init failure with retry #14

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/lotties/loading.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"v":"5.8.1","fr":30,"ip":0,"op":60,"w":300,"h":300,"nm":"loading_6","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60,"s":[360]}],"ix":10},"p":{"a":0,"k":[150.00000000000003,150.00000000000003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[30.000000000000004,30.000000000000004,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[300,300],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.22745098039215686,0.6627450980392157,0.8627450980392157,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":60,"s":[99]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[1]},{"t":50,"s":[100]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60,"s":[3]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":30,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150.00000000000003,150.00000000000003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[30.000000000000004,30.000000000000004,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[300,300],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.6666666666666666,0.8431372549019608,0.9215686274509803,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}],"markers":[]}
4 changes: 3 additions & 1 deletion ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -207,6 +207,7 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand All @@ -221,6 +222,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down
2 changes: 2 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
7 changes: 7 additions & 0 deletions lib/design_system/themes/custom_asset.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enum CustomAsset {
loadingWoundCoAnimation('assets/lotties/wc_loading_animation.json');
DavidGrunheidt marked this conversation as resolved.
Show resolved Hide resolved

const CustomAsset(this.path);

final String path;
}
47 changes: 47 additions & 0 deletions lib/design_system/widgets/screen_init_error.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';

import '../../helpers/app_constants.dart';

class ScreenInitError extends StatelessWidget {
final VoidCallback onTryAgain;
final bool allowRetry;

const ScreenInitError({
super.key,
required this.allowRetry,
required this.onTryAgain,
});

@override
Widget build(BuildContext context) {
// TODO(DavidGrunheidt): Use CustomXXX theme and widget classes
return Padding(
padding: const EdgeInsets.all(12),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
genericExceptionMessage,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
if (allowRetry)
InkWell(
onTap: onTryAgain,
child: const Text('Try again'),
)
else
Text(
'Try again later',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
)
],
),
),
);
}
}
51 changes: 51 additions & 0 deletions lib/features/base_view_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';

part 'base_view_model.g.dart';

class BaseViewModel = _BaseViewModel with _$BaseViewModel;

abstract class _BaseViewModel with Store {
@observable
var loading = false;

@observable
var firstLoading = true;

@observable
var initFailed = false;

@computed
bool get maxRetriesReached => initRetryCounter > 5;

@observable
var initRetryCounter = 0;

@computed
bool get allowInitRetry => !maxRetriesReached;

@action
@nonVirtual
Future<void> runInit() async {
try {
initFailed = false;
await init();
initRetryCounter = 0;
} catch (exception, stackTrace) {
// TODO send error to error reporting tool
debugPrint('Error initilializing viewModel: ${exception.toString()}:\n$stackTrace');

initFailed = true;
initRetryCounter++;
} finally {
firstLoading = false;
loading = false;
}
}

@action
@protected
Future<void> init() async {}

void dispose() {}
}
68 changes: 68 additions & 0 deletions lib/features/base_view_model_container.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:lottie/lottie.dart';

import '../design_system/themes/custom_asset.dart';
import '../design_system/widgets/screen_init_error.dart';
import 'base_view_model.dart';

class BaseViewModelContainer extends StatefulWidget {
final BaseViewModel viewModel;
final Widget child;

const BaseViewModelContainer({
super.key,
required this.viewModel,
required this.child,
});

@override
State<BaseViewModelContainer> createState() => _BaseViewModelContainerState();
}

class _BaseViewModelContainerState extends State<BaseViewModelContainer> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => widget.viewModel.runInit());
}

@override
void dispose() {
widget.viewModel.dispose();
super.dispose();
}

void closeKeyboard(BuildContext context) => FocusScope.of(context).unfocus();

Widget get loadingScreen => Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
color: Colors.white30.withOpacity(0.7),
DavidGrunheidt marked this conversation as resolved.
Show resolved Hide resolved
),
alignment: Alignment.center,
child: Center(
child: Lottie.asset(CustomAsset.loadingWoundCoAnimation.path),
),
);

@override
Widget build(BuildContext context) {
final viewModel = widget.viewModel;
return GestureDetector(
onTap: () => closeKeyboard(context),
child: Observer(
builder: (context) {
return Stack(
children: [
if (!viewModel.initFailed) widget.child,
if (viewModel.initFailed)
ScreenInitError(allowRetry: viewModel.allowInitRetry, onTryAgain: viewModel.runInit),
if (viewModel.loading) loadingScreen,
],
);
},
),
);
}
}
5 changes: 3 additions & 2 deletions lib/features/new_posts/new_post_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import 'package:mobx/mobx.dart';
import '../../helpers/dependencies/repository_locator.dart';
import '../../models/post.dart';
import '../../repository/post_repository.dart';
import '../base_view_model.dart';

part 'new_post_view_model.g.dart';

class NewPostViewModel = NewPostViewModelBase with _$NewPostViewModel;
class NewPostViewModel = _NewPostViewModel with _$NewPostViewModel;

abstract class NewPostViewModelBase with Store {
abstract class _NewPostViewModel extends BaseViewModel with Store {
final PostRepository _postRepository = repositoryLocator<PostRepository>();

Future<Post> addNewPost(String text, String creationDate) {
Expand Down
36 changes: 20 additions & 16 deletions lib/features/posts/post_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:intl/intl.dart';
import '../../design_system/widgets/refresh_list_adaptive.dart';
import '../../flavors.dart';
import '../../models/post.dart';
import '../base_view_model_container.dart';
import '../new_posts/new_post_view.dart';
import 'post_view_model.dart';
import 'widgets/post_card_item.dart';
Expand Down Expand Up @@ -35,22 +36,25 @@ class _PostViewState extends State<PostView> {
)
],
),
body: Observer(
builder: (_) {
return RefreshListAdaptive(
onRefresh: _viewModel.loadPosts,
itemBuilder: (context, i) {
final post = _viewModel.postsList[i];
return PostCardItem(
key: Key(post.id.toString()),
id: post.id.toString(),
text: post.text,
createdAt: DateFormat.yMd().add_Hms().format(post.createdAtDatetime),
);
},
itemCount: _viewModel.postsList.length,
);
},
body: BaseViewModelContainer(
viewModel: _viewModel,
child: Observer(
builder: (_) {
return RefreshListAdaptive(
onRefresh: _viewModel.loadPosts,
itemBuilder: (context, i) {
final post = _viewModel.postsList[i];
return PostCardItem(
key: Key(post.id.toString()),
id: post.id.toString(),
text: post.text,
createdAt: DateFormat.yMd().add_Hms().format(post.createdAtDatetime),
);
},
itemCount: _viewModel.postsList.length,
);
},
),
),
);
}
Expand Down
13 changes: 10 additions & 3 deletions lib/features/posts/post_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ import 'package:mobx/mobx.dart';
import '../../helpers/dependencies/repository_locator.dart';
import '../../models/post.dart';
import '../../repository/post_repository.dart';
import '../base_view_model.dart';

part 'post_view_model.g.dart';

class PostViewModel = PostViewModelBase with _$PostViewModel;
class PostViewModel = _PostsViewModel with _$PostViewModel;

// TODO(DavidGrunheidt): Add BaseViewModel extension with override to init method
abstract class PostViewModelBase with Store {
abstract class _PostsViewModel extends BaseViewModel with Store {
final PostRepository _postRepository = repositoryLocator<PostRepository>();

@computed
List<Post> get postsList => _postRepository.postsList;

@action
@override
Future<void> init() async {
await loadPosts();
return super.init();
}

@action
Future<void> loadPosts() => _postRepository.loadAll();
}
1 change: 1 addition & 0 deletions lib/helpers/app_constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const genericExceptionMessage = 'Oh no! Something went wrong.';
7 changes: 7 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ dependencies:
# Data
json_annotation: ^4.8.0

# UI:
lottie: ^2.2.0

dev_dependencies:
flutter_test:
sdk: flutter
Expand Down Expand Up @@ -52,6 +55,10 @@ dependency_overrides:
flutter:
uses-material-design: true

# To add assets to your application, add an assets section, like this:
assets:
- assets/lotties/

flavorizr:
app:
android:
Expand Down