chore: custom headers added, code refactoring (#624)

* chore: custom headers added, code refactoring

* fix: request timeout fixed

* chore: improved user initialization and retry mechanism

* chore: declare const for static value

* fix: navigation fixed, removed unnecessary requests

* chore: fixed naming convention

* chore: improved retry implementation

* chore: change provider name

---------

Co-authored-by: Michael Speed <michaelcspeed@users.noreply.github.com>
This commit is contained in:
Osama Asif 2023-12-19 14:50:27 +05:00 committed by GitHub
parent e2cc35c27a
commit d744e08fb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 342 additions and 286 deletions

View File

@ -1,4 +1,4 @@
platform :ios, '11.0'
platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -236,6 +236,6 @@ SPEC CHECKSUMS:
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241
PODFILE CHECKSUM: 9be41085f3ebdad4d4aaa1df125b86534775346b
PODFILE CHECKSUM: e02d150e307466fe01a4335b382fd16ee407a495
COCOAPODS: 1.12.1

View File

@ -480,6 +480,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -668,6 +669,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -698,6 +700,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -13,7 +13,6 @@ class HTTPConstants {
static const String PACKS = 'packs';
static const String TRACKS = 'tracks';
static const String BACKGROUND_SOUNDS = 'backgroundSounds';
static const String HOME = 'home';
static const String HEADER = 'main/header';
static const String QUOTE = 'main/quote';
static const String SHORTCUTS = 'main/shortcuts';

View File

@ -1,94 +0,0 @@
import 'dart:io';
import 'package:Medito/constants/constants.dart';
import 'package:Medito/models/models.dart';
import 'package:Medito/providers/providers.dart';
import 'package:Medito/services/network/dio_client_provider.dart';
import 'package:Medito/widgets/snackbar_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
final retryCounterProvider = StateProvider<int>((ref) => 0);
final appInitializationProvider =
FutureProvider.family<void, BuildContext>((ref, context) async {
var _authProvider = ref.read(authProvider);
try {
var retryCount = ref.read(retryCounterProvider);
if (retryCount < 2) {
_checkInternetConnectivity(context, ref);
}
await _authProvider.initializeUser();
var res = _authProvider.userRes;
if (res.body != null) {
var _user = res.body as UserTokenModel;
_assignNewTokenToDio(ref, _user.token);
var deviceInfo = await ref.read(deviceAndAppInfoProvider.future);
var data = _setAppOpenedModelData(deviceInfo);
await ref.read(eventsProvider(event: data.toJson()).future);
ref.read(retryCounterProvider.notifier).update((state) => 0);
context.go(RouteConstants.homePath);
} else {
_handleRetry(context, ref, false);
}
} catch (err) {
_handleRetry(context, ref, _authProvider.userRes.body != null);
}
});
void _handleRetry(BuildContext context, Ref ref, bool shouldNavigate) {
var retryCount = ref.read(retryCounterProvider);
if (retryCount < 2) {
_recallProvider(ref, context);
ref.read(retryCounterProvider.notifier).update((state) => ++state);
} else {
if (shouldNavigate) {
ref.read(retryCounterProvider.notifier).update((state) => 0);
context.go(RouteConstants.downloadsPath);
} else {
_recallProvider(ref, context);
if (retryCount < 2) {
showSnackBar(context, StringConstants.timeout);
}
}
}
}
void _recallProvider(Ref<Object?> ref, BuildContext context) {
Future.delayed(Duration(seconds: 2), () {
ref.refresh(appInitializationProvider(context));
});
}
void _checkInternetConnectivity(BuildContext context, Ref ref) {
var connectivityStatus = ref.read(connectivityStatusProvider);
if (connectivityStatus == ConnectivityStatus.isDisonnected) {
showSnackBar(context, StringConstants.connectivityError);
}
}
void _assignNewTokenToDio(Ref ref, String token) {
ref
.read(dioClientProvider)
.dio
.options
.headers[HttpHeaders.authorizationHeader] = 'Bearer $token';
}
EventsModel _setAppOpenedModelData(DeviceAndAppInfoModel val) {
var appOpenedModel = AppOpenedModel(
deviceOs: val.os,
deviceLanguage: val.languageCode,
deviceModel: val.model,
buildNumber: val.buildNumber,
appVersion: val.appVersion,
);
var event = EventsModel(
name: EventTypes.appOpened,
payload: appOpenedModel.toJson(),
);
return event;
}

View File

@ -22,9 +22,9 @@ class AuthNotifier extends ChangeNotifier {
final AuthRepository authRepository;
final Ref ref;
ApiResponse userRes = ApiResponse.completed(null);
ApiResponse sendOTPRes = ApiResponse.completed(null);
ApiResponse verifyOTPRes = ApiResponse.completed(null);
ApiResponse userResponse = ApiResponse.completed(null);
ApiResponse sendOTPResponse = ApiResponse.completed(null);
ApiResponse verifyOTPResponse = ApiResponse.completed(null);
String? userEmail;
bool counter = false;
bool isAGuest = false;
@ -49,45 +49,45 @@ class AuthNotifier extends ChangeNotifier {
if (res == null) {
await generateUserToken();
} else {
userRes = ApiResponse.completed(res);
userResponse = ApiResponse.completed(res);
notifyListeners();
}
}
Future<void> generateUserToken() async {
userRes = ApiResponse.loading();
userResponse = ApiResponse.loading();
notifyListeners();
try {
var res = await authRepository.generateUserToken();
await saveUserInSharedPref(res);
userRes = ApiResponse.completed(res);
userResponse = ApiResponse.completed(res);
} catch (e) {
userRes = ApiResponse.error(e.toString());
userResponse = ApiResponse.error(e.toString());
}
notifyListeners();
}
Future<void> sendOTP(String email) async {
sendOTPRes = ApiResponse.loading();
sendOTPResponse = ApiResponse.loading();
notifyListeners();
try {
var res = await authRepository.sendOTP(email);
sendOTPRes = ApiResponse.completed(res);
sendOTPResponse = ApiResponse.completed(res);
} catch (e) {
sendOTPRes = ApiResponse.error(e.toString());
sendOTPResponse = ApiResponse.error(e.toString());
}
notifyListeners();
}
Future<void> verifyOTP(String email, String OTP) async {
verifyOTPRes = ApiResponse.loading();
verifyOTPResponse = ApiResponse.loading();
notifyListeners();
try {
var res = await authRepository.verifyOTP(email, OTP);
await updateUserInSharedPref(email);
verifyOTPRes = ApiResponse.completed(res);
verifyOTPResponse = ApiResponse.completed(res);
} catch (e) {
verifyOTPRes = ApiResponse.error(e.toString());
verifyOTPResponse = ApiResponse.error(e.toString());
}
notifyListeners();
}
@ -97,9 +97,9 @@ class AuthNotifier extends ChangeNotifier {
}
Future<void> updateUserInSharedPref(String email) async {
var _userTokenModel = userRes.body as UserTokenModel;
var _userTokenModel = userResponse.body as UserTokenModel;
var _user = _userTokenModel.copyWith(email: email);
userRes = ApiResponse.completed(_user);
userResponse = ApiResponse.completed(_user);
await saveUserInSharedPref(_user);
}

View File

@ -0,0 +1,38 @@
import 'package:Medito/providers/providers.dart';
import 'package:Medito/services/network/dio_client_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
var retryCounter = 0;
final maxRetryCount = 2;
final userInitializationProvider =
FutureProvider<UserInitializationStatus>((ref) async {
try {
var auth = ref.read(authProvider);
await auth.initializeUser();
var response = auth.userResponse;
if (response.body != null) {
await ref.read(assignDioHeadersProvider.future);
await ref.read(appOpenedEventProvider.future);
return UserInitializationStatus.successful;
}
return UserInitializationStatus.error;
} catch (e) {
if (retryCounter < maxRetryCount) {
await Future.delayed(Duration(seconds: 2), () {
_incrementCounter();
});
return UserInitializationStatus.retry;
}
return UserInitializationStatus.error;
}
});
void _incrementCounter() => retryCounter += 1;
//ignore: prefer-match-file-name
enum UserInitializationStatus { retry, error, successful }

View File

@ -1,15 +1,18 @@
import 'package:connectivity/connectivity.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final connectivityStatusProvider = StateNotifierProvider((ref) {
return ConnectivityStatusNotifier();
});
final connectivityStatusProvider =
StateNotifierProvider<ConnectivityStatusNotifier, ConnectivityStatus>(
(ref) {
return ConnectivityStatusNotifier();
},
);
//ignore: prefer-match-file-name
enum ConnectivityStatus { NotDetermined, isConnected, isDisonnected }
enum ConnectivityStatus { NotDetermined, isConnected, isDisconnected }
class ConnectivityStatusNotifier extends StateNotifier<ConnectivityStatus> {
ConnectivityStatusNotifier() : super(ConnectivityStatus.isConnected) {
ConnectivityStatusNotifier() : super(ConnectivityStatus.NotDetermined) {
checkConnectivity();
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
_setConnectivityStatus(result);
@ -22,13 +25,14 @@ class ConnectivityStatusNotifier extends StateNotifier<ConnectivityStatus> {
}
void _setConnectivityStatus(ConnectivityResult result) {
//ignore: missing_enum_constant_in_switch
switch (result) {
case ConnectivityResult.mobile:
case ConnectivityResult.wifi:
state = ConnectivityStatus.isConnected;
break;
case ConnectivityResult.none:
state = ConnectivityStatus.isDisonnected;
state = ConnectivityStatus.isDisconnected;
break;
}
}

View File

@ -7,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'device_and_app_info_provider.g.dart';
@riverpod
Future<DeviceAndAppInfoModel> deviceAndAppInfo(ref) {
Future<DeviceAndAppInfoModel> deviceAndAppInfo(DeviceAndAppInfoRef ref) {
final info = ref.read(deviceAndAppInfoRepositoryProvider);
ref.keepAlive();

View File

@ -0,0 +1,27 @@
import 'package:Medito/constants/constants.dart';
import 'package:Medito/models/models.dart';
import 'package:Medito/providers/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
var appOpenedEventProvider = FutureProvider<void>((ref) async {
var deviceInfo = await ref.read(deviceAndAppInfoProvider.future);
var data = _createAppOpenedModelData(deviceInfo);
await ref.read(eventsProvider(event: data.toJson()).future);
});
EventsModel _createAppOpenedModelData(DeviceAndAppInfoModel val) {
var appOpenedModel = AppOpenedModel(
deviceOs: val.os,
deviceLanguage: val.languageCode,
deviceModel: val.model,
buildNumber: val.buildNumber,
appVersion: val.appVersion,
);
var event = EventsModel(
name: EventTypes.appOpened,
payload: appOpenedModel.toJson(),
);
return event;
}

View File

@ -1,17 +1,10 @@
import 'package:Medito/models/models.dart';
import 'package:Medito/providers/providers.dart';
import 'package:Medito/repositories/repositories.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'home_provider.g.dart';
@riverpod
Future<HomeModel> home(HomeRef ref) {
final homeRepository = ref.watch(homeRepositoryProvider);
ref.keepAlive();
return homeRepository.fetchHomeData();
}
@riverpod
Future<HomeHeaderModel> fetchHomeHeader(FetchHomeHeaderRef ref) {
final homeRepository = ref.watch(homeRepositoryProvider);
@ -45,3 +38,15 @@ Future<QuoteModel> fetchQuote(FetchQuoteRef ref) {
return homeRepository.fetchQuote();
}
@riverpod
Future<void> refreshHomeAPIs(RefreshHomeAPIsRef ref) async {
ref.invalidate(fetchHomeHeaderProvider);
await ref.read(fetchHomeHeaderProvider.future);
ref.invalidate(fetchShortcutsProvider);
await ref.read(fetchShortcutsProvider.future);
ref.invalidate(fetchQuoteProvider);
await ref.read(fetchQuoteProvider.future);
ref.invalidate(remoteStatsProvider);
await ref.read(remoteStatsProvider.future);
}

View File

@ -1,21 +1,23 @@
export 'auth/auth_provider.dart';
export 'auth/initialize_user_provider.dart';
export 'me/me_provider.dart';
export 'home/home_provider.dart';
export 'player/audio_player_provider.dart';
export 'background_sounds/background_sounds_provider.dart';
export 'connectivity/connectivity_provider.dart';
export 'device_and_app_info/device_and_app_info_provider.dart';
export 'events/events_provider.dart';
export 'home/home_provider.dart';
export 'me/me_provider.dart';
export 'meditation/download_track_provider.dart';
export 'meditation/track_provider.dart';
export 'notification/notification_provider.dart';
export 'pack/pack_provider.dart';
export 'page_view/bottom_padding_provider.dart';
export 'player/audio_play_pause_provider.dart';
export 'player/audio_player_provider.dart';
export 'player/audio_position_provider.dart';
export 'player/audio_speed_provider.dart';
export 'player/download/audio_downloader_provider.dart';
export 'player/player_provider.dart';
export 'search/search_provider.dart';
export 'shared_preference/shared_preference_provider.dart';
export 'events/app_opened_event_provider.dart';
export 'stats/stats_provider.dart';

View File

@ -54,7 +54,8 @@ void _handleAudioCompletion(Ref ref, BuildContext context) {
}
void _handleUserNotSignedIn(Ref ref, BuildContext context) {
var _user = ref.read(authProvider.notifier).userRes.body as UserTokenModel;
var _user =
ref.read(authProvider.notifier).userResponse.body as UserTokenModel;
if (_user.email == null) {
var params = JoinRouteParamsModel(screen: Screen.track);
context.push(

View File

@ -9,8 +9,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'home_repository.g.dart';
abstract class HomeRepository {
Future<HomeModel> fetchHomeData();
Future<HomeHeaderModel> fetchHomeHeader();
Future<QuoteModel> fetchQuote();
@ -34,13 +32,6 @@ class HomeRepositoryImpl extends HomeRepository {
HomeRepositoryImpl({required this.ref, required this.client});
@override
Future<HomeModel> fetchHomeData() async {
var res = await client.getRequest(HTTPConstants.HOME);
return HomeModel.fromJson(res);
}
@override
Future<HomeHeaderModel> fetchHomeHeader() async {
try {

View File

@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:io';
import 'package:Medito/constants/constants.dart';
import 'package:Medito/models/models.dart';
import 'package:Medito/providers/providers.dart';
import 'package:Medito/services/network/dio_api_service.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
@ -62,3 +64,27 @@ Future<void> _captureException(
stackTrace: err.stackTrace,
);
}
var assignDioHeadersProvider = FutureProvider<void>((ref) async {
var auth = ref.read(authProvider);
var deviceInfo = await ref.read(deviceAndAppInfoProvider.future);
var headers = ref.read(dioClientProvider).dio.options.headers;
var user = auth.userResponse.body as UserTokenModel;
headers[HttpHeaders.authorizationHeader] = 'Bearer ${user.token}';
var customHeaders = _createCustomHeaders(deviceInfo);
for (var key in customHeaders.keys) {
headers[key] = customHeaders[key];
}
});
Map<String, dynamic> _createCustomHeaders(DeviceAndAppInfoModel model) {
return {
'Device-Os': '${model.os}',
'Device-Language': '${model.languageCode}',
'Device-Model': '${model.model}',
'App-Version': '${model.appVersion}',
'Device-Time': '${DateTime.now()}',
};
}

View File

@ -45,7 +45,7 @@ class _JoinEmailViewState extends ConsumerState<JoinEmailView> {
var email = _emailController.text;
await auth.sendOTP(email);
auth.setCounter();
var status = auth.sendOTPRes.status;
var status = auth.sendOTPResponse.status;
if (status == Status.COMPLETED) {
var params =
JoinRouteParamsModel(screen: widget.fromScreen, email: email);
@ -54,7 +54,7 @@ class _JoinEmailViewState extends ConsumerState<JoinEmailView> {
extra: params,
);
} else if (status == Status.ERROR) {
showSnackBar(context, auth.sendOTPRes.message.toString());
showSnackBar(context, auth.sendOTPResponse.message.toString());
}
}
}
@ -63,7 +63,7 @@ class _JoinEmailViewState extends ConsumerState<JoinEmailView> {
Widget build(BuildContext context) {
var textTheme = Theme.of(context).textTheme;
auth = ref.watch(authProvider);
var isLoading = auth.sendOTPRes == ApiResponse.loading();
var isLoading = auth.sendOTPResponse == ApiResponse.loading();
return Scaffold(
backgroundColor: ColorConstants.ebony,

View File

@ -38,7 +38,7 @@ class _JoinVerifyOTPViewState extends ConsumerState<JoinVerifyOTPView> {
void _handleVerify() async {
if (_formKey.currentState!.validate()) {
await auth.verifyOTP(widget.email, _otpTextEditingController.text);
var status = auth.verifyOTPRes.status;
var status = auth.verifyOTPResponse.status;
if (status == Status.COMPLETED) {
await removeFirebaseToken();
await requestGenerateFirebaseToken();
@ -54,7 +54,7 @@ class _JoinVerifyOTPViewState extends ConsumerState<JoinVerifyOTPView> {
extra: params,
));
} else if (status == Status.ERROR) {
showSnackBar(context, auth.verifyOTPRes.message.toString());
showSnackBar(context, auth.verifyOTPResponse.message.toString());
}
}
}
@ -63,7 +63,7 @@ class _JoinVerifyOTPViewState extends ConsumerState<JoinVerifyOTPView> {
Widget build(BuildContext context) {
auth = ref.watch(authProvider);
var textTheme = Theme.of(context).textTheme;
var isLoading = auth.verifyOTPRes == ApiResponse.loading();
var isLoading = auth.verifyOTPResponse == ApiResponse.loading();
return Scaffold(
backgroundColor: ColorConstants.ebony,
@ -194,16 +194,16 @@ class _ResendCodeWidget extends ConsumerWidget {
void _handleResendOTP() async {
await auth.sendOTP(email);
auth.setCounter();
var status = auth.sendOTPRes.status;
var status = auth.sendOTPResponse.status;
if (status == Status.COMPLETED) {
showSnackBar(context, auth.sendOTPRes.body.toString());
showSnackBar(context, auth.sendOTPResponse.body.toString());
} else if (status == Status.ERROR) {
showSnackBar(context, auth.sendOTPRes.message.toString());
showSnackBar(context, auth.sendOTPResponse.message.toString());
auth.setCounter();
}
}
if (auth.sendOTPRes.status == Status.LOADING) {
if (auth.sendOTPResponse.status == Status.LOADING) {
return SizedBox(
height: 16,
width: 16,

View File

@ -61,12 +61,10 @@ class _BackgroundSoundViewState extends ConsumerState<BackgroundSoundView> {
@override
Widget build(BuildContext context) {
var connectivityStatus =
ref.watch(connectivityStatusProvider) as ConnectivityStatus;
var connectivityStatus = ref.watch(connectivityStatusProvider);
ref.listen(connectivityStatusProvider, (prev, next) {
var state = next as ConnectivityStatus;
if (state == ConnectivityStatus.isDisonnected) {
if (next == ConnectivityStatus.isDisconnected) {
showSnackBar(context, StringConstants.connectivityError);
}
});
@ -74,7 +72,7 @@ class _BackgroundSoundViewState extends ConsumerState<BackgroundSoundView> {
ref.watch(fetchLocallySavedBackgroundSoundsProvider);
var backgroundSounds = ref.watch(backgroundSoundsProvider);
if (connectivityStatus == ConnectivityStatus.isDisonnected) {
if (connectivityStatus == ConnectivityStatus.isDisconnected) {
return Scaffold(
body: localBackgroundSounds.when(
skipLoadingOnRefresh: true,
@ -128,7 +126,7 @@ class _BackgroundSoundViewState extends ConsumerState<BackgroundSoundView> {
) {
return RefreshIndicator(
onRefresh: () async {
if (status == ConnectivityStatus.isDisonnected) {
if (status == ConnectivityStatus.isDisconnected) {
return;
} else {
ref.invalidate(backgroundSoundsProvider);

View File

@ -36,6 +36,7 @@ class _DownloadsViewState extends ConsumerState<DownloadsView>
if (context.canPop()) {
context.pop();
} else {
ref.read(refreshHomeAPIsProvider.future);
context.go(RouteConstants.homePath);
}
},

View File

@ -24,12 +24,20 @@ class _HomeViewState extends ConsumerState<HomeView>
@override
Widget build(BuildContext context) {
super.build(context);
var connectivityStatus =
ref.watch(connectivityStatusProvider) as ConnectivityStatus;
if (connectivityStatus == ConnectivityStatus.isDisonnected) {
var homeAPIsResponse = ref.watch(refreshHomeAPIsProvider);
var connectivityStatus = ref.watch(connectivityStatusProvider);
if (connectivityStatus == ConnectivityStatus.isDisconnected) {
return ConnectivityErrorWidget();
}
if (homeAPIsResponse.hasError) {
return MeditoErrorWidget(
message: homeAPIsResponse.error.toString(),
onTap: () => _onRefresh(),
isLoading: homeAPIsResponse.isLoading,
);
}
return Scaffold(
floatingActionButton: _buildFloatingButton(context),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
@ -74,14 +82,8 @@ class _HomeViewState extends ConsumerState<HomeView>
}
Future<void> _onRefresh() async {
ref.invalidate(fetchHomeHeaderProvider);
await ref.read(fetchHomeHeaderProvider.future);
ref.invalidate(fetchShortcutsProvider);
await ref.read(fetchShortcutsProvider.future);
ref.invalidate(fetchQuoteProvider);
await ref.read(fetchQuoteProvider.future);
ref.invalidate(remoteStatsProvider);
await ref.read(remoteStatsProvider.future);
ref.invalidate(refreshHomeAPIsProvider);
await ref.read(refreshHomeAPIsProvider.future);
}
FloatingActionButton _buildFloatingButton(BuildContext context) {

View File

@ -0,0 +1,123 @@
import 'package:Medito/constants/constants.dart';
import 'package:Medito/models/models.dart';
import 'package:Medito/providers/providers.dart';
import 'package:Medito/routes/routes.dart';
import 'package:Medito/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:reorderables/reorderables.dart';
class ShortcutsItemsWidget extends ConsumerStatefulWidget {
const ShortcutsItemsWidget({super.key, required this.data});
final ShortcutsModel data;
@override
ConsumerState<ShortcutsItemsWidget> createState() =>
_ShortcutsItemsWidgetState();
}
class _ShortcutsItemsWidgetState extends ConsumerState<ShortcutsItemsWidget> {
late ShortcutsModel data;
late List<Widget> shortcutsWidgetList = [];
@override
void didChangeDependencies() {
data = widget.data;
shortcutsWidgetList = _getShortcutsItemWidgetList();
super.didChangeDependencies();
}
void _handleChipPress(
BuildContext context,
WidgetRef ref,
ShortcutsItemsModel element,
) async {
_handleTrackEvent(ref, element.id, element.title);
await handleNavigation(
context: context,
element.type,
[element.path.toString().getIdFromPath(), element.path],
ref: ref,
);
}
void _handleTrackEvent(WidgetRef ref, String chipId, String chipTitle) {
var chipViewedModel = ChipTappedModel(chipId: chipId, chipTitle: chipTitle);
var event = EventsModel(
name: EventTypes.chipTapped,
payload: chipViewedModel.toJson(),
);
ref.read(eventsProvider(event: event.toJson()));
}
void _onReorder(int oldIndex, int newIndex) {
setState(() {
_handleShortcutWidgetPlacement(newIndex, oldIndex);
_handleShortcutItemPlacementInPreference(oldIndex, newIndex);
});
}
void _handleShortcutItemPlacementInPreference(int oldIndex, int newIndex) {
var _data = [...data.shortcuts];
final element = _data.removeAt(oldIndex);
_data.insert(newIndex, element);
data = data.copyWith(shortcuts: _data);
var ids = _data.map((e) => e.id).toList();
ref.read(updateShortcutsIdsInPreferenceProvider(ids: ids));
}
void _handleShortcutWidgetPlacement(int newIndex, int oldIndex) {
shortcutsWidgetList.insert(
newIndex,
shortcutsWidgetList.removeAt(oldIndex),
);
}
@override
Widget build(BuildContext context) {
return ReorderableWrap(
spacing: 8.0,
runSpacing: 8.0,
padding: EdgeInsets.zero,
maxMainAxisCount: 2,
minMainAxisCount: 2,
onReorder: _onReorder,
children: shortcutsWidgetList,
);
}
List<Widget> _getShortcutsItemWidgetList() {
var size = MediaQuery.of(context).size;
final containerHeight = 48.0;
final containerWidth = (size.width / 2) - (padding20 + 2);
return data.shortcuts
.map((e) => IntrinsicWidth(
child: InkWell(
key: ValueKey(e.id),
onTap: () => _handleChipPress(context, ref, e),
child: Container(
height: containerHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
color: ColorConstants.onyx,
),
padding: EdgeInsets.all(12),
constraints: BoxConstraints(
maxWidth: containerWidth,
minWidth: containerWidth,
),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'${e.title}',
style: Theme.of(context).textTheme.titleSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
))
.toList();
}
}

View File

@ -1,12 +1,9 @@
import 'package:Medito/constants/constants.dart';
import 'package:Medito/models/models.dart';
import 'package:Medito/providers/providers.dart';
import 'package:Medito/routes/routes.dart';
import 'package:Medito/utils/utils.dart';
import 'package:Medito/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:reorderables/reorderables.dart';
import 'shortcuts_items_widget.dart';
class ShortcutsWidget extends ConsumerStatefulWidget {
const ShortcutsWidget({super.key});
@ -16,73 +13,21 @@ class ShortcutsWidget extends ConsumerStatefulWidget {
}
class _ShortcutsWidgetState extends ConsumerState<ShortcutsWidget> {
late ShortcutsModel data;
late List<Widget> shortcutsWidgetList = [];
@override
void initState() {
super.initState();
}
void _handleChipPress(
BuildContext context,
WidgetRef ref,
ShortcutsItemsModel element,
) async {
_handleTrackEvent(ref, element.id, element.title);
await handleNavigation(
context: context,
element.type,
[element.path.toString().getIdFromPath(), element.path],
ref: ref,
);
}
void _handleTrackEvent(WidgetRef ref, String chipId, String chipTitle) {
var chipViewedModel = ChipTappedModel(chipId: chipId, chipTitle: chipTitle);
var event = EventsModel(
name: EventTypes.chipTapped,
payload: chipViewedModel.toJson(),
);
ref.read(eventsProvider(event: event.toJson()));
}
void _onReorder(int oldIndex, int newIndex) {
setState(() {
_handleShortcutWidgetPlacement(newIndex, oldIndex);
_handleShortcutItemPlacementInPreference(oldIndex, newIndex);
});
}
void _handleShortcutItemPlacementInPreference(int oldIndex, int newIndex) {
var _data = [...data.shortcuts];
final element = _data.removeAt(oldIndex);
_data.insert(newIndex, element);
data = data.copyWith(shortcuts: _data);
var ids = _data.map((e) => e.id).toList();
ref.read(updateShortcutsIdsInPreferenceProvider(ids: ids));
}
void _handleShortcutWidgetPlacement(int newIndex, int oldIndex) {
shortcutsWidgetList.insert(
newIndex,
shortcutsWidgetList.removeAt(oldIndex),
);
}
@override
Widget build(BuildContext context) {
var response = ref.watch(fetchShortcutsProvider);
ref.listen(fetchShortcutsProvider, (previous, next) {
if (next.hasValue) {
data = next.value!;
shortcutsWidgetList = _getShortcutsItemWidgetList();
}
});
return response.when(
skipLoadingOnRefresh: false,
skipLoadingOnReload: true,
data: (_) => _buildShortcuts(),
data: (data) => ShortcutsItemsWidget(
data: data,
),
error: (err, stack) => MeditoErrorWidget(
message: err.toString(),
onTap: () => ref.refresh(fetchShortcutsProvider),
@ -92,52 +37,18 @@ class _ShortcutsWidgetState extends ConsumerState<ShortcutsWidget> {
loading: () => const ShortcutsShimmerWidget(),
);
}
}
ReorderableWrap _buildShortcuts() {
return ReorderableWrap(
spacing: 8.0,
runSpacing: 8.0,
padding: EdgeInsets.zero,
maxMainAxisCount: 2,
minMainAxisCount: 2,
onReorder: _onReorder,
children: shortcutsWidgetList,
);
}
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
List<Widget> _getShortcutsItemWidgetList() {
var size = MediaQuery.of(context).size;
final containerHeight = 48.0;
final containerWidth = (size.width / 2) - (padding20 + 2);
@override
State<MyWidget> createState() => _MyWidgetState();
}
return data.shortcuts
.map((e) => IntrinsicWidth(
child: InkWell(
key: ValueKey(e.id),
onTap: () => _handleChipPress(context, ref, e),
child: Container(
height: containerHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
color: ColorConstants.onyx,
),
padding: EdgeInsets.all(12),
constraints: BoxConstraints(
maxWidth: containerWidth,
minWidth: containerWidth,
),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'${e.title}',
style: Theme.of(context).textTheme.titleSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
))
.toList();
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@ -18,13 +18,11 @@ class TilesWidget extends ConsumerWidget {
return stats.when(
skipLoadingOnRefresh: false,
data: (data) => _buildTiles(ref, data.tiles),
error: (err, stack) => Expanded(
child: MeditoErrorWidget(
message: err.toString(),
onTap: () => ref.refresh(remoteStatsProvider),
isLoading: stats.isLoading,
isScaffold: false,
),
error: (err, stack) => MeditoErrorWidget(
message: err.toString(),
onTap: () => ref.refresh(remoteStatsProvider),
isLoading: stats.isLoading,
isScaffold: false,
),
loading: () => TilesShimmerWidget(),
);

View File

@ -56,7 +56,7 @@ class _RootPageViewState extends ConsumerState<RootPageView> {
children: [
_renderChild(
context,
connectivityStatus as ConnectivityStatus,
connectivityStatus,
),
],
),
@ -72,7 +72,7 @@ class _RootPageViewState extends ConsumerState<RootPageView> {
return widget.firstChild;
} else if ((location != RouteConstants.downloadsPath &&
location != RouteConstants.playerPath) &&
status == ConnectivityStatus.isDisonnected) {
status == ConnectivityStatus.isDisconnected) {
return ConnectivityErrorWidget();
} else {
return widget.firstChild;

View File

@ -1,8 +1,10 @@
import 'package:Medito/constants/constants.dart';
import 'package:Medito/providers/auth/app_initialization_provider.dart';
import 'package:Medito/providers/providers.dart';
import 'package:Medito/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
class SplashView extends ConsumerStatefulWidget {
const SplashView({super.key});
@ -14,14 +16,32 @@ class SplashView extends ConsumerStatefulWidget {
class _SplashViewState extends ConsumerState<SplashView> {
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(appInitializationProvider(context));
});
initializeUser();
super.initState();
}
void initializeUser() async {
var response = await ref.read(userInitializationProvider.future);
if (response == UserInitializationStatus.successful) {
context.go(RouteConstants.homePath);
} else if (response == UserInitializationStatus.error) {
showSnackBar(context, StringConstants.timeout);
context.go(RouteConstants.downloadsPath);
} else if (response == UserInitializationStatus.retry) {
ref.invalidate(userInitializationProvider);
initializeUser();
}
}
@override
Widget build(BuildContext context) {
ref.listen(connectivityStatusProvider, (previous, next) {
var isDisconnected = next == ConnectivityStatus.isDisconnected;
if (isDisconnected) {
showSnackBar(context, StringConstants.connectivityError);
}
});
return Scaffold(
backgroundColor: ColorConstants.ebony,
body: Center(

View File

@ -106,7 +106,7 @@ class _TrackViewState extends ConsumerState<TrackView>
ref.listen(trackOpenedFirstTimeProvider, (prev, next) {
var _user =
ref.read(authProvider.notifier).userRes.body as UserTokenModel;
ref.read(authProvider.notifier).userResponse.body as UserTokenModel;
if (_user.email == null && next.value != null && next.value!) {
var params = JoinRouteParamsModel(screen: Screen.track);
context.push(

View File

@ -28,7 +28,8 @@ class NetworkImageWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
var userTokenModel = ref.watch(authProvider).userRes.body as UserTokenModel;
var userTokenModel =
ref.watch(authProvider).userResponse.body as UserTokenModel;
if (url.contains('.svg')) {
return SvgPicture.network(
url,